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 +230 -0
- data/lib/endpoint.rb +175 -0
- data/lib/quaff.rb +2 -0
- data/lib/sip_parser.rb +133 -0
- data/lib/sources.rb +60 -0
- data/lib/utils.rb +23 -0
- metadata +51 -0
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
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: []
|