quaff 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.
data/lib/call.rb ADDED
@@ -0,0 +1,230 @@
1
+ # -*- coding: us-ascii -*-
2
+ require_relative './utils.rb'
3
+ require_relative './sources.rb'
4
+
5
+ class CSeq
6
+ def initialize cseq_str
7
+ @num, @method = cseq_str.split
8
+ @num = @num.to_i
9
+ end
10
+
11
+ def increment
12
+ @num = @num +1
13
+ to_s
14
+ end
15
+
16
+ def to_s
17
+ "#{@num.to_s} #{@method}"
18
+ end
19
+
20
+ end
21
+
22
+ class Call
23
+ attr_reader :cid
24
+
25
+ def initialize(cxn,
26
+ cid,
27
+ uri="sip:5557777888@#{QuaffUtils.local_ip}",
28
+ destination=nil,
29
+ target_uri=nil)
30
+ @cxn = cxn
31
+ change_cid cid
32
+ @uri = uri
33
+ @retrans = nil
34
+ @t1, @t2 = 0.5, 32
35
+ @last_From = "<#{uri}>"
36
+ update_branch
37
+ setdest(destination, recv_from_this: true) if destination
38
+ set_callee target_uri if target_uri
39
+ @routeset = []
40
+ end
41
+
42
+ def change_cid cid
43
+ @cid = cid
44
+ @cxn.add_call_id @cid
45
+ end
46
+
47
+ def update_branch
48
+ @last_Via = "SIP/2.0/#{@cxn.transport} #{QuaffUtils.local_ip}:#{@cxn.local_port};rport;branch=#{QuaffUtils.new_branch}"
49
+ end
50
+
51
+ def create_dialog msg
52
+ set_callee msg.first_header("Contact")
53
+ @routeset = msg.all_headers("Record-Route")
54
+ if msg.type == :request
55
+ @routeset = @routeset.reverse
56
+ end
57
+ end
58
+
59
+ def recv_something
60
+ data = @cxn.get_new_message @cid
61
+ @retrans = nil
62
+ @src = data['source']
63
+ @last_To = data["message"].header("To")
64
+ @last_From = data["message"].header("From")
65
+ @sip_destination ||= data["message"].header("From")
66
+ @last_Via = data["message"].headers["Via"]
67
+ @last_CSeq = CSeq.new(data["message"].header("CSeq"))
68
+ data
69
+ end
70
+
71
+ def set_callee uri
72
+ if /<(.*)>/ =~ uri
73
+ uri = $1
74
+ end
75
+
76
+ @sip_destination = "#{uri}"
77
+ @last_To = "<#{uri}>"
78
+ end
79
+
80
+ def setdest source, options={}
81
+ @src = source
82
+ if options[:recv_from_this] and source.sock
83
+ @cxn.add_sock source.sock
84
+ end
85
+ end
86
+
87
+ def recv_request(method)
88
+ begin
89
+ data = recv_something
90
+ rescue
91
+ raise "#{ @uri } timed out waiting for #{ method }"
92
+ end
93
+ unless data["message"].type == :request and Regexp.new(method) =~ data["message"].method
94
+ raise (data['message'].to_s || "Message is nil!")
95
+ end
96
+ data
97
+ end
98
+
99
+ def recv_response(code)
100
+ begin
101
+ data = recv_something
102
+ rescue
103
+ raise "#{ @uri } timed out waiting for #{ code }"
104
+ end
105
+ unless data["message"].type == :response and Regexp.new(code) =~ data["message"].status_code
106
+ raise "Expected #{ code}, got #{data["message"].status_code || data['message']}"
107
+ end
108
+ data
109
+ end
110
+
111
+ def send_response(code, retrans=nil, headers={})
112
+ msg = build_message headers, :response, code
113
+ send_something(msg, retrans)
114
+ end
115
+
116
+ def send_request(method, sdp=true, retrans=nil, headers={})
117
+ sdp="v=0
118
+ o=user1 53655765 2353687637 IN IP4 #{QuaffUtils.local_ip}
119
+ s=-
120
+ c=IN IP4 #{QuaffUtils.local_ip}
121
+ t=0 0
122
+ m=audio 7000 RTP/AVP 0
123
+ a=rtpmap:0 PCMU/8000"
124
+
125
+ msg = build_message headers, :request, method
126
+ send_something(msg, retrans)
127
+ end
128
+
129
+ def build_message headers, type, method_or_code
130
+ defaults = {
131
+ "From" => @last_From,
132
+ "To" => @last_To,
133
+ "Call-ID" => @cid,
134
+ "CSeq" => (/\d+/ =~ method_or_code) ? @last_CSeq.to_s : (method_or_code == "ACK") ? @last_CSeq.increment : "1 #{method_or_code}",
135
+ "Via" => @last_Via,
136
+ "Max-Forwards" => "70",
137
+ "Content-Length" => "0",
138
+ "User-Agent" => "Quaff SIP Scripting Engine",
139
+ "Contact" => "<sip:quaff@#{QuaffUtils.local_ip}:#{@cxn.local_port};transport=#{@cxn.transport};ob>",
140
+ }
141
+
142
+ if type == :request
143
+ defaults['Route'] = @routeset
144
+ else
145
+ defaults['Record-Route'] = @routeset
146
+ end
147
+
148
+ defaults.merge! headers
149
+
150
+
151
+ if type == :request
152
+ msg = "#{method_or_code} #{@sip_destination} SIP/2.0\r\n"
153
+ else
154
+ msg = "SIP/2.0 #{ method_or_code }\r\n"
155
+ end
156
+
157
+ defaults.each do |key, value|
158
+ if value.nil?
159
+ elsif not value.kind_of? Array
160
+ msg += "#{key}: #{value}\r\n"
161
+ else value.each do |subvalue|
162
+ msg += "#{key}: #{subvalue}\r\n"
163
+ end
164
+ end
165
+ end
166
+ msg += "\r\n"
167
+
168
+ msg
169
+ end
170
+
171
+ def send_something(msg, retrans)
172
+ @cxn.send(msg, @src)
173
+ if retrans and (@transport == "UDP") then
174
+ @retrans = true
175
+ Thread.new do
176
+ timer = @t1
177
+ sleep timer
178
+ while @retrans do
179
+ #puts "Retransmitting on call #{ @cid }"
180
+ @cxn.send(msg, @src)
181
+ timer *=2
182
+ if timer < @t2 then
183
+ raise "Too many retransmits!"
184
+ end
185
+ sleep timer
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ def end_call
192
+ @cxn.mark_call_dead @cid
193
+ end
194
+
195
+ def clear_tag str
196
+ str
197
+ end
198
+
199
+ def clone_details other_message
200
+ @headers['To'] = [clear_tag(other_message.header("To"))]
201
+ @headers['From'] = [clear_tag(other_message.header("From"))]
202
+ @headers['Route'] = [other_message.header("Route")]
203
+ end
204
+
205
+ def get_next_hop header
206
+ /<sip:(.+@)?(.+):(\d+);(.*)>/ =~ header
207
+ sock = TCPSocket.new $2, $3
208
+ return TCPSource.new sock
209
+ end
210
+
211
+ def register username=@username, password=@password, expires="3600"
212
+ @username, @password = username, password
213
+ set_callee(@uri)
214
+ send_request("REGISTER", nil, nil, { "Expires" => expires.to_s })
215
+ response_data = recv_response("401|200")
216
+ if response_data['message'].status_code == "401"
217
+ send_request("ACK")
218
+ auth_hdr = gen_auth_header response_data['message'].header("WWW-Authenticate"), username, password, "REGISTER", @uri
219
+ update_branch
220
+ send_request("REGISTER", nil, nil, {"Authorization" => auth_hdr, "Expires" => expires.to_s})
221
+ recv_response("200")
222
+ end
223
+ return true
224
+ end
225
+
226
+ def unregister
227
+ register @username, @password, 0
228
+ end
229
+
230
+ end
data/lib/endpoint.rb ADDED
@@ -0,0 +1,175 @@
1
+ # -*- coding: us-ascii -*-
2
+ require 'socket'
3
+ require 'thread'
4
+ require 'timeout'
5
+ require_relative './sip_parser.rb'
6
+ require_relative './sources.rb'
7
+
8
+ class BaseEndpoint
9
+ attr_accessor :msg_trace
10
+
11
+ def terminate
12
+ end
13
+
14
+ def add_sock sock
15
+ end
16
+
17
+ def new_call call_id=nil, *args
18
+ call_id ||= get_new_call_id
19
+ puts "Call-Id for endpoint on #{@lport} is #{call_id}" if @msg_trace
20
+ Call.new(self, call_id, *args)
21
+ end
22
+
23
+ def initialize(lport)
24
+ @lport = lport
25
+ initialize_connection lport
26
+ initialize_queues
27
+ start
28
+ end
29
+
30
+ def local_port
31
+ @lport
32
+ end
33
+
34
+ def initialize_queues
35
+ @messages = {}
36
+ @call_ids = Queue.new
37
+ @dead_calls = {}
38
+ @sockets
39
+ end
40
+
41
+ def start
42
+ Thread.new do
43
+ loop do
44
+ recv_msg
45
+ end
46
+ end
47
+ end
48
+
49
+ def queue_msg(msg, source)
50
+ puts "Endpoint on #{@lport} received #{msg} from #{source.inspect}" if @msg_trace
51
+ cid = @parser.message_identifier msg
52
+ if cid and not @dead_calls.has_key? cid then
53
+ unless @messages.has_key? cid then
54
+
55
+ add_call_id cid
56
+ @call_ids.enq cid
57
+ end
58
+ @messages[cid].enq({"message" => msg, "source" => source})
59
+ end
60
+ end
61
+
62
+ def add_call_id cid
63
+ @messages[cid] ||= Queue.new
64
+ end
65
+
66
+ def get_new_call_id time_limit=5
67
+ Timeout::timeout(time_limit) { @call_ids.deq }
68
+ end
69
+
70
+ def get_new_message(cid, time_limit=5)
71
+ Timeout::timeout(time_limit) { @messages[cid].deq }
72
+ end
73
+
74
+ def mark_call_dead(cid)
75
+ @messages.delete cid
76
+ now = Time.now
77
+ @dead_calls[cid] = now + 30
78
+ @dead_calls = @dead_calls.keep_if {|k, v| v > now}
79
+ end
80
+
81
+ def send(data, source)
82
+ puts "Endpoint on #{@lport} sending #{data} to #{source.inspect}" if @msg_trace
83
+ source.send(@cxn, data)
84
+ end
85
+
86
+ end
87
+
88
+ class TCPSIPEndpoint < BaseEndpoint
89
+ attr_accessor :sockets
90
+
91
+ def initialize_connection(lport)
92
+ @cxn = TCPServer.new(lport)
93
+ @parser = SipParser.new
94
+ @sockets = []
95
+ end
96
+
97
+ def transport
98
+ "TCP"
99
+ end
100
+
101
+ def new_source ip, port
102
+ return TCPSource.new ip, port
103
+ end
104
+
105
+ alias_method :new_connection, :new_source
106
+
107
+ def recv_msg
108
+ select_response = IO.select(@sockets, [], [], 0) || [[]]
109
+ readable = select_response[0]
110
+ for sock in readable do
111
+ recv_msg_from_sock sock
112
+ end
113
+ begin
114
+ if @cxn
115
+ sock = @cxn.accept_nonblock
116
+ @sockets.push sock if sock
117
+ end
118
+ rescue IO::WaitReadable, Errno::EINTR
119
+ sleep 0.3
120
+ end
121
+ end
122
+
123
+ def recv_msg_from_sock(sock)
124
+ @parser.parse_start
125
+ msg = nil
126
+ while msg.nil? and not sock.closed? do
127
+ line = sock.gets
128
+ msg = @parser.parse_partial line
129
+ end
130
+ queue_msg msg, TCPSourceFromSocket.new(sock)
131
+ end
132
+
133
+ def add_sock sock
134
+ @sockets.push sock
135
+ end
136
+
137
+ def terminate
138
+ oldsockets = @sockets.dup
139
+ @sockets = []
140
+ oldsockets.each do |s| s.close unless s.closed? end
141
+ mycxn = @cxn
142
+ @cxn = nil
143
+ mycxn.close
144
+ end
145
+
146
+ end
147
+
148
+ class UDPSIPEndpoint < BaseEndpoint
149
+
150
+ def recv_msg
151
+ data, addrinfo = @cxn.recvfrom(65535)
152
+ @parser.parse_start
153
+ msg = @parser.parse_partial(data)
154
+ queue_msg msg, UDPSourceFromAddrinfo.new(addrinfo) unless msg.nil?
155
+ end
156
+
157
+ def transport
158
+ "UDP"
159
+ end
160
+
161
+ def new_source ip, port
162
+ return UDPSource.new ip, port
163
+ end
164
+
165
+ alias_method :new_connection, :new_source
166
+
167
+ def initialize_connection(lport)
168
+ @cxn = UDPSocket.new
169
+ @cxn.bind('0.0.0.0', lport)
170
+ @sockets = []
171
+ @parser = SipParser.new
172
+ end
173
+
174
+ end
175
+
data/lib/quaff.rb ADDED
@@ -0,0 +1,2 @@
1
+ require_relative './call.rb'
2
+ require_relative './endpoint.rb'
data/lib/sip_parser.rb ADDED
@@ -0,0 +1,133 @@
1
+ # -*- coding: us-ascii -*-
2
+ require 'digest/md5'
3
+
4
+ class SipMessage
5
+ attr_accessor :type, :method, :requri, :reason, :status_code, :headers, :body
6
+
7
+ def initialize
8
+ @headers = {}
9
+ @method, @status_code, @reason, @req_uri = nil
10
+ @body = ""
11
+ end
12
+
13
+ def all_headers hdr
14
+ return @headers[hdr]
15
+ end
16
+
17
+ def header hdr
18
+ return @headers[hdr][0] unless @headers[hdr].nil?
19
+ end
20
+
21
+ alias_method :first_header, :header
22
+
23
+ def to_s
24
+ "#{@method} #{@status_code} #{@headers}"
25
+ end
26
+
27
+ end
28
+
29
+ class SipParser
30
+ attr_reader :state
31
+ def parse_start
32
+ @buf = ""
33
+ @msg = SipMessage.new
34
+ @state = :blank
35
+ end
36
+
37
+ def parse_partial(data)
38
+ return nil if data.nil?
39
+ data.lines.each do |line|
40
+ if @state == :blank
41
+ parse_line_blank line
42
+ elsif @state == :parsing_body
43
+ parse_line_body line
44
+ else
45
+ parse_line_first_line_parsed line
46
+ end
47
+ end
48
+ if @state == :done
49
+ return @msg
50
+ else
51
+ return nil
52
+ end
53
+ end
54
+
55
+ def message_identifier(msg)
56
+ msg.header("Call-ID")
57
+ end
58
+
59
+ def parse_line_blank line
60
+ if line =~ %r!^([A-Z]+) (.+) SIP/2.0\r$!
61
+ @msg.type = :request
62
+ @msg.method = $1
63
+ @msg.requri = $2
64
+ @state = :first_line_parsed
65
+ elsif line =~ %r!^SIP/2.0 (\d+) (.+)\r$!
66
+ @msg.type = :response
67
+ @msg.status_code = $1
68
+ @msg.reason = $3 || ""
69
+ @state = :first_line_parsed
70
+ elsif line == "\r" or line == "\r\n"
71
+ # skip empty lines
72
+ else
73
+ raise line.inspect
74
+ end
75
+ end
76
+
77
+ def parse_line_first_line_parsed line
78
+ if line =~ /^\s+(.+)\r/
79
+ @msg.headers[@cur_hdr][-1] += " "
80
+ @msg.headers[@cur_hdr][-1] += $1
81
+ if $1 == "Content-Length"
82
+ @state = :got_content_length
83
+ else
84
+ @state = :middle_of_headers
85
+ end
86
+ elsif line =~ /^([-\w]+)\s*:\s*(.+)\r/
87
+ @msg.headers[$1] ||= []
88
+ @msg.headers[$1].push $2
89
+ @cur_hdr = $1
90
+ if $1 == "Content-Length"
91
+ @state = :got_content_length
92
+ else
93
+ @state = :middle_of_headers
94
+ end
95
+ elsif line == "\r" or line == "\r\n"
96
+ if @state == :got_content_length and @msg.header("Content-Length").to_i > 0
97
+ @state = :parsing_body
98
+ else
99
+ @state = :done
100
+ end
101
+ else raise line.inspect
102
+ end
103
+ end
104
+
105
+ def parse_line_body line
106
+ @msg.body << line
107
+ if line == "\r" or @msg.body.length >= @msg.header("Content-Length").to_i
108
+ @state = :done
109
+ end
110
+ end
111
+
112
+ end
113
+
114
+ def gen_nonce auth_pairs, username, passwd, method, sip_uri
115
+ a1 = username + ":" + auth_pairs["realm"] + ":" + passwd
116
+ a2 = method + ":" + sip_uri
117
+ ha1 = Digest::MD5::hexdigest(a1)
118
+ ha2 = Digest::MD5::hexdigest(a2)
119
+ digest = Digest::MD5.hexdigest(ha1 + ":" + auth_pairs["nonce"] + ":" + ha2)
120
+ return digest
121
+ end
122
+
123
+ def gen_auth_header auth_line, username, passwd, method, sip_uri
124
+ # Split auth line on commas
125
+ auth_pairs = {}
126
+ auth_line.sub("Digest ", "").split(",") .each do |pair|
127
+ key, value = pair.split "="
128
+ auth_pairs[key.gsub(" ", "")] = value.gsub("\"", "").gsub(" ", "")
129
+ end
130
+ digest = gen_nonce auth_pairs, username, passwd, method, sip_uri
131
+ return %Q!Digest username="#{username}",realm="#{auth_pairs['realm']}",nonce="#{auth_pairs['nonce']}",uri="#{sip_uri}",response="#{digest}",algorithm="#{auth_pairs['algorithm']}",opaque="#{auth_pairs['opaque']}"!
132
+ # Return Authorization header with fields username, realm, nonce, uri, nc, cnonce, response, opaque
133
+ end
data/lib/sources.rb ADDED
@@ -0,0 +1,60 @@
1
+ require 'socket'
2
+
3
+ class Source
4
+ def remote_ip
5
+ @ip
6
+ end
7
+
8
+ def remote_port
9
+ @port
10
+ end
11
+
12
+ def close cxn
13
+ end
14
+
15
+ def sock
16
+ nil
17
+ end
18
+ end
19
+
20
+ class UDPSource < Source
21
+ def initialize ip, port
22
+ @ip, @port = ip, port
23
+ end
24
+
25
+ def send cxn, data
26
+ cxn.send(data, 0, @ip, @port)
27
+ end
28
+ end
29
+
30
+ class UDPSourceFromAddrinfo < UDPSource
31
+ def initialize addrinfo
32
+ @ip, @port = addrinfo[3], addrinfo[1]
33
+ end
34
+ end
35
+
36
+
37
+ class TCPSource < Source
38
+ attr_reader :sock
39
+
40
+ def initialize ip, port
41
+ @sock = TCPSocket.new ip, port
42
+ @port, @ip = port, ip
43
+ end
44
+
45
+ def send _, data
46
+ @sock.puts data
47
+ end
48
+
49
+ def close cxn
50
+ @sock.close
51
+ cxn.sockets.delete(@sock)
52
+ end
53
+ end
54
+
55
+ class TCPSourceFromSocket < TCPSource
56
+ def initialize sock
57
+ @sock = sock
58
+ @port, @ip = Socket.unpack_sockaddr_in(@sock.getpeername)
59
+ end
60
+ end
data/lib/utils.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'socket'
2
+
3
+ module QuaffUtils
4
+ def QuaffUtils.local_ip
5
+ Socket.ip_address_list.select {|i| !(i.ipv6? || i.ipv4_loopback?)}[0].ip_address
6
+ end
7
+
8
+ def local_ipv6
9
+ Socket.ip_address_list.select {|i| !(i.ipv4? || i.ipv6_loopback?)}[0].ip_address
10
+ end
11
+
12
+ def pid
13
+ Process.pid
14
+ end
15
+
16
+ def new_call_id
17
+ "#{pid}_#{Time.new.to_i}@#{local_ipv4}"
18
+ end
19
+
20
+ def QuaffUtils.new_branch
21
+ "z9hG4bK#{Time.new.to_f}"
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: quaff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Rob Day
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-24 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: A Ruby library for writing SIP test scenarios
15
+ email: rkd@rkd.me.uk
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/quaff.rb
21
+ - lib/utils.rb
22
+ - lib/call.rb
23
+ - lib/sources.rb
24
+ - lib/sip_parser.rb
25
+ - lib/endpoint.rb
26
+ homepage: http://github.com/rkd91/quaff
27
+ licenses:
28
+ - GPL3
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 1.8.23
48
+ signing_key:
49
+ specification_version: 3
50
+ summary: Quaff
51
+ test_files: []