quaff 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []