lib/sippy_cup/scenario.rb in sippy_cup-0.0.1 vs lib/sippy_cup/scenario.rb in sippy_cup-0.1.0
- old
+ new
@@ -1,100 +1,173 @@
require 'nokogiri'
module SippyCup
class Scenario
- def initialize(name, &block)
+ VALID_DTMF = %w{0 1 2 3 4 5 6 7 8 9 0 * # A B C D}.freeze
+ MSEC = 1_000
+
+ def initialize(name, args = {}, &block)
builder = Nokogiri::XML::Builder.new do |xml|
xml.scenario name: name
end
+ parse_args args
+
+ @filename = name.downcase.gsub(/\W+/, '_')
@doc = builder.doc
+ @media = Media.new @from_addr, @from_port, @to_addr, @to_port
@scenario = @doc.xpath('//scenario').first
instance_eval &block
end
+ def parse_args(args)
+ raise ArgumentError, "Must include source IP:PORT" unless args.keys.include? :source
+ raise ArgumentError, "Must include destination IP:PORT" unless args.keys.include? :destination
+
+ @from_addr, @from_port = args[:source].split ':'
+ @to_addr, @to_port = args[:destination].split ':'
+ @from_user = args[:from_user] || "sipp"
+ end
+
+ def compile_media
+ @media.compile!
+ end
+
def sleep(seconds)
# TODO play silent audio files to the server to fill the gap
- pause = Nokogiri::XML::Node.new 'pause', @doc
- pause['milliseconds'] = seconds
- @scenario.add_child pause
+ pause seconds * MSEC
+ @media << "silence:#{seconds * MSEC}"
end
- def invite
+ def invite(opts = {})
+ opts[:retrans] ||= 500
+ # FIXME: The DTMF mapping (101) is hard-coded. It would be better if we could
+ # get this from the DTMF payload generator
msg = <<-INVITE
+
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
- From: sipp <sip:[field0]@[local_ip]>;tag=[call_number]
+ From: sipp <sip:#{@from_user}@[local_ip]>;tag=[call_number]
To: <sip:[service]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: [cseq] INVITE
- Contact: sip:[field0]@[local_ip]:[local_port]
+ Contact: sip:#{@from_user}@[local_ip]:[local_port]
Max-Forwards: 100
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
- m=audio [media_port] RTP/AVP 0
+ m=audio [media_port] RTP/AVP 0 101
a=rtpmap:0 PCMU/8000
+ a=rtpmap:101 telephone-event/8000
+ a=fmtp:101 0-15
INVITE
- send = new_send msg
- # FIXME: Does this need to be configurable?
- send['retrans'] = 500
-
+ send = new_send msg, opts
@scenario << send
end
- def receive_trying(optional = true)
- @scenario.add_child new_recv response: 100, optional: optional
+ def receive_trying(opts = {})
+ opts[:optional] = true if opts[:optional].nil?
+ opts.merge! response: 100
+ @scenario << new_recv(opts)
end
alias :receive_100 :receive_trying
- def receive_ringing(optional = true)
- @scenario.add_child new_recv response: 180, optional: optional
+ def receive_ringing(opts = {})
+ opts[:optional] = true if opts[:optional].nil?
+ opts.merge! response: 180
+ @scenario << new_recv(opts)
end
alias :receive_180 :receive_ringing
- def receive_progress(optional = true)
- @scenario.add_child new_recv response: 183, optional: optional
+ def receive_progress(opts = {})
+ opts[:optional] = true if opts[:optional].nil?
+ opts.merge! response: 183
+ @scenario << new_recv(opts)
end
alias :receive_183 :receive_progress
- def receive_answer
- recv = new_recv response: 200, optional: false
+ def receive_answer(opts = {})
+ opts.merge! response: 200
+ recv = new_recv opts
# Record Record Set: Make the Route headers available via [route] later
recv['rrs'] = true
- @scenario.add_child recv
+ @scenario << recv
end
alias :receive_200 :receive_answer
- def ack_answer
+ def ack_answer(opts = {})
msg = <<-ACK
+
ACK [next_url] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
- From: <sip:[field0]@[local_ip]>;tag=[call_number]
+ From: <sip:#{@from_user}@[local_ip]>;tag=[call_number]
[last_To:]
[routes]
Call-ID: [call_id]
CSeq: [cseq] ACK
- Contact: sip:[field0]@[local_ip]:[local_port]
+ Contact: sip:#{@from_user}@[local_ip]:[local_port]
Max-Forwards: 100
Content-Length: 0
ACK
- @scenario << new_send(msg)
+ @scenario << new_send(msg, opts)
+ start_media
end
- def receive_bye
- @scenario.add_child new_recv response: 'BYE'
+ def start_media
+ nop = Nokogiri::XML::Node.new 'nop', @doc
+ action = Nokogiri::XML::Node.new 'action', @doc
+ nop << action
+ exec = Nokogiri::XML::Node.new 'exec', @doc
+ exec['play_pcap_audio'] = "#{@filename}.pcap"
+ action << exec
+ @scenario << nop
end
- def ack_bye
+ ##
+ # Send DTMF digits
+ # @param[String] DTMF digits to send. Must be 0-9, *, # or A-D
+ def send_digits(digits, delay = 0.250)
+ delay = 0.250 * MSEC # FIXME: Need to pass this down to the media layer
+ digits.split('').each do |digit|
+ raise ArgumentError, "Invalid DTMF digit requested: #{digit}" unless VALID_DTMF.include? digit
+
+ @media << "dtmf:#{digit}"
+ @media << "silence:#{delay}"
+ pause delay * 2
+ end
+ end
+
+ def send_bye(opts = {})
+ msg = <<-MSG
+
+ BYE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+ [last_Via:]
+ [last_From:]
+ [last_To:]
+ [last_Call-ID]
+ CSeq: [cseq] BYE
+ Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+ Max-Forwards: 100
+ Content-Length: 0
+ MSG
+ @scenario << new_send(msg, opts)
+ end
+
+ def receive_bye(opts = {})
+ opts.merge! request: 'BYE'
+ @scenario << new_recv(opts)
+ end
+
+ def ack_bye(opts = {})
msg = <<-ACK
+
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[routes]
@@ -102,30 +175,50 @@
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Max-Forwards: 100
Content-Length: 0
ACK
- @scenario << new_send(msg)
+ @scenario << new_send(msg, opts)
end
def to_xml
@doc.to_xml
end
+ def compile!
+ xml_file = File.open "#{@filename}.xml", 'w' do |file|
+ file.write @doc.to_xml
+ end
+ compile_media.to_file filename: "#{@filename}.pcap"
+ end
+
private
+ def pause(msec)
+ pause = Nokogiri::XML::Node.new 'pause', @doc
+ pause['milliseconds'] = msec.to_i
+ @scenario << pause
+ end
- def new_send(msg)
+ def new_send(msg, opts = {})
send = Nokogiri::XML::Node.new 'send', @doc
- send << Nokogiri::XML::Text.new(msg, @doc)
+ opts.each do |k,v|
+ send[k.to_s] = v
+ end
+ send << "\n"
+ send << Nokogiri::XML::CDATA.new(@doc, msg)
+ send << "\n" #Newlines are required before and after CDATA so SIPp will parse properly
send
end
def new_recv(opts = {})
raise ArgumentError, "Receive must include either a response or a request" unless opts.keys.include?(:response) || opts.keys.include?(:request)
recv = Nokogiri::XML::Node.new 'recv', @doc
- recv['request'] = opts[:request] if opts.keys.include? :request
- recv['response'] = opts[:response] if opts.keys.include? :response
- recv['optional'] = !!opts[:optional]
+ recv['request'] = opts.delete :request if opts.keys.include? :request
+ recv['response'] = opts.delete :response if opts.keys.include? :response
+ recv['optional'] = !!opts.delete(:optional)
+ opts.each do |k,v|
+ recv[k.to_s] = v
+ end
recv
end
end
end