quaff 0.1.0 → 0.2.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/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:
|