Class: UPnP::SSDP
- Inherits:
-
Object
- Object
- UPnP::SSDP
- Defined in:
- lib/UPnP/SSDP.rb
Overview
Simple Service Discovery Protocol for the UPnP Device Architecture.
Currently SSDP only handles the discovery portions of SSDP.
To listen for SSDP notifications from UPnP devices:
ssdp = SSDP.new
notifications = ssdp.listen
To discover all devices and services:
ssdp = SSDP.new
resources = ssdp.search
After a device has been found you can create a Device object for it:
UPnP::Control::Device.create resource.location
Based on code by Kazuhiro NISHIYAMA ([email protected])
Defined Under Namespace
Classes: Advertisement, Error, Notification, Response, Search
Constant Summary collapse
- BROADCAST =
Default broadcast address
'239.255.255.250'
- PORT =
Default port
1900
- TIMEOUT =
Default timeout
1
- TTL =
Default packet time to live (hops)
4
Instance Attribute Summary collapse
-
#broadcast ⇒ Object
Broadcast address to use when sending searches and listening for notifications.
-
#listener ⇒ Object
Listener accessor for tests.
- #log(level, message) ⇒ Object
-
#notify_thread ⇒ Object
readonly
Thread that periodically notifies for advertise.
-
#port ⇒ Object
Port to use for SSDP searching and listening.
-
#queue ⇒ Object
Queue accessor for tests.
-
#search_thread ⇒ Object
readonly
Thread that handles search requests for advertise.
-
#socket ⇒ Object
Socket accessor for tests.
-
#timeout ⇒ Object
Time to wait for SSDP responses.
-
#ttl ⇒ Object
TTL for SSDP packets.
Instance Method Summary collapse
-
#advertise(root_device, port, hosts) ⇒ Object
Listens for M-SEARCH requests and advertises the requested services.
- #byebye(root_device, hosts) ⇒ Object
-
#discover ⇒ Object
Discovers UPnP devices sending NOTIFY broadcasts.
-
#initialize ⇒ SSDP
constructor
Creates a new SSDP object.
-
#listen ⇒ Object
Listens for UDP packets from devices in a Thread and enqueues them for processing.
-
#new_socket ⇒ Object
Sets up a UDPSocket for multicast send and receive.
-
#parse(response) ⇒ Object
Returns a Notification, Response or Search created from
response
. -
#search(*targets) ⇒ Object
Sends M-SEARCH requests looking for
targets
. -
#send_notify(uri, type, obj) ⇒ Object
Builds and sends a NOTIFY message.
-
#send_notify_byebye(type, obj) ⇒ Object
Builds and sends a byebye NOTIFY message.
-
#send_response(uri, type, name, device) ⇒ Object
Builds and sends a response to an M-SEARCH request“.
-
#send_search(search_target) ⇒ Object
Builds and sends an M-SEARCH request looking for
search_target
. -
#stop_listening ⇒ Object
Stops and clears the listen thread.
Constructor Details
#initialize ⇒ SSDP
Creates a new SSDP object. Use the accessors to override broadcast, port, timeout or ttl.
397 398 399 400 401 402 403 404 405 406 407 408 409 410 |
# File 'lib/UPnP/SSDP.rb', line 397 def initialize @broadcast = BROADCAST @port = PORT @timeout = TIMEOUT @ttl = TTL @log = nil @listener = nil @queue = Queue.new @search_thread = nil @notify_thread = nil end |
Instance Attribute Details
#broadcast ⇒ Object
Broadcast address to use when sending searches and listening for notifications
346 347 348 |
# File 'lib/UPnP/SSDP.rb', line 346 def broadcast @broadcast end |
#listener ⇒ Object
Listener accessor for tests.
351 352 353 |
# File 'lib/UPnP/SSDP.rb', line 351 def listener @listener end |
#log(level, message) ⇒ Object
564 565 566 567 568 |
# File 'lib/UPnP/SSDP.rb', line 564 def log(level, ) return unless @log @log.send level, end |
#notify_thread ⇒ Object (readonly)
Thread that periodically notifies for advertise
361 362 363 |
# File 'lib/UPnP/SSDP.rb', line 361 def notify_thread @notify_thread end |
#port ⇒ Object
Port to use for SSDP searching and listening
366 367 368 |
# File 'lib/UPnP/SSDP.rb', line 366 def port @port end |
#queue ⇒ Object
Queue accessor for tests
371 372 373 |
# File 'lib/UPnP/SSDP.rb', line 371 def queue @queue end |
#search_thread ⇒ Object (readonly)
Thread that handles search requests for advertise
376 377 378 |
# File 'lib/UPnP/SSDP.rb', line 376 def search_thread @search_thread end |
#socket ⇒ Object
Socket accessor for tests
381 382 383 |
# File 'lib/UPnP/SSDP.rb', line 381 def socket @socket end |
#timeout ⇒ Object
Time to wait for SSDP responses
386 387 388 |
# File 'lib/UPnP/SSDP.rb', line 386 def timeout @timeout end |
#ttl ⇒ Object
TTL for SSDP packets
391 392 393 |
# File 'lib/UPnP/SSDP.rb', line 391 def ttl @ttl end |
Instance Method Details
#advertise(root_device, port, hosts) ⇒ Object
Listens for M-SEARCH requests and advertises the requested services
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 |
# File 'lib/UPnP/SSDP.rb', line 415 def advertise(root_device, port, hosts) @socket ||= new_socket @notify_thread = Thread.start do loop do hosts.each do |host| uri = "http://#{host}:#{port}/description" send_notify uri, 'upnp:rootdevice', root_device root_device.devices.each do |d| send_notify uri, d.name, d send_notify uri, d.type_urn, d end root_device.services.each do |s| send_notify uri, s.type_urn, s end end sleep 60 end end listen @search_thread = Thread.start do loop do search = @queue.pop break if search == :shutdown next unless Search === search case search.target when /^#{UPnP::DEVICE_SCHEMA_PREFIX}/ then devices = root_device.devices.select do |d| d.type_urn == search.target end devices.each do |d| hosts.each do |host| uri = "http://#{host}:#{port}/description" send_response uri, search.target, "#{d.name}::#{search.target}", d end end when 'upnp:rootdevice' then hosts.each do |host| uri = "http://#{host}:#{port}/description" send_response uri, search.target, search.target, root_device end else warn "Unhandled target #{search.target}" end end end sleep ensure @queue.push :shutdown stop_listening @notify_thread.kill @socket.close if @socket and not @socket.closed? @socket = nil end |
#byebye(root_device, hosts) ⇒ Object
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 |
# File 'lib/UPnP/SSDP.rb', line 483 def byebye(root_device, hosts) @socket ||= new_socket hosts.each do |host| send_notify_byebye 'upnp:rootdevice', root_device root_device.devices.each do |d| send_notify_byebye d.name, d send_notify_byebye d.type_urn, d end root_device.services.each do |s| send_notify_byebye s.type_urn, s end end end |
#discover ⇒ Object
Discovers UPnP devices sending NOTIFY broadcasts.
If given a block, yields each Notification as it is received and never returns. Otherwise, discover waits for timeout seconds and returns all notifications received in that time.
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 |
# File 'lib/UPnP/SSDP.rb', line 507 def discover @socket ||= new_socket listen if block_given? then loop do notification = @queue.pop yield notification end else sleep @timeout notifications = [] notifications << @queue.pop until @queue.empty? notifications end ensure stop_listening @socket.close if @socket and not @socket.closed? @socket = nil end |
#listen ⇒ Object
Listens for UDP packets from devices in a Thread and enqueues them for processing. Requires a socket from search or discover.
535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 |
# File 'lib/UPnP/SSDP.rb', line 535 def listen return @listener if @listener and @listener.alive? @listener = Thread.start do loop do response, (family, port, hostname, address) = @socket.recvfrom 1024 begin adv = parse response info = case adv when Notification then adv.type when Response then adv.target when Search then adv.target else 'unknown' end response =~ /\A(\S+)/ log :debug, "SSDP recv #{$1} #{hostname}:#{port} #{info}" @queue << adv rescue warn $!. warn $!.backtrace end end end end |
#new_socket ⇒ Object
Sets up a UDPSocket for multicast send and receive
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 |
# File 'lib/UPnP/SSDP.rb', line 573 def new_socket membership = IPAddr.new(@broadcast).hton + IPAddr.new('0.0.0.0').hton ttl = [@ttl].pack 'i' socket = UDPSocket.new socket.setsockopt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership socket.setsockopt Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\000" socket.setsockopt Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, ttl socket.setsockopt Socket::IPPROTO_IP, Socket::IP_TTL, ttl socket.bind '0.0.0.0', @port socket end |
#parse(response) ⇒ Object
Returns a Notification, Response or Search created from response
.
592 593 594 595 596 597 598 599 600 601 602 603 |
# File 'lib/UPnP/SSDP.rb', line 592 def parse(response) case response when /\ANOTIFY/ then Notification.parse response when /\AHTTP/ then Response.parse response when /\AM-SEARCH/ then Search.parse response else raise Error, "Unknown response #{response[/\A.*$/]}" end end |
#search(*targets) ⇒ Object
Sends M-SEARCH requests looking for targets
. Waits timeout seconds for responses then returns the collected responses.
Supply no arguments to search for all devices and services.
Supply :root
to search for root devices only.
Supply [:device, 'device_type:version']
to search for a specific device type.
Supply [:service, 'service_type:version']
to search for a specific service type.
Supply "uuid:..."
to search for a UUID.
Supply "urn:..."
to search for a URN.
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 |
# File 'lib/UPnP/SSDP.rb', line 623 def search(*targets) @socket ||= new_socket if targets.empty? then send_search 'ssdp:all' else targets.each do |target| if target == :root then send_search 'upnp:rootdevice' elsif Array === target and target.first == :device then target = [UPnP::DEVICE_SCHEMA_PREFIX, target.last] send_search target.join(':') elsif Array === target and target.first == :service then target = [UPnP::SERVICE_SCHEMA_PREFIX, target.last] send_search target.join(':') elsif String === target and target =~ /\A(urn|uuid|ssdp):/ then send_search target end end end listen sleep @timeout responses = [] responses << @queue.pop until @queue.empty? responses ensure stop_listening @socket.close if @socket and not @socket.closed? @socket = nil end |
#send_notify(uri, type, obj) ⇒ Object
Builds and sends a NOTIFY message
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 |
# File 'lib/UPnP/SSDP.rb', line 659 def send_notify(uri, type, obj) if type =~ /^uuid:/ then name = obj.name else # HACK maybe this should be .device? name = "#{obj.root_device.name}::#{type}" end server_info = "Ruby UPnP/#{UPnP::VERSION}" device_info = "#{obj.root_device.class}/#{obj.root_device.version}" http_notify = <<-HTTP_NOTIFY NOTIFY * HTTP/1.1\r HOST: #{@broadcast}:#{@port}\r CACHE-CONTROL: max-age=120\r LOCATION: #{uri}\r NT: #{type}\r NTS: ssdp:alive\r SERVER: #{server_info} UPnP/1.0 #{device_info}\r USN: #{name}\r \r HTTP_NOTIFY log :debug, "SSDP sent NOTIFY #{type}" @socket.send http_notify, 0, @broadcast, @port end |
#send_notify_byebye(type, obj) ⇒ Object
Builds and sends a byebye NOTIFY message
690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 |
# File 'lib/UPnP/SSDP.rb', line 690 def send_notify_byebye(type, obj) if type =~ /^uuid:/ then name = obj.name else # HACK maybe this should be .device? name = "#{obj.root_device.name}::#{type}" end http_notify = <<-HTTP_NOTIFY NOTIFY * HTTP/1.1\r HOST: #{@broadcast}:#{@port}\r NT: #{type}\r NTS: ssdp:byebye\r USN: #{name}\r \r HTTP_NOTIFY log :debug, "SSDP sent byebye #{type}" @socket.send http_notify, 0, @broadcast, @port end |
#send_response(uri, type, name, device) ⇒ Object
Builds and sends a response to an M-SEARCH request“
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 |
# File 'lib/UPnP/SSDP.rb', line 715 def send_response(uri, type, name, device) server_info = "Ruby UPnP/#{UPnP::VERSION}" device_info = "#{device.root_device.class}/#{device.root_device.version}" http_response = <<-HTTP_RESPONSE HTTP/1.1 200 OK\r CACHE-CONTROL: max-age=120\r EXT:\r LOCATION: #{uri}\r SERVER: #{server_info} UPnP/1.0 #{device_info}\r ST: #{type}\r NTS: ssdp:alive\r USN: #{name}\r Content-Length: 0\r \r HTTP_RESPONSE log :debug, "SSDP sent M-SEARCH OK #{type}" @socket.send http_response, 0, @broadcast, @port end |
#send_search(search_target) ⇒ Object
Builds and sends an M-SEARCH request looking for search_target
.
740 741 742 743 744 745 746 747 748 749 750 751 752 753 |
# File 'lib/UPnP/SSDP.rb', line 740 def send_search(search_target) search = <<-HTTP_REQUEST M-SEARCH * HTTP/1.1\r HOST: #{@broadcast}:#{@port}\r MAN: "ssdp:discover"\r MX: #{@timeout}\r ST: #{search_target}\r \r HTTP_REQUEST log :debug, "SSDP sent M-SEARCH #{search_target}" @socket.send search, 0, @broadcast, @port end |
#stop_listening ⇒ Object
Stops and clears the listen thread.
758 759 760 761 762 |
# File 'lib/UPnP/SSDP.rb', line 758 def stop_listening @listener.kill if @listener @queue = Queue.new @listener = nil end |