UPnP 1.0.0 → 1.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.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
|
##
|