UPnP 1.0.0 → 1.1.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 +9 -0
- data/History.txt +8 -0
- data/Manifest.txt +8 -0
- data/README.txt +18 -7
- data/Rakefile +3 -0
- data/lib/UPnP.rb +1 -1
- data/lib/UPnP/SSDP.rb +288 -25
- data/lib/UPnP/UUID.rb +187 -0
- data/lib/UPnP/control/service.rb +4 -1
- data/lib/UPnP/device.rb +692 -0
- data/lib/UPnP/root_server.rb +143 -0
- data/lib/UPnP/service.rb +376 -0
- data/test/test_UPnP_SSDP.rb +80 -17
- data/test/test_UPnP_SSDP_response.rb +2 -0
- data/test/test_UPnP_SSDP_search.rb +25 -0
- data/test/test_UPnP_control_device.rb +2 -0
- data/test/test_UPnP_control_service.rb +2 -0
- data/test/test_UPnP_device.rb +295 -0
- data/test/test_UPnP_root_server.rb +99 -0
- data/test/test_UPnP_service.rb +171 -0
- data/test/utilities.rb +61 -14
- metadata +36 -4
- metadata.gz.sig +0 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'UPnP'
|
2
|
+
require 'webrick'
|
3
|
+
|
4
|
+
##
|
5
|
+
# Master WEBrick server that publishes a root device's sub-devices and
|
6
|
+
# services for consumption by a UPnP control device.
|
7
|
+
#
|
8
|
+
# A root server is created automatiaclly for a device when you call #run on
|
9
|
+
# your device instance.
|
10
|
+
|
11
|
+
class UPnP::RootServer < WEBrick::HTTPServer
|
12
|
+
|
13
|
+
##
|
14
|
+
# The WEBrick logger
|
15
|
+
|
16
|
+
attr_reader :logger # :nodoc:
|
17
|
+
|
18
|
+
##
|
19
|
+
# This server's root UPnP device
|
20
|
+
|
21
|
+
attr_reader :root_device
|
22
|
+
|
23
|
+
##
|
24
|
+
# This server's SCPDs
|
25
|
+
|
26
|
+
attr_reader :scpds # :nodoc:
|
27
|
+
|
28
|
+
##
|
29
|
+
# Creates a new UPnP web server with +root_device+
|
30
|
+
|
31
|
+
def initialize(root_device)
|
32
|
+
@root_device = root_device
|
33
|
+
|
34
|
+
server_info = "RubyUPnP/#{UPnP::VERSION}"
|
35
|
+
device_info = "Ruby#{root_device.type}/#{root_device.version}"
|
36
|
+
@server_version = [server_info, 'UPnP/1.0', device_info].join ' '
|
37
|
+
|
38
|
+
@scpds = {}
|
39
|
+
|
40
|
+
level = if @root_device.class.debug? then
|
41
|
+
WEBrick::BasicLog::DEBUG
|
42
|
+
else
|
43
|
+
WEBrick::BasicLog::FATAL
|
44
|
+
end
|
45
|
+
|
46
|
+
@logger = WEBrick::Log.new $stderr, level
|
47
|
+
|
48
|
+
super :Logger => @logger, :Port => 0
|
49
|
+
|
50
|
+
mount_proc '/description', method(:description)
|
51
|
+
mount_proc '/', method(:root)
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Handler for the root device description
|
56
|
+
|
57
|
+
def description(req, res)
|
58
|
+
raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless
|
59
|
+
req.path == '/description'
|
60
|
+
|
61
|
+
res['content-type'] = 'text/xml'
|
62
|
+
res.body << @root_device.description
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Mounts WEBrick::HTTPServer +server+ at +path+
|
67
|
+
|
68
|
+
def mount_server(path, server)
|
69
|
+
server.config[:Logger] = @logger
|
70
|
+
|
71
|
+
mount_proc path do |req, res|
|
72
|
+
server.service req, res
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Mounts the appropriate paths for +service+ in this service
|
79
|
+
|
80
|
+
def mount_service(service)
|
81
|
+
mount_server service.control_url, service.server
|
82
|
+
|
83
|
+
service.mount_extra self
|
84
|
+
|
85
|
+
@scpds[service.scpd_url] = service
|
86
|
+
mount_proc service.scpd_url, method(:scpd)
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# A generic display page for the webserver root
|
91
|
+
|
92
|
+
def root(req, res)
|
93
|
+
raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless
|
94
|
+
req.path == '/'
|
95
|
+
|
96
|
+
res['content-type'] = 'text/html'
|
97
|
+
|
98
|
+
devices = @root_device.devices[1..-1].map do |d|
|
99
|
+
"<li>#{d.friendly_name} - #{d.type}"
|
100
|
+
end.join "\n"
|
101
|
+
|
102
|
+
services = @root_device.services.map do |s|
|
103
|
+
"<li>#{s.type}"
|
104
|
+
end.join "\n"
|
105
|
+
|
106
|
+
res.body = <<-EOF
|
107
|
+
<title>#{@root_device.friendly_name} - #{@root_device.type}</title>
|
108
|
+
|
109
|
+
<p>Devices:
|
110
|
+
|
111
|
+
<ul>
|
112
|
+
#{devices}
|
113
|
+
</ul>
|
114
|
+
|
115
|
+
<p>Services:
|
116
|
+
|
117
|
+
<ul>
|
118
|
+
#{services}
|
119
|
+
</ul>
|
120
|
+
EOF
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Handler for a service control protocol description request
|
125
|
+
|
126
|
+
def scpd(req, res)
|
127
|
+
service = @scpds[req.path]
|
128
|
+
raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless
|
129
|
+
service
|
130
|
+
|
131
|
+
res['content-type'] = 'text/xml'
|
132
|
+
res.body << service.scpd
|
133
|
+
end
|
134
|
+
|
135
|
+
def service(req, res)
|
136
|
+
super
|
137
|
+
|
138
|
+
res['Server'] = @server_version
|
139
|
+
res['EXT'] = ''
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
data/lib/UPnP/service.rb
ADDED
@@ -0,0 +1,376 @@
|
|
1
|
+
require 'UPnP'
|
2
|
+
require 'soap/rpc/standaloneServer'
|
3
|
+
require 'soap/filter/handler'
|
4
|
+
|
5
|
+
##
|
6
|
+
# A service contains a SOAP endpoint and the Service Control Protocol
|
7
|
+
# Definition (SCPD). It acts as a SOAP server that is mounted onto the
|
8
|
+
# RootServer along with the containing devices and other devices and services
|
9
|
+
# in a UPnP device.
|
10
|
+
#
|
11
|
+
# = Creating a UPnP::Service class
|
12
|
+
#
|
13
|
+
# A concrete UPnP service looks like this:
|
14
|
+
#
|
15
|
+
# require 'UPnP/service'
|
16
|
+
#
|
17
|
+
# class UPnP::Service::ContentDirectory < UPnP::Service
|
18
|
+
#
|
19
|
+
# add_action 'Browse',
|
20
|
+
# [IN, 'ObjectID', 'A_ARG_TYPE_ObjectID'],
|
21
|
+
# # ...
|
22
|
+
#
|
23
|
+
# [OUT, 'Result', 'A_ARG_TYPE_Result'],
|
24
|
+
# # ...
|
25
|
+
#
|
26
|
+
# add_variable 'A_ARG_TYPE_ObjectID', 'string'
|
27
|
+
# add_variable 'A_ARG_TYPE_Result', 'string'
|
28
|
+
#
|
29
|
+
# def Browse(object_id, ...)
|
30
|
+
# # ...
|
31
|
+
#
|
32
|
+
# [nil, result]
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# Subclass UPnP::Service in the UPnP::Service namespace. UPnP::Service looks
|
38
|
+
# in its own namespace for various information when instantiating the service.
|
39
|
+
#
|
40
|
+
# == Service Control Protocol Definition
|
41
|
+
#
|
42
|
+
# #add_action defines a service's action. The action's arguments follow the
|
43
|
+
# name as arrays of direction (IN, OUT, RETVAL), argument name, and related
|
44
|
+
# state variable.
|
45
|
+
#
|
46
|
+
# #add_variable defines a state table variable. The name is followed by the
|
47
|
+
# type, allowed values, default value and whether or not the variable is
|
48
|
+
# evented.
|
49
|
+
#
|
50
|
+
# == Implementing methods
|
51
|
+
#
|
52
|
+
# Define a regular ruby method matching the name in add_action for soap4r to
|
53
|
+
# call when it receives a request. It will be called with the IN parameters
|
54
|
+
# in order. The method needs to return an Array of OUT parameters in-order.
|
55
|
+
# If there is no RETVAL, the first item in the Array should be nil.
|
56
|
+
#
|
57
|
+
# = Instantiating a UPnP::Service
|
58
|
+
#
|
59
|
+
# A UPnP::Service will be instantiated automatically for you if you call
|
60
|
+
# add_service in the UPnP::Device initialization block. If you want to
|
61
|
+
# instantiate a service by hand, use ::create to pick the correct subclass
|
62
|
+
# automatically.
|
63
|
+
|
64
|
+
class UPnP::Service < SOAP::RPC::StandaloneServer
|
65
|
+
|
66
|
+
##
|
67
|
+
# Base service error class
|
68
|
+
|
69
|
+
class Error < UPnP::Error
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Adds the s:encodingStyle to the SOAP envelope rs equired by UPnP
|
74
|
+
|
75
|
+
class Filter < SOAP::Filter::Handler
|
76
|
+
|
77
|
+
def on_outbound(envelope, opt)
|
78
|
+
opt[:generate_explicit_type] = false
|
79
|
+
envelope.extraattr['s:encodingStyle'] = SOAP::EncodingNamespace
|
80
|
+
envelope
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Maps actions for a service to their arguments
|
87
|
+
|
88
|
+
ACTIONS = Hash.new { |h, service| h[service] = {} }
|
89
|
+
|
90
|
+
##
|
91
|
+
# Maps state variables for a service to their variable information
|
92
|
+
|
93
|
+
VARIABLES = Hash.new { |h, service| h[service] = {} }
|
94
|
+
|
95
|
+
##
|
96
|
+
# SOAP input argument type
|
97
|
+
|
98
|
+
IN = SOAP::RPC::SOAPMethod::IN
|
99
|
+
|
100
|
+
##
|
101
|
+
# SOAP output argument type
|
102
|
+
|
103
|
+
OUT = SOAP::RPC::SOAPMethod::OUT
|
104
|
+
|
105
|
+
##
|
106
|
+
# SOAP return value argument type
|
107
|
+
|
108
|
+
RETVAL = SOAP::RPC::SOAPMethod::RETVAL
|
109
|
+
|
110
|
+
##
|
111
|
+
# UPnP 1.0 service schema
|
112
|
+
|
113
|
+
SCHEMA_URN = 'urn:schemas-upnp-org:service-1-0'
|
114
|
+
|
115
|
+
##
|
116
|
+
# This service's parent
|
117
|
+
|
118
|
+
attr_reader :device
|
119
|
+
|
120
|
+
##
|
121
|
+
# Type of UPnP service. Use type_urn for the full URN
|
122
|
+
|
123
|
+
attr_reader :type
|
124
|
+
|
125
|
+
##
|
126
|
+
# Adds the action +name+ to this class with +arguments+
|
127
|
+
|
128
|
+
def self.add_action(name, *arguments)
|
129
|
+
ACTIONS[self][name] = arguments
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# Adds a state variable +name+ to this class
|
134
|
+
|
135
|
+
def self.add_variable(name, type, allowed_values = nil, default = nil,
|
136
|
+
evented = false)
|
137
|
+
VARIABLES[self][name] = [type, allowed_values, default, evented]
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Creates a new service under +device+ of the given +type+. Requires a
|
142
|
+
# concrete subclass of UPnP::Service.
|
143
|
+
|
144
|
+
def self.create(device, type, &block)
|
145
|
+
klass = const_get type
|
146
|
+
klass.new(device, type, &block)
|
147
|
+
rescue NameError => e
|
148
|
+
raise unless e.message =~ /UPnP::Service::#{type}/
|
149
|
+
raise Error, "unknown service type #{type}"
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Creates a new service under +device+ of the given +type+
|
154
|
+
|
155
|
+
def initialize(device, type, &block)
|
156
|
+
@device = device
|
157
|
+
@type = type
|
158
|
+
|
159
|
+
# HACK PS3 disobeys spec
|
160
|
+
SOAP::NS::KNOWN_TAG[type_urn] = 'u'
|
161
|
+
SOAP::NS::KNOWN_TAG[SOAP::EnvelopeNamespace] = 's'
|
162
|
+
|
163
|
+
super @type, type_urn
|
164
|
+
|
165
|
+
filterchain.add Filter.new
|
166
|
+
|
167
|
+
add_actions
|
168
|
+
|
169
|
+
yield self if block_given?
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Actions for this service
|
174
|
+
|
175
|
+
def actions
|
176
|
+
ACTIONS[self.class]
|
177
|
+
end
|
178
|
+
|
179
|
+
##
|
180
|
+
# Adds RPC actions to this service
|
181
|
+
|
182
|
+
def add_actions
|
183
|
+
opts = {
|
184
|
+
:request_style => :rpc,
|
185
|
+
:response_style => :rpc,
|
186
|
+
:request_use => :encoded,
|
187
|
+
:response_use => :literal,
|
188
|
+
}
|
189
|
+
|
190
|
+
actions.each do |name, params|
|
191
|
+
qname = XSD::QName.new @default_namespace, name
|
192
|
+
param_def = SOAP::RPC::SOAPMethod.derive_rpc_param_def self, name, params
|
193
|
+
@router.add_method self, qname, nil, name, param_def, opts
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
##
|
198
|
+
# The control URL for this service
|
199
|
+
|
200
|
+
def control_url
|
201
|
+
File.join service_path, 'control'
|
202
|
+
end
|
203
|
+
|
204
|
+
##
|
205
|
+
# Tell the StandaloneServer to not listen, RootServer does this for us
|
206
|
+
|
207
|
+
def create_config
|
208
|
+
hash = super
|
209
|
+
hash[:DoNotListen] = true
|
210
|
+
hash
|
211
|
+
end
|
212
|
+
|
213
|
+
##
|
214
|
+
# Adds a description of this service to XML::Builder +xml+
|
215
|
+
|
216
|
+
def description(xml)
|
217
|
+
xml.service do
|
218
|
+
xml.serviceType type_urn
|
219
|
+
xml.serviceId "urn:upnp-org:serviceId:#{root_device.service_id self}"
|
220
|
+
xml.SCPDURL scpd_url
|
221
|
+
xml.controlURL control_url
|
222
|
+
xml.eventSubURL event_sub_url
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
##
|
227
|
+
# The path for this service's parent device
|
228
|
+
|
229
|
+
def device_path
|
230
|
+
devices = []
|
231
|
+
device = @device
|
232
|
+
|
233
|
+
until device.nil? do
|
234
|
+
devices << device
|
235
|
+
device = device.parent
|
236
|
+
end
|
237
|
+
|
238
|
+
File.join('/', *devices.map { |d| d.type })
|
239
|
+
end
|
240
|
+
|
241
|
+
##
|
242
|
+
# The event subscription url for this service
|
243
|
+
|
244
|
+
def event_sub_url
|
245
|
+
File.join service_path, 'event_sub'
|
246
|
+
end
|
247
|
+
|
248
|
+
##
|
249
|
+
# Dumps only information necessary to run initialize. Server state is not
|
250
|
+
# persisted.
|
251
|
+
|
252
|
+
def marshal_dump
|
253
|
+
[
|
254
|
+
@device,
|
255
|
+
@type
|
256
|
+
]
|
257
|
+
end
|
258
|
+
|
259
|
+
##
|
260
|
+
# Loads data and initializes the server
|
261
|
+
|
262
|
+
def marshal_load(data)
|
263
|
+
device = data.shift
|
264
|
+
type = data.shift
|
265
|
+
|
266
|
+
initialize device, type
|
267
|
+
|
268
|
+
add_actions
|
269
|
+
end
|
270
|
+
|
271
|
+
##
|
272
|
+
# Callback to mount extra WEBrick servlets
|
273
|
+
|
274
|
+
def mount_extra(http_server)
|
275
|
+
end
|
276
|
+
|
277
|
+
##
|
278
|
+
# The root device for this service
|
279
|
+
|
280
|
+
def root_device
|
281
|
+
@device.root_device
|
282
|
+
end
|
283
|
+
|
284
|
+
##
|
285
|
+
# The SCPD for this service
|
286
|
+
|
287
|
+
def scpd
|
288
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
289
|
+
xml.instruct!
|
290
|
+
|
291
|
+
xml.scpd :xmlns => SCHEMA_URN do
|
292
|
+
xml.specVersion do
|
293
|
+
xml.major 1
|
294
|
+
xml.minor 0
|
295
|
+
end
|
296
|
+
|
297
|
+
scpd_action_list xml
|
298
|
+
|
299
|
+
scpd_service_state_table xml
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
##
|
304
|
+
# Adds the SCPD actionList to XML::Builder +xml+.
|
305
|
+
|
306
|
+
def scpd_action_list(xml)
|
307
|
+
xml.actionList do
|
308
|
+
actions.sort_by { |name,| name }.each do |name, arguments|
|
309
|
+
xml.action do
|
310
|
+
xml.name name
|
311
|
+
xml.argumentList do
|
312
|
+
arguments.each do |direction, arg_name, state_variable|
|
313
|
+
xml.argument do
|
314
|
+
xml.direction direction
|
315
|
+
xml.name arg_name
|
316
|
+
xml.relatedStateVariable state_variable
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
##
|
326
|
+
# Adds the SCPD serviceStateTable to XML::Builder +xml+.
|
327
|
+
|
328
|
+
def scpd_service_state_table(xml)
|
329
|
+
xml.serviceStateTable do
|
330
|
+
variables.each do |name, (type, allowed_values, default, send_events)|
|
331
|
+
send_events = send_events ? 'yes' : 'no'
|
332
|
+
xml.stateVariable :sendEvents => send_events do
|
333
|
+
xml.name name
|
334
|
+
xml.dataType type
|
335
|
+
if allowed_values then
|
336
|
+
xml.allowedValueList do
|
337
|
+
allowed_values.each do |value|
|
338
|
+
xml.allowedValue value
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
##
|
348
|
+
# The SCPD url for this service
|
349
|
+
|
350
|
+
def scpd_url
|
351
|
+
service_path
|
352
|
+
end
|
353
|
+
|
354
|
+
##
|
355
|
+
# The HTTP path to this service
|
356
|
+
|
357
|
+
def service_path
|
358
|
+
File.join device_path, @type
|
359
|
+
end
|
360
|
+
|
361
|
+
##
|
362
|
+
# URN of this service's type
|
363
|
+
|
364
|
+
def type_urn
|
365
|
+
"#{UPnP::SERVICE_SCHEMA_PREFIX}:#{@type}:1"
|
366
|
+
end
|
367
|
+
|
368
|
+
##
|
369
|
+
# Returns a Hash of state variables for this service
|
370
|
+
|
371
|
+
def variables
|
372
|
+
VARIABLES[self.class]
|
373
|
+
end
|
374
|
+
|
375
|
+
end
|
376
|
+
|