UPnP 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|