lib/sippy_cup/scenario.rb in sippy_cup-0.2.3 vs lib/sippy_cup/scenario.rb in sippy_cup-0.3.0
- old
+ new
@@ -1,328 +1,558 @@
require 'nokogiri'
-require 'yaml'
+require 'psych'
+require 'active_support/core_ext/hash'
+require 'tempfile'
module SippyCup
+ #
+ # A representation of a SippyCup scenario from a manifest or created in code. Allows building a scenario from a set of basic primitives, and then exporting to SIPp scenario files, including the XML scenario and PCAP audio.
+ #
class Scenario
USER_AGENT = "SIPp/sippy_cup"
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
+ #
+ # Build a scenario based on either a manifest string or a file handle. Manifests are supplied in YAML format.
+ # All manifest keys can be overridden by passing in a Hash of corresponding values.
+ #
+ # @param [String, File] manifest The YAML manifest
+ # @param [Hash] options Options to override (see #initialize)
+ # @option options [String] :input_filename The name of the input file if there is one. Used as a preferable fallback if no name is included in the manifest.
+ #
+ # @return [SippyCup::Scenario]
+ #
+ # @example Parse a manifest string
+ # manifest = <<-MANIFEST
+ # source: 192.168.1.1
+ # destination: 192.168.1.2
+ # steps:
+ # - invite
+ # - wait_for_answer
+ # - ack_answer
+ # - sleep 3
+ # - wait_for_hangup
+ # MANIFEST
+ # Scenario.from_manifest(manifest)
+ #
+ # @example Parse a manifest file by path
+ # File.open("/my/manifest.yml") { |f| Scenario.from_manifest(f) }
+ # # or
+ # Scenario.from_manifest(File.read("/my/manifest.yml"))
+ #
+ # @example Override keys from the manifest
+ # Scenario.from_manifest(manifest, source: '192.168.12.1')
+ #
+ def self.from_manifest(manifest, options = {})
+ args = ActiveSupport::HashWithIndifferentAccess.new(Psych.safe_load(manifest)).merge options
+
+ input_name = options.has_key?(:input_filename) ? File.basename(options[:input_filename]).gsub(/\.ya?ml/, '') : nil
+ name = args.delete(:name) || input_name || 'My Scenario'
+
+ scenario = if args[:scenario]
+ media = args.has_key?(:media) ? File.read(args[:media], mode: 'rb') : nil
+ SippyCup::XMLScenario.new name, File.read(args[:scenario]), media, args
+ else
+ steps = args.delete :steps
+ scenario = Scenario.new name, args
+ scenario.build steps
+ scenario
end
+ scenario
+ end
+
+ # @return [Hash] The options the scenario was created with, either from a manifest or passed as overrides
+ attr_reader :scenario_options
+
+ # @return [Array<Hash>] a collection of errors encountered while building the scenario.
+ attr_reader :errors
+
+ #
+ # Create a scenario instance
+ #
+ # @param [String] name The scenario's name
+ # @param [Hash] args options to customise the scenario
+ # @option options [String] :name The name of the scenario, used for the XML scenario and for determining the compiled filenames. Defaults to 'My Scenario'.
+ # @option options [String] :filename The name of the files to be saved to disk.
+ # @option options [String] :source The source IP/hostname with which to invoke SIPp.
+ # @option options [String, Numeric] :source_port The source port to bind SIPp to (defaults to 8836).
+ # @option options [String] :destination The target system at which to direct traffic.
+ # @option options [String] :from_user The SIP user from which traffic should appear.
+ # @option options [Integer] :media_port The RTCP (media) port to bind to locally.
+ # @option options [String, Numeric] :max_concurrent The maximum number of concurrent calls to execute.
+ # @option options [String, Numeric] :number_of_calls The maximum number of calls to execute in the test run.
+ # @option options [String, Numeric] :calls_per_second The rate at which to initiate calls.
+ # @option options [String] :stats_file The path at which to dump statistics.
+ # @option options [String, Numeric] :stats_interval The interval (in seconds) at which to dump statistics (defaults to 1s).
+ # @option options [String] :transport_mode The transport mode over which to direct SIP traffic.
+ # @option options [String] :scenario_variables A path to a CSV file of variables to be interpolated with the scenario at runtime.
+ # @option options [Array<String>] :steps A collection of steps
+ #
+ # @yield [scenario] Builder block to construct scenario
+ # @yieldparam [Scenario] scenario the initialized scenario instance
+ #
+ def initialize(name, args = {}, &block)
parse_args args
- @rtcp_port = args[:rtcp_port]
+
+ @scenario_options = args.merge name: name
@filename = args[:filename] || name.downcase.gsub(/\W+/, '_')
- @filename = File.expand_path @filename
- @doc = builder.doc
+ @filename = File.expand_path @filename, Dir.pwd
@media = Media.new '127.0.0.255', 55555, '127.255.255.255', 5060
- @scenario_opts = get_scenario_opts args
- @scenario = @doc.xpath('//scenario').first
+ @errors = []
instance_eval &block if block_given?
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"
+ # @return [true, false] the validity of the scenario. Will be false if errors were encountered while building the scenario from a manifest
+ def valid?
+ @errors.size.zero?
end
- def get_scenario_opts(args)
- defaults = { source: "#{@from_addr}", destination: "#{@to_addr}",
- scenario: "#{@filename}.xml", max_concurrent: 10,
- calls_per_second: 5, number_of_calls: 20 }
-
- opts = args.select {|k,v| true unless [:source, :destination, :filename].include? k}
- defaults.merge! args
+ #
+ # Build the scenario steps provided
+ #
+ # @param [Array<String>] steps A collection of steps to build the scenario
+ #
+ def build(steps)
+ raise ArgumentError, "Must provide scenario steps" unless steps
+ steps.each_with_index do |step, index|
+ begin
+ instruction, arg = step.split ' ', 2
+ if arg && !arg.empty?
+ # Strip leading/trailing quotes if present
+ arg.gsub!(/^'|^"|'$|"$/, '')
+ self.__send__ instruction, arg
+ else
+ self.__send__ instruction
+ end
+ rescue => e
+ @errors << {step: index + 1, message: "#{step}: #{e.message}"}
+ end
+ end
end
- def compile_media
- @media.compile!
- end
-
- def sleep(seconds)
- seconds = seconds.to_i
- # TODO play silent audio files to the server to fill the gap
- pause seconds * MSEC
- @media << "silence:#{seconds * MSEC}"
- end
-
+ #
+ # Send an invite message
+ #
+ # @param [Hash] opts A set of options to modify the message
+ # @option opts [Integer] :retrans
+ # @option opts [String] :headers Extra headers to place into the INVITE
+ #
def invite(opts = {})
opts[:retrans] ||= 500
- rtp_string = @static_rtcp ? "m=audio #{@rtcp_port.to_i - 1} RTP/AVP 0 101\na=rtcp:#{@rtcp_port}\n" : "m=audio [media_port] RTP/AVP 0 101\n"
# FIXME: The DTMF mapping (101) is hard-coded. It would be better if we could
# get this from the DTMF payload generator
- msg = <<-INVITE
+ msg = <<-MSG
- INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
- Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
- From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
- To: <sip:[service]@[remote_ip]:[remote_port]>
- Call-ID: [call_id]
- CSeq: [cseq] INVITE
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
- Max-Forwards: 100
- User-Agent: #{USER_AGENT}
- 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
- #{rtp_string}
- a=rtpmap:0 PCMU/8000
- a=rtpmap:101 telephone-event/8000
- a=fmtp:101 0-15
- INVITE
- send = new_send msg, opts
- @scenario << send
+INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
+To: <sip:[service]@[remote_ip]:[remote_port]>
+Call-ID: [call_id]
+CSeq: [cseq] INVITE
+Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
+Max-Forwards: 100
+User-Agent: #{USER_AGENT}
+Content-Type: application/sdp
+Content-Length: [len]
+#{opts.has_key?(:headers) ? opts.delete(:headers).sub(/\n*\Z/, "\n") : ''}
+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 101
+a=rtpmap:0 PCMU/8000
+a=rtpmap:101 telephone-event/8000
+a=fmtp:101 0-15
+ MSG
+ send msg, opts
end
+ #
+ # Send a REGISTER message with the specified credentials
+ #
+ # @param [String] user the user to register as. May be given as a full SIP URI (sip:user@domain.com), in email-address format (user@domain.com) or as a simple username ('user'). If no domain is supplied, the source IP from SIPp will be used.
+ # @param [optional, String, nil] password the password to authenticate with.
+ # @param [Hash] opts A set of options to modify the message
+ #
+ # @example Register with authentication
+ # s.register 'frank@there.com', 'abc123'
+ #
+ # @example Register without authentication or a domain
+ # s.register 'frank'
+ #
def register(user, password = nil, opts = {})
opts[:retrans] ||= 500
user, domain = parse_user user
- msg = register_message user, domain: domain
- send = new_send msg, opts
- @scenario << send
- register_auth(user, password, domain: domain) if password
+ msg = if password
+ register_auth domain, user, password
+ else
+ register_message domain, user
+ end
+ send msg, opts
end
- def register_message(user, opts = {})
- <<-REGISTER
-
- REGISTER sip:#{opts[:domain]} SIP/2.0
- Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
- From: <sip:#{user}@#{opts[:domain]}>;tag=[call_number]
- To: <sip:#{user}@#{opts[:domain]}>
- Call-ID: [call_id]
- CSeq: [cseq] REGISTER
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
- Max-Forwards: 10
- Expires: 120
- User-Agent: #{USER_AGENT}
- Content-Length: 0
- REGISTER
- end
-
- def register_auth(user, password, opts = {})
- opts[:retrans] ||= 500
- @scenario << new_recv(response: '401', auth: true, optional: false)
- msg = <<-AUTH
-
- REGISTER sip:#{opts[:domain]} SIP/2.0
- Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
- From: <sip:#{user}@#{opts[:domain]}>;tag=[call_number]
- To: <sip:#{user}@#{opts[:domain]}>
- Call-ID: [call_id]
- CSeq: [cseq] REGISTER
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
- Max-Forwards: 20
- Expires: 3600
- [authentication username=#{user} password=#{password}]
- User-Agent: #{USER_AGENT}
- Content-Length: 0
- AUTH
- send = new_send msg, opts
- @scenario << send
- end
-
+ #
+ # Sets an expectation for a SIP 100 message from the remote party
+ #
+ # @param [Hash] opts A set of options to modify the expectation
+ # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
+ #
def receive_trying(opts = {})
- opts[:optional] = true if opts[:optional].nil?
- opts.merge! response: 100
- @scenario << new_recv(opts)
+ handle_response 100, opts
end
alias :receive_100 :receive_trying
+ #
+ # Sets an expectation for a SIP 180 message from the remote party
+ #
+ # @param [Hash] opts A set of options to modify the expectation
+ # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
+ #
def receive_ringing(opts = {})
- opts[:optional] = true if opts[:optional].nil?
- opts.merge! response: 180
- @scenario << new_recv(opts)
+ handle_response 180, opts
end
alias :receive_180 :receive_ringing
+ #
+ # Sets an expectation for a SIP 183 message from the remote party
+ #
+ # @param [Hash] opts A set of options to modify the expectation
+ # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
+ #
def receive_progress(opts = {})
- opts[:optional] = true if opts[:optional].nil?
- opts.merge! response: 183
- @scenario << new_recv(opts)
+ handle_response 183, opts
end
alias :receive_183 :receive_progress
+ #
+ # Sets an expectation for a SIP 200 message from the remote party
+ #
+ # @param [Hash] opts A set of options to modify the expectation
+ # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
+ #
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
- # Response Time Duration: Record the response time
- recv['rtd'] = true
- @scenario << recv
+ options = {
+ response: 200,
+ rrs: true, # Record Record Set: Make the Route headers available via [route] later
+ rtd: true # Response Time Duration: Record the response time
+ }
+
+ recv options.merge(opts)
end
alias :receive_200 :receive_answer
- ##
- # Shortcut method that tells SIPp optionally receive
- # SIP 100, 180, and 183 messages, and require a SIP 200 message.
+ #
+ # Shortcut that sets expectations for optional SIP 100, 180 and 183, followed by a required 200.
+ #
+ # @param [Hash] opts A set of options to modify the expectations
+ #
def wait_for_answer(opts = {})
receive_trying({optional: true}.merge opts)
receive_ringing({optional: true}.merge opts)
receive_progress({optional: true}.merge opts)
receive_answer opts
end
+ #
+ # Acknowledge a received answer message (SIP 200) and start media playback
+ #
+ # @param [Hash] opts A set of options to modify the message parameters
+ #
def ack_answer(opts = {})
- msg = <<-ACK
+ msg = <<-BODY
- ACK [next_url] SIP/2.0
- Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
- From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
- [last_To:]
- Call-ID: [call_id]
- CSeq: [cseq] ACK
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
- Max-Forwards: 100
- User-Agent: #{USER_AGENT}
- Content-Length: 0
- [routes]
- ACK
- @scenario << new_send(msg, opts)
+ACK [next_url] SIP/2.0
+Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
+[last_To:]
+Call-ID: [call_id]
+CSeq: [cseq] ACK
+Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
+Max-Forwards: 100
+User-Agent: #{USER_AGENT}
+Content-Length: 0
+[routes]
+ BODY
+ send msg, opts
start_media
end
- 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
+ #
+ # Insert a pause into the scenario and its media of the specified duration
+ #
+ # @param [Numeric] seconds The duration of the pause in seconds
+ #
+ def sleep(seconds)
+ milliseconds = (seconds.to_f * MSEC).to_i
+ pause milliseconds
+ @media << "silence:#{milliseconds}"
end
- ##
+ #
# 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
+ #
+ # @param [String] DTMF digits to send. Must be 0-9, *, # or A-D
+ #
+ # @example Send a single DTMF digit
+ # send_digits '1'
+ #
+ # @example Enter a pin number
+ # send_digits '1234'
+ #
+ def send_digits(digits)
+ delay = (0.250 * MSEC).to_i # 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.to_i}"
- pause delay * 2
+ @media << "silence:#{delay}"
end
+ pause delay * 2 * digits.size
end
+ #
+ # Send a BYE message
+ #
+ # @param [Hash] opts A set of options to modify the message parameters
+ #
def send_bye(opts = {})
msg = <<-MSG
- BYE [next_url] SIP/2.0
- [last_Via:]
- From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
- [last_To:]
- [last_Call-ID]
- CSeq: [cseq] BYE
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
- Max-Forwards: 100
- User-Agent: #{USER_AGENT}
- Content-Length: 0
- [routes]
+BYE [next_url] SIP/2.0
+[last_Via:]
+From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
+[last_To:]
+[last_Call-ID]
+CSeq: [cseq] BYE
+Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
+Max-Forwards: 100
+User-Agent: #{USER_AGENT}
+Content-Length: 0
+[routes]
MSG
- @scenario << new_send(msg, opts)
+ send msg, opts
end
- ##
- # Shortcut method that tells SIPp receive a BYE and acknowledge it
- def wait_for_hangup(opts = {})
- receive_bye(opts)
- ack_bye(opts)
- end
-
-
+ #
+ # Expect to receive a BYE message
+ #
+ # @param [Hash] opts A set of options to modify the expectation
+ #
def receive_bye(opts = {})
- opts.merge! request: 'BYE'
- @scenario << new_recv(opts)
+ recv opts.merge request: 'BYE'
end
+ #
+ # Acknowledge a received BYE message
+ #
+ # @param [Hash] opts A set of options to modify the message parameters
+ #
def ack_bye(opts = {})
msg = <<-ACK
- SIP/2.0 200 OK
- [last_Via:]
- [last_From:]
- [last_To:]
- [last_Call-ID:]
- [last_CSeq:]
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
- Max-Forwards: 100
- User-Agent: #{USER_AGENT}
- Content-Length: 0
- [routes]
+SIP/2.0 200 OK
+[last_Via:]
+[last_From:]
+[last_To:]
+[last_Call-ID:]
+[last_CSeq:]
+Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
+Max-Forwards: 100
+User-Agent: #{USER_AGENT}
+Content-Length: 0
+[routes]
ACK
- @scenario << new_send(msg, opts)
+ send msg, opts
end
+ #
+ # Shortcut to set an expectation for a BYE and acknowledge it when received
+ #
+ # @param [Hash] opts A set of options to modify the expectation
+ #
+ def wait_for_hangup(opts = {})
+ receive_bye(opts)
+ ack_bye(opts)
+ end
+
+ #
+ # Dump the scenario to a SIPp XML string
+ #
+ # @return [String] the SIPp XML scenario
def to_xml
- @doc.to_xml
+ doc.to_xml
end
+ #
+ # Compile the scenario and its media to disk
+ #
+ # Writes the SIPp scenario file to disk at {filename}.xml, and the PCAP media to {filename}.pcap.
+ # {filename} is taken from the :filename option when creating the scenario, or falls back to a down-snake-cased version of the scenario name.
+ #
+ # @return [String] the path to the resulting scenario file
+ #
+ # @example Export a scenario to a specified filename
+ # scenario = Scenario.new 'Test Scenario', filename: 'my_scenario'
+ # scenario.compile! # Leaves files at my_scenario.xml and my_scenario.pcap
+ #
+ # @example Export a scenario to a calculated filename
+ # scenario = Scenario.new 'Test Scenario'
+ # scenario.compile! # Leaves files at test_scenario.xml and test_scenario.pcap
+ #
def compile!
- print "Compiling media to #{@filename}.xml..."
- File.open "#{@filename}.xml", 'w' do |file|
- file.write @doc.to_xml
+ scenario_filename = "#{@filename}.xml"
+ print "Compiling scenario to #{scenario_filename}..."
+ File.open scenario_filename, 'w' do |file|
+ file.write doc.to_xml
end
puts "done."
- print "Compiling scenario to #{@filename}.pcap..."
+ print "Compiling media to #{@filename}.pcap..."
compile_media.to_file filename: "#{@filename}.pcap"
puts "done."
+
+ scenario_filename
end
+ #
+ # Write compiled Scenario XML and PCAP media to tempfiles.
+ #
+ # These will automatically be closed and deleted once they have gone out of scope, and can be used to execute the scenario without leaving stuff behind.
+ #
+ # @return [Hash<Symbol => Tempfile>] handles to created Tempfiles at :scenario and :media
+ #
+ # @see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/Tempfile.html
+ #
+ def to_tmpfiles
+ scenario_file = Tempfile.new 'scenario'
+ scenario_file.write to_xml
+ scenario_file.rewind
+
+ media_file = Tempfile.new 'media'
+ media_file.write compile_media.to_s
+ media_file.rewind
+
+ {scenario: scenario_file, media: media_file}
+ end
+
+ private
+
#TODO: SIPS support?
def parse_user(user)
user.slice! 0, 4 if user =~ /sip:/
user = user.split(":")[0]
user, domain = user.split("@")
domain ||= "[remote_ip]"
[user, domain]
end
- private
+ def doc
+ @doc ||= begin
+ Nokogiri::XML::Builder.new do |xml|
+ xml.scenario name: @scenario_options[:name]
+ end.doc
+ end
+ end
+
+ def scenario_node
+ @scenario_node = doc.xpath('//scenario').first
+ end
+
+ def parse_args(args)
+ raise ArgumentError, "Must include source IP:PORT" unless args.has_key? :source
+ raise ArgumentError, "Must include destination IP:PORT" unless args.has_key? :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 register_message(domain, user, opts = {})
+ <<-BODY
+
+REGISTER sip:#{domain} SIP/2.0
+Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+From: <sip:#{user}@#{domain}>;tag=[call_number]
+To: <sip:#{user}@#{domain}>
+Call-ID: [call_id]
+CSeq: [cseq] REGISTER
+Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
+Max-Forwards: 10
+Expires: 120
+User-Agent: #{USER_AGENT}
+Content-Length: 0
+ BODY
+ end
+
+ def register_auth(domain, user, password, opts = {})
+ recv response: '401', auth: true, optional: false
+ <<-AUTH
+
+REGISTER sip:#{domain} SIP/2.0
+Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+From: <sip:#{user}@#{domain}>;tag=[call_number]
+To: <sip:#{user}@#{domain}>
+Call-ID: [call_id]
+CSeq: [cseq] REGISTER
+Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
+Max-Forwards: 20
+Expires: 3600
+[authentication username=#{user} password=#{password}]
+User-Agent: #{USER_AGENT}
+Content-Length: 0
+ AUTH
+ end
+
+ 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_node << nop
+ end
+
def pause(msec)
- pause = Nokogiri::XML::Node.new 'pause', @doc
+ pause = Nokogiri::XML::Node.new 'pause', doc
pause['milliseconds'] = msec.to_i
- @scenario << pause
+ scenario_node << pause
end
- def new_send(msg, opts = {})
- send = Nokogiri::XML::Node.new 'send', @doc
+ def send(msg, opts = {})
+ send = Nokogiri::XML::Node.new 'send', doc
opts.each do |k,v|
send[k.to_s] = v
end
send << "\n"
- send << Nokogiri::XML::CDATA.new(@doc, msg)
+ send << Nokogiri::XML::CDATA.new(doc, msg)
send << "\n" #Newlines are required before and after CDATA so SIPp will parse properly
- send
+ scenario_node << send
end
- def new_recv(opts = {})
+ def 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.delete :request if opts.keys.include? :request
- recv['response'] = opts.delete :response if opts.keys.include? :response
- recv['optional'] = !!opts.delete(:optional)
+ recv = Nokogiri::XML::Node.new 'recv', doc
opts.each do |k,v|
recv[k.to_s] = v
end
- recv
+ scenario_node << recv
end
- end
-end
+ def optional_recv(opts)
+ opts[:optional] = true if opts[:optional].nil?
+ recv opts
+ end
+ def handle_response(code, opts)
+ optional_recv opts.merge(response: code)
+ end
+ end
+end