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.
@@ -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
+
@@ -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
+