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/lib/UPnP/control.rb
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'rexml/document'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
require 'UPnP'
|
6
|
+
require 'UPnP/SSDP'
|
7
|
+
require 'UPnP/control/service'
|
8
|
+
|
9
|
+
##
|
10
|
+
# A device on a UPnP control point.
|
11
|
+
#
|
12
|
+
# A Device holds information about a device and its associated sub-devices and
|
13
|
+
# services.
|
14
|
+
#
|
15
|
+
# Devices should be created using ::create instead of ::new. This allows a
|
16
|
+
# subclass of Device to be automatically instantiated.
|
17
|
+
#
|
18
|
+
# When creating a device subclass, it must have a URN_version constant set to
|
19
|
+
# the schema URN for that version.
|
20
|
+
#
|
21
|
+
# For details on UPnP devices, see http://www.upnp.org/resources/documents.asp
|
22
|
+
|
23
|
+
class UPnP::Control::Device
|
24
|
+
|
25
|
+
##
|
26
|
+
# All embedded devices
|
27
|
+
|
28
|
+
attr_reader :devices
|
29
|
+
|
30
|
+
##
|
31
|
+
# Short device description for the end user
|
32
|
+
|
33
|
+
attr_reader :friendly_name
|
34
|
+
|
35
|
+
##
|
36
|
+
# Manufacturer's name
|
37
|
+
|
38
|
+
attr_reader :manufacturer
|
39
|
+
|
40
|
+
##
|
41
|
+
# Manufacturer's web site
|
42
|
+
|
43
|
+
attr_reader :manufacturer_url
|
44
|
+
|
45
|
+
##
|
46
|
+
# Long model description for the end user
|
47
|
+
|
48
|
+
attr_reader :model_description
|
49
|
+
|
50
|
+
##
|
51
|
+
# Model name
|
52
|
+
|
53
|
+
attr_reader :model_name
|
54
|
+
|
55
|
+
##
|
56
|
+
# Model number
|
57
|
+
|
58
|
+
attr_reader :model_number
|
59
|
+
|
60
|
+
##
|
61
|
+
# Web site for model
|
62
|
+
|
63
|
+
attr_reader :model_url
|
64
|
+
|
65
|
+
##
|
66
|
+
# Unique Device Name (UDN), a universally unique identifier for the device
|
67
|
+
# whether root or embedded.
|
68
|
+
|
69
|
+
attr_reader :name
|
70
|
+
|
71
|
+
##
|
72
|
+
# URL for device control via a browser
|
73
|
+
|
74
|
+
attr_reader :presentation_url
|
75
|
+
|
76
|
+
##
|
77
|
+
# Serial number
|
78
|
+
|
79
|
+
attr_reader :serial_number
|
80
|
+
|
81
|
+
##
|
82
|
+
# All services provided by this device and its sub-devices.
|
83
|
+
|
84
|
+
attr_reader :services
|
85
|
+
|
86
|
+
##
|
87
|
+
# Devices embedded directly into this device.
|
88
|
+
|
89
|
+
attr_reader :sub_devices
|
90
|
+
|
91
|
+
##
|
92
|
+
# Services provided directly by this device.
|
93
|
+
|
94
|
+
attr_reader :sub_services
|
95
|
+
|
96
|
+
##
|
97
|
+
# Type of UPnP device (URN)
|
98
|
+
|
99
|
+
attr_reader :type
|
100
|
+
|
101
|
+
##
|
102
|
+
# Universal Product Code
|
103
|
+
|
104
|
+
attr_reader :upc
|
105
|
+
|
106
|
+
##
|
107
|
+
# Base URL for this device
|
108
|
+
|
109
|
+
attr_reader :url
|
110
|
+
|
111
|
+
##
|
112
|
+
# If a concrete class exists for +description+ it is used to instantiate the
|
113
|
+
# device, otherwise a concrete class is created subclassing Device and
|
114
|
+
# used.
|
115
|
+
|
116
|
+
def self.create(device_url)
|
117
|
+
description = REXML::Document.new open(device_url)
|
118
|
+
url = device_url + '/'
|
119
|
+
|
120
|
+
type = description.elements['root/device/deviceType'].text.strip
|
121
|
+
klass_name = type.sub(/#{UPnP::DEVICE_SCHEMA_PREFIX}:([^:]+):.*/, '\1')
|
122
|
+
|
123
|
+
begin
|
124
|
+
klass = const_get klass_name
|
125
|
+
rescue NameError
|
126
|
+
klass = const_set klass_name, Class.new(self)
|
127
|
+
klass.const_set :URN_1, "#{UPnP::DEVICE_SCHEMA_PREFIX}:#{klass.name}:1"
|
128
|
+
end
|
129
|
+
|
130
|
+
klass.new description.elements['root/device'], url
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Searches for devices using +ssdp+ and instantiates Device objects for
|
135
|
+
# them. By calling this method on a subclass only devices of that type will
|
136
|
+
# be returned.
|
137
|
+
|
138
|
+
def self.search(ssdp = UPnP::SSDP.new)
|
139
|
+
responses = if self == UPnP::Control::Service then
|
140
|
+
ssdp.search.select do |response|
|
141
|
+
response.type =~ /^#{UPnP::DEVICE_SCHEMA_PREFIX}/
|
142
|
+
end
|
143
|
+
else
|
144
|
+
urns = constants.select { |name| name =~ /^URN_/ }
|
145
|
+
devices = urns.map { |name| const_get name }
|
146
|
+
ssdp.search(*devices)
|
147
|
+
end
|
148
|
+
|
149
|
+
responses.map { |response| create response.location }
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Creates a new Device from +device+ which can be an REXML::Element
|
154
|
+
# describing the device or a URI for the device's description. If an XML
|
155
|
+
# description is provided, the parent device's +url+ must also be provided.
|
156
|
+
|
157
|
+
def initialize(device, url = nil)
|
158
|
+
@devices = []
|
159
|
+
@sub_devices = []
|
160
|
+
|
161
|
+
@services = []
|
162
|
+
@sub_services = []
|
163
|
+
|
164
|
+
case device
|
165
|
+
when URI::Generic then
|
166
|
+
description = REXML::Document.new open(device)
|
167
|
+
|
168
|
+
@url = description.elements['root/URLBase']
|
169
|
+
@url = @url ? URI.parse(@url.text.strip) : device + '/'
|
170
|
+
|
171
|
+
device = parse_device description.elements['root/device']
|
172
|
+
when REXML::Element then
|
173
|
+
raise ArgumentError, 'url not provided with REXML::Element' if url.nil?
|
174
|
+
@url = url
|
175
|
+
parse_device device
|
176
|
+
else
|
177
|
+
raise ArgumentError, 'must be a URI or an REXML::Element'
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# Parses the REXML::Element +description+ and fills in various attributes,
|
183
|
+
# sub-devices and sub-services
|
184
|
+
|
185
|
+
def parse_device(description)
|
186
|
+
@friendly_name = description.elements['friendlyName'].text.strip
|
187
|
+
|
188
|
+
@manufacturer = description.elements['manufacturer'].text.strip
|
189
|
+
|
190
|
+
manufacturer_url = description.elements['manufacturerURL']
|
191
|
+
@manufacturer_url = URI.parse manufacturer_url.text.strip if
|
192
|
+
manufacturer_url
|
193
|
+
|
194
|
+
model_description = description.elements['modelDescription']
|
195
|
+
@model_description = model_description.text.strip if model_description
|
196
|
+
|
197
|
+
@model_name = description.elements['modelName'].text.strip
|
198
|
+
|
199
|
+
model_number = description.elements['modelNumber']
|
200
|
+
@model_number = model_number.text.strip if model_number
|
201
|
+
|
202
|
+
model_url = description.elements['modelURL']
|
203
|
+
@model_url = URI.parse model_url.text.strip if model_url
|
204
|
+
|
205
|
+
@name = description.elements['UDN'].text.strip
|
206
|
+
|
207
|
+
presentation_url = description.elements['presentationURL']
|
208
|
+
@presentation_url = URI.parse presentation_url.text.strip if
|
209
|
+
presentation_url
|
210
|
+
|
211
|
+
serial_number = description.elements['serialNumber']
|
212
|
+
@serial_number = serial_number.text.strip if serial_number
|
213
|
+
|
214
|
+
@type = description.elements['deviceType'].text.strip
|
215
|
+
|
216
|
+
upc = description.elements['UPC']
|
217
|
+
@upc = upc.text.strip if upc
|
218
|
+
|
219
|
+
description.each_element 'deviceList/device' do |sub_device_description|
|
220
|
+
sub_device = UPnP::Control::Device.new sub_device_description, @url
|
221
|
+
@sub_devices << sub_device
|
222
|
+
end
|
223
|
+
|
224
|
+
@devices = @sub_devices.map do |device|
|
225
|
+
[device, device.devices]
|
226
|
+
end.flatten
|
227
|
+
|
228
|
+
description.each_element 'serviceList/service' do |service_description|
|
229
|
+
service = UPnP::Control::Service.create service_description, @url
|
230
|
+
@sub_services << service
|
231
|
+
end
|
232
|
+
|
233
|
+
@services = (@sub_services +
|
234
|
+
@devices.map { |device| device.services }.flatten).uniq
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
|
@@ -0,0 +1,461 @@
|
|
1
|
+
require 'UPnP/control'
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'rexml/document'
|
6
|
+
require 'soap/rpc/driver'
|
7
|
+
require 'time'
|
8
|
+
require 'uri'
|
9
|
+
|
10
|
+
##
|
11
|
+
# A service on a UPnP control point.
|
12
|
+
#
|
13
|
+
# A Service exposes the UPnP actions as ordinary ruby methods, which are
|
14
|
+
# handled via method_missing. A Service responds appropriately to respond_to?
|
15
|
+
# and methods to make introspection easy.
|
16
|
+
#
|
17
|
+
# Services should be created using ::create instead of ::new. This allows a
|
18
|
+
# subclass of Service to be automatically instantiated.
|
19
|
+
#
|
20
|
+
# When creating a service subclass, it must have a URN_version constant set to
|
21
|
+
# the schema URN for that version.
|
22
|
+
#
|
23
|
+
# For details on UPnP services, see http://www.upnp.org/resources/documents.asp
|
24
|
+
|
25
|
+
class UPnP::Control::Service
|
26
|
+
|
27
|
+
##
|
28
|
+
# Service error class
|
29
|
+
|
30
|
+
class Error < UPnP::Error
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Error raised when there was an error while calling an action
|
35
|
+
|
36
|
+
class UPnPError < Error
|
37
|
+
|
38
|
+
##
|
39
|
+
# The UPnP fault code
|
40
|
+
|
41
|
+
attr_accessor :code
|
42
|
+
|
43
|
+
##
|
44
|
+
# The UPnP fault description
|
45
|
+
|
46
|
+
attr_accessor :description
|
47
|
+
|
48
|
+
##
|
49
|
+
# Creates a new UPnP error using +description+ and +code+
|
50
|
+
|
51
|
+
def initialize(description, code)
|
52
|
+
@code = code
|
53
|
+
@description = description
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Error string including code and description
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
"#{@description} (#{@code})"
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# UPnP relies on the client to do type conversions. This registry casts the
|
67
|
+
# SOAPString returned by the service into the real SOAP type so soap4r can
|
68
|
+
# make the conversion.
|
69
|
+
|
70
|
+
class Registry < SOAP::Mapping::EncodedRegistry
|
71
|
+
|
72
|
+
##
|
73
|
+
# If +node+ is a simple type, cast it to the real type from the registered
|
74
|
+
# definition and super, otherwise just let EncodedRegistry do the work.
|
75
|
+
|
76
|
+
def soap2obj(node, klass = nil)
|
77
|
+
case node
|
78
|
+
when XSD::XSDAnySimpleType then
|
79
|
+
definition = find_node_definition node
|
80
|
+
|
81
|
+
return super if definition.nil?
|
82
|
+
|
83
|
+
new_class = definition.class_for
|
84
|
+
new_node = new_class.new node.data
|
85
|
+
|
86
|
+
return super(new_node, klass)
|
87
|
+
when SOAP::SOAPStruct then
|
88
|
+
return '' if node.members.empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
super
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Namespace for UPnP type extensions
|
98
|
+
|
99
|
+
module Types
|
100
|
+
|
101
|
+
##
|
102
|
+
# A one-byte string
|
103
|
+
|
104
|
+
class Char < SOAP::SOAPString
|
105
|
+
|
106
|
+
##
|
107
|
+
# Ensures the string is only one character long.
|
108
|
+
|
109
|
+
def screen_data(value)
|
110
|
+
super
|
111
|
+
|
112
|
+
if value.sub(/./mu, '').length > 1 then
|
113
|
+
raise ValueSpaceError, "#{type}: cannot accept '#{value}'."
|
114
|
+
end
|
115
|
+
|
116
|
+
value
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# A Universally Unique Identifier
|
123
|
+
|
124
|
+
class UUID < SOAP::SOAPString
|
125
|
+
|
126
|
+
def screen_data(data)
|
127
|
+
super
|
128
|
+
|
129
|
+
unless value.gsub('-', '') =~ /\A[a-f\d]{32}\z/ then
|
130
|
+
raise ValueSpaceError, "#{type}: cannot accept '#{value}'."
|
131
|
+
end
|
132
|
+
|
133
|
+
value
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# Map UPnP data types to SOAP data types
|
140
|
+
|
141
|
+
MAP = {
|
142
|
+
'ui1' => SOAP::SOAPUnsignedByte,
|
143
|
+
'ui2' => SOAP::SOAPUnsignedShort,
|
144
|
+
'ui4' => SOAP::SOAPUnsignedInt,
|
145
|
+
|
146
|
+
'i1' => SOAP::SOAPByte,
|
147
|
+
'i2' => SOAP::SOAPShort,
|
148
|
+
'i4' => SOAP::SOAPInt,
|
149
|
+
'int' => SOAP::SOAPInt,
|
150
|
+
|
151
|
+
'r4' => SOAP::SOAPFloat,
|
152
|
+
'r8' => SOAP::SOAPDouble,
|
153
|
+
'number' => SOAP::SOAPDouble,
|
154
|
+
'float' => SOAP::SOAPDecimal,
|
155
|
+
'fixed.14.4' => SOAP::SOAPDouble, # HACK not accurate
|
156
|
+
|
157
|
+
'char' => Char,
|
158
|
+
'string' => SOAP::SOAPString,
|
159
|
+
|
160
|
+
'date' => SOAP::SOAPDate,
|
161
|
+
'dateTime' => SOAP::SOAPDateTime,
|
162
|
+
'dateTime.tz' => SOAP::SOAPDateTime,
|
163
|
+
'time' => SOAP::SOAPTime,
|
164
|
+
'time.tz' => SOAP::SOAPTime,
|
165
|
+
|
166
|
+
'boolean' => SOAP::SOAPBoolean,
|
167
|
+
|
168
|
+
'bin.base64' => SOAP::SOAPBase64,
|
169
|
+
'bin.hex' => SOAP::SOAPHexBinary,
|
170
|
+
|
171
|
+
'uri' => SOAP::SOAPAnyURI,
|
172
|
+
|
173
|
+
'uuid' => UUID,
|
174
|
+
}
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
##
|
179
|
+
# Control URL
|
180
|
+
|
181
|
+
attr_reader :control_url
|
182
|
+
|
183
|
+
##
|
184
|
+
# SOAP driver for this service
|
185
|
+
|
186
|
+
attr_reader :driver
|
187
|
+
|
188
|
+
##
|
189
|
+
# Eventing URL
|
190
|
+
|
191
|
+
attr_reader :event_sub_url
|
192
|
+
|
193
|
+
##
|
194
|
+
# Service identifier, unique within this service's devices
|
195
|
+
|
196
|
+
attr_reader :id
|
197
|
+
|
198
|
+
##
|
199
|
+
# Service description URL
|
200
|
+
|
201
|
+
attr_reader :scpd_url
|
202
|
+
|
203
|
+
##
|
204
|
+
# UPnP service type
|
205
|
+
|
206
|
+
attr_reader :type
|
207
|
+
|
208
|
+
##
|
209
|
+
# Base URL for this service's device
|
210
|
+
|
211
|
+
attr_reader :url
|
212
|
+
|
213
|
+
##
|
214
|
+
# If a concrete class exists for +description+ it is used to instantiate the
|
215
|
+
# service, otherwise a concrete class is created subclassing Service and
|
216
|
+
# used.
|
217
|
+
|
218
|
+
def self.create(description, url)
|
219
|
+
type = description.elements['serviceType'].text.strip
|
220
|
+
klass_name = type.sub(/#{UPnP::SERVICE_SCHEMA_PREFIX}:([^:]+):.*/, '\1')
|
221
|
+
|
222
|
+
begin
|
223
|
+
klass = const_get klass_name
|
224
|
+
rescue NameError
|
225
|
+
klass = const_set klass_name, Class.new(self)
|
226
|
+
klass.const_set :URN_1, "#{UPnP::SERVICE_SCHEMA_PREFIX}:#{klass.name}:1"
|
227
|
+
end
|
228
|
+
|
229
|
+
klass.new description, url
|
230
|
+
end
|
231
|
+
|
232
|
+
##
|
233
|
+
# Creates a new service from REXML::Element +description+ and +url+. The
|
234
|
+
# description must be a service fragment from a device description.
|
235
|
+
|
236
|
+
def initialize(description, url)
|
237
|
+
@url = url
|
238
|
+
|
239
|
+
@type = description.elements['serviceType'].text.strip
|
240
|
+
@id = description.elements['serviceId'].text.strip
|
241
|
+
@control_url = @url + description.elements['controlURL'].text.strip
|
242
|
+
@event_sub_url = @url + description.elements['eventSubURL'].text.strip
|
243
|
+
@scpd_url = @url + description.elements['SCPDURL'].text.strip
|
244
|
+
|
245
|
+
create_driver
|
246
|
+
end
|
247
|
+
|
248
|
+
##
|
249
|
+
# Creates the SOAP driver from description at scpd_url
|
250
|
+
|
251
|
+
def create_driver
|
252
|
+
parse_service_description
|
253
|
+
|
254
|
+
@driver = SOAP::RPC::Driver.new @control_url, @type
|
255
|
+
|
256
|
+
mapping_registry = Registry.new
|
257
|
+
|
258
|
+
@actions.each do |name, arguments|
|
259
|
+
soapaction = "#{@type}##{name}"
|
260
|
+
qname = XSD::QName.new @type, soapaction
|
261
|
+
|
262
|
+
# TODO map ranges, enumerations
|
263
|
+
arguments = arguments.map do |direction, arg_name, variable|
|
264
|
+
type, = @variables[variable]
|
265
|
+
|
266
|
+
schema_name = XSD::QName.new nil, arg_name
|
267
|
+
|
268
|
+
mapping_registry.register :class => type, :schema_name => schema_name
|
269
|
+
|
270
|
+
[direction, arg_name, @variables[variable].first]
|
271
|
+
end
|
272
|
+
|
273
|
+
@driver.proxy.add_rpc_method qname, soapaction, name, arguments
|
274
|
+
@driver.send :add_rpc_method_interface, name, arguments
|
275
|
+
end
|
276
|
+
|
277
|
+
@driver.mapping_registry = mapping_registry
|
278
|
+
|
279
|
+
@actions = nil
|
280
|
+
@variables = nil
|
281
|
+
end
|
282
|
+
|
283
|
+
##
|
284
|
+
# Handles this service's actions
|
285
|
+
|
286
|
+
def method_missing(message, *arguments)
|
287
|
+
return super unless respond_to? message
|
288
|
+
|
289
|
+
begin
|
290
|
+
@driver.send(message, *arguments)
|
291
|
+
rescue SOAP::FaultError => e
|
292
|
+
backtrace = caller 0
|
293
|
+
|
294
|
+
fault_code = e.faultcode.data
|
295
|
+
fault_string = e.faultstring.data
|
296
|
+
|
297
|
+
detail = e.detail[fault_string]
|
298
|
+
code = detail['errorCode'].to_i
|
299
|
+
description = detail['errorDescription']
|
300
|
+
|
301
|
+
backtrace.first.gsub!(/:(\d+):in `([^']+)'/) do
|
302
|
+
line = $1.to_i - 2
|
303
|
+
":#{line}:in `#{message}' (method_missing)"
|
304
|
+
end
|
305
|
+
|
306
|
+
e = UPnPError.new description, code
|
307
|
+
e.set_backtrace backtrace
|
308
|
+
raise e
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
##
|
313
|
+
# Includes this service's actions
|
314
|
+
|
315
|
+
def methods(include_ancestors = true)
|
316
|
+
super + @driver.methods(false)
|
317
|
+
end
|
318
|
+
|
319
|
+
##
|
320
|
+
# Extracts arguments for an action from +argument_list+
|
321
|
+
|
322
|
+
def parse_action_arguments(argument_list)
|
323
|
+
arguments = []
|
324
|
+
|
325
|
+
argument_list.each_element 'argument' do |argument|
|
326
|
+
name = argument.elements['name'].text.strip
|
327
|
+
|
328
|
+
direction = argument.elements['direction'].text.strip.upcase
|
329
|
+
direction = 'RETVAL' if argument.elements['retval']
|
330
|
+
direction = SOAP::RPC::SOAPMethod.const_get direction
|
331
|
+
variable = argument.elements['relatedStateVariable'].text.strip
|
332
|
+
|
333
|
+
arguments << [direction, name, variable]
|
334
|
+
end if argument_list
|
335
|
+
|
336
|
+
arguments
|
337
|
+
end
|
338
|
+
|
339
|
+
##
|
340
|
+
# Extracts service actions from +action_list+
|
341
|
+
|
342
|
+
def parse_actions(action_list)
|
343
|
+
@actions = {}
|
344
|
+
|
345
|
+
action_list.each_element 'action' do |action|
|
346
|
+
name = action.elements['name'].text.strip
|
347
|
+
|
348
|
+
raise Error, "insecure action name #{name}" unless name =~ /\A\w*\z/
|
349
|
+
|
350
|
+
|
351
|
+
@actions[name] = parse_action_arguments action.elements['argumentList']
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
##
|
356
|
+
# Extracts a list of allowed values from +state_variable+
|
357
|
+
|
358
|
+
def parse_allowed_value_list(state_variable)
|
359
|
+
list = state_variable.elements['allowedValueList']
|
360
|
+
|
361
|
+
return nil unless list
|
362
|
+
|
363
|
+
values = []
|
364
|
+
|
365
|
+
list.each_element 'allowedValue' do |value|
|
366
|
+
value = value.text.strip
|
367
|
+
raise Error, "insecure allowed value #{value}" unless value =~ /\A\w*\z/
|
368
|
+
values << value
|
369
|
+
end
|
370
|
+
|
371
|
+
values
|
372
|
+
end
|
373
|
+
|
374
|
+
##
|
375
|
+
# Extracts an allowed value range from +state_variable+
|
376
|
+
|
377
|
+
def parse_allowed_value_range(state_variable)
|
378
|
+
range = state_variable.elements['allowedValueRange']
|
379
|
+
|
380
|
+
return nil unless range
|
381
|
+
|
382
|
+
minimum = range.elements['minimum']
|
383
|
+
maximum = range.elements['maximum']
|
384
|
+
step = range.elements['step']
|
385
|
+
|
386
|
+
range = [minimum, maximum, step]
|
387
|
+
|
388
|
+
range.map do |value|
|
389
|
+
value =~ /\./ ? Float(value) : Integer(value)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
##
|
394
|
+
# Parses a service description from the scpd_url
|
395
|
+
|
396
|
+
def parse_service_description
|
397
|
+
description = REXML::Document.new open(@scpd_url)
|
398
|
+
|
399
|
+
validate_scpd description
|
400
|
+
|
401
|
+
parse_actions description.elements['scpd/actionList']
|
402
|
+
|
403
|
+
service_state_table = description.elements['scpd/serviceStateTable']
|
404
|
+
parse_service_state_table service_state_table
|
405
|
+
end
|
406
|
+
|
407
|
+
##
|
408
|
+
# Extracts state variables from +service_state_table+
|
409
|
+
|
410
|
+
def parse_service_state_table(service_state_table)
|
411
|
+
@variables = {}
|
412
|
+
|
413
|
+
service_state_table.each_element 'stateVariable' do |var|
|
414
|
+
name = var.elements['name'].text.strip
|
415
|
+
data_type = Types::MAP[var.elements['dataType'].text.strip]
|
416
|
+
default = var.elements['defaultValue']
|
417
|
+
|
418
|
+
if default then
|
419
|
+
default = default.text.strip
|
420
|
+
raise Error, "insecure default value #{default}" unless
|
421
|
+
default =~ /\A\w*\z/
|
422
|
+
end
|
423
|
+
|
424
|
+
allowed_value_list = parse_allowed_value_list var
|
425
|
+
allowed_value_range = parse_allowed_value_range var
|
426
|
+
|
427
|
+
@variables[name] = [
|
428
|
+
data_type,
|
429
|
+
default,
|
430
|
+
allowed_value_list,
|
431
|
+
allowed_value_range
|
432
|
+
]
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
##
|
437
|
+
# Returns true for this service's actions as well as the usual behavior
|
438
|
+
|
439
|
+
def respond_to?(message)
|
440
|
+
@driver.methods(false).include? message.to_s || super
|
441
|
+
end
|
442
|
+
|
443
|
+
##
|
444
|
+
# Ensures +service_description+ has the correct namespace, root element, and
|
445
|
+
# version numbers. Raises an exception if the service isn't valid.
|
446
|
+
|
447
|
+
def validate_scpd(service_description)
|
448
|
+
namespace = service_description.elements["//scpd"].namespace
|
449
|
+
|
450
|
+
raise Error, "invalid namespace #{namespace}" unless
|
451
|
+
namespace == 'urn:schemas-upnp-org:service-1-0'
|
452
|
+
|
453
|
+
major = service_description.elements["//scpd/specVersion/major"].text.strip
|
454
|
+
minor = service_description.elements["//scpd/specVersion/minor"].text.strip
|
455
|
+
|
456
|
+
raise Error, "invalid version #{major}.#{minor}" unless
|
457
|
+
major == '1' and minor == '0'
|
458
|
+
end
|
459
|
+
|
460
|
+
end
|
461
|
+
|