sippy_cup 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a8574849c4b88b7655ae212461569d8578ce21cb
4
- data.tar.gz: 29d281da44037bff5259cd498bf86893882b41c3
3
+ metadata.gz: 6dd7d94d947152635902fedf054429f24ffa2de8
4
+ data.tar.gz: 621cb762e6bbef801190e0aeb855c82eeb9ed064
5
5
  SHA512:
6
- metadata.gz: cd4c8d367d9d931018ce1a09870c0dc1c666fb692e38c51489b2c4ab2d5a63c23b468a0c5e42fe8b3b0c8826c3a6e4a2e0ba1872366c23da2ed383eefd71cc12
7
- data.tar.gz: 0c0570408e0370593818fe55eb8756c7edbba3c88289e54208e3bb417425f304d4495f8d7b3ef513c484c34ed6500796091c8773808064a56ae4d4fdee983ba1
6
+ metadata.gz: 2423aa6cff4c235c5416b3f3fdbea27a973a79eff72fee91a56862386827eaa0a3d04bc260515d4031331623abbc2da0271589d7cb505380eb89c499434d2f34
7
+ data.tar.gz: a45bac4f0fbb3a98c5e40dcc8aa4761a49bf6b4d8f3452f8fed86f141456cc200148ab2c2739681a81abe53e07510259837f5f9a3dd23f5dd7fd74b15f061543
data/.gitignore CHANGED
@@ -5,3 +5,6 @@
5
5
  Gemfile.lock
6
6
  .ruby-gemset
7
7
  tmp/
