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 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
@@ -1,3 +1,11 @@
1
+ === 1.1.0 / 2008-07-23
2
+
3
+ * 2 major enhancements
4
+ * Server support
5
+ * SSDP now supports sending advertisements
6
+ * 1 bug fix
7
+ * Gem dependencies now listed
8
+
1
9
  === 1.0.0 / 2008-06-25
2
10
 
3
11
  * 1 major enhancement
@@ -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
- * Discovers UPnP devices and services via SSDP, see UPnP::SSDP
14
- * Creates a SOAP RPC driver for discovered services, see UPnP::Control::Service
15
- * Creates concrete UPnP device and service classes that may be extended with
16
- utility methods, see UPnP::Control::Device::create,
17
- UPnP::Control::Service::create and the UPnP-IGD gem.
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
- All code copyright 2008 Eric Hodel. All rights reserved.
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
@@ -7,6 +7,9 @@ require './lib/upnp.rb'
7
7
  Hoe.new('UPnP', UPnP::VERSION) do |p|
8
8
  p.rubyforge_name = 'seattlerb'
9
9
  p.developer('Eric Hodel', 'drbrain@segment7.net')
10
+
11
+ p.extra_deps << 'soap4r'
12
+ p.extra_deps << 'builder'
10
13
  end
11
14
 
12
15
  # vim: syntax=Ruby
@@ -23,7 +23,7 @@ module UPnP
23
23
  ##
24
24
  # The version of UPnP you are using
25
25
 
26
- VERSION = '1.0.0'
26
+ VERSION = '1.1.0'
27
27
 
28
28
  ##
29
29
  # UPnP error base class
@@ -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
- membership = IPAddr.new(@broadcast).hton + IPAddr.new('0.0.0.0').hton
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(1024).first
533
+ response, (family, port, hostname, address) = @socket.recvfrom 1024
391
534
 
392
535
  begin
393
- @queue << parse(response)
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
- puts $!.message
396
- puts $!.backtrace
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 Response created from +response+.
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
- # Broadcasts M-SEARCH requests looking for +targets+. Waits timeout seconds
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 ||= UDPSocket.new
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
- http_request = <<HTTP_REQUEST
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 http_request, 0, @broadcast, @port
745
+ @socket.send search, 0, @broadcast, @port
483
746
  end
484
747
 
485
748
  ##