sip-notify 0.0.1

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.
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: