sippy_cup 0.0.1 → 0.1.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: 0bf8b63922a80ba9eb886d9ad0713f953376f6f6
4
- data.tar.gz: df89e36db406772721fb83164372f1e7c7787261
3
+ metadata.gz: a6c679bd0c325ab8fb92408015dfacb1c5a88373
4
+ data.tar.gz: 557e222b71aa5286356bf63f37f63ba9233a46e3
5
5
  SHA512:
6
- metadata.gz: 436f979aeee94dfbe201ec011ba339cf12977dc9929741bb07c9beee9fe4ccfda3d0f113263b0ca5ad1f003d475e9e2dc2e37c247cabe527cb549f4d26c164cc
7
- data.tar.gz: 7c613e7768152d9e6a84d3145abfeca4396363fb2aaeaabec2927d45329ecf04f3a5ebc61f06b5a01dd71631aba37618ec05a5c6f2e1909586924986a18d36a6
6
+ metadata.gz: 2a3f0ef5f1ffcbe843918382fa318e34476c2e7ecdb0dff8fe762f27efd26339d34c7ceeb69ed1d409b45fb329ba687f2e6552c051def0cff88a222c5d972ca4
7
+ data.tar.gz: d8cbeeb69fde07e94936b5e20835a56c4df265f4ded018a6a3d051709d40ab5006df6f58e005ab165f10559dfab4e03d926a3370f76605aedba737c84dd202eb
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  .*.sw*
2
2
  .DS_Store
3
+ *.gem
data/Gemfile CHANGED
@@ -1,5 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
-
5
- gem 'pcap', github: 'ahobson/ruby-pcap'
data/README.markdown CHANGED
@@ -3,3 +3,52 @@ Sippy Cup
3
3
 
