UPnP 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/.autotest +9 -0
- data/History.txt +8 -0
- data/Manifest.txt +8 -0
- data/README.txt +18 -7
- data/Rakefile +3 -0
- data/lib/UPnP.rb +1 -1
- data/lib/UPnP/SSDP.rb +288 -25
- data/lib/UPnP/UUID.rb +187 -0
- data/lib/UPnP/control/service.rb +4 -1
- data/lib/UPnP/device.rb +692 -0
- data/lib/UPnP/root_server.rb +143 -0
- data/lib/UPnP/service.rb +376 -0
- data/test/test_UPnP_SSDP.rb +80 -17
- data/test/test_UPnP_SSDP_response.rb +2 -0
- data/test/test_UPnP_SSDP_search.rb +25 -0
- data/test/test_UPnP_control_device.rb +2 -0
- data/test/test_UPnP_control_service.rb +2 -0
- data/test/test_UPnP_device.rb +295 -0
- data/test/test_UPnP_root_server.rb +99 -0
- data/test/test_UPnP_service.rb +171 -0
- data/test/utilities.rb +61 -14
- metadata +36 -4
- metadata.gz.sig +0 -0
data.tar.gz.sig
CHANGED
Binary file
|
data/.autotest
CHANGED
@@ -6,6 +6,7 @@ Autotest.add_hook :initialize do |at|
|
|
6
6
|
['test/test_UPnP_SSDP.rb',
|
7
7
|
'test/test_UPnP_SSDP_notification.rb',
|
8
8
|
'test/test_UPnP_SSDP_response.rb',
|
9
|
+
'test/test_UPnP_SSDP_search.rb',
|
9
10
|
]
|
10
11
|
end
|
11
12
|
|
@@ -19,11 +20,19 @@ Autotest.add_hook :initialize do |at|
|
|
19
20
|
|
20
21
|
at.extra_class_map["TestUPnPControlDevice"] =
|
21
22
|
'test/test_UPnP_control_device.rb'
|
23
|
+
at.extra_class_map["TestUPnPDevice"] =
|
24
|
+
'test/test_UPnP_device.rb'
|
25
|
+
at.extra_class_map["TestUPnPService"] =
|
26
|
+
'test/test_UPnP_service.rb'
|
27
|
+
at.extra_class_map["TestUPnPRootServer"] =
|
28
|
+
'test/test_UPnP_root_server.rb'
|
22
29
|
at.extra_class_map["TestUPnPSSDP"] = 'test/test_UPnP_SSDP.rb'
|
23
30
|
at.extra_class_map["TestUPnPSSDPNotification"] =
|
24
31
|
'test/test_UPnP_SSDP_notification.rb'
|
25
32
|
at.extra_class_map["TestUPnPSSDPResponse"] =
|
26
33
|
'test/test_UPnP_SSDP_response.rb'
|
34
|
+
at.extra_class_map["TestUPnPSSDPSearch"] =
|
35
|
+
'test/test_UPnP_SSDP_search.rb'
|
27
36
|
at.extra_class_map["TestUPnPControlService"] =
|
28
37
|
'test/test_UPnP_control_service.rb'
|
29
38
|
end
|
data/History.txt
CHANGED
data/Manifest.txt
CHANGED
@@ -7,12 +7,20 @@ bin/upnp_discover
|
|
7
7
|
bin/upnp_listen
|
8
8
|
lib/UPnP.rb
|
9
9
|
lib/UPnP/SSDP.rb
|
10
|
+
lib/UPnP/UUID.rb
|
10
11
|
lib/UPnP/control.rb
|
11
12
|
lib/UPnP/control/device.rb
|
12
13
|
lib/UPnP/control/service.rb
|
14
|
+
lib/UPnP/device.rb
|
15
|
+
lib/UPnP/root_server.rb
|
16
|
+
lib/UPnP/service.rb
|
13
17
|
test/test_UPnP_SSDP.rb
|
14
18
|
test/test_UPnP_SSDP_notification.rb
|
15
19
|
test/test_UPnP_SSDP_response.rb
|
20
|
+
test/test_UPnP_SSDP_search.rb
|
16
21
|
test/test_UPnP_control_device.rb
|
17
22
|
test/test_UPnP_control_service.rb
|
23
|
+
test/test_UPnP_device.rb
|
24
|
+
test/test_UPnP_root_server.rb
|
25
|
+
test/test_UPnP_service.rb
|
18
26
|
test/utilities.rb
|
data/README.txt
CHANGED
@@ -10,16 +10,27 @@ An implementation of the UPnP protocol
|
|
10
10
|
|
11
11
|
== FEATURES/PROBLEMS:
|
12
12
|
|
13
|
-
*
|
14
|
-
*
|
15
|
-
* Creates
|
16
|
-
|
17
|
-
UPnP
|
13
|
+
* Client support:
|
14
|
+
* Discovers UPnP devices and services via SSDP, see UPnP::SSDP
|
15
|
+
* Creates a SOAP RPC driver for discovered services, see
|
16
|
+
UPnP::Control::Service
|
17
|
+
* Creates concrete UPnP device and service classes that may be extended with
|
18
|
+
utility methods, see UPnP::Control::Device::create,
|
19
|
+
UPnP::Control::Service::create and the UPnP-IGD gem.
|
20
|
+
* Server support:
|
21
|
+
* Easy creation of device and service skeletons from UPnP specifications
|
22
|
+
* Advertises UPnP devices and services via SSDP
|
23
|
+
* Creates a SOAP RPC server for each service
|
24
|
+
* Mounts services in a single WEBrick server
|
25
|
+
* Easy creation of executables for devices
|
18
26
|
* Eventing not implemented
|
19
|
-
* Servers not implemented
|
20
27
|
|
21
28
|
== SYNOPSIS:
|
22
29
|
|
30
|
+
See UPnP::Device for instructions on creating a UPnP device.
|
31
|
+
|
32
|
+
See UPnP::Service for instructinos on creating a UPnP service.
|
33
|
+
|
23
34
|
Print out information about UPnP devices nearby:
|
24
35
|
|
25
36
|
upnp_discover
|
@@ -66,7 +77,7 @@ service, print out the external IP address for the gateway:
|
|
66
77
|
|
67
78
|
== LICENSE:
|
68
79
|
|
69
|
-
|
80
|
+
Original code copyright 2008 Eric Hodel. All rights reserved.
|
70
81
|
|
71
82
|
Redistribution and use in source and binary forms, with or without
|
72
83
|
modification, are permitted provided that the following conditions
|
data/Rakefile
CHANGED
data/lib/UPnP.rb
CHANGED
data/lib/UPnP/SSDP.rb
CHANGED
@@ -94,11 +94,6 @@ class UPnP::SSDP
|
|
94
94
|
|
95
95
|
attr_reader :name
|
96
96
|
|
97
|
-
##
|
98
|
-
# Type of the advertised service or device
|
99
|
-
|
100
|
-
attr_reader :type
|
101
|
-
|
102
97
|
##
|
103
98
|
# Server name and version of the advertised service or device
|
104
99
|
|
@@ -109,6 +104,11 @@ class UPnP::SSDP
|
|
109
104
|
|
110
105
|
attr_reader :sub_type
|
111
106
|
|
107
|
+
##
|
108
|
+
# Type of the advertised service or device
|
109
|
+
|
110
|
+
attr_reader :type
|
111
|
+
|
112
112
|
##
|
113
113
|
# Parses a NOTIFY advertisement into its component pieces
|
114
114
|
|
@@ -270,6 +270,48 @@ class UPnP::SSDP
|
|
270
270
|
|
271
271
|
end
|
272
272
|
|
273
|
+
##
|
274
|
+
# Holds information about an M-SEARCH
|
275
|
+
|
276
|
+
class Search < Advertisement
|
277
|
+
|
278
|
+
attr_reader :date
|
279
|
+
|
280
|
+
attr_reader :target
|
281
|
+
|
282
|
+
attr_reader :wait_time
|
283
|
+
|
284
|
+
##
|
285
|
+
# Creates a new Search by parsing the text in +response+
|
286
|
+
|
287
|
+
def self.parse(response)
|
288
|
+
response =~ /^mx:\s*(\d+)/i
|
289
|
+
wait_time = Integer $1
|
290
|
+
|
291
|
+
response =~ /^st:\s*(\S*)/i
|
292
|
+
target = $1.strip
|
293
|
+
|
294
|
+
new Time.now, target, wait_time
|
295
|
+
end
|
296
|
+
|
297
|
+
##
|
298
|
+
# Creates a new Search
|
299
|
+
|
300
|
+
def initialize(date, target, wait_time)
|
301
|
+
@date = date
|
302
|
+
@target = target
|
303
|
+
@wait_time = wait_time
|
304
|
+
end
|
305
|
+
|
306
|
+
##
|
307
|
+
# A friendlier inspect
|
308
|
+
|
309
|
+
def inspect
|
310
|
+
"#<#{self.class}:0x#{object_id.to_s 16} #{target}>"
|
311
|
+
end
|
312
|
+
|
313
|
+
end
|
314
|
+
|
273
315
|
##
|
274
316
|
# Default broadcast address
|
275
317
|
|
@@ -301,6 +343,16 @@ class UPnP::SSDP
|
|
301
343
|
|
302
344
|
attr_accessor :listener # :nodoc:
|
303
345
|
|
346
|
+
##
|
347
|
+
# A WEBrick::Log logger for unified logging
|
348
|
+
|
349
|
+
attr_writer :log
|
350
|
+
|
351
|
+
##
|
352
|
+
# Thread that periodically notifies for advertise
|
353
|
+
|
354
|
+
attr_reader :notify_thread # :nodoc:
|
355
|
+
|
304
356
|
##
|
305
357
|
# Port to use for SSDP searching and listening
|
306
358
|
|
@@ -311,6 +363,11 @@ class UPnP::SSDP
|
|
311
363
|
|
312
364
|
attr_accessor :queue # :nodoc:
|
313
365
|
|
366
|
+
##
|
367
|
+
# Thread that handles search requests for advertise
|
368
|
+
|
369
|
+
attr_reader :search_thread # :nodoc:
|
370
|
+
|
314
371
|
##
|
315
372
|
# Socket accessor for tests
|
316
373
|
|
@@ -336,8 +393,101 @@ class UPnP::SSDP
|
|
336
393
|
@timeout = TIMEOUT
|
337
394
|
@ttl = TTL
|
338
395
|
|
396
|
+
@log = nil
|
397
|
+
|
339
398
|
@listener = nil
|
340
399
|
@queue = Queue.new
|
400
|
+
|
401
|
+
@search_thread = nil
|
402
|
+
@notify_thread = nil
|
403
|
+
end
|
404
|
+
|
405
|
+
##
|
406
|
+
# Listens for M-SEARCH requests and advertises the requested services
|
407
|
+
|
408
|
+
def advertise(root_device, port, hosts)
|
409
|
+
@socket ||= new_socket
|
410
|
+
|
411
|
+
@notify_thread = Thread.start do
|
412
|
+
loop do
|
413
|
+
hosts.each do |host|
|
414
|
+
uri = "http://#{host}:#{port}/description"
|
415
|
+
|
416
|
+
send_notify uri, 'upnp:rootdevice', root_device
|
417
|
+
|
418
|
+
root_device.devices.each do |d|
|
419
|
+
send_notify uri, d.name, d
|
420
|
+
send_notify uri, d.type_urn, d
|
421
|
+
end
|
422
|
+
|
423
|
+
root_device.services.each do |s|
|
424
|
+
send_notify uri, s.type_urn, s
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
sleep 60
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
listen
|
433
|
+
|
434
|
+
@search_thread = Thread.start do
|
435
|
+
loop do
|
436
|
+
search = @queue.pop
|
437
|
+
|
438
|
+
break if search == :shutdown
|
439
|
+
|
440
|
+
next unless Search === search
|
441
|
+
|
442
|
+
case search.target
|
443
|
+
when /^#{UPnP::DEVICE_SCHEMA_PREFIX}/ then
|
444
|
+
devices = root_device.devices.select do |d|
|
445
|
+
d.type_urn == search.target
|
446
|
+
end
|
447
|
+
|
448
|
+
devices.each do |d|
|
449
|
+
hosts.each do |host|
|
450
|
+
uri = "http://#{host}:#{port}/description"
|
451
|
+
send_response uri, search.target, "#{d.name}::#{search.target}", d
|
452
|
+
end
|
453
|
+
end
|
454
|
+
when 'upnp:rootdevice' then
|
455
|
+
hosts.each do |host|
|
456
|
+
uri = "http://#{host}:#{port}/description"
|
457
|
+
send_response uri, search.target, search.target, root_device
|
458
|
+
end
|
459
|
+
else
|
460
|
+
warn "Unhandled target #{search.target}"
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
sleep
|
466
|
+
|
467
|
+
ensure
|
468
|
+
@queue.push :shutdown
|
469
|
+
stop_listening
|
470
|
+
@notify_thread.kill
|
471
|
+
|
472
|
+
@socket.close if @socket and not @socket.closed?
|
473
|
+
@socket = nil
|
474
|
+
end
|
475
|
+
|
476
|
+
def byebye(root_device, hosts)
|
477
|
+
@socket ||= new_socket
|
478
|
+
|
479
|
+
hosts.each do |host|
|
480
|
+
send_notify_byebye 'upnp:rootdevice', root_device
|
481
|
+
|
482
|
+
root_device.devices.each do |d|
|
483
|
+
send_notify_byebye d.name, d
|
484
|
+
send_notify_byebye d.type_urn, d
|
485
|
+
end
|
486
|
+
|
487
|
+
root_device.services.each do |s|
|
488
|
+
send_notify_byebye s.type_urn, s
|
489
|
+
end
|
490
|
+
end
|
341
491
|
end
|
342
492
|
|
343
493
|
##
|
@@ -348,14 +498,7 @@ class UPnP::SSDP
|
|
348
498
|
# notifications received in that time.
|
349
499
|
|
350
500
|
def discover
|
351
|
-
|
352
|
-
|
353
|
-
@socket ||= UDPSocket.new
|
354
|
-
|
355
|
-
@socket.setsockopt Socket::IPPROTO_IP, Socket::IP_TTL, [@ttl].pack('i')
|
356
|
-
@socket.setsockopt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership
|
357
|
-
|
358
|
-
@socket.bind Socket::INADDR_ANY, @port
|
501
|
+
@socket ||= new_socket
|
359
502
|
|
360
503
|
listen
|
361
504
|
|
@@ -387,20 +530,57 @@ class UPnP::SSDP
|
|
387
530
|
|
388
531
|
@listener = Thread.start do
|
389
532
|
loop do
|
390
|
-
response = @socket.recvfrom
|
533
|
+
response, (family, port, hostname, address) = @socket.recvfrom 1024
|
391
534
|
|
392
535
|
begin
|
393
|
-
|
536
|
+
adv = parse response
|
537
|
+
|
538
|
+
info = case adv
|
539
|
+
when Notification then adv.type
|
540
|
+
when Response then adv.target
|
541
|
+
when Search then adv.target
|
542
|
+
else 'unknown'
|
543
|
+
end
|
544
|
+
|
545
|
+
response =~ /\A(\S+)/
|
546
|
+
log :debug, "SSDP recv #{$1} #{hostname}:#{port} #{info}"
|
547
|
+
|
548
|
+
@queue << adv
|
394
549
|
rescue
|
395
|
-
|
396
|
-
|
550
|
+
warn $!.message
|
551
|
+
warn $!.backtrace
|
397
552
|
end
|
398
553
|
end
|
399
554
|
end
|
400
555
|
end
|
401
556
|
|
557
|
+
def log(level, message)
|
558
|
+
return unless @log
|
559
|
+
|
560
|
+
@log.send level, message
|
561
|
+
end
|
562
|
+
|
563
|
+
##
|
564
|
+
# Sets up a UDPSocket for multicast send and receive
|
565
|
+
|
566
|
+
def new_socket
|
567
|
+
membership = IPAddr.new(@broadcast).hton + IPAddr.new('0.0.0.0').hton
|
568
|
+
ttl = [@ttl].pack 'i'
|
569
|
+
|
570
|
+
socket = UDPSocket.new
|
571
|
+
|
572
|
+
socket.setsockopt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership
|
573
|
+
socket.setsockopt Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\000"
|
574
|
+
socket.setsockopt Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, ttl
|
575
|
+
socket.setsockopt Socket::IPPROTO_IP, Socket::IP_TTL, ttl
|
576
|
+
|
577
|
+
socket.bind '0.0.0.0', @port
|
578
|
+
|
579
|
+
socket
|
580
|
+
end
|
581
|
+
|
402
582
|
##
|
403
|
-
# Returns a Notification or
|
583
|
+
# Returns a Notification, Response or Search created from +response+.
|
404
584
|
|
405
585
|
def parse(response)
|
406
586
|
case response
|
@@ -408,13 +588,15 @@ class UPnP::SSDP
|
|
408
588
|
Notification.parse response
|
409
589
|
when /\AHTTP/ then
|
410
590
|
Response.parse response
|
591
|
+
when /\AM-SEARCH/ then
|
592
|
+
Search.parse response
|
411
593
|
else
|
412
594
|
raise Error, "Unknown response #{response[/\A.*$/]}"
|
413
595
|
end
|
414
596
|
end
|
415
597
|
|
416
598
|
##
|
417
|
-
#
|
599
|
+
# Sends M-SEARCH requests looking for +targets+. Waits timeout seconds
|
418
600
|
# for responses then returns the collected responses.
|
419
601
|
#
|
420
602
|
# Supply no arguments to search for all devices and services.
|
@@ -432,9 +614,7 @@ class UPnP::SSDP
|
|
432
614
|
# Supply <tt>"urn:..."</tt> to search for a URN.
|
433
615
|
|
434
616
|
def search(*targets)
|
435
|
-
@socket ||=
|
436
|
-
|
437
|
-
@socket.setsockopt Socket::IPPROTO_IP, Socket::IP_TTL, [@ttl].pack('i')
|
617
|
+
@socket ||= new_socket
|
438
618
|
|
439
619
|
if targets.empty? then
|
440
620
|
send_search 'ssdp:all'
|
@@ -466,20 +646,103 @@ class UPnP::SSDP
|
|
466
646
|
@socket = nil
|
467
647
|
end
|
468
648
|
|
649
|
+
##
|
650
|
+
# Builds and sends a NOTIFY message
|
651
|
+
|
652
|
+
def send_notify(uri, type, obj)
|
653
|
+
if type =~ /^uuid:/ then
|
654
|
+
name = obj.name
|
655
|
+
else
|
656
|
+
# HACK maybe this should be .device?
|
657
|
+
name = "#{obj.root_device.name}::#{type}"
|
658
|
+
end
|
659
|
+
|
660
|
+
server_info = "Ruby UPnP/#{UPnP::VERSION}"
|
661
|
+
device_info = "#{obj.root_device.class}/#{obj.root_device.version}"
|
662
|
+
|
663
|
+
http_notify = <<-HTTP_NOTIFY
|
664
|
+
NOTIFY * HTTP/1.1\r
|
665
|
+
HOST: #{@broadcast}:#{@port}\r
|
666
|
+
CACHE-CONTROL: max-age=120\r
|
667
|
+
LOCATION: #{uri}\r
|
668
|
+
NT: #{type}\r
|
669
|
+
NTS: ssdp:alive\r
|
670
|
+
SERVER: #{server_info} UPnP/1.0 #{device_info}\r
|
671
|
+
USN: #{name}\r
|
672
|
+
\r
|
673
|
+
HTTP_NOTIFY
|
674
|
+
|
675
|
+
log :debug, "SSDP sent NOTIFY #{type}"
|
676
|
+
|
677
|
+
@socket.send http_notify, 0, @broadcast, @port
|
678
|
+
end
|
679
|
+
|
680
|
+
##
|
681
|
+
# Builds and sends a byebye NOTIFY message
|
682
|
+
|
683
|
+
def send_notify_byebye(type, obj)
|
684
|
+
if type =~ /^uuid:/ then
|
685
|
+
name = obj.name
|
686
|
+
else
|
687
|
+
# HACK maybe this should be .device?
|
688
|
+
name = "#{obj.root_device.name}::#{type}"
|
689
|
+
end
|
690
|
+
|
691
|
+
http_notify = <<-HTTP_NOTIFY
|
692
|
+
NOTIFY * HTTP/1.1\r
|
693
|
+
HOST: #{@broadcast}:#{@port}\r
|
694
|
+
NT: #{type}\r
|
695
|
+
NTS: ssdp:byebye\r
|
696
|
+
USN: #{name}\r
|
697
|
+
\r
|
698
|
+
HTTP_NOTIFY
|
699
|
+
|
700
|
+
log :debug, "SSDP sent byebye #{type}"
|
701
|
+
|
702
|
+
@socket.send http_notify, 0, @broadcast, @port
|
703
|
+
end
|
704
|
+
|
705
|
+
##
|
706
|
+
# Builds and sends a response to an M-SEARCH request"
|
707
|
+
|
708
|
+
def send_response(uri, type, name, device)
|
709
|
+
server_info = "Ruby UPnP/#{UPnP::VERSION}"
|
710
|
+
device_info = "#{device.root_device.class}/#{device.root_device.version}"
|
711
|
+
|
712
|
+
http_response = <<-HTTP_RESPONSE
|
713
|
+
HTTP/1.1 200 OK\r
|
714
|
+
CACHE-CONTROL: max-age=120\r
|
715
|
+
EXT:\r
|
716
|
+
LOCATION: #{uri}\r
|
717
|
+
SERVER: #{server_info} UPnP/1.0 #{device_info}\r
|
718
|
+
ST: #{type}\r
|
719
|
+
NTS: ssdp:alive\r
|
720
|
+
USN: #{name}\r
|
721
|
+
Content-Length: 0\r
|
722
|
+
\r
|
723
|
+
HTTP_RESPONSE
|
724
|
+
|
725
|
+
log :debug, "SSDP sent M-SEARCH OK #{type}"
|
726
|
+
|
727
|
+
@socket.send http_response, 0, @broadcast, @port
|
728
|
+
end
|
729
|
+
|
469
730
|
##
|
470
731
|
# Builds and sends an M-SEARCH request looking for +search_target+.
|
471
732
|
|
472
733
|
def send_search(search_target)
|
473
|
-
|
734
|
+
search = <<-HTTP_REQUEST
|
474
735
|
M-SEARCH * HTTP/1.1\r
|
475
736
|
HOST: #{@broadcast}:#{@port}\r
|
476
737
|
MAN: "ssdp:discover"\r
|
477
738
|
MX: #{@timeout}\r
|
478
739
|
ST: #{search_target}\r
|
479
740
|
\r
|
480
|
-
HTTP_REQUEST
|
741
|
+
HTTP_REQUEST
|
742
|
+
|
743
|
+
log :debug, "SSDP sent M-SEARCH #{search_target}"
|
481
744
|
|
482
|
-
@socket.send
|
745
|
+
@socket.send search, 0, @broadcast, @port
|
483
746
|
end
|
484
747
|
|
485
748
|
##
|