druzy-upnp 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 05af46c9b98de0c1073ad74433e387c9616252f0
4
+ data.tar.gz: 2b98f3eefbe44f41b9e158bf82d21f2de30e5f9b
5
+ SHA512:
6
+ metadata.gz: 0c6f0f5a91514c5199a1927fb19e79da36ecc887ace1e340a4e276e8ecea1d862d95049b25cc0621c31cb613ca31cdac675907d2f19019145c03d22632cd7bb4
7
+ data.tar.gz: 1694f9edf91260a4d5e145ed1a392560015f02a8949468cae629f66d770af45af600e4d0ae320f3ed4b596ddb16315765d33670b39d589f344af115edacd97e0
@@ -0,0 +1,5 @@
1
+ class Hash
2
+ def symbolize_keys!
3
+ self.inject({}) { |result, (k, v)| result[k.to_sym] = v; result }
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ require 'socket'
2
+
3
+ # Workaround for missing constants on Windows
4
+ module Socket::Constants
5
+ IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
6
+ IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
7
+ IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
8
+ IP_TTL = 4 unless defined? IP_TTL
9
+ end
10
+
11
+ class Socket
12
+ IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
13
+ IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
14
+ IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
15
+ IP_TTL = 4 unless defined? IP_TTL
16
+ end
@@ -0,0 +1,65 @@
1
+ class Hash
2
+
3
+ # Converts Hash search targets to SSDP search target String. Conversions are
4
+ # as follows:
5
+ # uuid: "someUUID" # => "uuid:someUUID"
6
+ # device_type: "someDeviceType:1" # => "urn:schemas-upnp-org:device:someDeviceType:1"
7
+ # service_type: "someServiceType:2" # => "urn:schemas-upnp-org:service:someServiceType:2"
8
+ #
9
+ # You can use custom UPnP domain names too:
10
+ # { device_type: "someDeviceType:3",
11
+ # domain_name: "mydomain-com" } # => "urn:my-domain:device:someDeviceType:3"
12
+ # { service_type: "someServiceType:4",
13
+ # domain_name: "mydomain-com" } # => "urn:my-domain:service:someDeviceType:4"
14
+ #
15
+ # @return [String] The converted String, according to the UPnP spec.
16
+ def to_upnp_s
17
+ if self.has_key? :uuid
18
+ return "uuid:#{self[:uuid]}"
19
+ elsif self.has_key? :device_type
20
+ if self.has_key? :domain_name
21
+ return "urn:#{self[:domain_name]}:device:#{self[:device_type]}"
22
+ else
23
+ return "urn:schemas-upnp-org:device:#{self[:device_type]}"
24
+ end
25
+ elsif self.has_key? :service_type
26
+ if self.has_key? :domain_name
27
+ return "urn:#{self[:domain_name]}:service:#{self[:service_type]}"
28
+ else
29
+ return "urn:schemas-upnp-org:service:#{self[:service_type]}"
30
+ end
31
+ else
32
+ self.to_s
33
+ end
34
+ end
35
+ end
36
+
37
+
38
+ class Symbol
39
+
40
+ # Converts Symbol search targets to SSDP search target String. Conversions are
41
+ # as follows:
42
+ # :all # => "ssdp:all"
43
+ # :root # => "upnp:rootdevice"
44
+ # "root" # => "upnp:rootdevice"
45
+ #
46
+ # @return [String] The converted String, according to the UPnP spec.
47
+ def to_upnp_s
48
+ if self == :all
49
+ 'ssdp:all'
50
+ elsif self == :root
51
+ 'upnp:rootdevice'
52
+ else
53
+ self
54
+ end
55
+ end
56
+ end
57
+
58
+
59
+ class String
60
+ # This doesn't do anything to the string; just allows users to call the
61
+ # method without having to check type first.
62
+ def to_upnp_s
63
+ self
64
+ end
65
+ end
@@ -0,0 +1,70 @@
1
+ require 'nori'
2
+ require 'em-http-request'
3
+ require_relative 'error'
4
+ require_relative '../../upnp'
5
+
6
+ module Druzy
7
+ module Upnp
8
+ class ControlPoint
9
+ class Base
10
+
11
+ protected
12
+
13
+ def get_description(location, description_getter)
14
+ http = EM::HttpRequest.new(location).aget
15
+
16
+ t = EM::Timer.new(30) do
17
+ http.fail(:timeout)
18
+ end
19
+
20
+ http.errback do |error|
21
+ if error == :timeout
22
+ http = EM::HttpRequest.new(location).get
23
+ else
24
+ description_getter.set_deferred_status(:failed)
25
+
26
+ if ControlPoint.raise_on_remote_error
27
+ raise ControlPoint::Error, "Unable to retrieve DDF from #{location}"
28
+ end
29
+ end
30
+ end
31
+
32
+ http.callback {
33
+ if http.response_header.status != 200
34
+ description_getter.set_deferred_status(:failed)
35
+ else
36
+ response = xml_parser.parse(http.response)
37
+ description_getter.set_deferred_status(:succeeded, response)
38
+ end
39
+ }
40
+ end
41
+
42
+ def build_url(url_base, rest_of_url)
43
+ if url_base.end_with?('/') && rest_of_url.start_with?('/')
44
+ rest_of_url.sub!('/', '')
45
+ end
46
+
47
+ url_base + rest_of_url
48
+ end
49
+
50
+ # @return [Nori::Parser]
51
+ def xml_parser
52
+ @xml_parser if @xml_parser
53
+
54
+ options = {
55
+ convert_tags_to: lambda { |tag| tag.to_sym }
56
+ }
57
+
58
+ begin
59
+ require 'nokogiri'
60
+ options.merge! parser: :nokogiri
61
+ rescue LoadError
62
+ warn "Tried loading nokogiri for XML couldn't. This is OK, just letting you know."
63
+ end
64
+
65
+ @xml_parser = Nori.new(options)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,465 @@
1
+ require_relative 'base'
2
+ require_relative 'service'
3
+ require_relative 'error'
4
+ require 'uri'
5
+ require 'eventmachine'
6
+ require 'time'
7
+
8
+ module Druzy
9
+ module Upnp
10
+ class ControlPoint
11
+ class Device < Base
12
+ include EM::Deferrable
13
+
14
+ attr_reader :ssdp_notification
15
+
16
+ #vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
17
+ # Passed in as part of +device_info+; given by the SSDP search response.
18
+ #
19
+ attr_reader :cache_control
20
+ attr_reader :date
21
+ attr_reader :ext
22
+ attr_reader :location
23
+ attr_reader :server
24
+ attr_reader :st
25
+ attr_reader :usn
26
+ #
27
+ # DONE +device_info+
28
+ #^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29
+
30
+ # @return [Hash] The whole parsed description... just in case.
31
+ attr_reader :description
32
+
33
+ attr_reader :expiration
34
+
35
+ #vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
36
+ # Determined by device description file.
37
+ #
38
+
39
+ #------- <root> elements -------
40
+
41
+ # @return [String] The xmlns attribute of the device from the description file.
42
+ attr_reader :xmlns
43
+
44
+ # @return [Hash] :major and :minor revisions of the UPnP spec this device adheres to.
45
+ attr_reader :spec_version
46
+
47
+ # @return [String] URLBase from the device's description file.
48
+ attr_reader :url_base
49
+
50
+ #------- <root><device> elements -------
51
+
52
+ # @return [String] The type of UPnP device (URN) from the description file.
53
+ attr_reader :device_type
54
+
55
+ # @return [String] Short device description for the end user.
56
+ attr_reader :friendly_name
57
+
58
+ # @return [String] Manufacturer's name.
59
+ attr_reader :manufacturer
60
+
61
+ # @return [String] Manufacturer's web site.
62
+ attr_reader :manufacturer_url
63
+
64
+ # @return [String] Long model description for the end user, from the description file.
65
+ attr_reader :model_description
66
+
67
+ # @return [String] Model name of this device from the description file.
68
+ attr_reader :model_name
69
+
70
+ # @return [String] Model number of this device from the description file.
71
+ attr_reader :model_number
72
+
73
+ # @return [String] Web site for model of this device.
74
+ attr_reader :model_url
75
+
76
+ # @return [String] The serial number from the description file.
77
+ attr_reader :serial_number
78
+
79
+ # @return [String] The UDN for the device, from the description file.
80
+ attr_reader :udn
81
+
82
+ # @return [String] The UPC of the device from the description file.
83
+ attr_reader :upc
84
+
85
+ # @return [Array<Hash>] An Array where each element is a Hash that describes an icon.
86
+ attr_reader :icon_list
87
+
88
+ # Services provided directly by this device.
89
+ attr_reader :service_list
90
+
91
+ # Devices embedded directly into this device.
92
+ attr_reader :device_list
93
+
94
+ # @return [String] URL for device control via a browser.
95
+ attr_reader :presentation_url
96
+
97
+ #
98
+ # DONE description file
99
+ #^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
100
+
101
+ # @param [Hash] device_info
102
+ # @option device_info [Hash] ssdp_notification
103
+ # @option device_info [Hash] device_description
104
+ # @option device_info [Hash] parent_base_url
105
+ def initialize(device_info)
106
+ super()
107
+
108
+ @device_info = device_info
109
+ @device_list = []
110
+ @service_list = []
111
+ @icon_list = []
112
+ @xmlns = ''
113
+ @done_creating_devices = false
114
+ @done_creating_services = false
115
+ end
116
+
117
+ def fetch
118
+ description_getter = EventMachine::DefaultDeferrable.new
119
+
120
+ description_getter.errback do
121
+ msg = 'Failed getting description.'
122
+ @done_creating_devices = true
123
+ @done_creating_services = true
124
+ set_deferred_status(:failed, msg)
125
+
126
+ if ControlPoint.raise_on_remote_error
127
+ raise ControlPoint::Error, msg
128
+ end
129
+ end
130
+
131
+ if @device_info.has_key? :ssdp_notification
132
+ extract_from_ssdp_notification(description_getter)
133
+ elsif @device_info.has_key? :device_description
134
+ description_getter.set_deferred_success @device_info[:device_description]
135
+ else
136
+ description_getter.set_deferred_failure
137
+ end
138
+
139
+ description_getter.callback do |description|
140
+ @description = description
141
+
142
+ if @description.nil?
143
+ set_deferred_status(:failed, 'Got back an empty description...')
144
+ return
145
+ end
146
+
147
+ extract_spec_version
148
+
149
+ @url_base = extract_url_base
150
+
151
+ if @device_info[:ssdp_notification]
152
+ @xmlns = @description[:root][:@xmlns]
153
+ extract_description(@description[:root][:device])
154
+ elsif @device_info.has_key? :device_description
155
+ extract_description(@description)
156
+ end
157
+ end
158
+
159
+ tickloop = EM.tick_loop do
160
+ if @done_creating_devices && @done_creating_services
161
+ :stop
162
+ end
163
+ end
164
+
165
+ tickloop.on_stop { set_deferred_status :succeeded, self }
166
+ end
167
+
168
+ def extract_from_ssdp_notification(callback)
169
+ @ssdp_notification = @device_info[:ssdp_notification]
170
+
171
+ @cache_control = @ssdp_notification[:cache_control]
172
+ @location = ssdp_notification[:location]
173
+ @server = ssdp_notification[:server]
174
+ @st = ssdp_notification[:st] || ssdp_notification[:nt]
175
+ @ext = ssdp_notification.has_key?(:ext) ? true : false
176
+ @usn = ssdp_notification[:usn]
177
+ @date = ssdp_notification[:date] || ''
178
+ @expiration = if @date.empty?
179
+ Time.now + @cache_control.match(/\d+/)[0].to_i
180
+ else
181
+ Time.at(Time.parse(@date).to_i + @cache_control.match(/\d+/)[0].to_i)
182
+ end
183
+
184
+ if @location
185
+ get_description(@location, callback)
186
+ else
187
+ message = 'M-SEARCH response is either missing the Location header or has an empty value.'
188
+ message << "Response: #{@ssdp_notification}"
189
+
190
+ if ControlPoint.raise_on_remote_error
191
+ raise ControlPoint::Error, message
192
+ end
193
+ end
194
+ end
195
+
196
+ def extract_url_base
197
+ if @description[:root] && @description[:root][:URLBase]
198
+ @description[:root][:URLBase]
199
+ elsif @device_info[:parent_base_url]
200
+ @device_info[:parent_base_url]
201
+ else
202
+ tmp_uri = URI(@location)
203
+ "#{tmp_uri.scheme}://#{tmp_uri.host}:#{tmp_uri.port}/"
204
+ end
205
+ end
206
+
207
+ # True if the device hasn't received an alive notification since it last
208
+ # told its max age.
209
+ def expired?
210
+ Time.now > @expiration if @expiration
211
+ end
212
+
213
+ def has_devices?
214
+ !@device_list.empty?
215
+ end
216
+
217
+ def has_services?
218
+ !@service_list.empty?
219
+ end
220
+
221
+ def extract_description(ddf)
222
+
223
+ @device_type = ddf[:deviceType] || ''
224
+ @friendly_name = ddf[:friendlyName] || ''
225
+ @manufacturer = ddf[:manufacturer] || ''
226
+ @manufacturer_url = ddf[:manufacturerURL] || ''
227
+ @model_description = ddf[:modelDescription] || ''
228
+ @model_name = ddf[:modelName] || ''
229
+ @model_number = ddf[:modelNumber] || ''
230
+ @model_url = ddf[:modelURL] || ''
231
+ @serial_number = ddf[:serialNumber] || ''
232
+ @udn = ddf[:UDN] || ''
233
+ @upc = ddf[:UPC] || ''
234
+ @icon_list = extract_icons(ddf[:iconList])
235
+ @presentation_url = ddf[:presentationURL] || ''
236
+
237
+ start_device_extraction
238
+ start_service_extraction
239
+ end
240
+
241
+ def extract_spec_version
242
+ if @description[:root]
243
+ "#{@description[:root][:specVersion][:major]}.#{@description[:root][:specVersion][:minor]}"
244
+ end
245
+ end
246
+
247
+ def start_service_extraction
248
+ services_extractor = EventMachine::DefaultDeferrable.new
249
+
250
+ if @description[:serviceList]
251
+ extract_services(@description[:serviceList], services_extractor)
252
+ elsif @description[:root][:device][:serviceList]
253
+ extract_services(@description[:root][:device][:serviceList], services_extractor)
254
+ end
255
+
256
+ services_extractor.errback do
257
+ msg = 'Failed extracting services.'
258
+ @done_creating_services = true
259
+
260
+ if ControlPoint.raise_on_remote_error
261
+ raise ControlPoint::Error, msg
262
+ end
263
+ end
264
+
265
+ services_extractor.callback do |services|
266
+ @service_list = services
267
+
268
+ @done_creating_services = true
269
+ end
270
+ end
271
+
272
+ def start_device_extraction
273
+ device_extractor = EventMachine::DefaultDeferrable.new
274
+ extract_devices(device_extractor)
275
+
276
+ device_extractor.errback do
277
+ msg = 'Failed extracting device.'
278
+ @done_creating_devices = true
279
+
280
+ if ControlPoint.raise_on_remote_error
281
+ raise ControlPoint::Error, msg
282
+ end
283
+ end
284
+
285
+ device_extractor.callback do |device|
286
+ if device
287
+ @device_list << device
288
+ end
289
+
290
+ @done_creating_devices = true
291
+ end
292
+ end
293
+
294
+ # @return [Array<Hash>]
295
+ def extract_icons(ddf_icon_list)
296
+ return [] unless ddf_icon_list
297
+
298
+ if ddf_icon_list[:icon].is_a? Array
299
+ ddf_icon_list[:icon].map do |values|
300
+ values[:url] = build_url(@url_base, values[:url])
301
+ values
302
+ end
303
+ else
304
+ [{
305
+ mimetype: ddf_icon_list[:icon][:mimetype],
306
+ width: ddf_icon_list[:icon][:width],
307
+ height: ddf_icon_list[:icon][:height],
308
+ depth: ddf_icon_list[:icon][:depth],
309
+ url: build_url(@url_base, ddf_icon_list[:icon][:url])
310
+ }]
311
+ end
312
+ end
313
+
314
+ def extract_devices(group_device_extractor)
315
+
316
+ device_list_hash = if @description.has_key? :root
317
+
318
+ if @description[:root][:device][:deviceList]
319
+ @description[:root][:device][:deviceList][:device]
320
+ else
321
+ group_device_extractor.set_deferred_status(:succeeded)
322
+ end
323
+ elsif @description[:deviceList]
324
+ @description[:deviceList][:device]
325
+ else
326
+ group_device_extractor.set_deferred_status(:succeeded)
327
+ end
328
+
329
+ if device_list_hash.nil? || device_list_hash.empty?
330
+ group_device_extractor.set_deferred_status(:succeeded)
331
+ return
332
+ end
333
+
334
+ if device_list_hash.is_a? Array
335
+ EM::Iterator.new(device_list_hash, device_list_hash.count).map(
336
+ proc do |device, iter|
337
+ single_device_extractor = EventMachine::DefaultDeferrable.new
338
+
339
+ single_device_extractor.errback do
340
+ msg = 'Failed extracting device.'
341
+
342
+ if ControlPoint.raise_on_remote_error
343
+ raise ControlPoint::Error, msg
344
+ end
345
+ end
346
+
347
+ single_device_extractor.callback do |d|
348
+ iter.return(d)
349
+ end
350
+
351
+ extract_device(device, single_device_extractor)
352
+ end,
353
+ proc do |found_devices|
354
+ group_device_extractor.set_deferred_status(:succeeded, found_devices)
355
+ end
356
+ )
357
+ else
358
+ single_device_extractor = EventMachine::DefaultDeferrable.new
359
+
360
+ single_device_extractor.errback do
361
+ msg = 'Failed extracting device.'
362
+ group_device_extractor.set_deferred_status(:failed, msg)
363
+
364
+ if ControlPoint.raise_on_remote_error
365
+ raise ControlPoint::Error, msg
366
+ end
367
+ end
368
+
369
+ single_device_extractor.callback do |device|
370
+ group_device_extractor.set_deferred_status(:succeeded, [device])
371
+ end
372
+
373
+ extract_device(device_list_hash, group_device_extractor)
374
+ end
375
+ end
376
+
377
+ def extract_device(device, device_extractor)
378
+ deferred_device = Device.new(device_description: device, parent_base_url: @url_base)
379
+
380
+ deferred_device.errback do
381
+ msg = "Couldn't build device!"
382
+ device_extractor.set_deferred_status(:failed, msg)
383
+
384
+ if ControlPoint.raise_on_remote_error
385
+ raise ControlPoint::Error, msg
386
+ end
387
+ end
388
+
389
+ deferred_device.callback do |built_device|
390
+ device_extractor.set_deferred_status(:succeeded, built_device)
391
+ end
392
+
393
+ deferred_device.fetch
394
+ end
395
+
396
+ def extract_services(service_list, group_service_extractor)
397
+ return if service_list.nil?
398
+
399
+ service_list.each_value do |service|
400
+ if service.is_a? Array
401
+ EM::Iterator.new(service, service.count).map(
402
+ proc do |s, iter|
403
+ single_service_extractor = EventMachine::DefaultDeferrable.new
404
+
405
+ single_service_extractor.errback do
406
+ msg = 'Failed to create service.'
407
+
408
+ if ControlPoint.raise_on_remote_error
409
+ raise ControlPoint::Error, msg
410
+ end
411
+ end
412
+
413
+ single_service_extractor.callback do |serv|
414
+ iter.return(serv)
415
+ end
416
+
417
+ extract_service(s, single_service_extractor)
418
+ end,
419
+ proc do |found_services|
420
+ group_service_extractor.set_deferred_status(:succeeded, found_services)
421
+ end
422
+ )
423
+ else
424
+ single_service_extractor = EventMachine::DefaultDeferrable.new
425
+
426
+ single_service_extractor.errback do
427
+ msg = 'Failed to create service.'
428
+ group_service_extractor.set_deferred_status :failed, msg
429
+
430
+ if ControlPoint.raise_on_remote_error
431
+ raise ControlPoint::Error, msg
432
+ end
433
+ end
434
+
435
+ single_service_extractor.callback do |s|
436
+ group_service_extractor.set_deferred_status :succeeded, [s]
437
+ end
438
+
439
+ extract_service(service, single_service_extractor)
440
+ end
441
+ end
442
+ end
443
+
444
+ def extract_service(service, single_service_extractor)
445
+ service_getter = Service.new(@url_base, service)
446
+
447
+ service_getter.errback do |message|
448
+ msg = "Couldn't build service with info: #{service}"
449
+ single_service_extractor.set_deferred_status(:failed, msg)
450
+
451
+ if ControlPoint.raise_on_remote_error
452
+ raise ControlPoint::Error, message
453
+ end
454
+ end
455
+
456
+ service_getter.callback do |built_service|
457
+ single_service_extractor.set_deferred_status(:succeeded, built_service)
458
+ end
459
+
460
+ service_getter.fetch
461
+ end
462
+ end
463
+ end
464
+ end
465
+ end
@@ -0,0 +1,15 @@
1
+ module Druzy
2
+ module Upnp
3
+ class ControlPoint
4
+ class Error < StandardError
5
+ #
6
+ end
7
+
8
+ # Indicates an error occurred when performing a UPnP action while controlling
9
+ # a device. See section 3.2 of the UPnP spec.
10
+ class ActionError < StandardError
11
+ #
12
+ end
13
+ end
14
+ end
15
+ end