UPnP 1.0.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 +30 -0
- data/History.txt +6 -0
- data/Manifest.txt +18 -0
- data/README.txt +95 -0
- data/Rakefile +12 -0
- data/bin/upnp_discover +82 -0
- data/bin/upnp_listen +26 -0
- data/lib/UPnP.rb +35 -0
- data/lib/UPnP/SSDP.rb +495 -0
- data/lib/UPnP/control.rb +11 -0
- data/lib/UPnP/control/device.rb +238 -0
- data/lib/UPnP/control/service.rb +461 -0
- data/test/test_UPnP_SSDP.rb +237 -0
- data/test/test_UPnP_SSDP_notification.rb +74 -0
- data/test/test_UPnP_SSDP_response.rb +30 -0
- data/test/test_UPnP_control_device.rb +72 -0
- data/test/test_UPnP_control_service.rb +108 -0
- data/test/utilities.rb +1028 -0
- metadata +109 -0
- metadata.gz.sig +1 -0
data.tar.gz.sig
ADDED
Binary file
|
data/.autotest
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# vim: filetype=ruby
|
2
|
+
|
3
|
+
Autotest.add_hook :initialize do |at|
|
4
|
+
|
5
|
+
at.add_mapping %r%^lib/UPnP/SSDP.rb$% do
|
6
|
+
['test/test_UPnP_SSDP.rb',
|
7
|
+
'test/test_UPnP_SSDP_notification.rb',
|
8
|
+
'test/test_UPnP_SSDP_response.rb',
|
9
|
+
]
|
10
|
+
end
|
11
|
+
|
12
|
+
at.add_mapping %r%^lib/UPnP/control/(\w+).rb$% do |_,m|
|
13
|
+
"test/test_UPnP_Control_#{m[1].capitalize}.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
at.add_mapping %r%^test/utilities.rb$% do
|
17
|
+
at.known_files
|
18
|
+
end
|
19
|
+
|
20
|
+
at.extra_class_map["TestUPnPControlDevice"] =
|
21
|
+
'test/test_UPnP_control_device.rb'
|
22
|
+
at.extra_class_map["TestUPnPSSDP"] = 'test/test_UPnP_SSDP.rb'
|
23
|
+
at.extra_class_map["TestUPnPSSDPNotification"] =
|
24
|
+
'test/test_UPnP_SSDP_notification.rb'
|
25
|
+
at.extra_class_map["TestUPnPSSDPResponse"] =
|
26
|
+
'test/test_UPnP_SSDP_response.rb'
|
27
|
+
at.extra_class_map["TestUPnPControlService"] =
|
28
|
+
'test/test_UPnP_control_service.rb'
|
29
|
+
end
|
30
|
+
|
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
.autotest
|
2
|
+
History.txt
|
3
|
+
Manifest.txt
|
4
|
+
README.txt
|
5
|
+
Rakefile
|
6
|
+
bin/upnp_discover
|
7
|
+
bin/upnp_listen
|
8
|
+
lib/UPnP.rb
|
9
|
+
lib/UPnP/SSDP.rb
|
10
|
+
lib/UPnP/control.rb
|
11
|
+
lib/UPnP/control/device.rb
|
12
|
+
lib/UPnP/control/service.rb
|
13
|
+
test/test_UPnP_SSDP.rb
|
14
|
+
test/test_UPnP_SSDP_notification.rb
|
15
|
+
test/test_UPnP_SSDP_response.rb
|
16
|
+
test/test_UPnP_control_device.rb
|
17
|
+
test/test_UPnP_control_service.rb
|
18
|
+
test/utilities.rb
|
data/README.txt
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
= UPnP
|
2
|
+
|
3
|
+
* http://seattlerb.org/UPnP
|
4
|
+
* http://upnp.org
|
5
|
+
* Bugs: http://rubyforge.org/tracker/?atid=5921&group_id=1513
|
6
|
+
|
7
|
+
== DESCRIPTION:
|
8
|
+
|
9
|
+
An implementation of the UPnP protocol
|
10
|
+
|
11
|
+
== FEATURES/PROBLEMS:
|
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.
|
18
|
+
* Eventing not implemented
|
19
|
+
* Servers not implemented
|
20
|
+
|
21
|
+
== SYNOPSIS:
|
22
|
+
|
23
|
+
Print out information about UPnP devices nearby:
|
24
|
+
|
25
|
+
upnp_discover
|
26
|
+
|
27
|
+
Listen for UPnP resource notifications:
|
28
|
+
|
29
|
+
upnp_listen
|
30
|
+
|
31
|
+
Search for root UPnP devices and print out their description URLs:
|
32
|
+
|
33
|
+
require 'UPnP/SSDP'
|
34
|
+
|
35
|
+
resources = UPnP::SSDP.new.search :root
|
36
|
+
locations = resources.map { |resource| resource.location }
|
37
|
+
puts locations.join("\n")
|
38
|
+
|
39
|
+
Create a UPnP::Control::Device from the first discovered root device:
|
40
|
+
|
41
|
+
require 'UPnP/control/device'
|
42
|
+
|
43
|
+
device = UPnP::Control::Device.create locations.first
|
44
|
+
|
45
|
+
Enumerate actions on all services on the device:
|
46
|
+
|
47
|
+
service_names = device.services.map do |service|
|
48
|
+
service.methods(false)
|
49
|
+
end
|
50
|
+
|
51
|
+
puts service_names.sort.join("\n")
|
52
|
+
|
53
|
+
Assuming the root device is an InternetGatewayDevice with a WANIPConnection
|
54
|
+
service, print out the external IP address for the gateway:
|
55
|
+
|
56
|
+
wic = device.services.find { |service| service.type =~ /WANIPConnection/ }
|
57
|
+
puts wic.GetExternalIPAddress
|
58
|
+
|
59
|
+
== REQUIREMENTS:
|
60
|
+
|
61
|
+
* UPnP devices
|
62
|
+
|
63
|
+
== INSTALL:
|
64
|
+
|
65
|
+
sudo gem install UPnP
|
66
|
+
|
67
|
+
== LICENSE:
|
68
|
+
|
69
|
+
All code copyright 2008 Eric Hodel. All rights reserved.
|
70
|
+
|
71
|
+
Redistribution and use in source and binary forms, with or without
|
72
|
+
modification, are permitted provided that the following conditions
|
73
|
+
are met:
|
74
|
+
|
75
|
+
1. Redistributions of source code must retain the above copyright
|
76
|
+
notice, this list of conditions and the following disclaimer.
|
77
|
+
2. Redistributions in binary form must reproduce the above copyright
|
78
|
+
notice, this list of conditions and the following disclaimer in the
|
79
|
+
documentation and/or other materials provided with the distribution.
|
80
|
+
3. Neither the names of the authors nor the names of their contributors
|
81
|
+
may be used to endorse or promote products derived from this software
|
82
|
+
without specific prior written permission.
|
83
|
+
|
84
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
|
85
|
+
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
86
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
87
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
|
88
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
89
|
+
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
90
|
+
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
91
|
+
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
92
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
93
|
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
94
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
95
|
+
|
data/Rakefile
ADDED
data/bin/upnp_discover
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'UPnP/SSDP'
|
4
|
+
require 'UPnP/control'
|
5
|
+
|
6
|
+
ssdp = UPnP::SSDP.new
|
7
|
+
timeout = ARGV.shift
|
8
|
+
timeout = if timeout then
|
9
|
+
begin
|
10
|
+
timeout = Integer timeout
|
11
|
+
rescue
|
12
|
+
abort <<-EOF
|
13
|
+
Usage: #{$0} [timeout]
|
14
|
+
|
15
|
+
Prints information about UPnP internet gateway devices
|
16
|
+
EOF
|
17
|
+
end
|
18
|
+
else
|
19
|
+
1
|
20
|
+
end
|
21
|
+
|
22
|
+
ssdp.timeout = timeout
|
23
|
+
|
24
|
+
devices = ssdp.search(:root).map do |resource|
|
25
|
+
UPnP::Control::Device.new resource.location
|
26
|
+
end
|
27
|
+
|
28
|
+
if devices.empty? then
|
29
|
+
puts 'No UPnP devices found'
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
|
33
|
+
def print_device(device, indent = ' ')
|
34
|
+
out = []
|
35
|
+
|
36
|
+
out << "Friendly name: #{device.friendly_name}"
|
37
|
+
out << "Presentation URL: #{device.presentation_url}"
|
38
|
+
out << nil
|
39
|
+
out << "Unique device name: #{device.name}"
|
40
|
+
out << nil
|
41
|
+
out << "Manufacturer: #{device.manufacturer}"
|
42
|
+
out << "Manufacturer URL: #{device.manufacturer_url}"
|
43
|
+
out << nil
|
44
|
+
out << "Model name: #{device.model_name}"
|
45
|
+
out << "Model description: #{device.model_description}"
|
46
|
+
out << "Model URL: #{device.model_url}"
|
47
|
+
out << "UPC: #{device.upc}"
|
48
|
+
out << "Serial number: #{device.serial_number}"
|
49
|
+
out << nil
|
50
|
+
|
51
|
+
puts indent + out.join("\n#{indent}")
|
52
|
+
end
|
53
|
+
|
54
|
+
devices.each do |device|
|
55
|
+
type = device.type.sub "#{UPnP::DEVICE_SCHEMA_PREFIX}:", ''
|
56
|
+
puts "#{device.url}: #{type}"
|
57
|
+
print_device device
|
58
|
+
|
59
|
+
device.devices.each do |sub_device|
|
60
|
+
type = sub_device.type.sub "#{UPnP::DEVICE_SCHEMA_PREFIX}:", ''
|
61
|
+
puts " Sub-device #{type}:"
|
62
|
+
print_device sub_device, ' '
|
63
|
+
end
|
64
|
+
|
65
|
+
device.services.each do |service|
|
66
|
+
type = service.type.sub("#{UPnP::DEVICE_SCHEMA_PREFIX}:", '')
|
67
|
+
puts " Service: #{type}"
|
68
|
+
puts " Id: #{service.id}"
|
69
|
+
puts " Type: #{service.type}"
|
70
|
+
puts " SCPD URL: #{service.scpd_url}"
|
71
|
+
puts " Control URL: #{service.control_url}"
|
72
|
+
puts " Event subscription URL: #{service.event_sub_url}"
|
73
|
+
puts
|
74
|
+
puts " Actions:"
|
75
|
+
service.driver.methods(false).sort.each do |method|
|
76
|
+
puts " #{method}"
|
77
|
+
end
|
78
|
+
|
79
|
+
puts
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
data/bin/upnp_listen
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'UPnP/SSDP'
|
4
|
+
require 'UPnP/control'
|
5
|
+
|
6
|
+
ssdp = UPnP::SSDP.new
|
7
|
+
|
8
|
+
ssdp.discover do |notification|
|
9
|
+
schemas = Regexp.union UPnP::DEVICE_SCHEMA_PREFIX, UPnP::SERVICE_SCHEMA_PREFIX
|
10
|
+
|
11
|
+
type = notification.type.sub(/#{schemas}:/, '')
|
12
|
+
|
13
|
+
if notification.alive? then
|
14
|
+
puts "#{type} is alive"
|
15
|
+
puts "Description: #{notification.location}"
|
16
|
+
expiration = notification.expiration.strftime '%c'
|
17
|
+
puts "Valid until #{expiration}"
|
18
|
+
else
|
19
|
+
puts "#{type} says byebye"
|
20
|
+
end
|
21
|
+
|
22
|
+
puts "USN: #{notification.name}"
|
23
|
+
|
24
|
+
puts
|
25
|
+
end
|
26
|
+
|
data/lib/UPnP.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
$KCODE = 'u'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
gem 'soap4r'
|
5
|
+
|
6
|
+
##
|
7
|
+
# An implementation of the Universal Plug and Play protocol.
|
8
|
+
#
|
9
|
+
# http://upnp.org/
|
10
|
+
|
11
|
+
module UPnP
|
12
|
+
|
13
|
+
##
|
14
|
+
# UPnP device schema prefix
|
15
|
+
|
16
|
+
DEVICE_SCHEMA_PREFIX = 'urn:schemas-upnp-org:device'
|
17
|
+
|
18
|
+
##
|
19
|
+
# UPnP service schema prefix
|
20
|
+
|
21
|
+
SERVICE_SCHEMA_PREFIX = 'urn:schemas-upnp-org:service'
|
22
|
+
|
23
|
+
##
|
24
|
+
# The version of UPnP you are using
|
25
|
+
|
26
|
+
VERSION = '1.0.0'
|
27
|
+
|
28
|
+
##
|
29
|
+
# UPnP error base class
|
30
|
+
|
31
|
+
class Error < RuntimeError
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
data/lib/UPnP/SSDP.rb
ADDED
@@ -0,0 +1,495 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
require 'socket'
|
3
|
+
require 'thread'
|
4
|
+
require 'time'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
require 'UPnP'
|
8
|
+
require 'UPnP/control'
|
9
|
+
|
10
|
+
##
|
11
|
+
# Simple Service Discovery Protocol for the UPnP Device Architecture.
|
12
|
+
#
|
13
|
+
# Currently SSDP only handles the discovery portions of SSDP.
|
14
|
+
#
|
15
|
+
# To listen for SSDP notifications from UPnP devices:
|
16
|
+
#
|
17
|
+
# ssdp = SSDP.new
|
18
|
+
# notifications = ssdp.listen
|
19
|
+
#
|
20
|
+
# To discover all devices and services:
|
21
|
+
#
|
22
|
+
# ssdp = SSDP.new
|
23
|
+
# resources = ssdp.search
|
24
|
+
#
|
25
|
+
# After a device has been found you can create a Device object for it:
|
26
|
+
#
|
27
|
+
# UPnP::Control::Device.create resource.location
|
28
|
+
#
|
29
|
+
# Based on code by Kazuhiro NISHIYAMA (zn@mbf.nifty.com)
|
30
|
+
|
31
|
+
class UPnP::SSDP
|
32
|
+
|
33
|
+
##
|
34
|
+
# SSDP Error class
|
35
|
+
|
36
|
+
class Error < UPnP::Error
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Abstract class for SSDP advertisements
|
41
|
+
|
42
|
+
class Advertisement
|
43
|
+
|
44
|
+
##
|
45
|
+
# Expiration time of this advertisement
|
46
|
+
|
47
|
+
def expiration
|
48
|
+
date + max_age
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# True if this advertisement has expired
|
53
|
+
|
54
|
+
def expired?
|
55
|
+
Time.now > expiration
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Holds information about a NOTIFY message. For an alive notification, all
|
62
|
+
# fields will be present. For a byebye notification, location, max_age and
|
63
|
+
# server will be nil.
|
64
|
+
|
65
|
+
class Notification < Advertisement
|
66
|
+
|
67
|
+
##
|
68
|
+
# Date the notification was received
|
69
|
+
|
70
|
+
attr_reader :date
|
71
|
+
|
72
|
+
##
|
73
|
+
# Host the notification was sent from
|
74
|
+
|
75
|
+
attr_reader :host
|
76
|
+
|
77
|
+
##
|
78
|
+
# Port the notification was sent from
|
79
|
+
|
80
|
+
attr_reader :port
|
81
|
+
|
82
|
+
##
|
83
|
+
# Location of the advertised service or device
|
84
|
+
|
85
|
+
attr_reader :location
|
86
|
+
|
87
|
+
##
|
88
|
+
# Maximum age the advertisement is valid for
|
89
|
+
|
90
|
+
attr_reader :max_age
|
91
|
+
|
92
|
+
##
|
93
|
+
# Unique Service Name of the advertisement
|
94
|
+
|
95
|
+
attr_reader :name
|
96
|
+
|
97
|
+
##
|
98
|
+
# Type of the advertised service or device
|
99
|
+
|
100
|
+
attr_reader :type
|
101
|
+
|
102
|
+
##
|
103
|
+
# Server name and version of the advertised service or device
|
104
|
+
|
105
|
+
attr_reader :server
|
106
|
+
|
107
|
+
##
|
108
|
+
# \Notification sub-type
|
109
|
+
|
110
|
+
attr_reader :sub_type
|
111
|
+
|
112
|
+
##
|
113
|
+
# Parses a NOTIFY advertisement into its component pieces
|
114
|
+
|
115
|
+
def self.parse(advertisement)
|
116
|
+
advertisement = advertisement.gsub "\r", ''
|
117
|
+
|
118
|
+
advertisement =~ /^host:\s*(\S*)/i
|
119
|
+
host, port = $1.split ':'
|
120
|
+
|
121
|
+
advertisement =~ /^nt:\s*(\S*)/i
|
122
|
+
type = $1
|
123
|
+
|
124
|
+
advertisement =~ /^nts:\s*(\S*)/i
|
125
|
+
sub_type = $1
|
126
|
+
|
127
|
+
advertisement =~ /^usn:\s*(\S*)/i
|
128
|
+
name = $1
|
129
|
+
|
130
|
+
if sub_type == 'ssdp:alive' then
|
131
|
+
advertisement =~ /^cache-control:\s*max-age\s*=\s*(\d+)/i
|
132
|
+
max_age = Integer $1
|
133
|
+
|
134
|
+
advertisement =~ /^location:\s*(\S*)/i
|
135
|
+
location = URI.parse $1
|
136
|
+
|
137
|
+
advertisement =~ /^server:\s*(.*)/i
|
138
|
+
server = $1.strip
|
139
|
+
end
|
140
|
+
|
141
|
+
new Time.now, max_age, host, port, location, type, sub_type, server, name
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Creates a \new Notification
|
146
|
+
|
147
|
+
def initialize(date, max_age, host, port, location, type, sub_type,
|
148
|
+
server, name)
|
149
|
+
@date = date
|
150
|
+
@max_age = max_age
|
151
|
+
@host = host
|
152
|
+
@port = port
|
153
|
+
@location = location
|
154
|
+
@type = type
|
155
|
+
@sub_type = sub_type
|
156
|
+
@server = server
|
157
|
+
@name = name
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Returns true if this is a notification for a resource being alive
|
162
|
+
|
163
|
+
def alive?
|
164
|
+
sub_type == 'ssdp:alive'
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Returns true if this is a notification for a resource going away
|
169
|
+
|
170
|
+
def byebye?
|
171
|
+
sub_type == 'ssdp:byebye'
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# A friendlier inspect
|
176
|
+
|
177
|
+
def inspect
|
178
|
+
location = " #{@location}" if @location
|
179
|
+
"#<#{self.class}:0x#{object_id.to_s 16} #{@type} #{@sub_type}#{location}>"
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Holds information about a M-SEARCH response
|
186
|
+
|
187
|
+
class Response < Advertisement
|
188
|
+
|
189
|
+
##
|
190
|
+
# Date response was created or received
|
191
|
+
|
192
|
+
attr_reader :date
|
193
|
+
|
194
|
+
##
|
195
|
+
# true if MAN header was understood
|
196
|
+
|
197
|
+
attr_reader :ext
|
198
|
+
|
199
|
+
##
|
200
|
+
# URI where this device or service is described
|
201
|
+
|
202
|
+
attr_reader :location
|
203
|
+
|
204
|
+
##
|
205
|
+
# Maximum age this advertisement is valid for
|
206
|
+
|
207
|
+
attr_reader :max_age
|
208
|
+
|
209
|
+
##
|
210
|
+
# Unique Service Name
|
211
|
+
|
212
|
+
attr_reader :name
|
213
|
+
|
214
|
+
##
|
215
|
+
# Server version string
|
216
|
+
|
217
|
+
attr_reader :server
|
218
|
+
|
219
|
+
##
|
220
|
+
# Search target
|
221
|
+
|
222
|
+
attr_reader :target
|
223
|
+
|
224
|
+
##
|
225
|
+
# Creates a new Response by parsing the text in +response+
|
226
|
+
|
227
|
+
def self.parse(response)
|
228
|
+
response =~ /^cache-control:\s*max-age\s*=\s*(\d+)/i
|
229
|
+
max_age = Integer $1
|
230
|
+
|
231
|
+
response =~ /^date:\s*(.*)/i
|
232
|
+
date = $1 ? Time.parse($1) : Time.now
|
233
|
+
|
234
|
+
ext = !!(response =~ /^ext:/i)
|
235
|
+
|
236
|
+
response =~ /^location:\s*(\S*)/i
|
237
|
+
location = URI.parse $1.strip
|
238
|
+
|
239
|
+
response =~ /^server:\s*(.*)/i
|
240
|
+
server = $1.strip
|
241
|
+
|
242
|
+
response =~ /^st:\s*(\S*)/i
|
243
|
+
target = $1.strip
|
244
|
+
|
245
|
+
response =~ /^usn:\s*(\S*)/i
|
246
|
+
name = $1.strip
|
247
|
+
|
248
|
+
new date, max_age, location, server, target, name, ext
|
249
|
+
end
|
250
|
+
|
251
|
+
##
|
252
|
+
# Creates a new Response
|
253
|
+
|
254
|
+
def initialize(date, max_age, location, server, target, name, ext)
|
255
|
+
@date = date
|
256
|
+
@max_age = max_age
|
257
|
+
@location = location
|
258
|
+
@server = server
|
259
|
+
@target = target
|
260
|
+
@name = name
|
261
|
+
@ext = ext
|
262
|
+
end
|
263
|
+
|
264
|
+
##
|
265
|
+
# A friendlier inspect
|
266
|
+
|
267
|
+
def inspect
|
268
|
+
"#<#{self.class}:0x#{object_id.to_s 16} #{target} #{location}>"
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
|
273
|
+
##
|
274
|
+
# Default broadcast address
|
275
|
+
|
276
|
+
BROADCAST = '239.255.255.250'
|
277
|
+
|
278
|
+
##
|
279
|
+
# Default port
|
280
|
+
|
281
|
+
PORT = 1900
|
282
|
+
|
283
|
+
##
|
284
|
+
# Default timeout
|
285
|
+
|
286
|
+
TIMEOUT = 1
|
287
|
+
|
288
|
+
##
|
289
|
+
# Default packet time to live (hops)
|
290
|
+
|
291
|
+
TTL = 4
|
292
|
+
|
293
|
+
##
|
294
|
+
# Broadcast address to use when sending searches and listening for
|
295
|
+
# notifications
|
296
|
+
|
297
|
+
attr_accessor :broadcast
|
298
|
+
|
299
|
+
##
|
300
|
+
# Listener accessor for tests.
|
301
|
+
|
302
|
+
attr_accessor :listener # :nodoc:
|
303
|
+
|
304
|
+
##
|
305
|
+
# Port to use for SSDP searching and listening
|
306
|
+
|
307
|
+
attr_accessor :port
|
308
|
+
|
309
|
+
##
|
310
|
+
# Queue accessor for tests
|
311
|
+
|
312
|
+
attr_accessor :queue # :nodoc:
|
313
|
+
|
314
|
+
##
|
315
|
+
# Socket accessor for tests
|
316
|
+
|
317
|
+
attr_accessor :socket # :nodoc:
|
318
|
+
|
319
|
+
##
|
320
|
+
# Time to wait for SSDP responses
|
321
|
+
|
322
|
+
attr_accessor :timeout
|
323
|
+
|
324
|
+
##
|
325
|
+
# TTL for SSDP packets
|
326
|
+
|
327
|
+
attr_accessor :ttl
|
328
|
+
|
329
|
+
##
|
330
|
+
# Creates a new SSDP object. Use the accessors to override broadcast, port,
|
331
|
+
# timeout or ttl.
|
332
|
+
|
333
|
+
def initialize
|
334
|
+
@broadcast = BROADCAST
|
335
|
+
@port = PORT
|
336
|
+
@timeout = TIMEOUT
|
337
|
+
@ttl = TTL
|
338
|
+
|
339
|
+
@listener = nil
|
340
|
+
@queue = Queue.new
|
341
|
+
end
|
342
|
+
|
343
|
+
##
|
344
|
+
# Discovers UPnP devices sending NOTIFY broadcasts.
|
345
|
+
#
|
346
|
+
# If given a block, yields each Notification as it is received and never
|
347
|
+
# returns. Otherwise, discover waits for timeout seconds and returns all
|
348
|
+
# notifications received in that time.
|
349
|
+
|
350
|
+
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
|
359
|
+
|
360
|
+
listen
|
361
|
+
|
362
|
+
if block_given? then
|
363
|
+
loop do
|
364
|
+
notification = @queue.pop
|
365
|
+
|
366
|
+
yield notification
|
367
|
+
end
|
368
|
+
else
|
369
|
+
sleep @timeout
|
370
|
+
|
371
|
+
notifications = []
|
372
|
+
notifications << @queue.pop until @queue.empty?
|
373
|
+
notifications
|
374
|
+
end
|
375
|
+
ensure
|
376
|
+
stop_listening
|
377
|
+
@socket.close if @socket and not @socket.closed?
|
378
|
+
@socket = nil
|
379
|
+
end
|
380
|
+
|
381
|
+
##
|
382
|
+
# Listens for UDP packets from devices in a Thread and enqueues them for
|
383
|
+
# processing. Requires a socket from search or discover.
|
384
|
+
|
385
|
+
def listen
|
386
|
+
return @listener if @listener and @listener.alive?
|
387
|
+
|
388
|
+
@listener = Thread.start do
|
389
|
+
loop do
|
390
|
+
response = @socket.recvfrom(1024).first
|
391
|
+
|
392
|
+
begin
|
393
|
+
@queue << parse(response)
|
394
|
+
rescue
|
395
|
+
puts $!.message
|
396
|
+
puts $!.backtrace
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
##
|
403
|
+
# Returns a Notification or Response created from +response+.
|
404
|
+
|
405
|
+
def parse(response)
|
406
|
+
case response
|
407
|
+
when /\ANOTIFY/ then
|
408
|
+
Notification.parse response
|
409
|
+
when /\AHTTP/ then
|
410
|
+
Response.parse response
|
411
|
+
else
|
412
|
+
raise Error, "Unknown response #{response[/\A.*$/]}"
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
##
|
417
|
+
# Broadcasts M-SEARCH requests looking for +targets+. Waits timeout seconds
|
418
|
+
# for responses then returns the collected responses.
|
419
|
+
#
|
420
|
+
# Supply no arguments to search for all devices and services.
|
421
|
+
#
|
422
|
+
# Supply <tt>:root</tt> to search for root devices only.
|
423
|
+
#
|
424
|
+
# Supply <tt>[:device, 'device_type:version']</tt> to search for a specific
|
425
|
+
# device type.
|
426
|
+
#
|
427
|
+
# Supply <tt>[:service, 'service_type:version']</tt> to search for a
|
428
|
+
# specific service type.
|
429
|
+
#
|
430
|
+
# Supply <tt>"uuid:..."</tt> to search for a UUID.
|
431
|
+
#
|
432
|
+
# Supply <tt>"urn:..."</tt> to search for a URN.
|
433
|
+
|
434
|
+
def search(*targets)
|
435
|
+
@socket ||= UDPSocket.new
|
436
|
+
|
437
|
+
@socket.setsockopt Socket::IPPROTO_IP, Socket::IP_TTL, [@ttl].pack('i')
|
438
|
+
|
439
|
+
if targets.empty? then
|
440
|
+
send_search 'ssdp:all'
|
441
|
+
else
|
442
|
+
targets.each do |target|
|
443
|
+
if target == :root then
|
444
|
+
send_search 'upnp:rootdevice'
|
445
|
+
elsif Array === target and target.first == :device then
|
446
|
+
target = [UPnP::DEVICE_SCHEMA_PREFIX, target.last]
|
447
|
+
send_search target.join(':')
|
448
|
+
elsif Array === target and target.first == :service then
|
449
|
+
target = [UPnP::SERVICE_SCHEMA_PREFIX, target.last]
|
450
|
+
send_search target.join(':')
|
451
|
+
elsif String === target and target =~ /\A(urn|uuid|ssdp):/ then
|
452
|
+
send_search target
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
listen
|
458
|
+
sleep @timeout
|
459
|
+
|
460
|
+
responses = []
|
461
|
+
responses << @queue.pop until @queue.empty?
|
462
|
+
responses
|
463
|
+
ensure
|
464
|
+
stop_listening
|
465
|
+
@socket.close if @socket and not @socket.closed?
|
466
|
+
@socket = nil
|
467
|
+
end
|
468
|
+
|
469
|
+
##
|
470
|
+
# Builds and sends an M-SEARCH request looking for +search_target+.
|
471
|
+
|
472
|
+
def send_search(search_target)
|
473
|
+
http_request = <<HTTP_REQUEST
|
474
|
+
M-SEARCH * HTTP/1.1\r
|
475
|
+
HOST: #{@broadcast}:#{@port}\r
|
476
|
+
MAN: "ssdp:discover"\r
|
477
|
+
MX: #{@timeout}\r
|
478
|
+
ST: #{search_target}\r
|
479
|
+
\r
|
480
|
+
HTTP_REQUEST
|
481
|
+
|
482
|
+
@socket.send http_request, 0, @broadcast, @port
|
483
|
+
end
|
484
|
+
|
485
|
+
##
|
486
|
+
# Stops and clears the listen thread.
|
487
|
+
|
488
|
+
def stop_listening
|
489
|
+
@listener.kill if @listener
|
490
|
+
@queue = Queue.new
|
491
|
+
@listener = nil
|
492
|
+
end
|
493
|
+
|
494
|
+
end
|
495
|
+
|