8
+ /examples/*.xml
9
+ /examples/*.pcap
10
+ /examples/*.log
data/.travis.yml CHANGED
@@ -8,8 +8,6 @@ rvm:
8
8
  - ruby-head
9
9
  matrix:
10
10
  allow_failures:
11
- - rvm: 2.0.0
12
- - rvm: 2.1.0
13
11
  - rvm: jruby-19mode
14
12
  - rvm: rbx-19mode
15
13
  - rvm: ruby-head
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # develop
2
+
3
+ # [0.5.0](https://github.com/mojolingo/sippy_cup/compare/v0.4.1...v0.5.0)
4
+ SYNTAX CHANGES!
5
+ This is a backward incompatible change. If upgrading from Sippy Cup 0.4.x please see the [documentation](http://mojolingo.github.io/sippy_cup/#available-scenario-steps) and the `Change` items below.
6
+ You will also need to compile the latest [SIPp from Github](https://github.com/sipp/sipp) to make use of all the features.
7
+ * Feature: Add support for saving screen and error reports to specified files
8
+ * Feature: Add support for UAS actions (waiting for an incoming call)
9
+ * Feature: Permit supplying a SIP advertise address that is different from the bind IP for NAT traversal purposes
10
+ * Feature: Add support for CallLengthRepartition and ResponseTimeRepartition tables
11
+ * Bugfix: Much improved support for sending a hangup from SIPp, rather than waiting for the far end to do it
12
+ * Bugfix: If scenario compilation fails on the CLI, explain why
13
+ * Change: Rework the `register` command so it works without any other expectations
14
+ * Change: `wait_for_answer` now includes `ack_answer`
15
+ * Documentation: Create `examples/` directory with example scenarios
16
+
1
17
  # [0.4.1](https://github.com/bklang/sippy_cup/compare/v0.4.0...v0.4.1)
2
18
  * Bugfix: Fix some Ruby 2 string encoding problems
3
19
  * Bugfix: Fix backward alias of `respond_ok` with `respond_200`
data/Guardfile CHANGED
@@ -1,4 +1,4 @@
1
- guard 'rspec', :cli => '--format documentation', :all_on_start => true, :all_after_pass => true do
1
+ guard 'rspec', cmd: 'bundle exec rspec --format documentation', :all_on_start => true, :all_after_pass => true do
2
2
  watch(%r{^spec/.+_spec\.rb$})
3
3
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
4
  watch('spec/spec_helper.rb') { "spec/" }
data/README.markdown CHANGED
@@ -105,6 +105,8 @@ Password:
105
105
  I, [2013-09-30T14:48:16.728712 #9883] INFO -- : Test completed successfully.
106
106
  ```
107
107
 
108
+ More examples are [available in the source repository](https://github.com/mojolingo/sippy_cup/tree/develop/examples).
109
+
108
110
  ### Example embedding SIPp in another Ruby process
109
111
 
110
112
  ```Ruby
@@ -135,17 +137,23 @@ The above code can be executed as a standalone Ruby script and the resulting sce
135
137
 
136
138
  ### Available Scenario Steps
137
139
 
138
- Each command below can take [SIPp attributes](http://sipp.sourceforge.net/doc/reference.html) as optional arguments. For a full list of available steps, see the [API documentation](http://rubydoc.info/gems/sippy_cup/SippyCup/Scenario).
140
+ Each command below can take [SIPp attributes](http://sipp.sourceforge.net/doc/reference.html) as optional arguments. For a full list of available steps with arguments explained, see the [API documentation](http://rubydoc.info/gems/sippy_cup/SippyCup/Scenario).
139
141
 
140
142
  * `sleep <seconds>` Wait a specified number of seconds
141
143
  * `invite` Send a SIP INVITE to the specified target
144
+ * `receive_invite` Wait for an INVITE to be received
142
145
  * `register <username> [password]` Register the specified user to the target with an optional password
146
+ * `send_trying` Send a `100 Trying` provisional response
143
147
  * `receive_trying` Expect to receive a `100 Trying` response from the target
148
+ * `send_ringing` Send a `180 Ringing` provisional response
144
149
  * `receive_ringing` Expect to receive a `180 Ringing` response from the target
145
150
  * `receive_progress` Expect to receive a `183 Progress` response from the target
151
+ * `send_answer` Send a `200 Ok` response to an INVITE (answer the call)
146
152
  * `receive_answer` Expect to receive a `200 OK` (answering the call) response from the target
153
+ * `answer` Convenient shortcut for `send_answer; receive_ack`
147
154
  * `wait_for_answer` Convenient shortcut for `receive_trying; receive_ringing; receive_progress; receive_answer`, with all but the `answer` marked as optional
148
155
  * `ack_answer` Send an `ACK` in response to a `200 OK`
156
+ * `receive_ack` Expect to receive an `ACK`
149
157
  * `send_digits <string>` Send a DTMF string. May send one or many digits, including `0-9`, `*`, `#`, and `A-D`
150
158
  * `receive_ok` Expect to receive a `200 OK`
151
159
  * `receive_message [regex]` Expect to receive a SIP MESSAGE, optionally matching a regex
@@ -153,6 +161,9 @@ Each command below can take [SIPp attributes](http://sipp.sourceforge.net/doc/re
153
161
  * `receive_bye` Expect to receive a `BYE` from the target
154
162
  * `ack_bye` Send a `200 OK` response to a `BYE`
155
163
  * `wait_for_hangup` Convenient shortcut for `receive_bye; ack_bye`
164
+ * `hangup` Convenient shortcut for `send_bye; receive_ok`
165
+ * `call_length_repartition` Creates a histogram table of individual call lengths in milliseconds between min length and max length, at the specified interval
166
+ * `response_time_repartition` Creates a histogram table of individual SIP request response times in milliseconds between min length and max length, at the specified interval
156
167
 
157
168
  ### Alternate Output File Path
158
169
 
@@ -179,6 +190,7 @@ This will create the files `somewhere.xml` and `somewhere.pcap` in the `/path/to
179
190
  ### Customizing the Test Run
180
191
 
181
192
  Each parameter has an impact on the test, and may either be changed once the XML file is generated or specified in the options hash for `SippyCup::Scenario.new`. In addition to the default parameters, some additional parameters can be set:
193
+
182
194
  <dl>
183
195
  <dt>stats_file</dt>
184
196
  <dd>Path to a file where call statistics will be stored in a CSV format, defaults to not storing stats</dd>
@@ -198,6 +210,12 @@ Each parameter has an impact on the test, and may either be changed once the XML
198
210
  <dt>full_sipp_output</dt>
199
211
  <dd>By default, SippyCup will show SIPp's command line output while running a scenario. Set this parameter to `false` to hide full command line output</dd>
200
212
 
213
+ <dt>summary_report_file</dt>
214
+ <dd>Write a summary of the SIPp run to the specified file. This summary is the output from the SIPp `-trace_screen` command. Requires a development build of SIPp; see https://github.com/SIPp/sipp/pull/106.</dd>
215
+
216
+ <dt>errors_report_file</dt>
217
+ <dd>Record SIPp's errors to the specified file. This report is the output from the SIPp `-trace_err` command.</dd>
218
+
201
219
  <dt>options</dt>
202
220
  <dd>A string of SIPp command line options included with the SIPp run.</dd>
203
221
 
@@ -205,10 +223,25 @@ Each parameter has an impact on the test, and may either be changed once the XML
205
223
  <dd>By default, SIPp assigns RTP ports dynamically. However, if there is a need for a static RTP port (say, for data collection purposes), it can be done by supplying a port number here.</dd>
206
224
 
207
225
  <dt>dtmf_mode</dt>
208
- <dd>Specify the mechanism by which DTMF is signaled. Valid options are `rfc2833` for within the RTP media, or `info` for SIP INFO.</dd>
226
+ <dd>Specify the mechanism by which DTMF is signaled. Valid options are `rfc2833` for within the RTP media, or `info` for SIP INFO. Default: rfc2833</dd>
209
227
 
210
228
  <dt>scenario_variables</dt>
211
229
  <dd>If you're using sippy_cup to run a SIPp XML file, there may be CSV fields in the scenario ([field0], [field1], etc.). Specify a path to a CSV file containing the required information using this option. (File is semicolon delimeted, information can be found [here](http://sipp.sourceforge.net/doc/reference.html#inffile).)</dd>
230
+
231
+ <dt>concurrent_max</dt>
232
+ <dd>The maximum number of calls permitted to be active at any given time. When this limit is reached, SIPp will slow down or stop sending new calls until there it falls below the limit. Default: 5</dd>
233
+
234
+ <dt>calls_per_second</dt>
235
+ <dd>The rate at which new calls should be created. Note that SIPp will automatically adjust this downward to stay at or beneath the maximum number of concurrent calls (`concurrent_max`). Default: 10</dt>
236
+
237
+ <dt>calls_per_second_incr</dt>
238
+ <dd>When used with `calls_per_second_max`, tells SIPp the amount by which calls-per-second should be incremented. CPS rate is adjusted each `stats_interval`. Default: 1.</dd>
239
+
240
+ <dt>calls_per_second_max</dt>
241
+ <dd>The maximum rate of calls-per-second. If unset, the CPS rate will remain at the level set by `calls_per_second`.</dd>
242
+
243
+ <dt>advertise_address</dt>
244
+ <dd>The IP address to advertise in SIP and SDP if different from the bind IP (defaults to the bind IP).</dd>
212
245
  </dl>
213
246
 
214
247
  ### Additional SIPp Scenario Attributes
data/bin/sippy_cup CHANGED
@@ -60,7 +60,15 @@ unless compile || run
60
60
  end
61
61
 
62
62
  scenario = SippyCup::Scenario.from_manifest File.read(manifest_path), input_filename: manifest_path
63
- scenario.compile! if compile
63
+ if scenario.valid?
64
+ scenario.compile! if compile
65
+ else
66
+ $stderr.puts "Errors encountered while building the scenario!"
67
+ puts "Step\tError Message"
68
+ scenario.errors.each do |error|
69
+ puts "#{error[:step]}\t#{error[:message]}"
70
+ end
71
+ end
64
72
 
65
73
  if run
66
74
  runner = SippyCup::Runner.new scenario
@@ -0,0 +1,14 @@
1
+ source: 192.0.2.15
2
+ destination: 192.0.2.200
3
+ max_concurrent: 10
4
+ calls_per_second: 5
5
+ number_of_calls: 20
6
+ steps:
7
+ - invite
8
+ - wait_for_answer
9
+ - ack_answer
10
+ - sleep 3
11
+ - send_digits '3125551234'
12
+ - sleep 5
13
+ - send_digits '#'
14
+ - wait_for_hangup
@@ -0,0 +1,11 @@
1
+ source: 192.0.2.15
2
+ destination: 192.0.2.200
3
+ max_concurrent: 10
4
+ calls_per_second: 5
5
+ number_of_calls: 20
6
+ steps:
7
+ - invite
8
+ - wait_for_answer
9
+ - ack_answer
10
+ - sleep 5
11
+ - hangup
@@ -0,0 +1,15 @@
1
+ ---
2
+ source: 10.3.18.108
3
+ destination: 10.3.18.134
4
+ max_concurrent: 1
5
+ calls_per_second: 1
6
+ number_of_calls: 1
7
+ steps:
8
+ - wait_for_call
9
+ - send_trying
10
+ - sleep 1
11
+ - send_ringing
12
+ - sleep 3
13
+ - answer
14
+ - sleep 5
15
+ - hangup
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  require 'logger'
3
+ require 'fileutils'
3
4
 
4
5
  #
5
6
  # Service object to oversee the execution of a Scenario
@@ -67,7 +68,8 @@ module SippyCup
67
68
  #
68
69
  def wait
69
70
  exit_status = Process.wait2 @sipp_pid.to_i
70
- @rd.close if @rd
71
+ @err_rd.close if @err_rd
72
+ @stdout_rd.close if @stdout_rd
71
73
  final_result = process_exit_status exit_status, @stderr_buffer
72
74
  if final_result
73
75
  @logger.info "Test completed successfully!"
@@ -95,23 +97,39 @@ module SippyCup
95
97
 
96
98
  def command_options
97
99
  options = {
98
- i: @scenario_options[:source],
99
100
  p: @scenario_options[:source_port] || '8836',
100
101
  sf: @input_files[:scenario].path,
101
- l: @scenario_options[:max_concurrent] || 5,
102
+ l: @scenario_options[:concurrent_max] || @scenario_options[:max_concurrent] || 5,
102
103
  m: @scenario_options[:number_of_calls] || 10,
103
104
  r: @scenario_options[:calls_per_second] || 10,
104
105
  s: @scenario_options[:to_user] || '1'
105
106
  }
106
107
 
108
+ options[:i] = @scenario_options[:source] if @scenario_options[:source]
107
109
  options[:mp] = @scenario_options[:media_port] if @scenario_options[:media_port]
108
110
 
111
+ if @scenario_options[:calls_per_second_max]
112
+ options[:no_rate_quit] = nil
113
+ options[:rate_max] = @scenario_options[:calls_per_second_max]
114
+ options[:rate_increase] = @scenario_options[:calls_per_second_incr] || 1
115
+ end
116
+
109
117
  if @scenario_options[:stats_file]
110
118
  options[:trace_stat] = nil
111
119
  options[:stf] = @scenario_options[:stats_file]
112
120
  options[:fd] = @scenario_options[:stats_interval] || 1
113
121
  end
114
122
 
123
+ if @scenario_options[:summary_report_file]
124
+ options[:trace_screen] = nil
125
+ options[:screen_file] = @scenario_options[:summary_report_file]
126
+ end
127
+
128
+ if @scenario_options[:errors_report_file]
129
+ options[:trace_err] = nil
130
+ options[:error_file] = @scenario_options[:errors_report_file]
131
+ end
132
+
115
133
  if @scenario_options[:transport_mode]
116
134
  options[:t] = @scenario_options[:transport_mode]
117
135
  end
@@ -126,21 +144,39 @@ module SippyCup
126
144
  end
127
145
 
128
146
  def execute_with_redirected_streams
129
- @rd, wr = IO.pipe
130
- stdout_target = @options[:full_sipp_output] ? $stdout : '/dev/null'
147
+ @err_rd, err_wr = IO.pipe
148
+ stdout_target = if @options[:full_sipp_output]
149
+ @stdout_rd, stdout_wr = IO.pipe
150
+ stdout_wr
151
+ else
152
+ '/dev/null'
153
+ end
131
154
 
132
- @sipp_pid = spawn command, err: wr, out: stdout_target
155
+ @sipp_pid = spawn command, err: err_wr, out: stdout_target
133
156
 
134
157
  @stderr_buffer = String.new
135
158
 
136
159
  Thread.new do
137
- wr.close
138
- until @rd.eof?
139
- buffer = @rd.readpartial(1024).strip
160
+ err_wr.close
161
+ until @err_rd.eof?
162
+ buffer = @err_rd.readpartial(1024).strip
140
163
  @stderr_buffer += buffer
141
164
  $stderr << buffer if @options[:full_sipp_output]
142
165
  end
143
166
  end
167
+
168
+ if @stdout_rd
169
+ @stdout_buffer = String.new
170
+
171
+ Thread.new do
172
+ stdout_wr.close
173
+ until @stdout_rd.eof?
174
+ buffer = @stdout_rd.readpartial(1024).strip
175
+ @stdout_buffer += buffer
176
+ $stdout << buffer
177
+ end
178
+ end
179
+ end
144
180
  end
145
181
 
146
182
  def process_exit_status(process_status, error_message = nil)
@@ -167,7 +203,7 @@ module SippyCup
167
203
  @input_files.values.compact.each do |value|
168
204
  value.close
169
205
  value.unlink
170
- end
206
+ end if @input_files
171
207
  end
172
208
  end
173
209
 
@@ -3,6 +3,7 @@ require 'nokogiri'
3
3
  require 'psych'
4
4
  require 'active_support/core_ext/hash'
5
5
  require 'tempfile'
6
+ require 'set'
6
7
 
7
8
  module SippyCup
8
9
  #
@@ -12,6 +13,7 @@ module SippyCup
12
13
  USER_AGENT = "SIPp/sippy_cup"
13
14
  VALID_DTMF = %w{0 1 2 3 4 5 6 7 8 9 0 * # A B C D}.freeze
14
15
  MSEC = 1_000
16
+ DEFAULT_RETRANS = 500
15
17
 
16
18
  #
17
19
  # Build a scenario based on either a manifest string or a file handle. Manifests are supplied in YAML format.
@@ -79,6 +81,7 @@ module SippyCup
79
81
  # @option options [String] :source The source IP/hostname with which to invoke SIPp.
80
82
  # @option options [String, Numeric] :source_port The source port to bind SIPp to (defaults to 8836).
81
83
  # @option options [String] :destination The target system at which to direct traffic.
84
+ # @option options [String] :advertise_address The IP address to advertise in SIP and SDP if different from the bind IP (defaults to the bind IP).
82
85
  # @option options [String] :from_user The SIP user from which traffic should appear.
83
86
  # @option options [String] :to_user The SIP user to send requests to.
84
87
  # @option options [Integer] :media_port The RTCP (media) port to bind to locally.
@@ -102,10 +105,13 @@ module SippyCup
102
105
  @scenario_options = args.merge name: name
103
106
  @filename = args[:filename] || name.downcase.gsub(/\W+/, '_')
104
107
  @filename = File.expand_path @filename, Dir.pwd
105
- @media = Media.new '127.0.0.255', 55555, '127.255.255.255', 5060
108
+ @media = nil
106
109
  @message_variables = 0
110
+ # Reference variables don't generate warnings/errors if unused in the scenario
111
+ @reference_variables = Set.new
107
112
  @media_nodes = []
108
113
  @errors = []
114
+ @adv_ip = args[:advertise_address] || "[local_ip]"
109
115
 
110
116
  instance_eval &block if block_given?
111
117
  end
@@ -124,11 +130,10 @@ module SippyCup
124
130
  raise ArgumentError, "Must provide scenario steps" unless steps
125
131
  steps.each_with_index do |step, index|
126
132
  begin
127
- instruction, arg = step.split ' ', 2
128
- if arg && !arg.empty?
129
- # Strip leading/trailing quotes if present
130
- arg.gsub!(/^'|^"|'$|"$/, '')
131
- self.__send__ instruction, arg
133
+ instruction, args = step.split ' ', 2
134
+ args = split_quoted_string args
135
+ if args && !args.empty?
136
+ self.__send__ instruction, *args
132
137
  else
133
138
  self.__send__ instruction
134
139
  end
@@ -149,22 +154,24 @@ module SippyCup
149
154
  opts[:retrans] ||= 500
150
155
  # FIXME: The DTMF mapping (101) is hard-coded. It would be better if we could
151
156
  # get this from the DTMF payload generator
157
+ from_addr = "#{@from_user}@#{@adv_ip}:[local_port]"
158
+ to_addr = "[service]@[remote_ip]:[remote_port]"
152
159
  msg = <<-MSG
153
160
 
154
- INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
155
- Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
156
- From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
157
- To: <sip:[service]@[remote_ip]:[remote_port]>
161
+ INVITE sip:#{to_addr} SIP/2.0
162
+ Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
163
+ From: "#{@from_user}" <sip:#{from_addr}>;tag=[call_number]
164
+ To: <sip:#{to_addr}>
158
165
  Call-ID: [call_id]
159
166
  CSeq: [cseq] INVITE
160
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
167
+ Contact: <sip:#{from_addr};transport=[transport]>
161
168
  Max-Forwards: 100
162
169
  User-Agent: #{USER_AGENT}
163
170
  Content-Type: application/sdp
164
171
  Content-Length: [len]
165
172
  #{opts.has_key?(:headers) ? opts.delete(:headers).sub(/\n*\Z/, "\n") : ''}
166
173
  v=0
167
- o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
174
+ o=user1 53655765 2353687637 IN IP[local_ip_type] #{@adv_ip}
168
175
  s=-
169
176
  c=IN IP[media_ip_type] [media_ip]
170
177
  t=0 0
@@ -173,7 +180,24 @@ a=rtpmap:0 PCMU/8000
173
180
  a=rtpmap:101 telephone-event/8000
174
181
  a=fmtp:101 0-15
175
182
  MSG
176
- send msg, opts
183
+ send msg, opts do |send|
184
+ send << doc.create_element('action') do |action|
185
+ action << doc.create_element('assignstr') do |assignstr|
186
+ assignstr['assign_to'] = "remote_addr"
187
+ assignstr['value'] = to_addr
188
+ end
189
+ action << doc.create_element('assignstr') do |assignstr|
190
+ assignstr['assign_to'] = "local_addr"
191
+ assignstr['value'] = from_addr
192
+ end
193
+ action << doc.create_element('assignstr') do |assignstr|
194
+ assignstr['assign_to'] = "call_addr"
195
+ assignstr['value'] = to_addr
196
+ end
197
+ end
198
+ end
199
+ # These variables will only be used if we initiate a hangup
200
+ @reference_variables += %w(remote_addr local_addr call_addr)
177
201
  end
178
202
 
179
203
  #
@@ -190,16 +214,142 @@ a=fmtp:101 0-15
190
214
  # s.register 'frank'
191
215
  #
192
216
  def register(user, password = nil, opts = {})
193
- opts[:retrans] ||= 500
217
+ send_opts = opts.dup
218
+ send_opts[:retrans] ||= DEFAULT_RETRANS
194
219
  user, domain = parse_user user
195
- msg = if password
196
- register_auth domain, user, password
220
+ if password
221
+ send register_message(domain, user), send_opts
222
+ recv opts.merge(response: 401, auth: true, optional: false)
223
+ send register_auth(domain, user, password), send_opts
224
+ receive_ok opts.merge(optional: false)
197
225
  else
198
- register_message domain, user
226
+ send register_message(domain, user), send_opts
227
+ end
228
+ end
229
+
230
+ #
231
+ # Expect to receive a SIP INVITE
232
+ #
233
+ # @param [Hash] opts A set of options containing SIPp <recv> element attributes
234
+ #
235
+ def receive_invite(opts = {})
236
+ recv(opts.merge(request: 'INVITE', rrs: true)) do |recv|
237
+ action = doc.create_element('action') do |action|
238
+ action << doc.create_element('ereg') do |ereg|
239
+ ereg['regexp'] = '<sip:(.*)>.*;tag=([^;]*)'
240
+ ereg['search_in'] = 'hdr'
241
+ ereg['header'] = 'From:'
242
+ ereg['assign_to'] = 'dummy,remote_addr,remote_tag'
243
+ end
244
+ action << doc.create_element('ereg') do |ereg|
245
+ ereg['regexp'] = '<sip:(.*)>'
246
+ ereg['search_in'] = 'hdr'
247
+ ereg['header'] = 'To:'
248
+ ereg['assign_to'] = 'dummy,local_addr'
249
+ end
250
+ action << doc.create_element('assignstr') do |assignstr|
251
+ assignstr['assign_to'] = "call_addr"
252
+ assignstr['value'] = "[$local_addr]"
253
+ end
254
+ end
255
+ recv << action
199
256
  end
257
+ # These variables (except dummy) will only be used if we initiate a hangup
258
+ @reference_variables += %w(dummy remote_addr remote_tag local_addr call_addr)
259
+ end
260
+ alias :wait_for_call :receive_invite
261
+
262
+ #
263
+ # Send a "100 Trying" response
264
+ #
265
+ # @param [Hash] opts A set of options containing SIPp <recv> element attributes
266
+ #
267
+ def send_trying(opts = {})
268
+ msg = <<-MSG
269
+
270
+ SIP/2.0 100 Trying
271
+ [last_Via:]
272
+ From: <sip:[$remote_addr]>;tag=[$remote_tag]
273
+ To: <sip:[$local_addr]>;tag=[call_number]
274
+ [last_Call-ID:]
275
+ [last_CSeq:]
276
+ Server: #{USER_AGENT}
277
+ Contact: <sip:[$local_addr];transport=[transport]>
278
+ Content-Length: 0
279
+ MSG
280
+ send msg, opts
281
+ end
282
+ alias :send_100 :send_trying
283
+
284
+ #
285
+ # Send a "180 Ringing" response
286
+ #
287
+ # @param [Hash] opts A set of options containing SIPp <recv> element attributes
288
+ #
289
+ def send_ringing(opts = {})
290
+ msg = <<-MSG
291
+
292
+ SIP/2.0 180 Ringing
293
+ [last_Via:]
294
+ From: <sip:[$remote_addr]>;tag=[$remote_tag]
295
+ To: <sip:[$local_addr]>;tag=[call_number]
296
+ [last_Call-ID:]
297
+ [last_CSeq:]
298
+ Server: #{USER_AGENT}
299
+ Contact: <sip:[$local_addr];transport=[transport]>
300
+ Content-Length: 0
301
+ MSG
302
+ send msg, opts
303
+ end
304
+ alias :send_180 :send_ringing
305
+
306
+ #
307
+ # Answer an incoming call
308
+ #
309
+ # @param [Hash] opts A set of options containing SIPp <send> element attributes
310
+ #
311
+ def send_answer(opts = {})
312
+ opts[:retrans] ||= DEFAULT_RETRANS
313
+ msg = <<-MSG
314
+
315
+ SIP/2.0 200 Ok
316
+ [last_Via:]
317
+ From: <sip:[$remote_addr]>;tag=[$remote_tag]
318
+ To: <sip:[$local_addr]>;tag=[call_number]
319
+ [last_Call-ID:]
320
+ [last_CSeq:]
321
+ Server: #{USER_AGENT}
322
+ Contact: <sip:[$local_addr];transport=[transport]>
323
+ Content-Type: application/sdp
324
+ [routes]
325
+ Content-Length: [len]
326
+
327
+ v=0
328
+ o=user1 53655765 2353687637 IN IP[local_ip_type] #{@adv_ip}
329
+ s=-
330
+ c=IN IP[media_ip_type] [media_ip]
331
+ t=0 0
332
+ m=audio [media_port] RTP/AVP 0
333
+ a=rtpmap:0 PCMU/8000
334
+ MSG
335
+ start_media
200
336
  send msg, opts
201
337
  end
202
338
 
339
+ #
340
+ # Helper method to answer an INVITE and expect the ACK
341
+ #
342
+ # @param [Hash] opts A set of options containing SIPp element attributes - will be passed to both the <send> and <recv> elements
343
+ #
344
+ def answer(opts = {})
345
+ send_answer opts
346
+ receive_ack opts
347
+ end
348
+
349
+ def receive_ack(opts = {})
350
+ recv opts.merge request: 'ACK'
351
+ end
352
+
203
353
  #
204
354
  # Sets an expectation for a SIP 100 message from the remote party
205
355
  #
@@ -242,11 +392,22 @@ a=fmtp:101 0-15
242
392
  #
243
393
  def receive_answer(opts = {})
244
394
  options = {
245
- rrs: true, # Record Record Set: Make the Route headers available via [route] later
395
+ rrs: true, # Record Record Set: Make the Route headers available via [routes] later
246
396
  rtd: true # Response Time Duration: Record the response time
247
397
  }
248
398
 
249
- receive_200 options.merge(opts)
399
+ receive_200(options.merge(opts)) do |recv|
400
+ recv << doc.create_element('action') do |action|
401
+ action << doc.create_element('ereg') do |ereg|
402
+ ereg['regexp'] = '<sip:(.*)>.*;tag=([^;]*)'
403
+ ereg['search_in'] = 'hdr'
404
+ ereg['header'] = 'To:'
405
+ ereg['assign_to'] = 'dummy,remote_addr,remote_tag'
406
+ end
407
+ end
408
+ end
409
+ # These variables will only be used if we initiate a hangup
410
+ @reference_variables += %w(dummy remote_addr remote_tag)
250
411
  end
251
412
 
252
413
  #
@@ -255,13 +416,16 @@ a=fmtp:101 0-15
255
416
  # @param [Hash] opts A set of options to modify the expectation
256
417
  # @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to false.
257
418
  #
258
- def receive_ok(opts = {})
259
- recv({ response: 200 }.merge(opts))
419
+ def receive_ok(opts = {}, &block)
420
+ recv({ response: 200 }.merge(opts), &block)
260
421
  end
261
422
  alias :receive_200 :receive_ok
262
423
 
263
424
  #
264
- # Shortcut that sets expectations for optional SIP 100, 180 and 183, followed by a required 200.
425
+ # Convenience method to wait for an answer from the called party
426
+ #
427
+ # This sets expectations for optional SIP 100, 180 and 183,
428
+ # followed by a required 200 and sending the acknowledgement.
265
429
  #
266
430
  # @param [Hash] opts A set of options to modify the expectations
267
431
  #
@@ -270,10 +434,11 @@ a=fmtp:101 0-15
270
434
  receive_ringing opts
271
435
  receive_progress opts
272
436
  receive_answer opts
437
+ ack_answer opts
273
438
  end
274
439
 
275
440
  #
276
- # Acknowledge a received answer message (SIP 200) and start media playback
441
+ # Acknowledge a received answer message and start media playback
277
442
  #
278
443
  # @param [Hash] opts A set of options to modify the message parameters
279
444
  #
@@ -281,12 +446,12 @@ a=fmtp:101 0-15
281
446
  msg = <<-BODY
282
447
 
283
448
  ACK [next_url] SIP/2.0
284
- Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
285
- From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
449
+ Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
450
+ From: "#{@from_user}" <sip:#{@from_user}@#{@adv_ip}:[local_port]>;tag=[call_number]
286
451
  To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
287
452
  Call-ID: [call_id]
288
453
  CSeq: [cseq] ACK
289
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
454
+ Contact: <sip:[$local_addr];transport=[transport]>
290
455
  Max-Forwards: 100
291
456
  User-Agent: #{USER_AGENT}
292
457
  Content-Length: 0
@@ -304,7 +469,7 @@ Content-Length: 0
304
469
  def sleep(seconds)
305
470
  milliseconds = (seconds.to_f * MSEC).to_i
306
471
  pause milliseconds
307
- @media << "silence:#{milliseconds}"
472
+ @media << "silence:#{milliseconds}" if @media
308
473
  end
309
474
 
310
475
  #
@@ -319,6 +484,7 @@ Content-Length: 0
319
484
  # send_digits '1234'
320
485
  #
321
486
  def send_digits(digits)
487
+ raise "Media not started" unless @media
322
488
  delay = (0.250 * MSEC).to_i # FIXME: Need to pass this down to the media layer
323
489
  digits.split('').each do |digit|
324
490
  raise ArgumentError, "Invalid DTMF digit requested: #{digit}" unless VALID_DTMF.include? digit
@@ -331,12 +497,12 @@ Content-Length: 0
331
497
  info = <<-INFO
332
498
 
333
499
  INFO [next_url] SIP/2.0
334
- Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
335
- From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
500
+ Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
501
+ From: "#{@from_user}" <sip:#{@from_user}@#{@adv_ip}:[local_port]>;tag=[call_number]
336
502
  To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
337
503
  Call-ID: [call_id]
338
504
  CSeq: [cseq] INFO
339
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
505
+ Contact: <sip:[$local_addr];transport=[transport]>
340
506
  Max-Forwards: 100
341
507
  User-Agent: #{USER_AGENT}
342
508
  [routes]
@@ -370,18 +536,17 @@ Duration=#{delay}
370
536
  if regexp
371
537
  action = Nokogiri::XML::Node.new 'action', doc
372
538
  ereg = Nokogiri::XML::Node.new 'ereg', doc
373
- ref = Nokogiri::XML::Node.new 'Reference', doc
374
539
 
375
540
  ereg['regexp'] = regexp
376
541
  ereg['search_in'] = 'body'
377
542
  ereg['check_it'] = true
378
543
 
379
544
  var = "message_#{@message_variables += 1}"
380
- ereg['assign_to'] = ref['variables'] = var
545
+ ereg['assign_to'] = var
546
+ @reference_variables << var
381
547
 
382
548
  action << ereg
383
549
  recv << action
384
- scenario_node << ref
385
550
  end
386
551
 
387
552
  okay
@@ -395,13 +560,13 @@ Duration=#{delay}
395
560
  def send_bye(opts = {})
396
561
  msg = <<-MSG
397
562
 
398
- BYE [next_url] SIP/2.0
399
- Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
400
- From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
401
- To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
563
+ BYE sip:[$call_addr] SIP/2.0
564
+ Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
565
+ From: <sip:[$local_addr]>;tag=[call_number]
566
+ To: <sip:[$remote_addr]>;tag=[$remote_tag]
567
+ Contact: <sip:[$local_addr];transport=[transport]>
402
568
  Call-ID: [call_id]
403
569
  CSeq: [cseq] BYE
404
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
405
570
  Max-Forwards: 100
406
571
  User-Agent: #{USER_AGENT}
407
572
  Content-Length: 0
@@ -433,7 +598,7 @@ SIP/2.0 200 OK
433
598
  [last_To:]
434
599
  [last_Call-ID:]
435
600
  [last_CSeq:]
436
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
601
+ Contact: <sip:[$local_addr];transport=[transport]>
437
602
  Max-Forwards: 100
438
603
  User-Agent: #{USER_AGENT}
439
604
  Content-Length: 0
@@ -453,6 +618,34 @@ Content-Length: 0
453
618
  ack_bye(opts)
454
619
  end
455
620
 
621
+ #
622
+ # Shortcut to send a BYE and wait for the acknowledgement
623
+ #
624
+ # @param [Hash] opts A set of options containing SIPp <recv> element attributes - will be passed to both the <send> and <recv> elements
625
+ #
626
+ def hangup(opts = {})
627
+ send_bye opts
628
+ receive_ok opts
629
+ end
630
+
631
+ # Create partition table for Call Length
632
+ #
633
+ # @param [Integer] min An value specifying the minimum time in milliseconds for the table
634
+ # @param [Integer] max An value specifying the maximum time in milliseconds for the table
635
+ # @param [Integer] interval An value specifying the interval in milliseconds for the table
636
+ def call_length_repartition(min, max, interval)
637
+ partition_table 'CallLengthRepartition', min.to_i, max.to_i, interval.to_i
638
+ end
639
+
640
+ # Create partition table for Response Time
641
+ #
642
+ # @param [Integer] min An value specifying the minimum time in milliseconds for the table
643
+ # @param [Integer] max An value specifying the maximum time in milliseconds for the table
644
+ # @param [Integer] interval An value specifying the interval in milliseconds for the table
645
+ def response_time_repartition(min, max, interval)
646
+ partition_table 'ResponseTimeRepartition', min.to_i, max.to_i, interval.to_i
647
+ end
648
+
456
649
  #
457
650
  # Dump the scenario to a SIPp XML string
458
651
  #
@@ -474,6 +667,13 @@ Content-Length: 0
474
667
  end
475
668
  end
476
669
 
670
+ unless @reference_variables.empty?
671
+ scenario_node = docdup.xpath('scenario').first
672
+ scenario_node << docdup.create_element('Reference') do |ref|
673
+ ref[:variables] = @reference_variables.to_a.join ','
674
+ end
675
+ end
676
+
477
677
  docdup.to_xml
478
678
  end
479
679
 
@@ -494,7 +694,7 @@ Content-Length: 0
494
694
  # scenario.compile! # Leaves files at test_scenario.xml and test_scenario.pcap
495
695
  #
496
696
  def compile!
497
- unless @media.empty?
697
+ unless @media.nil?
498
698
  print "Compiling media to #{@filename}.pcap..."
499
699
  compile_media.to_file filename: "#{@filename}.pcap"
500
700
  puts "done."
@@ -520,8 +720,9 @@ Content-Length: 0
520
720
  # @see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/Tempfile.html
521
721
  #
522
722
  def to_tmpfiles
523
- unless @media.empty?
723
+ unless @media.nil? || @media.empty?
524
724
  media_file = Tempfile.new 'media'
725
+ media_file.binmode
525
726
  media_file.write compile_media.to_s
526
727
  media_file.rewind
527
728
  end
@@ -544,6 +745,12 @@ Content-Length: 0
544
745
  [user, domain]
545
746
  end
546
747
 
748
+ # Split a string into space-delimited components, optionally allowing quoted groups
749
+ # Example: cars "cats and dogs" fish 'hammers' => ["cars", "cats and dogs", "fish", "hammers"]
750
+ def split_quoted_string(args)
751
+ args.to_s.scan(/'.+?'|".+?"|[^ ]+/).map { |s| s.gsub /^['"]|['"]$/, '' }
752
+ end
753
+
547
754
  def doc
548
755
  @doc ||= begin
549
756
  Nokogiri::XML::Builder.new do |xml|
@@ -560,9 +767,6 @@ Content-Length: 0
560
767
  end
561
768
 
562
769
  def parse_args(args)
563
- raise ArgumentError, "Must include source IP:PORT" unless args.has_key? :source
564
- raise ArgumentError, "Must include destination IP:PORT" unless args.has_key? :destination
565
-
566
770
  if args[:dtmf_mode]
567
771
  @dtmf_mode = args[:dtmf_mode].to_sym
568
772
  raise ArgumentError, "dtmf_mode must be rfc2833 or info" unless [:rfc2833, :info].include?(@dtmf_mode)
@@ -570,25 +774,26 @@ Content-Length: 0
570
774
  @dtmf_mode = :rfc2833
571
775
  end
572
776
 
573
- @from_addr, @from_port = args[:source].split ':'
574
- @to_addr, @to_port = args[:destination].split ':'
777
+ @from_addr, @from_port = args[:source].split ':' if args[:source]
778
+ @to_addr, @to_port = args[:destination].split ':' if args[:destination]
575
779
  @from_user = args[:from_user] || "sipp"
576
780
  end
577
781
 
578
782
  def compile_media
783
+ raise "Media not started" unless @media
579
784
  @media.compile!
580
785
  end
581
786
 
582
- def register_message(domain, user, opts = {})
787
+ def register_message(domain, user)
583
788
  <<-BODY
584
789
 
585
790
  REGISTER sip:#{domain} SIP/2.0
586
- Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
791
+ Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
587
792
  From: <sip:#{user}@#{domain}>;tag=[call_number]
588
793
  To: <sip:#{user}@#{domain}>
589
794
  Call-ID: [call_id]
590
795
  CSeq: [cseq] REGISTER
591
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
796
+ Contact: <sip:#{@from_user}@#{@adv_ip}:[local_port];transport=[transport]>
592
797
  Max-Forwards: 10
593
798
  Expires: 120
594
799
  User-Agent: #{USER_AGENT}
@@ -596,17 +801,16 @@ Content-Length: 0
596
801
  BODY
597
802
  end
598
803
 
599
- def register_auth(domain, user, password, opts = {})
600
- recv response: '401', auth: true, optional: false
804
+ def register_auth(domain, user, password)
601
805
  <<-AUTH
602
806
 
603
807
  REGISTER sip:#{domain} SIP/2.0
604
- Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
808
+ Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
605
809
  From: <sip:#{user}@#{domain}>;tag=[call_number]
606
810
  To: <sip:#{user}@#{domain}>
607
811
  Call-ID: [call_id]
608
812
  CSeq: [cseq] REGISTER
609
- Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
813
+ Contact: <sip:#{@from_user}@#{@adv_ip}:[local_port];transport=[transport]>
610
814
  Max-Forwards: 20
611
815
  Expires: 3600
612
816
  [authentication username=#{user} password=#{password}]
@@ -616,6 +820,7 @@ Content-Length: 0
616
820
  end
617
821
 
618
822
  def start_media
823
+ @media = Media.new '127.0.0.255', 55555, '127.255.255.255', 44444
619
824
  nop = doc.create_element('nop') { |nop|
620
825
  nop << doc.create_element('action') { |action|
621
826
  action << doc.create_element('exec')
@@ -640,15 +845,17 @@ Content-Length: 0
640
845
  send << "\n"
641
846
  send << Nokogiri::XML::CDATA.new(doc, msg)
642
847
  send << "\n" #Newlines are required before and after CDATA so SIPp will parse properly
848
+ yield send if block_given?
643
849
  scenario_node << send
644
850
  end
645
851
 
646
- def recv(opts = {})
852
+ def recv(opts = {}, &block)
647
853
  raise ArgumentError, "Receive must include either a response or a request" unless opts.keys.include?(:response) || opts.keys.include?(:request)
648
854
  recv = Nokogiri::XML::Node.new 'recv', doc
649
855
  opts.each do |k,v|
650
856
  recv[k.to_s] = v
651
857
  end
858
+ yield recv if block_given?
652
859
  scenario_node << recv
653
860
  end
654
861
 
@@ -660,5 +867,12 @@ Content-Length: 0
660
867
  def handle_response(code, opts)
661
868
  optional_recv opts.merge(response: code)
662
869
  end
870
+
871
+ def partition_table(name, min, max, interval)
872
+ range = Range.new(min, max).step interval
873
+ partition_table = Nokogiri::XML::Node.new name, doc
874
+ partition_table[:value] = range.inject{ |n,m| "#{n},#{m}"}
875
+ scenario_node << partition_table
876
+ end
663
877
  end
664
878
  end