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