sippy_cup 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile +0 -2
- data/README.markdown +49 -0
- data/lib/sippy_cup.rb +4 -1
- data/lib/sippy_cup/media.rb +120 -0
- data/lib/sippy_cup/media/dtmf_payload.rb +54 -0
- data/lib/sippy_cup/media/pcmu_payload.rb +27 -0
- data/lib/sippy_cup/media/rtp_header.rb +69 -0
- data/lib/sippy_cup/media/rtp_payload.rb +28 -0
- data/lib/sippy_cup/rtp_generator.rb +19 -0
- data/lib/sippy_cup/scenario.rb +127 -34
- data/lib/sippy_cup/version.rb +1 -1
- data/sippy_cup.gemspec +3 -3
- metadata +13 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a6c679bd0c325ab8fb92408015dfacb1c5a88373
|
4
|
+
data.tar.gz: 557e222b71aa5286356bf63f37f63ba9233a46e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a3f0ef5f1ffcbe843918382fa318e34476c2e7ecdb0dff8fe762f27efd26339d34c7ceeb69ed1d409b45fb329ba687f2e6552c051def0cff88a222c5d972ca4
|
7
|
+
data.tar.gz: d8cbeeb69fde07e94936b5e20835a56c4df265f4ded018a6a3d051709d40ab5006df6f58e005ab165f10559dfab4e03d926a3370f76605aedba737c84dd202eb
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
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
@@ -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
|
data/lib/sippy_cup/scenario.rb
CHANGED
@@ -2,33 +2,56 @@ require 'nokogiri'
|
|
2
2
|
|
3
3
|
module SippyCup
|
4
4
|
class Scenario
|
5
|
-
|
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
|
19
|
-
|
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
|
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
|
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(
|
52
|
-
|
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(
|
57
|
-
|
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(
|
62
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
91
|
-
|
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
|
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
|
-
|
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
|
126
|
-
recv['response'] = opts
|
127
|
-
recv['optional'] = !!opts
|
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
|
data/lib/sippy_cup/version.rb
CHANGED
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 '
|
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
|
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-
|
12
|
+
date: 2013-07-03 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
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:
|
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
|