346 lines
8.2 KiB
Ruby
346 lines
8.2 KiB
Ruby
# -*- 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( 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( IAX_SUBTYPE_REGAUTH, IAX_SUBTYPE_REGREJ )
|
|
return if not res
|
|
|
|
if res[1] == IAX_SUBTYPE_REGREJ
|
|
reason = res[2][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][IAX_IE_CHALLENGE_DATA]
|
|
self.dcall = res[0][0]
|
|
chall = res[2][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( IAX_SUBTYPE_REGACK, IAX_SUBTYPE_REGREJ )
|
|
return if not res
|
|
|
|
if res[1] == IAX_SUBTYPE_REGREJ
|
|
reason = res[2][IAX_IE_REGREJ_CAUSE] || "Unknown Reason"
|
|
dprint("REGREJ: #{reason}")
|
|
return
|
|
end
|
|
|
|
if res[2][IAX_IE_APPARENT_ADDR]
|
|
r_fam, r_port, r_addr = res[2][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(IAX_SUBTYPE_AUTHREQ, IAX_SUBTYPE_ACCEPT)
|
|
return if not res
|
|
|
|
# Handle authentication if its requested
|
|
if res[1] == 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][IAX_IE_CHALLENGE_DATA]
|
|
self.dcall = res[0][0]
|
|
chall = res[2][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( IAX_SUBTYPE_ACCEPT)
|
|
return if not res
|
|
end
|
|
|
|
self.codec = res[2][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 [IAX_TYPE_IAX, 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 IAX_TYPE_DTMF_BEGIN
|
|
self.dprint("DTMF BEG: #{pkt[11,1]}")
|
|
self.dtmf << pkt[11,1]
|
|
|
|
when IAX_TYPE_DTMF_END
|
|
self.dprint("DTMF END: #{pkt[11,1]}")
|
|
|
|
when IAX_TYPE_CONTROL
|
|
case stype
|
|
when IAX_CTRL_HANGUP
|
|
dprint("HANGUP")
|
|
self.client.send_ack(self)
|
|
self.state = :hangup
|
|
|
|
when IAX_CTRL_RINGING
|
|
dprint("RINGING")
|
|
self.client.send_ack(self)
|
|
|
|
when IAX_CTRL_BUSY
|
|
dprint("BUSY")
|
|
self.busy = true
|
|
self.state = :hangup
|
|
self.client.send_ack(self)
|
|
|
|
when 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 IAX_CTRL_PROGRESS
|
|
dprint("PROGRESS")
|
|
|
|
when IAX_CTRL_PROCEED
|
|
dprint("PROCEED")
|
|
|
|
when 255
|
|
dprint("STOP SOUNDS")
|
|
end
|
|
# Acknowledge all control packets
|
|
# self.client.send_ack(self)
|
|
|
|
when IAX_TYPE_IAX
|
|
|
|
dprint( ["RECV", phdr, stype, info].inspect )
|
|
case stype
|
|
when IAX_SUBTYPE_HANGUP
|
|
self.state = :hangup
|
|
self.client.send_ack(self)
|
|
when IAX_SUBTYPE_LAGRQ
|
|
# Lagrps echo the timestamp
|
|
self.client.send_lagrp(self, tstamp)
|
|
when IAX_SUBTYPE_ACK
|
|
# Nothing to do here
|
|
when IAX_SUBTYPE_PING
|
|
# Pongs echo the timestamp
|
|
self.client.send_pong(self, tstamp)
|
|
when IAX_SUBTYPE_PONG
|
|
self.client.send_ack(self)
|
|
else
|
|
dprint( ["RECV-QUEUE", phdr, stype, info].inspect )
|
|
self.queue.push( [phdr, stype, info ] )
|
|
end
|
|
|
|
when 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 IAX_CODEC_G711_MULAW
|
|
Rex::Proto::IAX2::Codecs::MuLaw.decode(buff)
|
|
|
|
# Convert a-law into signed PCM
|
|
when IAX_CODEC_G711_ALAW
|
|
Rex::Proto::IAX2::Codecs::ALaw.decode(buff)
|
|
|
|
# Linear little-endian signed PCM is our native format
|
|
when 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
|