UPnP 1.0.0 → 1.1.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 +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
|
+
|