sip-notify 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.md +4 -0
  2. data/bin/sip-notify +138 -0
  3. data/lib/sip_notify.rb +855 -0
  4. metadata +65 -0
@@ -0,0 +1,4 @@
1
+ Sends SIP `NOTIFY` events ("`check-sync`" etc.).
2
+
3
+ Author: Philipp Kempgen, [http://kempgen.net](http://kempgen.net)
4
+
@@ -0,0 +1,138 @@
1
+ #! /usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ require 'optparse'
5
+
6
+ $LOAD_PATH.unshift( File.join( File.dirname( __FILE__ ), '..', 'lib' ))
7
+
8
+ require 'sip_notify'
9
+
10
+
11
+ opts_defaults = {
12
+ :port => 5060,
13
+ :user => nil,
14
+ :verbosity => 0,
15
+ :event => 'check-sync;reboot=false',
16
+ :spoof_src_addr => nil,
17
+ }
18
+ opts = {}
19
+
20
+ opts_parser = ::OptionParser.new { |op|
21
+ op.banner = "Usage: #{ ::File.basename(__FILE__) } HOST [options]"
22
+
23
+ op.on( "-p", "--port=PORT", Integer,
24
+ "Port. (Default: #{opts_defaults[:port].inspect})"
25
+ ) { |v|
26
+ opts[:port] = v.to_i
27
+ if ! v.between?( 1, 65535 )
28
+ $stderr.puts "Invalid port."
29
+ $stderr.puts op
30
+ exit 1
31
+ end
32
+ }
33
+
34
+ op.on( "-u", "--user=USER", String,
35
+ "User/extension. (Default: #{opts_defaults[:user].inspect})"
36
+ ) { |v|
37
+ opts[:user] = v
38
+ }
39
+
40
+ op.on( "-e", "--event=EVENT", String,
41
+ "Event. (Default: #{opts_defaults[:event].inspect})"
42
+ ) { |v|
43
+ opts[:event] = v
44
+ }
45
+
46
+ op.on( "-t", "--type=EVENT_TYPE", String,
47
+ "Pre-defined event type."
48
+ ) { |v|
49
+ opts[:event_type] = v
50
+ }
51
+
52
+ op.on( "--types", String,
53
+ "List pre-defined event types."
54
+ ) { |v|
55
+ puts "Pre-defined event types:"
56
+ puts " #{'Name'.to_s.ljust(32)} #{'Event header'.to_s.ljust(30)} #{'Content-Type header'.to_s.ljust(35)} +"
57
+ puts " #{'-' * 32} #{'-' * 30} #{'-' * 35} #{'-' * 1}"
58
+ ::SipNotify.event_templates.each { |name, info|
59
+ puts " #{name.to_s.ljust(32)} #{info[:event].to_s.ljust(30)} #{info[:content_type].to_s.ljust(35)} #{info[:content] ? '+' : ' '}"
60
+ }
61
+ exit 0
62
+ }
63
+
64
+ op.on( "--spoof-src-addr=SOURCE_ADDRESS", String,
65
+ "Spoof source IP address. (Must be run as root.)"
66
+ ) { |v|
67
+ opts[:spoof_src_addr] = v
68
+ }
69
+
70
+ op.on_tail( "-v", "--verbose", "Increase verbosity level. Can be repeated." ) { |v|
71
+ opts[:verbosity] ||= 0
72
+ opts[:verbosity] += 1
73
+ }
74
+
75
+ op.on_tail("-?", "-h", "--help", "Show this help message." ) {
76
+ puts op
77
+ exit 0
78
+ }
79
+
80
+ }
81
+ begin
82
+ opts_parser.parse!
83
+ rescue ::OptionParser::ParseError => e
84
+ $stderr.puts e.message
85
+ $stderr.puts opts_parser
86
+ exit 1
87
+ end
88
+
89
+ opts[:host] = ::ARGV[0]
90
+
91
+ if ! opts[:host]
92
+ $stderr.puts "Missing host argument."
93
+ $stderr.puts opts_parser
94
+ exit 1
95
+ end
96
+
97
+ if opts[:event] && opts[:event_type]
98
+ $stderr.puts "Event and event type arguments don't make sense together."
99
+ $stderr.puts opts_parser
100
+ exit 1
101
+ end
102
+
103
+ opts = opts_defaults.merge( opts )
104
+ opts[:domain] = opts[:host]
105
+
106
+ if opts[:event_type]
107
+ et = ::SipNotify.event_templates[ opts[:event_type].to_sym ]
108
+ if ! et
109
+ $stderr.puts "Event type not found: #{opts[:event_type].inspect}"
110
+ exit 1
111
+ end
112
+ opts[:event] = et[:event]
113
+ opts[:content_type] = et[:content_type]
114
+ opts[:content] = et[:content]
115
+ end
116
+
117
+ begin
118
+ ::SipNotify.send_variations( opts[:host], {
119
+ :port => opts[:port],
120
+ :user => opts[:user],
121
+ :domain => opts[:domain],
122
+ :verbosity => opts[:verbosity],
123
+ :via_rport => true,
124
+ :event => opts[:event],
125
+ :content_type => opts[:content_type],
126
+ :content => opts[:content],
127
+ :spoof_src_addr => opts[:spoof_src_addr],
128
+ })
129
+ rescue ::SipNotifyError => e
130
+ $stderr.puts "Error: #{e.message}"
131
+ exit 1
132
+ end
133
+
134
+
135
+ # Local Variables:
136
+ # mode: ruby
137
+ # End:
138
+
@@ -0,0 +1,855 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Send SIP "NOTIFY" "check-sync" events.
4
+
5
+ require 'socket'
6
+ require 'securerandom'
7
+ require 'rbconfig'
8
+ require 'bindata'
9
+
10
+
11
+ class SipNotifyError < RuntimeError; end
12
+
13
+ class SipNotify
14
+
15
+ REQUEST_METHOD = 'NOTIFY'.freeze
16
+ VIA_BRANCH_TOKEN_MAGIC = 'z9hG4bK'.freeze # http://tools.ietf.org/html/rfc3261#section-8.1.1.7
17
+
18
+ # Whether the length in a raw IP packet must be little-endian
19
+ # (i.e. native-endian) and the Kernel auto-reverses the value.
20
+ # IP spec. says big-endian (i.e. network order).
21
+ RAW_IP_PKT_LITTLE_ENDIAN_LENGTH = !! ::RbConfig::CONFIG['host_os'].to_s.match( /\A darwin/xi )
22
+
23
+ # Whether the fragment offset in a raw IP packet must be
24
+ # little-endian (i.e. native-endian) and the Kernel
25
+ # auto-reverses the value. IP spec. says big-endian (i.e.
26
+ # network order).
27
+ RAW_IP_PKT_LITTLE_ENDIAN_FRAG_OFF = !! ::RbConfig::CONFIG['host_os'].to_s.match( /\A darwin/xi )
28
+
29
+ SOCK_OPT_LEVEL_IP = (
30
+ if defined?( ::Socket::SOL_IP )
31
+ ::Socket::SOL_IP # Linux
32
+ else
33
+ ::Socket::IPPROTO_IP # Solaris, BSD, Darwin
34
+ end
35
+ )
36
+
37
+ # Pre-defined event types.
38
+ #
39
+ EVENT_TEMPLATES = {
40
+
41
+ # Compatibility.
42
+ # The most widely implemented event:
43
+ :'compat-check-cfg' => { :event => 'check-sync' },
44
+
45
+ # Snom:
46
+ :'snom-check-cfg' => { :event => 'check-sync;reboot=false' },
47
+ :'snom-reboot' => { :event => 'reboot' },
48
+
49
+ # Polycom:
50
+ # In the Polycom's sip.cfg make sure that
51
+ # voIpProt.SIP.specialEvent.checkSync.alwaysReboot="0"
52
+ # (0 will only reboot if the files on the settings server have changed.)
53
+ :'polycom-check-cfg' => { :event => 'check-sync' },
54
+ :'polycom-reboot' => { :event => 'check-sync' },
55
+
56
+ # Aastra:
57
+ :'aastra-check-cfg' => { :event => 'check-sync' },
58
+ :'aastra-reboot' => { :event => 'check-sync' },
59
+ :'aastra-xml' => { :event => 'aastra-xml' }, # triggers the XML SIP NOTIFY action URI
60
+
61
+ # Sipura by Linksys by Cisco:
62
+ :'sipura-check-cfg' => { :event => 'resync' },
63
+ :'sipura-reboot' => { :event => 'reboot' },
64
+ :'sipura-get-report' => { :event => 'report' },
65
+
66
+ # Linksys by Cisco:
67
+ :'linksys-check-cfg' => { :event => 'restart_now' }, # warm restart
68
+ :'linksys-reboot' => { :event => 'reboot_now' }, # cold reboot
69
+
70
+ # Cisco:
71
+ # In order for "cisco-check-cfg"/"cisco-reboot" to work make
72
+ # sure the syncinfo.xml has a different sync number than the
73
+ # phone's current sync number (e.g. by setting the "sync"
74
+ # parameter to "0" and having "1" in syncinfo.xml). If that
75
+ # is the case the phone will reboot (no check-sync without
76
+ # reboot).
77
+ #
78
+ # http://dxr.mozilla.org/mozilla-central/media/webrtc/signaling/src/sipcc/core/sipstack/ccsip_task.c#l2358
79
+ # http://dxr.mozilla.org/mozilla-central/media/webrtc/signaling/src/sipcc/core/sipstack/h/ccsip_protocol.h#l196
80
+ # http://dxr.mozilla.org/mozilla-central/media/webrtc/signaling/src/sipcc/core/sipstack/h/ccsip_protocol.h#l224
81
+ # http://dxr.mozilla.org/mozilla-central/media/webrtc/signaling/src/sipcc/core/sipstack/ccsip_pmh.c#l4978
82
+ # https://issues.asterisk.org/jira/secure/attachment/32746/sip-trace-7941-9-1-1SR1.txt
83
+ # telnet: "debug sip-task"
84
+
85
+ :'cisco-check-cfg' => { :event => 'check-sync' },
86
+ :'cisco-reboot' => { :event => 'check-sync' },
87
+
88
+ # Phone must be in CCM (Cisco Call Manager) registration
89
+ # mode (TCP) for "service-control" events to work, and the
90
+ # firmware must be >= 8.0. 7905/7912/7949/7969.
91
+
92
+ # Causes the phone to unregister, request config file
93
+ # (SIP{mac}.cnf), register.
94
+ :'cisco-sc-restart-unregistered' => { :event => 'service-control',
95
+ :content_type => 'text/plain',
96
+ :content => [
97
+ 'action=' + 'restart',
98
+ 'RegisterCallId=' + '{' + '' + '}', # not registered
99
+ 'ConfigVersionStamp=' + '{' + '0000000000000000' + '}',
100
+ 'DialplanVersionStamp=' + '{' + '0000000000000000' + '}',
101
+ 'SoftkeyVersionStamp=' + '{' + '0000000000000000' + '}',
102
+ '',
103
+ ].join("\r\n"),
104
+ },
105
+
106
+ #:'cisco-sc-restart' => { :event => 'service-control',
107
+ # :content_type => 'text/plain',
108
+ # :content => [
109
+ # 'action=' + 'restart',
110
+ # # 'RegisterCallId=' + '{' + '${SIPPEER(${PEERNAME},regcallid)}' '}',
111
+ # 'RegisterCallId=' + '{0022555d-aa850002-a2b54180-bb702fe7@192.168.1.130}',
112
+ # #00119352-91f60002-1df7530e-4190441e@192.168.1.130
113
+ # #00119352-91f60041-4a7455b9-00e0f121@192.168.1.130
114
+ # 'ConfigVersionStamp=' + '{' + '0000000000000000' + '}',
115
+ # 'DialplanVersionStamp=' + '{' + '0000000000000000' + '}',
116
+ # 'SoftkeyVersionStamp=' + '{' + '0000000000000000' + '}',
117
+ # '',
118
+ # ].join("\r\n"),
119
+ #},
120
+
121
+ # Causes the phone to unregister and do a full reboot cycle.
122
+ :'cisco-sc-reset-unregistered' => { :event => 'service-control',
123
+ :content_type => 'text/plain',
124
+ :content => [
125
+ 'action=' + 'reset',
126
+ 'RegisterCallId=' + '{' + '' + '}', # not registered
127
+ 'ConfigVersionStamp=' + '{' + '0000000000000000' + '}',
128
+ 'DialplanVersionStamp=' + '{' + '0000000000000000' + '}',
129
+ 'SoftkeyVersionStamp=' + '{' + '0000000000000000' + '}',
130
+ '',
131
+ ].join("\r\n"),
132
+ },
133
+
134
+ #:'cisco-sc-reset' => { :event => 'service-control',
135
+ # :content_type => 'text/plain',
136
+ # :content => [
137
+ # 'action=' + 'reset',
138
+ # # 'RegisterCallId=' + '{' + '${SIPPEER(${PEERNAME},regcallid)}' '}',
139
+ # 'RegisterCallId=' + '{123}',
140
+ # 'ConfigVersionStamp=' + '{' + '0000000000000000' + '}',
141
+ # 'DialplanVersionStamp=' + '{' + '0000000000000000' + '}',
142
+ # 'SoftkeyVersionStamp=' + '{' + '0000000000000000' + '}',
143
+ # '',
144
+ # ].join("\r\n"),
145
+ #},
146
+
147
+ # Causes the phone to unregister, request dialplan
148
+ # (dialplan.xml) and config file (SIP{mac}.cnf), register.
149
+ # This is the action to send if the config "file" on the
150
+ # TFTP server changes.
151
+ :'cisco-sc-check-unregistered' => { :event => 'service-control',
152
+ :content_type => 'text/plain',
153
+ :content => [
154
+ 'action=' + 'check-version',
155
+ 'RegisterCallId=' + '{' + '' + '}', # not registered
156
+ 'ConfigVersionStamp=' + '{' + '0000000000000000' + '}',
157
+ 'DialplanVersionStamp=' + '{' + '0000000000000000' + '}',
158
+ 'SoftkeyVersionStamp=' + '{' + '0000000000000000' + '}',
159
+ '',
160
+ ].join("\r\n"),
161
+ },
162
+
163
+ #:'cisco-sc-apply-config' => { :event => 'service-control',
164
+ # :content_type => 'text/plain',
165
+ # :content => [
166
+ # 'action=' + 'apply-config',
167
+ # 'RegisterCallId=' + '{' + '' + '}', # not registered
168
+ # 'ConfigVersionStamp=' + '{' + '0000000000000000' + '}',
169
+ # 'DialplanVersionStamp=' + '{' + '0000000000000000' + '}',
170
+ # 'SoftkeyVersionStamp=' + '{' + '0000000000000000' + '}',
171
+ # 'FeatureControlVersionStamp=' + '{' + '0000000000000000' + '}',
172
+ # 'CUCMResult=' + '{' + 'config_applied' + '}', # "no_change" / "config_applied" / "reregister_needed"
173
+ # 'FirmwareLoadId=' + '{' + 'SIP70.8-4-0-28S' + '}',
174
+ # 'LoadServer=' + '{' + '192.168.1.97' + '}',
175
+ # 'LogServer=' + '{' + '192.168.1.97' + '}', # <ipv4 address or ipv6 address or fqdn> <port> // This is used for ppid
176
+ # 'PPID=' + '{' + 'disabled' + '}', # "enabled" / "disabled" // peer-to-peer upgrade
177
+ # '',
178
+ # ].join("\r\n"),
179
+ #},
180
+
181
+ #:'cisco-sc-call-preservation' => { :event => 'service-control',
182
+ # :content_type => 'text/plain',
183
+ # :content => [
184
+ # 'action=' + 'call-preservation',
185
+ # 'RegisterCallId=' + '{' + '' + '}', # not registered
186
+ # 'ConfigVersionStamp=' + '{' + '0000000000000000' + '}',
187
+ # 'DialplanVersionStamp=' + '{' + '0000000000000000' + '}',
188
+ # 'SoftkeyVersionStamp=' + '{' + '0000000000000000' + '}',
189
+ # '',
190
+ # ].join("\r\n"),
191
+ #},
192
+
193
+ # Grandstream:
194
+ :'grandstream-check-cfg' => { :event => 'sys-control' },
195
+ :'grandstream-reboot' => { :event => 'sys-control' },
196
+ :'grandstream-idle-screen-refresh' => { :event => 'x-gs-screen' },
197
+
198
+ # Gigaset (Pro Nxxx):
199
+ :'gigaset-check-cfg' => { :event => 'check-sync;reboot=false' },
200
+ :'gigaset-reboot' => { :event => 'check-sync;reboot=true' },
201
+
202
+ # Siemens (Enterprise Networks) OpenStage:
203
+ :'siemens-check-cfg' => { :event => 'check-sync;reboot=false' },
204
+ :'siemens-reboot' => { :event => 'check-sync;reboot=true' },
205
+
206
+ # Yealink:
207
+ :'yealink-check-cfg' => { :event => 'check-sync;reboot=true' }, #OPTIMIZE can do without reboot?
208
+ :'yealink-reboot' => { :event => 'check-sync;reboot=true' },
209
+
210
+ # Thomson (ST2030?):
211
+ :'thomson-check-cfg' => { :event => 'check-sync;reboot=false' },
212
+ :'thomson-reboot' => { :event => 'check-sync;reboot=true' },
213
+ :'thomson-talk' => { :event => 'talk' },
214
+ :'thomson-hold' => { :event => 'hold' },
215
+
216
+ # Misc:
217
+ #
218
+
219
+ :'mwi-clear-full' => { :event => 'message-summary',
220
+ :content_type => 'application/simple-message-summary',
221
+ :content => [
222
+ 'Messages-Waiting: ' + 'no', # "yes"/"no"
223
+ # 'Message-Account: sip:voicemail@127.0.0.1',
224
+ 'voice-message' + ': 0/0 (0/0)',
225
+ 'fax-message' + ': 0/0 (0/0)',
226
+ 'pager-message' + ': 0/0 (0/0)',
227
+ 'multimedia-message' + ': 0/0 (0/0)',
228
+ 'text-message' + ': 0/0 (0/0)',
229
+ 'none' + ': 0/0 (0/0)',
230
+ '',
231
+ ],#.join("\r\n"),
232
+ },
233
+
234
+ :'mwi-clear-simple' => { :event => 'message-summary',
235
+ :content_type => 'application/simple-message-summary',
236
+ :content => [
237
+ 'Messages-Waiting: ' + 'no', # "yes"/"no"
238
+ # 'Message-Account: sip:voicemail@127.0.0.1',
239
+ 'voice-message' + ': 0/0',
240
+ '',
241
+ ],#.join("\r\n"),
242
+ },
243
+
244
+ :'mwi-test-full' => { :event => 'message-summary',
245
+ :content_type => 'application/simple-message-summary',
246
+ :content => [
247
+ 'Messages-Waiting: ' + 'yes', # "yes"/"no"
248
+ # 'Message-Account: sip:voicemail@127.0.0.1',
249
+ 'voice-message' + ': 3/4 (1/2)',
250
+ 'fax-message' + ': 3/4 (1/2)',
251
+ 'pager-message' + ': 3/4 (1/2)',
252
+ 'multimedia-message' + ': 3/4 (1/2)',
253
+ 'text-message' + ': 3/4 (1/2)',
254
+ 'none' + ': 3/4 (1/2)',
255
+ '',
256
+ ],#.join("\r\n"),
257
+ },
258
+
259
+ :'mwi-test-simple' => { :event => 'message-summary',
260
+ :content_type => 'application/simple-message-summary',
261
+ :content => [
262
+ 'Messages-Waiting: ' + 'yes', # "yes"/"no"
263
+ # 'Message-Account: sip:voicemail@127.0.0.1',
264
+ 'voice-message' + ': 3/4',
265
+ '',
266
+ ],#.join("\r\n"),
267
+ },
268
+
269
+ }
270
+
271
+ def initialize( host, opts=nil )
272
+ re_initialize!( host, opts )
273
+ end
274
+
275
+ def re_initialize!( host, opts=nil )
276
+ @opts = {
277
+ :host => host,
278
+ :domain => host,
279
+ :port => 5060,
280
+ :user => nil,
281
+ :to_user => nil,
282
+ :verbosity => 0,
283
+ :via_rport => true,
284
+ :event => nil,
285
+ :content_type => nil,
286
+ :content => nil,
287
+ }.merge( opts || {} )
288
+ self
289
+ end
290
+
291
+ def self.event_templates
292
+ EVENT_TEMPLATES
293
+ end
294
+
295
+ # DSCP value (Differentiated Services Code Point).
296
+ #
297
+ def ip_dscp
298
+ @ip_dscp ||= 0b110_000 # == 48 == 0x30 == DSCP CS6 ~= IP Precedence 5
299
+ end
300
+
301
+ # IP ToS value (Type of Service).
302
+ #
303
+ def ip_tos
304
+ @ip_tos ||= (ip_dscp << 2)
305
+ end
306
+
307
+ # Create a socket.
308
+ #
309
+ def socket
310
+ if @socket
311
+ if @socket_is_raw
312
+ if (! @opts[:spoof_src_addr]) || @opts[:spoof_src_addr].empty?
313
+ @socket = nil
314
+ end
315
+ else
316
+ if @opts[:spoof_src_addr]
317
+ @socket = nil
318
+ end
319
+ end
320
+ end
321
+
322
+ if @opts[:spoof_src_addr]
323
+ unless @raw_socket
324
+ #local_addr, local_port = * our_addr
325
+ #if @opts[:spoof_src_addr] != local_addr
326
+ puts "Spoofing source IP address: #{@opts[:spoof_src_addr].inspect}." if @opts[:verbosity] >= 1
327
+ @raw_socket = raw_socket
328
+ #end
329
+ end
330
+ @socket = @raw_socket
331
+ @socket_is_raw = true
332
+ return @raw_socket
333
+ end
334
+
335
+ unless @socket
336
+ begin
337
+ ::BasicSocket.do_not_reverse_lookup = true
338
+
339
+ @socket = ::Socket.new( ::Socket::AF_INET, ::Socket::SOCK_DGRAM )
340
+ @socket.setsockopt( ::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1 )
341
+ @socket.setsockopt( SOCK_OPT_LEVEL_IP, ::Socket::IP_TOS, ip_tos )
342
+ @socket.setsockopt( SOCK_OPT_LEVEL_IP, ::Socket::IP_TTL, 255 ) # default 64
343
+ #@socket.settimeout( 1.0 )
344
+
345
+ rescue ::SystemCallError, ::SocketError, ::IOError => e
346
+ socket_destroy!
347
+ raise ::SipNotifyError.new( "Failed to create socket: #{e.message} (#{e.class.name})" )
348
+ end
349
+
350
+ begin
351
+ sock_addr = ::Socket.sockaddr_in( @opts[:port], @opts[:host] )
352
+ @socket.connect( sock_addr )
353
+
354
+ rescue ::SystemCallError, ::SocketError, ::IOError => e
355
+ socket_destroy!
356
+ raise ::SipNotifyError.new( "Failed to connect socket to %{addr}: #{e.message} (#{e.class.name})" % {
357
+ :addr => ip_addr_and_port_url_repr( @opts[:host], @opts[:port] ),
358
+ })
359
+ end
360
+ end
361
+
362
+ @socket_is_raw = false
363
+ return @socket
364
+ end
365
+ private :socket
366
+
367
+ # Close and unset the socket.
368
+ #
369
+ def socket_destroy!
370
+ @socket.close() if @socket && ! @socket.closed?
371
+ @socket = nil
372
+ end
373
+
374
+ # Create a raw socket.
375
+ #
376
+ def raw_socket
377
+ begin
378
+ ::BasicSocket.do_not_reverse_lookup = true
379
+
380
+ #sock = ::Socket.new( ::Socket::PF_INET, ::Socket::SOCK_RAW, ::Socket::IPPROTO_RAW )
381
+ sock = ::Socket.new( ::Socket::PF_INET, ::Socket::SOCK_RAW, ::Socket::IPPROTO_UDP )
382
+
383
+ # Make sure IP_HDRINCL is set on the raw socket,
384
+ # otherwise the kernel would prepend outbound packets
385
+ # with an IP header.
386
+ # https://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man4/ip.4.html
387
+ #
388
+ so = sock.getsockopt( SOCK_OPT_LEVEL_IP, ::Socket::IP_HDRINCL )
389
+ if so.bool == false || so.int == 0 || so.data == [0].pack('L')
390
+ #puts "IP_HDRINCL is supposed to be the default for IPPROTO_RAW."
391
+ # ... not on Darwin though.
392
+ sock.setsockopt( SOCK_OPT_LEVEL_IP, ::Socket::IP_HDRINCL, true )
393
+ end
394
+
395
+ sock.setsockopt( ::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1 )
396
+
397
+ rescue ::Errno::EPERM => e
398
+ $stderr.puts "Must be run as root."
399
+ raise ::SipNotifyError.new( "Failed to create socket: #{e.message} (#{e.class.name})" )
400
+ rescue ::SystemCallError, ::SocketError, ::IOError => e
401
+ raise ::SipNotifyError.new( "Failed to create socket: #{e.message} (#{e.class.name})" )
402
+ end
403
+
404
+ return sock
405
+ end
406
+
407
+ # The socket type.
408
+ #
409
+ # `nil` if no socket.
410
+ # 1 = Socket::SOCK_STREAM
411
+ # 2 = Socket::SOCK_DGRAM
412
+ # 3 = Socket::SOCK_RAW
413
+ # 4 = Socket::SOCK_RDM
414
+ # 5 = Socket::SOCK_SEQPACKET
415
+ #
416
+ def socket_type
417
+ @socket ? @socket.local_address.socktype : nil
418
+ end
419
+
420
+ # The UDP source port number to use for spoofed packets.
421
+ #
422
+ def spoof_src_port
423
+ # Note:
424
+ # Port 5060 is likely to cause the actual PBX/proxy receive
425
+ # responses for out NOTIFY requests.
426
+ # Port 0 is a valid port to use for UDP if responses are to
427
+ # be irgnored, but is likely to be taken for an invalid port
428
+ # number in devices.
429
+ 65535
430
+ end
431
+ private :spoof_src_port
432
+
433
+ # Return out address.
434
+ #
435
+ def our_addr
436
+ if @opts[:spoof_src_addr]
437
+ @our_addr = [ @opts[:spoof_src_addr], spoof_src_port ]
438
+ end
439
+
440
+ unless @our_addr
441
+ our_sock_addr = socket.getsockname()
442
+ local_port, local_addr = * ::Socket.unpack_sockaddr_in( our_sock_addr )
443
+ @our_addr = [ local_addr, local_port ]
444
+ end
445
+
446
+ @our_addr
447
+ end
448
+ private :our_addr
449
+
450
+ # Returns an IP address (or hostname) in URL representation.
451
+ # I.e. an IPv6 address will be enclosed in "["..."]".
452
+ #
453
+ def self.ip_addr_url_repr( host )
454
+ host.include?(':') ? "[#{host}]" : host.to_s
455
+ end
456
+
457
+ # Shortcut as an instance method
458
+ #
459
+ def ip_addr_url_repr( host )
460
+ self.class.ip_addr_url_repr( host )
461
+ end
462
+
463
+ # Returns an IP address (or hostname) and port number in URL
464
+ # representation.
465
+ # I.e. an IPv6 address will be enclosed in "["..."]".
466
+ #
467
+ def self.ip_addr_and_port_url_repr( host, port )
468
+ "%{addr}:%{port}" % { :addr => ip_addr_url_repr( host ), :port => port }
469
+ end
470
+
471
+ # Shortcut as an instance method
472
+ #
473
+ def ip_addr_and_port_url_repr( host, port )
474
+ self.class.ip_addr_and_port_url_repr( host, port )
475
+ end
476
+
477
+ # Send the SIP NOTIFY message.
478
+ #
479
+ def send
480
+ begin
481
+ sip_msg = to_s
482
+ if @opts[:verbosity] >= 3
483
+ puts "-----------------------------------------------------------------{"
484
+ puts sip_msg.gsub( /\r\n/, "\n" )
485
+ puts "-----------------------------------------------------------------}"
486
+ end
487
+
488
+ socket # Touch socket
489
+
490
+ case socket_type
491
+
492
+ when ::Socket::SOCK_DGRAM
493
+ num_bytes_written = nil
494
+ 2.times {
495
+ num_bytes_written = socket.write( sip_msg )
496
+ }
497
+
498
+ when ::Socket::SOCK_RAW
499
+ #local_addr, local_port = * our_addr
500
+
501
+ ::BasicSocket.do_not_reverse_lookup = true
502
+
503
+ src_addr = @opts[:spoof_src_addr]
504
+ src_addr_info = (::Addrinfo.getaddrinfo( src_addr, 'sip', nil, :DGRAM, ::Socket::IPPROTO_UDP, ::Socket::AI_V4MAPPED || ::Socket::AI_ALL ) || []).select{ |a|
505
+ a.afamily == ::Socket::AF_INET
506
+ }.first
507
+ src_sock_addr = src_addr_info.to_sockaddr
508
+
509
+ src_addr_ipv4_packed = src_sock_addr[4,4]
510
+ src_port_packed = src_sock_addr[2,2]
511
+
512
+ dst_addr = @opts[:host]
513
+ dst_addr_info = (::Addrinfo.getaddrinfo( dst_addr, 'sip', nil, :DGRAM, ::Socket::IPPROTO_UDP, ::Socket::AI_V4MAPPED || ::Socket::AI_ALL ) || []).select{ |a|
514
+ a.afamily == ::Socket::AF_INET
515
+ }.first
516
+ dst_sock_addr = dst_addr_info.to_sockaddr
517
+
518
+ dst_addr_ipv4_packed = dst_sock_addr[4,4]
519
+ dst_port_packed = dst_sock_addr[2,2]
520
+
521
+ #udp_pkt = UdpPktBitStruct.new { |b|
522
+ # b.src_port = spoof_src_port
523
+ # b.dst_port = 5060
524
+ # b.body = sip_msg.to_s
525
+ # b.udp_len = b.length
526
+ # b.udp_sum = 0
527
+ #}
528
+
529
+ udp_pkt = UdpPktBinData.new
530
+ udp_pkt.src_port = spoof_src_port
531
+ udp_pkt.dst_port = 5060
532
+ udp_pkt.data = sip_msg.to_s
533
+ udp_pkt.len = 4 + udp_pkt.data.bytesize
534
+ udp_pkt.checksum = 0 # none. UDP checksum is optional.
535
+
536
+ #ip_pkt = IpPktBitStruct.new { |b|
537
+ # # ip_v and ip_hl are set for us by IpPktBitStruct class
538
+ # b.ip_tos = ip_tos
539
+ # b.ip_id = 0
540
+ # b.ip_off = 0
541
+ # b.ip_ttl = 255 # default: 64
542
+ # b.ip_p = ::Socket::IPPROTO_UDP
543
+ # b.ip_src = @opts[:spoof_src_addr]
544
+ # b.ip_dst = @opts[:host]
545
+ # b.body = udp_pkt.to_s
546
+ # b.ip_len = b.length
547
+ # b.ip_sum = 0 # Linux/Darwin will calculate this for us (QNX won't)
548
+ #}
549
+
550
+ ip_pkt = IpPktBinData.new
551
+ ip_pkt.hdr_len = 5 # 5 * 8 bytes == 20 bytes
552
+ ip_pkt.tos = ip_tos
553
+ ip_pkt.ident = 0 # kernel sets appropriate value
554
+ ip_pkt.flags = 0
555
+ frag_off = 0
556
+ if RAW_IP_PKT_LITTLE_ENDIAN_FRAG_OFF && frag_off != 0
557
+ ip_pkt.frag_os = [ frag_off ].pack('n').unpack('S').first
558
+ else
559
+ ip_pkt.frag_os = frag_off
560
+ end
561
+ ip_pkt.ttl = 255 # default: 64
562
+ ip_pkt.proto = ::Socket::IPPROTO_UDP
563
+ ip_pkt.src_addr = src_addr_ipv4_packed .unpack('N').first
564
+ ip_pkt.dst_addr = dst_addr_ipv4_packed .unpack('N').first
565
+ ip_pkt.data = udp_pkt.to_binary_s
566
+ #len = (ip_pkt.hdr_len * 8) + ip_pkt.data.bytesize
567
+ len = ip_pkt.to_binary_s.bytesize
568
+ if RAW_IP_PKT_LITTLE_ENDIAN_LENGTH
569
+ ip_pkt.len = [ len ].pack('n').unpack('S').first
570
+ else
571
+ ip_pkt.len = len
572
+ end
573
+ ip_pkt.checksum = 0 # Linux/Darwin will calculate this for us (QNX won't)
574
+
575
+ #puts "-" * 80,
576
+ # "UDP packet:",
577
+ # udp_pkt.inspect,
578
+ # "-" * 80,
579
+ # udp_pkt.to_binary_s.inspect,
580
+ # "-" * 80
581
+
582
+ #puts "-" * 80,
583
+ # "IP packet:",
584
+ # ip_pkt.inspect,
585
+ # "-" * 80,
586
+ # ip_pkt.to_binary_s.inspect,
587
+ # "-" * 80
588
+
589
+ sock_addr = ::Socket.sockaddr_in( @opts[:port], @opts[:host] )
590
+
591
+ # Send 2 times. (UDP is an unreliable transport.)
592
+ num_bytes_written = nil
593
+ 2.times {
594
+ num_bytes_written = socket.send( ip_pkt.to_binary_s, 0, sock_addr )
595
+ }
596
+
597
+ else
598
+ raise ::SipNotifyError.new( "Socket type not supported." )
599
+ end
600
+
601
+ puts "Sent NOTIFY to #{@opts[:host]}." if @opts[:verbosity] >= 1
602
+ return num_bytes_written
603
+
604
+ rescue ::SystemCallError, ::SocketError, ::IOError => e
605
+ socket_destroy!
606
+ return false
607
+ end
608
+ end
609
+
610
+ # Generate a random token.
611
+ #
612
+ def self.random_token( num_bytes=5 )
613
+ ::SecureRandom.random_bytes( num_bytes ).unpack('H*').first
614
+ end
615
+
616
+ # Shortcut as an instance method.
617
+ #
618
+ def random_token
619
+ self.class.random_token
620
+ end
621
+ private :random_token
622
+
623
+ # Build the SIP message.
624
+ #
625
+ def to_s
626
+ local_addr, local_port = * our_addr
627
+ local_ip_addr_and_port_url_repr = ip_addr_and_port_url_repr( local_addr, local_port )
628
+ puts "Local address is: #{local_ip_addr_and_port_url_repr}" if @opts[:verbosity] >= 2
629
+
630
+ now = ::Time.now()
631
+
632
+ transport = 'UDP' # "UDP"/"TCP"/"TLS"/"SCTP"
633
+
634
+ ra = 60466176 # 100000 (36)
635
+ rb = 2000000000 # x2qxvk (36)
636
+
637
+ via_branch_token = VIA_BRANCH_TOKEN_MAGIC + '_' + random_token()
638
+
639
+ ruri_scheme = 'sip' # "sip"/"sips"
640
+ if @opts[:user] == nil
641
+ ruri_userinfo = ''
642
+ elsif @opts[:user] == ''
643
+ ruri_userinfo = "#{@opts[:user]}@"
644
+ # This isn't a valid SIP Request-URI as the user must not be
645
+ # empty if the userinfo is present ("@").
646
+ else
647
+ ruri_userinfo = "#{@opts[:user]}@"
648
+ end
649
+
650
+ ruri_hostport = ip_addr_and_port_url_repr( @opts[:host], @opts[:port] )
651
+ ruri = "#{ruri_scheme}:#{ruri_userinfo}#{ruri_hostport}"
652
+
653
+ from_display_name = 'Provisioning'
654
+ from_scheme = 'sip'
655
+ from_user = '_'
656
+ from_userinfo = "#{from_user}@"
657
+ #from_hostport = domain # should be a domain
658
+ from_hostport = ip_addr_url_repr( local_addr )
659
+ from_uri = "#{from_scheme}:#{from_userinfo}#{from_hostport}"
660
+ from_tag_param_token = random_token()
661
+
662
+ to_scheme = 'sip'
663
+ #to_hostport = domain
664
+ to_hostport = ip_addr_url_repr( @opts[:host] )
665
+ if @opts[:to_user] != nil
666
+ to_userinfo = "#{@opts[:to_user]}@"
667
+ # Even for to_user == '' (which is invalid).
668
+ else
669
+ if @opts[:user] == nil
670
+ to_userinfo = ''
671
+ elsif @opts[:user] == ''
672
+ to_userinfo = '@'
673
+ # This isn't a valid SIP To-URI as the user must not be
674
+ # empty if the userinfo is present ("@").
675
+ else
676
+ to_userinfo = "#{@opts[:user]}@"
677
+ end
678
+ end
679
+ to_uri = "#{to_scheme}:#{to_userinfo}#{to_hostport}"
680
+
681
+ contact_display_name = from_display_name
682
+ contact_scheme = 'sip'
683
+ contact_user = '_'
684
+ contact_userinfo = "#{contact_user}@"
685
+ contact_hostport = local_ip_addr_and_port_url_repr
686
+ contact_uri = "#{contact_scheme}:#{contact_userinfo}#{contact_hostport}"
687
+
688
+ call_id = "#{random_token()}@#{local_ip_addr_and_port_url_repr}"
689
+
690
+ cseq_num = 102
691
+ cseq = "#{cseq_num} #{REQUEST_METHOD}"
692
+
693
+ max_forwards = 70
694
+
695
+ unless @opts[:event]
696
+ event_package = 'check-sync'
697
+ event_type = event_package + ''
698
+ @opts[:event] = "#{event_type};reboot=false"
699
+ end
700
+
701
+ if @opts[:content]
702
+ if @opts[:content].kind_of?( ::Array )
703
+ body = @opts[:content].map(& :to_s).join("\r\n")
704
+ else
705
+ body = @opts[:content].to_s
706
+ end
707
+ else
708
+ body = ''
709
+ end
710
+ body.encode!( ::Encoding::UTF_8, { :undef => :replace, :invalid => :replace })
711
+
712
+ sip_msg = [
713
+ '%{request_method} %{ruri} SIP/2.0',
714
+ 'Via: SIP/2.0/%{transport} %{via_hostport}' +
715
+ ';branch=%{via_branch_token}' +
716
+ '%{rport_param}',
717
+ 'From: %{from_display_name} <%{from_uri}>' +
718
+ ';tag=%{from_tag_param_token}',
719
+ 'To: <%{to_uri}>',
720
+ 'Contact: %{contact_display_name} <%{contact_uri}>',
721
+ 'Call-ID: %{call_id}',
722
+ 'CSeq: %{cseq}',
723
+ 'Date: %{date}',
724
+ 'Max-Forwards: %{max_forwards}',
725
+ # 'Allow: %{allow}',
726
+ 'Subscription-State: %{subscription_state}',
727
+ 'Event: %{event}',
728
+ (@opts[:content_type] ?
729
+ 'Content-Type: %{content_type}' :
730
+ nil),
731
+ 'Content-Length: %{content_length}',
732
+ '',
733
+ body,
734
+ ].compact.join("\r\n") % {
735
+ :request_method => REQUEST_METHOD,
736
+ :ruri => ruri,
737
+ :transport => transport,
738
+ :via_hostport => local_ip_addr_and_port_url_repr,
739
+ :via_branch_token => via_branch_token,
740
+ :rport_param => (@opts[:via_rport] ? ';rport' : ''),
741
+ :from_display_name => from_display_name,
742
+ :from_uri => from_uri,
743
+ :from_tag_param_token => from_tag_param_token,
744
+ :to_uri => to_uri,
745
+ :contact_display_name => contact_display_name,
746
+ :contact_uri => contact_uri,
747
+ :call_id => call_id,
748
+ :cseq => cseq,
749
+ :date => now.utc.strftime('%c') +' GMT', # must be "GMT"
750
+ :max_forwards => max_forwards,
751
+ # :allow => [ 'ACK' ].join(', '),
752
+ :subscription_state => 'active',
753
+ :event => @opts[:event].to_s,
754
+ :content_type => @opts[:content_type].to_s.gsub(/\s+/, ' '),
755
+ :content_length => body.bytesize.to_s,
756
+ }
757
+ sip_msg.encode!( ::Encoding::UTF_8, { :undef => :replace, :invalid => :replace })
758
+ sip_msg
759
+ end
760
+
761
+ # Send variations of the SIP message.
762
+ #
763
+ def self.send_variations( host, opts=nil )
764
+ opts ||= {}
765
+ opts[:domain] ||= host
766
+
767
+ # Create a single SipNotify instance so it will reuse the
768
+ # socket.
769
+ notify = ::SipNotify.new( nil )
770
+
771
+ if opts[:user] && ! opts[:user].empty?
772
+ # Send NOTIFY with user:
773
+ puts "Sending NOTIFY with user ..." if opts[:verbosity] >= 1
774
+ notify.re_initialize!( host, opts.merge({
775
+ })).send
776
+ end
777
+
778
+ #if true
779
+ # Send NOTIFY without user:
780
+ puts "Sending NOTIFY without user ..." if opts[:verbosity] >= 1
781
+ notify.re_initialize!( host, opts.merge({
782
+ :user => nil,
783
+ })).send
784
+ #end
785
+
786
+ #if true
787
+ # Send invalid NOTIFY with empty user ("") in Request-URI:
788
+ puts "Sending invalid NOTIFY with empty user ..." if opts[:verbosity] >= 1
789
+ notify.re_initialize!( host, opts.merge({
790
+ :user => '',
791
+ })).send
792
+ #end
793
+
794
+ #if true
795
+ # Send invalid NOTIFY with empty user ("") in Request-URI
796
+ # but non-empty user in To-URI:
797
+ # (for Cisco 7960/7940)
798
+ puts "Sending invalid NOTIFY with empty user in Request-URI but non-empty user in To-URI ..." if opts[:verbosity] >= 1
799
+ notify.re_initialize!( host, opts.merge({
800
+ :user => '',
801
+ :to_user => '_',
802
+ })).send
803
+ #end
804
+
805
+ notify.socket_destroy!
806
+
807
+ nil
808
+ end
809
+
810
+ end
811
+
812
+
813
+ # An IP paket.
814
+ #
815
+ class IpPktBinData < BinData::Record
816
+ endian :big
817
+ bit4 :vers, :value => 4 # IP version
818
+ bit4 :hdr_len # header length
819
+ uint8 :tos # TOS / DiffServ
820
+ uint16 :len # total length
821
+ uint16 :ident # identifier
822
+ bit3 :flags # flags
823
+ bit13 :frag_os # fragment offset
824
+ uint8 :ttl # time-to-live
825
+ uint8 :proto # IP protocol
826
+ uint16 :checksum # checksum
827
+ uint32 :src_addr # source IP address
828
+ uint32 :dst_addr # destination IP address
829
+ string :options, :read_length => :options_length_in_bytes
830
+ string :data, :read_length => lambda { total_length - header_length_in_bytes }
831
+
832
+ def header_length_in_bytes
833
+ hdr_len * 4
834
+ end
835
+
836
+ def options_length_in_bytes
837
+ header_length_in_bytes - 20
838
+ end
839
+ end
840
+
841
+
842
+ # An UDP packet (payload in an IP packet).
843
+ #
844
+ class UdpPktBinData < BinData::Record
845
+ endian :big
846
+ uint16 :src_port
847
+ uint16 :dst_port
848
+ uint16 :len
849
+ uint16 :checksum
850
+ string :data, :read_length => :len
851
+ end
852
+
853
+
854
+ # vim:noexpandtab:
855
+
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sip-notify
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Philipp Kempgen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bindata
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.4.5
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.4.5
30
+ description: Sends SIP NOTIFY events ("check-sync" etc.).
31
+ email:
32
+ executables:
33
+ - sip-notify
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - lib/sip_notify.rb
38
+ - bin/sip-notify
39
+ - README.md
40
+ homepage: https://github.com/philipp-kempgen/sip-notify
41
+ licenses: []
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubyforge_project:
60
+ rubygems_version: 1.8.25
61
+ signing_key:
62
+ specification_version: 3
63
+ summary: Sends SIP NOTIFY events.
64
+ test_files: []
65
+ has_rdoc: