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 +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
|