4
4
  Sippy Cup is a tool to generate [SIPp](http://sipp.sourceforge.net/) load test profiles. The goal is to take an input document that describes a load test in a very simple way (call this number, wait this many seconds, send this digit, wait a few more seconds, etc). The ideas are taken from [LoadBot](https://github.com/mojolingo/ahn-loadbot), but the goal is for a more performant load generating tool with no dependency on Asterisk.
5
5
 
6
+
7
+ Example
8
+ =======
9
+
10
+ ```Ruby
11
+ require 'sippy_cup'
12
+
13
+ scenario = SippyCup::Scenario.new 'Sippy Cup', source: '192.168.5.5:10001', destination: '10.10.0.3:19995' do |s|
14
+ s.invite
15
+ s.receive_trying
16
+ s.receive_ringing
17
+ s.receive_progress
18
+
19
+ s.receive_answer
20
+ s.ack_answer
21
+
22
+ s.sleep 3
23
+ s.send_digits '3125551234'
24
+ s.sleep 5
25
+ s.send_digits '#'
26
+
27
+ s.receive_bye
28
+ s.ack_bye
29
+ end
30
+
31
+ # Create the scenario XML and PCAP media. File will be named after the scenario name, in our case:
32
+ # * sippy_cup.xml
33
+ # * sippy_cup.pcap
34
+ scenario.compile!
35
+ ```
36
+
37
+ Customize Your Scenarios
38
+ ========================
39
+
40
+ With Sippy Cup, you can add additional attributes to each step of the scenario:
41
+ ```Ruby
42
+
43
+ #This limits the amount of time the server has to reply to an invite (3 seconds)
44
+ s.receive_answer timeout: 3000
45
+
46
+ #You can override the default 'optional' parameters
47
+ s.receive_ringing optional: false
48
+ s.receive_answer optional: true
49
+
50
+ #Let's combine multiple attributes...
51
+ s.receive_answer timeout: 3000, crlf: true
52
+ ```
53
+
54
+ For more information on possible attributes, visit the [SIPp Documentation](http://sipp.sourceforge.net/doc/reference.html)
data/lib/sippy_cup.rb CHANGED
@@ -1,4 +1,7 @@
1
- require 'sippy_cup/scenario'
1
+ %w{
2
+ scenario
3
+ media
4
+ }.each {|r| require "sippy_cup/#{r}"}
2
5
 
3
6
  module SippyCup
4
7
 
@@ -0,0 +1,120 @@
1
+ require 'ipaddr'
2
+ require 'sippy_cup/media/pcmu_payload'
3
+ require 'sippy_cup/media/dtmf_payload'
4
+
5
+ module SippyCup
6
+ class Media
7
+ VALID_STEPS = %w{silence dtmf}.freeze
8
+ USEC = 1_000_000
9
+ MSEC = 1_000
10
+ attr_accessor :sequence
11
+ attr_reader :packets
12
+
13
+ def initialize(from_addr, from_port, to_addr, to_port, generator = PCMUPayload)
14
+ @from_addr, @to_addr = IPAddr.new(from_addr), IPAddr.new(to_addr)
15
+ @from_port, @to_port, @generator = from_port, to_port, generator
16
+ reset!
17
+ end
18
+
19
+ def reset!
20
+ @sequence = []
21
+ @packets = []
22
+ end
23
+
24
+ def <<(input)
25
+ get_step input # validation
26
+ @sequence << input
27
+ end
28
+
29
+ def compile!
30
+ sequence_number = 0
31
+ start_time = Time.now
32
+ @pcap_file = PacketFu::PcapFile.new
33
+ timestamp = 0
34
+ elapsed = 0
35
+ ssrc_id = rand 2147483648
36
+ first_audio = true
37
+
38
+ @sequence.each do |input|
39
+ action, value = get_step input
40
+
41
+ case action
42
+ when 'silence'
43
+ # value is the duration in milliseconds
44
+ # append that many milliseconds of silent RTP audio
45
+ (value.to_i / @generator::PTIME).times do
46
+ packet = new_packet
47
+ rtp_frame = @generator.new
48
+
49
+ # The first RTP audio packet should have the marker bit set
50
+ if first_audio
51
+ rtp_frame.rtp_marker = 1
52
+ first_audio = false
53
+ end
54
+
55
+ rtp_frame.rtp_timestamp = timestamp += rtp_frame.timestamp_interval
56
+ elapsed += rtp_frame.ptime
57
+ rtp_frame.rtp_sequence_num = sequence_number += 1
58
+ rtp_frame.rtp_ssrc_id = ssrc_id
59
+ packet.headers.last.body = rtp_frame.to_bytes
60
+ packet.recalc
61
+ @pcap_file.body << get_pcap_packet(packet, next_ts(start_time, elapsed))
62
+ end
63
+ when 'dtmf'
64
+ # value is the DTMF digit to send
65
+ # append that RFC2833 digit
66
+ # Assume 0.25 second duration for now
67
+ count = 250 / DTMFPayload::PTIME
68
+ count.times do |i|
69
+ packet = new_packet
70
+ dtmf_frame = DTMFPayload.new value
71
+ dtmf_frame.rtp_marker = 1 if i == 0
72
+ dtmf_frame.rtp_timestamp = timestamp # Is this correct? This is what Blink does...
73
+ #dtmf_frame.rtp_timestamp = timestamp += dtmf_frame.timestamp_interval
74
+ dtmf_frame.rtp_sequence_num = sequence_number += 1
75
+ dtmf_frame.rtp_ssrc_id = ssrc_id
76
+ dtmf_frame.end_of_event = (count == i) # Last packet?
77
+ packet.headers.last.body = dtmf_frame.to_bytes
78
+ packet.recalc
79
+ @pcap_file.body << get_pcap_packet(packet, next_ts(start_time, elapsed))
80
+ end
81
+ # Now bump up the timestamp to cover the gap
82
+ timestamp += count * DTMFPayload::TIMESTAMP_INTERVAL
83
+ else
84
+ end
85
+ end
86
+ @pcap_file
87
+ end
88
+ private
89
+ def get_step(input)
90
+ action, value = input.split ':'
91
+ raise "Invalid Sequence: #{input}" unless VALID_STEPS.include? action
92
+
93
+ [action, value]
94
+ end
95
+
96
+
97
+ def get_pcap_packet(packet, timestamp)
98
+ PacketFu::PcapPacket.new :timestamp => timestamp,
99
+ :incl_len => packet.to_s.size,
100
+ :orig_len => packet.to_s.size,
101
+ :data => packet.to_s
102
+ end
103
+
104
+ def next_ts(start_time, offset)
105
+ distance = offset * MSEC
106
+ sec = start_time.to_i + (distance / USEC)
107
+ usec = distance % USEC
108
+ PacketFu::Timestamp.new(sec: sec, usec: usec).to_s
109
+ end
110
+
111
+ def new_packet
112
+ packet = PacketFu::UDPPacket.new
113
+ packet.ip_src = @from_addr.to_i
114
+ packet.ip_dst = @to_addr.to_i
115
+ packet.udp_src = @from_port
116
+ packet.udp_dst = @to_port
117
+ packet
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,54 @@
1
+ require 'sippy_cup/media/rtp_payload'
2
+
3
+ module SippyCup
4
+ class Media
5
+ class DTMFPayload < RTPPayload
6
+ RTP_PAYLOAD_ID = 101
7
+ PTIME = 20 # in milliseconds
8
+ TIMESTAMP_INTERVAL = 160
9
+ END_OF_EVENT = 1 << 7
10
+ DTMF = %w{0 1 2 3 4 5 6 7 8 9 * # A B C D}.freeze
11
+ attr_accessor :ptime
12
+
13
+ def initialize(digit, opts = {})
14
+ super RTP_PAYLOAD_ID
15
+ @flags = 0
16
+ @digit = atoi digit
17
+ @ptime = opts[:ptime] || PTIME
18
+
19
+ volume opts[:volume] || 10
20
+ end
21
+
22
+ def end_of_event=(bool)
23
+ if bool
24
+ @flags |= END_OF_EVENT
25
+ else
26
+ @flags &= (0xf - END_OF_EVENT)
27
+ end
28
+ end
29
+
30
+ def atoi(digit)
31
+ DTMF.index digit.to_s
32
+ end
33
+
34
+ def volume(value)
35
+ value = [value, 0x3f].min # Cap to 6 bits
36
+ @flags &= 0xc0 # zero out old volume
37
+ @flags += value
38
+ end
39
+
40
+ def end_of_event
41
+ @flags & END_OF_EVENT
42
+ end
43
+
44
+ def media
45
+ [@digit, @flags, timestamp_interval].pack 'CCn'
46
+ end
47
+
48
+ def timestamp_interval
49
+ TIMESTAMP_INTERVAL
50
+ end
51
+ end
52
+ end
53
+ end
54
+
@@ -0,0 +1,27 @@
1
+ require 'sippy_cup/media/rtp_payload'
2
+
3
+ module SippyCup
4
+ class Media
5
+ class PCMUPayload < RTPPayload
6
+ RTP_PAYLOAD_ID = 0x0
7
+ SILENT_BYTE = 0xff.chr
8
+ PTIME = 20 # in milliseconds
9
+ RATE = 8 # in KHz
10
+ attr_accessor :ptime
11
+
12
+ def initialize(opts = {})
13
+ super RTP_PAYLOAD_ID
14
+ @ptime = opts[:ptime] || PTIME
15
+ @rate = opts[:rate] || RATE
16
+ end
17
+
18
+ def media
19
+ SILENT_BYTE * timestamp_interval
20
+ end
21
+
22
+ def timestamp_interval
23
+ @rate * @ptime
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+ require 'packetfu'
3
+
4
+ module SippyCup
5
+ class Media
6
+ class RTPHeader < Struct.new(:version, :padding, :extension, :marker, :payload_id, :sequence_num, :timestamp, :ssrc_id, :csrc_ids)
7
+ VERSION = 2
8
+
9
+ include StructFu
10
+
11
+ def initialize(args = {})
12
+ # TODO: Support Extension Header
13
+ super(
14
+ (args[:version] ? args[:version] : VERSION),
15
+ (args[:padding] ? args[:padding] : 0),
16
+ (args[:extension] ? args[:extension] : 0),
17
+ (args[:marker] ? args[:marker] : 0),
18
+ (args[:payload_id] ? args[:payload_id] : 0),
19
+ Int16.new(args[:sequence_num] ? args[:sequence_num] : 0),
20
+ Int32.new(args[:timestamp] ? args[:timestamp] : 0),
21
+ Int32.new(args[:ssrc_id] ? args[:ssrc_id] : 0),
22
+ (args[:csrc_ids] ? Array(args[:csrc_ids]) : []),
23
+ )
24
+ end
25
+
26
+ def read(str)
27
+ self[:version] = str[0].ord >> 6
28
+ self[:padding] = (str[0].ord >> 5) & 1
29
+ self[:extension] = (str[0].ord >> 4) & 1
30
+ num_csrcs = str[0].ord & 0xf
31
+ self[:marker] = str[1] >> 7
32
+ self[:payload_id] = str[1] & 0x7f
33
+ self[:sequence_num].read str[2,2]
34
+ self[:timestamp].read str[4,4]
35
+ self[:ssrc_id].read str[8,4]
36
+ i = 8
37
+ num_csrcs.times do
38
+ self[:csrc_ids] << Int32.new(str[i += 4, 4])
39
+ end
40
+ self[:body] = str[i, str.length - i]
41
+ end
42
+
43
+ def csrc_count
44
+ csrc_ids.count
45
+ end
46
+
47
+ def csrc_ids_readable
48
+ csrc_ids.to_s
49
+ end
50
+
51
+ def to_s
52
+ bytes = [
53
+ (version << 6) + (padding << 5) + (extension << 4) + (csrc_count),
54
+ (marker << 7) + (payload_id),
55
+ sequence_num,
56
+ timestamp,
57
+ ssrc_id
58
+ ].pack 'CCnNN'
59
+
60
+ csrc_ids.each do |csrc_id|
61
+ bytes << [csrc_id].pack('N')
62
+ end
63
+
64
+ bytes
65
+ end
66
+ end
67
+ end
68
+ end
69
+
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+ require 'packetfu'
3
+ require 'sippy_cup/media/rtp_header'
4
+
5
+ module SippyCup
6
+ class Media
7
+ class RTPPayload
8
+ attr_reader :header
9
+
10
+ def initialize(payload_id = 0)
11
+ @header = RTPHeader.new payload_id: payload_id
12
+ end
13
+
14
+ def to_bytes
15
+ @header.to_s + media
16
+ end
17
+
18
+ def method_missing(method, *args)
19
+ if method.to_s =~ /^rtp_/
20
+ method = method.to_s.sub(/^rtp_/, '').to_sym
21
+ @header.send method, *args
22
+ else
23
+ super
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ require 'pcap'
2
+
3
+ module SippyCup
4
+ class RTPGenerator
5
+ DEFAULT_DATALINK = 1 # Corresponds to DLT_EN10MB, Ethernet (10Mb) from pcap/bpf.h
6
+
7
+ def initialize
8
+ @output = Pcap::Capture.open_dead DEFAULT_DATALINK, 65535
9
+
10
+
11
+
12
+ def save!(file)
13
+ pcap_file = Pcap::Dumper.open @output, file
14
+ @output.loop(-1) do |packet|
15
+ pcap_file.dump packet
16
+ end
17
+ end
18
+ end
19
+ end
@@ -2,33 +2,56 @@ require 'nokogiri'
2
2
 
3
3
  module SippyCup
4
4
  class Scenario
5
- def initialize(name, &block)
5
+ VALID_DTMF = %w{0 1 2 3 4 5 6 7 8 9 0 * # A B C D}.freeze
6
+ MSEC = 1_000
7
+
8
+ def initialize(name, args = {}, &block)
6
9
  builder = Nokogiri::XML::Builder.new do |xml|
7
10
  xml.scenario name: name
8
11
  end
9
12
 
13
+ parse_args args
14
+
15
+ @filename = name.downcase.gsub(/\W+/, '_')
10
16
  @doc = builder.doc
17
+ @media = Media.new @from_addr, @from_port, @to_addr, @to_port
11
18
  @scenario = @doc.xpath('//scenario').first
12
19
 
13
20
  instance_eval &block
14
21
  end
15
22
 
23
+ def parse_args(args)
24
+ raise ArgumentError, "Must include source IP:PORT" unless args.keys.include? :source
25
+ raise ArgumentError, "Must include destination IP:PORT" unless args.keys.include? :destination
26
+
27
+ @from_addr, @from_port = args[:source].split ':'
28
+ @to_addr, @to_port = args[:destination].split ':'
29
+ @from_user = args[:from_user] || "sipp"
30
+ end
31
+
32
+ def compile_media
33
+ @media.compile!
34
+ end
35
+
16
36
  def sleep(seconds)
17
37
  # TODO play silent audio files to the server to fill the gap
18
- pause = Nokogiri::XML::Node.new 'pause', @doc
19
- pause['milliseconds'] = seconds
20
- @scenario.add_child pause
38
+ pause seconds * MSEC
39
+ @media << "silence:#{seconds * MSEC}"
21
40
  end
22
41
 
23
- def invite
42
+ def invite(opts = {})
43
+ opts[:retrans] ||= 500
44
+ # FIXME: The DTMF mapping (101) is hard-coded. It would be better if we could
45
+ # get this from the DTMF payload generator
24
46
  msg = <<-INVITE
47
+
25
48
  INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
26
49
  Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
27
- From: sipp <sip:[field0]@[local_ip]>;tag=[call_number]
50
+ From: sipp <sip:#{@from_user}@[local_ip]>;tag=[call_number]
28
51
  To: <sip:[service]@[remote_ip]:[remote_port]>
29
52
  Call-ID: [call_id]
30
53
  CSeq: [cseq] INVITE
31
- Contact: sip:[field0]@[local_ip]:[local_port]
54
+ Contact: sip:#{@from_user}@[local_ip]:[local_port]
32
55
  Max-Forwards: 100
33
56
  Content-Type: application/sdp
34
57
  Content-Length: [len]
@@ -38,61 +61,111 @@ module SippyCup
38
61
  s=-
39
62
  c=IN IP[media_ip_type] [media_ip]
40
63
  t=0 0
41
- m=audio [media_port] RTP/AVP 0
64
+ m=audio [media_port] RTP/AVP 0 101
42
65
  a=rtpmap:0 PCMU/8000
66
+ a=rtpmap:101 telephone-event/8000
67
+ a=fmtp:101 0-15
43
68
  INVITE
44
- send = new_send msg
45
- # FIXME: Does this need to be configurable?
46
- send['retrans'] = 500
47
-
69
+ send = new_send msg, opts
48
70
  @scenario << send
49
71
  end
50
72
 
51
- def receive_trying(optional = true)
52
- @scenario.add_child new_recv response: 100, optional: optional
73
+ def receive_trying(opts = {})
74
+ opts[:optional] = true if opts[:optional].nil?
75
+ opts.merge! response: 100
76
+ @scenario << new_recv(opts)
53
77
  end
54
78
  alias :receive_100 :receive_trying
55
79
 
56
- def receive_ringing(optional = true)
57
- @scenario.add_child new_recv response: 180, optional: optional
80
+ def receive_ringing(opts = {})
81
+ opts[:optional] = true if opts[:optional].nil?
82
+ opts.merge! response: 180
83
+ @scenario << new_recv(opts)
58
84
  end
59
85
  alias :receive_180 :receive_ringing
60
86
 
61
- def receive_progress(optional = true)
62
- @scenario.add_child new_recv response: 183, optional: optional
87
+ def receive_progress(opts = {})
88
+ opts[:optional] = true if opts[:optional].nil?
89
+ opts.merge! response: 183
90
+ @scenario << new_recv(opts)
63
91
  end
64
92
  alias :receive_183 :receive_progress
65
93
 
66
- def receive_answer
67
- recv = new_recv response: 200, optional: false
94
+ def receive_answer(opts = {})
95
+ opts.merge! response: 200
96
+ recv = new_recv opts
68
97
  # Record Record Set: Make the Route headers available via [route] later
69
98
  recv['rrs'] = true
70
- @scenario.add_child recv
99
+ @scenario << recv
71
100
  end
72
101
  alias :receive_200 :receive_answer
73
102
 
74
- def ack_answer
103
+ def ack_answer(opts = {})
75
104
  msg = <<-ACK
105
+
76
106
  ACK [next_url] SIP/2.0
77
107
  Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
78
- From: <sip:[field0]@[local_ip]>;tag=[call_number]
108
+ From: <sip:#{@from_user}@[local_ip]>;tag=[call_number]
79
109
  [last_To:]
80
110
  [routes]
81
111
  Call-ID: [call_id]
82
112
  CSeq: [cseq] ACK
83
- Contact: sip:[field0]@[local_ip]:[local_port]
113
+ Contact: sip:#{@from_user}@[local_ip]:[local_port]
84
114
  Max-Forwards: 100
85
115
  Content-Length: 0
86
116
  ACK
87
- @scenario << new_send(msg)
117
+ @scenario << new_send(msg, opts)
118
+ start_media
119
+ end
120
+
121
+ def start_media
122
+ nop = Nokogiri::XML::Node.new 'nop', @doc
123
+ action = Nokogiri::XML::Node.new 'action', @doc
124
+ nop << action
125
+ exec = Nokogiri::XML::Node.new 'exec', @doc
126
+ exec['play_pcap_audio'] = "#{@filename}.pcap"
127
+ action << exec
128
+ @scenario << nop
88
129
  end
89
130
 
90
- def receive_bye
91
- @scenario.add_child new_recv response: 'BYE'
131
+ ##
132
+ # Send DTMF digits
133
+ # @param[String] DTMF digits to send. Must be 0-9, *, # or A-D
134
+ def send_digits(digits, delay = 0.250)
135
+ delay = 0.250 * MSEC # FIXME: Need to pass this down to the media layer
136
+ digits.split('').each do |digit|
137
+ raise ArgumentError, "Invalid DTMF digit requested: #{digit}" unless VALID_DTMF.include? digit
138
+
139
+ @media << "dtmf:#{digit}"
140
+ @media << "silence:#{delay}"
141
+ pause delay * 2
142
+ end
92
143
  end
93
144
 
94
- def ack_bye
145
+ def send_bye(opts = {})
146
+ msg = <<-MSG
147
+
148
+ BYE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
149
+ [last_Via:]
150
+ [last_From:]
151
+ [last_To:]
152
+ [last_Call-ID]
153
+ CSeq: [cseq] BYE
154
+ Contact: <sip:[local_ip]:[local_port];transport=[transport]>
155
+ Max-Forwards: 100
156
+ Content-Length: 0
157
+ MSG
158
+ @scenario << new_send(msg, opts)
159
+ end
160
+
161
+ def receive_bye(opts = {})
162
+ opts.merge! request: 'BYE'
163
+ @scenario << new_recv(opts)
164
+ end
165
+
166
+ def ack_bye(opts = {})
95
167
  msg = <<-ACK
168
+
96
169
  SIP/2.0 200 OK
97
170
  [last_Via:]
98
171
  [last_From:]
@@ -104,27 +177,47 @@ module SippyCup
104
177
  Max-Forwards: 100
105
178
  Content-Length: 0
106
179
  ACK
107
- @scenario << new_send(msg)
180
+ @scenario << new_send(msg, opts)
108
181
  end
109
182
 
110
183
  def to_xml
111
184
  @doc.to_xml
112
185
  end
113
186
 
187
+ def compile!
188
+ xml_file = File.open "#{@filename}.xml", 'w' do |file|
189
+ file.write @doc.to_xml
190
+ end
191
+ compile_media.to_file filename: "#{@filename}.pcap"
192
+ end
193
+
114
194
  private
195
+ def pause(msec)
196
+ pause = Nokogiri::XML::Node.new 'pause', @doc
197
+ pause['milliseconds'] = msec.to_i
198
+ @scenario << pause
199
+ end
115
200
 
116
- def new_send(msg)
201
+ def new_send(msg, opts = {})
117
202
  send = Nokogiri::XML::Node.new 'send', @doc
118
- send << Nokogiri::XML::Text.new(msg, @doc)
203
+ opts.each do |k,v|
204
+ send[k.to_s] = v
205
+ end
206
+ send << "\n"
207
+ send << Nokogiri::XML::CDATA.new(@doc, msg)
208
+ send << "\n" #Newlines are required before and after CDATA so SIPp will parse properly
119
209
  send
120
210
  end
121
211
 
122
212
  def new_recv(opts = {})
123
213
  raise ArgumentError, "Receive must include either a response or a request" unless opts.keys.include?(:response) || opts.keys.include?(:request)
124
214
  recv = Nokogiri::XML::Node.new 'recv', @doc
125
- recv['request'] = opts[:request] if opts.keys.include? :request
126
- recv['response'] = opts[:response] if opts.keys.include? :response
127
- recv['optional'] = !!opts[:optional]
215
+ recv['request'] = opts.delete :request if opts.keys.include? :request
216
+ recv['response'] = opts.delete :response if opts.keys.include? :response
217
+ recv['optional'] = !!opts.delete(:optional)
218
+ opts.each do |k,v|
219
+ recv[k.to_s] = v
220
+ end
128
221
  recv
129
222
  end
130
223
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module SippyCup
4
- VERSION = '0.0.1'
4
+ VERSION = '0.1.0'
5
5
  end
data/sippy_cup.gemspec CHANGED
@@ -6,8 +6,8 @@ Gem::Specification.new do |s|
6
6
  s.name = "sippy_cup"
7
7
  s.version = SippyCup::VERSION
8
8
  s.platform = Gem::Platform::RUBY
9
- s.authors = ["Ben Klang"]
10
- s.email = "bklang&mojolingo.com"
9
+ s.authors = ["Ben Klang", "Will Drexler"]
10
+ s.email = ["bklang&mojolingo.com", "wdrexler&mojolingo.com"]
11
11
  s.homepage = "https://github.com/bklang/sippy_cup"
12
12
  s.summary = "SIPp profile and RTP stream generator"
13
13
  s.description = "This tool makes it easier to generate SIPp load tests with DTMF interactions."
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  s.require_paths = ["lib"]
19
19
 
20
- s.add_runtime_dependency 'pcap'
20
+ s.add_runtime_dependency 'packetfu'
21
21
  s.add_runtime_dependency 'nokogiri', ["~> 1.5.0"]
22
22
 
23
23
  s.add_development_dependency 'guard-rspec'
metadata CHANGED
@@ -1,17 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sippy_cup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Klang
8
+ - Will Drexler
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2013-06-13 00:00:00.000000000 Z
12
+ date: 2013-07-03 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
- name: pcap
15
+ name: packetfu
15
16
  requirement: !ruby/object:Gem::Requirement
16
17
  requirements:
17
18
  - - '>='
@@ -95,7 +96,9 @@ dependencies:
95
96
  - !ruby/object:Gem::Version
96
97
  version: '0'
97
98
  description: This tool makes it easier to generate SIPp load tests with DTMF interactions.
98
- email: bklang&mojolingo.com
99
+ email:
100
+ - bklang&mojolingo.com
101
+ - wdrexler&mojolingo.com
99
102
  executables:
100
103
  - sippy_cup
101
104
  extensions: []
@@ -108,6 +111,12 @@ files:
108
111
  - README.markdown
109
112
  - bin/sippy_cup
110
113
  - lib/sippy_cup.rb
114
+ - lib/sippy_cup/media.rb
115
+ - lib/sippy_cup/media/dtmf_payload.rb
116
+ - lib/sippy_cup/media/pcmu_payload.rb
117
+ - lib/sippy_cup/media/rtp_header.rb
118
+ - lib/sippy_cup/media/rtp_payload.rb
119
+ - lib/sippy_cup/rtp_generator.rb
111
120
  - lib/sippy_cup/scenario.rb
112
121
  - lib/sippy_cup/version.rb
113
122
  - sippy_cup.gemspec