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.
390 391 392 393 394 395 396 397 398 399 400 401 402 403 |
# File 'lib/UPnP/SSDP.rb', line 390 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
339 340 341 |
# File 'lib/UPnP/SSDP.rb', line 339 def broadcast @broadcast end |
#listener ⇒ Object
Listener accessor for tests.
344 345 346 |
# File 'lib/UPnP/SSDP.rb', line 344 def listener @listener end |
#log(level, message) ⇒ Object
557 558 559 560 561 |
# File 'lib/UPnP/SSDP.rb', line 557 def log(level, ) return unless @log @log.send level, end |
#notify_thread ⇒ Object (readonly)
Thread that periodically notifies for advertise
354 355 356 |
# File 'lib/UPnP/SSDP.rb', line 354 def notify_thread @notify_thread end |
#port ⇒ Object
Port to use for SSDP searching and listening
359 360 361 |
# File 'lib/UPnP/SSDP.rb', line 359 def port @port end |
#queue ⇒ Object
Queue accessor for tests
364 365 366 |
# File 'lib/UPnP/SSDP.rb', line 364 def queue @queue end |
#search_thread ⇒ Object (readonly)
Thread that handles search requests for advertise
369 370 371 |
# File 'lib/UPnP/SSDP.rb', line 369 def search_thread @search_thread end |
#socket ⇒ Object
Socket accessor for tests
374 375 376 |
# File 'lib/UPnP/SSDP.rb', line 374 def socket @socket end |
#timeout ⇒ Object
Time to wait for SSDP responses
379 380 381 |
# File 'lib/UPnP/SSDP.rb', line 379 def timeout @timeout end |
#ttl ⇒ Object
TTL for SSDP packets
384 385 386 |
# File 'lib/UPnP/SSDP.rb', line 384 def ttl @ttl end |
Instance Method Details
#advertise(root_device, port, hosts) ⇒ Object
Listens for M-SEARCH requests and advertises the requested services
408 409 410 411 412 413 414 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 |
# File 'lib/UPnP/SSDP.rb', line 408 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
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 |
# File 'lib/UPnP/SSDP.rb', line 476 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.
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 |
# File 'lib/UPnP/SSDP.rb', line 500 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.
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 |
# File 'lib/UPnP/SSDP.rb', line 528 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
566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 |
# File 'lib/UPnP/SSDP.rb', line 566 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.
585 586 587 588 589 590 591 592 593 594 595 596 |
# File 'lib/UPnP/SSDP.rb', line 585 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.
616 617 618 619 620 621 622 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 |
# File 'lib/UPnP/SSDP.rb', line 616 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
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 |
# File 'lib/UPnP/SSDP.rb', line 652 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
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 |
# File 'lib/UPnP/SSDP.rb', line 683 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“
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 |
# File 'lib/UPnP/SSDP.rb', line 708 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.
733 734 735 736 737 738 739 740 741 742 743 744 745 746 |
# File 'lib/UPnP/SSDP.rb', line 733 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.
751 752 753 754 755 |
# File 'lib/UPnP/SSDP.rb', line 751 def stop_listening @listener.kill if @listener @queue = Queue.new @listener = nil end |