rapid7/metasploit-framework

View on GitHub
lib/rex/proto/iax2/call.rb

Summary

Maintainability
C
1 day
Test Coverage
# -*- coding: binary -*-
module Rex
module Proto
module IAX2
class Call

  attr_accessor :client
  attr_accessor :oseq, :iseq
  attr_accessor :scall, :dcall
  attr_accessor :codec, :state
  attr_accessor :ring_start, :ring_finish
  attr_accessor :itime
  attr_accessor :queue
  attr_accessor :audio_hook
  attr_accessor :audio_buff
  attr_accessor :time_limit
  attr_accessor :busy

  attr_accessor :caller_name
  attr_accessor :caller_number
  attr_accessor :dtmf


  def initialize(client, src_id)
    self.client = client
    self.scall  = src_id
    self.dcall  = 0
    self.iseq   = 0
    self.oseq   = 0
    self.state  = nil

    self.itime  = ::Time.now
    self.queue  = ::Queue.new

    self.audio_buff = []

    self.busy = false
    self.dtmf = ''
  end


  def dprint(msg)
    self.client.dprint(msg)
  end

  def wait_for(*stypes)
    begin
      ::Timeout.timeout( Constants::IAX_DEFAULT_TIMEOUT ) do
        while (res = self.queue.pop )
          if stypes.include?(res[1])
            return res
          end
        end
      end
    rescue ::Timeout::Error
      return nil
    end
  end

  # Register with the IAX endpoint
  def register
    self.client.send_regreq(self)
    res = wait_for( Constants::IAX_SUBTYPE_REGAUTH, Constants::IAX_SUBTYPE_REGREJ )
    return if not res

    if res[1] == Constants::IAX_SUBTYPE_REGREJ
      reason = res[2][Constants::IAX_IE_REGREJ_CAUSE] || "Unknown Reason"
      dprint("REGREJ: #{reason}")
      # Acknowledge the REGREJ
      self.client.send_ack(self)
      return
    end

    chall = nil

    # Look for IAX_AUTH_MD5 (2) as an available auth method
    if res[2][14].unpack("n")[0] & 2 <= 0
      dprint("REGAUTH: MD5 authentication is not enabled on the server")
      return
    end

    if res[2][Constants::IAX_IE_CHALLENGE_DATA]
      self.dcall = res[0][0]
      chall = res[2][Constants::IAX_IE_CHALLENGE_DATA]
    end

    if chall.nil?
      dprint("REGAUTH: No challenge data received")
      return
    end

    self.client.send_regreq_chall_response(self, chall)
    res = wait_for( Constants::IAX_SUBTYPE_REGACK, Constants::IAX_SUBTYPE_REGREJ )
    return if not res

    if res[1] == Constants::IAX_SUBTYPE_REGREJ
      reason = res[2][Constants::IAX_IE_REGREJ_CAUSE] || "Unknown Reason"
      dprint("REGREJ: #{reason}")
      return
    end

    if res[2][Constants::IAX_IE_APPARENT_ADDR]
      r_fam, r_port, r_addr = res[2][Constants::IAX_IE_APPARENT_ADDR].unpack('nnA4')
      r_addr = r_addr.unpack("C*").map{|x| x.to_s }.join(".")
      dprint("REGACK: Registered from address #{r_addr}:#{r_port}")
    end

    # Acknowledge the REGACK
    self.client.send_ack(self)

    self.state = :registered

    true
  end

  def dial(number)
    self.client.send_new(self, number)
    res = wait_for(Constants::IAX_SUBTYPE_AUTHREQ, Constants::IAX_SUBTYPE_ACCEPT)
    return if not res

    # Handle authentication if its requested
    if res[1] == Constants::IAX_SUBTYPE_AUTHREQ
      chall = nil

      # Look for IAX_AUTH_MD5 (2) as an available auth method
      if res[2][14].unpack("n")[0] & 2 <= 0
        dprint("REGAUTH: MD5 authentication is not enabled on the server")
        return
      end

      if res[2][Constants::IAX_IE_CHALLENGE_DATA]
        self.dcall = res[0][0]
        chall = res[2][Constants::IAX_IE_CHALLENGE_DATA]
      end

      if chall.nil?
        dprint("REGAUTH: No challenge data received")
        return
      end

      self.client.send_authrep_chall_response(self, chall)
      res = wait_for( Constants::IAX_SUBTYPE_ACCEPT)
      return if not res
    end

    self.codec = res[2][Constants::IAX_IE_DESIRED_CODEC].unpack("N")[0]
    self.state = :ringing
    self.ring_start = ::Time.now.to_i
    self.client.send_ack(self)
    true
  end

  def hangup
    self.client.send_hangup(self)
    self.state = :hangup
    true
  end

  def ring_time
    (self.ring_finish || ::Time.now).to_i - self.ring_start.to_i
  end

  def timestamp
    (( ::Time.now - self.itime) * 1000.0 ).to_i & 0xffffffff
  end

  def process_elements(data,off=0)
    res = {}
    while( off < data.length )
      ie_type = data[off    ,1].unpack("C")[0]
      ie_len  = data[off + 1,2].unpack("C")[0]
      res[ie_type] = data[off + 2, ie_len]
      off += ie_len + 2
    end
    res
  end

  # Handling incoming control packets
  # TODO: Enforce sequence order to prevent duplicates from breaking our state
  def handle_control(pkt)
    src_call, dst_call, tstamp, out_seq, inp_seq, itype = pkt.unpack('nnNCCC')

    # Scrub the high bits out of the call IDs
    src_call ^= 0x8000 if (src_call & 0x8000 != 0)
    dst_call ^= 0x8000 if (dst_call & 0x8000 != 0)

    phdr = [ src_call, dst_call, tstamp, out_seq, inp_seq, itype ]

    info  = nil
    stype = pkt[11,1].unpack("C")[0]
    info  = process_elements(pkt, 12) if [Constants::IAX_TYPE_IAX, Constants::IAX_TYPE_CONTROL].include?(itype)

    if dst_call != self.scall
      dprint("Incoming packet to inactive call: #{dst_call} vs #{self.scall}: #{phdr.inspect} #{stype.inspect} #{info.inspect}")
      return
    end

    # Increment the received sequence number
    self.iseq = (self.iseq + 1) & 0xff

    if self.state == :hangup
      dprint("Packet received after hangup, replying with invalid")
      self.client.send_invalid(self)
      return
    end

    # Technically these all require an ACK reply
    # NEW, HANGUP, REJECT, ACCEPT, PONG, AUTHREP, REGREL, REGACK, REGREJ, TXREL

    case itype
    when Constants::IAX_TYPE_DTMF_BEGIN
      self.dprint("DTMF BEG: #{pkt[11,1]}")
      self.dtmf << pkt[11,1]

    when Constants::IAX_TYPE_DTMF_END
      self.dprint("DTMF END: #{pkt[11,1]}")

    when Constants::IAX_TYPE_CONTROL
      case stype
      when Constants::IAX_CTRL_HANGUP
        dprint("HANGUP")
        self.client.send_ack(self)
        self.state = :hangup

      when Constants::IAX_CTRL_RINGING
        dprint("RINGING")
        self.client.send_ack(self)

      when Constants::IAX_CTRL_BUSY
        dprint("BUSY")
        self.busy  = true
        self.state = :hangup
        self.client.send_ack(self)

      when Constants::IAX_CTRL_ANSWER
        dprint("ANSWER")
        if self.state == :ringing
          self.state = :answered
          self.ring_finish = ::Time.now.to_i
        end
        self.client.send_ack(self)

      when Constants::IAX_CTRL_PROGRESS
        dprint("PROGRESS")

      when Constants::IAX_CTRL_PROCEED
        dprint("PROCEED")

      when 255
        dprint("STOP SOUNDS")
      end
      # Acknowledge all control packets
      # self.client.send_ack(self)

    when Constants::IAX_TYPE_IAX

      dprint( ["RECV", phdr, stype, info].inspect )
      case stype
      when Constants::IAX_SUBTYPE_HANGUP
        self.state = :hangup
        self.client.send_ack(self)
      when Constants::IAX_SUBTYPE_LAGRQ
        # Lagrps echo the timestamp
        self.client.send_lagrp(self, tstamp)
      when Constants::IAX_SUBTYPE_ACK
        # Nothing to do here
      when Constants::IAX_SUBTYPE_PING
        # Pongs echo the timestamp
        self.client.send_pong(self, tstamp)
      when Constants::IAX_SUBTYPE_PONG
        self.client.send_ack(self)
      else
        dprint( ["RECV-QUEUE", phdr, stype, info].inspect )
        self.queue.push( [phdr, stype, info ] )
      end

    when Constants::IAX_TYPE_VOICE
      v_codec = stype
      if self.state == :answered
        handle_audio(pkt)
      end
      self.client.send_ack(self)

    when nil
      dprint("Invalid control packet: #{pkt.unpack("H*")[0]}")
    end
  end


  # Encoded audio from the client
  def handle_audio(pkt)
    # Ignore audio received before the call is answered (ring ring)
    return if self.state != :answered

    # Extract the data from the packet (full or mini)
    data = audio_packet_data(pkt)

    # Decode the data into linear PCM frames
    buff = decode_audio_frame(data)

    # Call the caller-provided hook if its exists
    if self.audio_hook
      self.audio_buff(buff)
    # Otherwise append the frame to the buffer
    else
      self.audio_buff << buff
    end
  end

  def each_audio_frame(&block)
    self.audio_buff.each do |frame|
      block.call(frame)
    end
  end

  def decode_audio_frame(buff)
    case self.codec

    # Convert u-law into signed PCM
    when Constants::IAX_CODEC_G711_MULAW
      Rex::Proto::IAX2::Codecs::MuLaw.decode(buff)

    # Convert a-law into signed PCM
    when Constants::IAX_CODEC_G711_ALAW
      Rex::Proto::IAX2::Codecs::ALaw.decode(buff)

    # Linear little-endian signed PCM is our native format
    when Constants::IAX_CODEC_LINEAR_PCM
      buff

    # Unsupported codec, return empty
    else
      dprint("UNKNOWN CODEC: #{self.codec.inspect}")
      ''
    end
  end

  def audio_packet_data(pkt)
    (pkt[0,1].unpack("C")[0] & 0x80 == 0) ? pkt[4,pkt.length-4] : pkt[12,pkt.length-12]
  end

end
end
end
end