UPnP 1.0.0 → 1.1.0

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