rbupnptools 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,316 @@
1
+ require_relative 'upnp_model.rb'
2
+ require_relative 'ssdp.rb'
3
+ require_relative 'upnp_event.rb'
4
+ require "webrick"
5
+ require 'securerandom'
6
+ require 'time'
7
+ require 'digest'
8
+
9
+ class UPnPServerServlet < WEBrick::HTTPServlet::AbstractServlet
10
+ def do_GET(req, res)
11
+ server = @options[0]
12
+ if req.path.end_with? "device.xml"
13
+ device_description = server.get_device_description req.path
14
+ res.status = 200
15
+ res['Content-Type'] = 'text/xml; charset="utf-8"'
16
+ res.body = device_description
17
+ elsif req.path.end_with? "scpd.xml"
18
+ scpd = server.get_scpd req.path
19
+ res.status = 200
20
+ res['Content-Type'] = 'text/xml; charset="utf-8"'
21
+ res.body = scpd
22
+ end
23
+ end
24
+
25
+ def do_POST(req, res)
26
+ server = @options[0]
27
+ soap_req = UPnPSoapRequest.read req.body
28
+ soap_res = server.on_action_request req.path soap_req
29
+ res.status = 200
30
+ res['Content-Type'] = 'text/xml; charset="utf-8"'
31
+ res.body = soap_res.to_s
32
+ end
33
+
34
+ def do_SUBSCRIBE(req, res)
35
+ server = @options[0]
36
+ path = req.path
37
+ if req['SID']
38
+ server.on_renew_subscription req['SID']
39
+ res.status = 200
40
+ else
41
+ callback_urls = req['CALLBACK'].split(' ').map { |elem| elem[2..-1] }
42
+ timeout = Integer(req['TIMEOUT'].split('-')[-1])
43
+ server.on_subscribe req.path timeout, callback_urls
44
+ res.status = 200
45
+ end
46
+ end
47
+
48
+ def do_UNSUBSCRIBE(req, res)
49
+ server = @options[0]
50
+ server.on_unsubscribe req['SID']
51
+ res.status = 200
52
+ end
53
+ end
54
+
55
+
56
+ class UPnPServer
57
+
58
+ def initialize(host='0.0.0.0', port=0)
59
+ @finishing = false
60
+ @devices = {}
61
+ @ssdp_listener = SSDP::SsdpListener.new
62
+ @ssdp_listener.handler = self
63
+ @http_server = WEBrick::HTTPServer.new :BindAddress => host, :Port => port
64
+ @http_server.mount '/', UPnPServerServlet, self
65
+ @action_handler = nil
66
+ @subscriptions = {}
67
+ @interval_timer = 10
68
+ end
69
+
70
+ attr_accessor :action_handler, :devices
71
+
72
+ def register_device(device, scpd_table)
73
+ device.all_services.each do |service|
74
+ hash = get_hash service.service_type
75
+ service.scpdurl = "/#{hash}/scpd.xml"
76
+ service.control_url = "/#{hash}/control.xml"
77
+ service.event_sub_url = "/#{hash}/event.xml"
78
+ service.scpd = scpd_table[service.service_type]
79
+ end
80
+ @devices[device.udn] = device
81
+ end
82
+
83
+
84
+ def unregister_device(device)
85
+ @devices.remove device.udn
86
+ end
87
+
88
+
89
+ def notify_alive_all
90
+ end
91
+
92
+
93
+ def get_device_description(path)
94
+ hash = path.split('/').reject(&:empty?).first
95
+ @devices.each do |udn, device|
96
+ if get_hash(udn) == hash
97
+ return UPnPDevice.to_xml_doc device
98
+ end
99
+ end
100
+ end
101
+
102
+
103
+ def get_scpd(path)
104
+ hash = path.split('/').reject(&:empty?).first
105
+ @devices.values.each do |device|
106
+ device.all_services.each do |service|
107
+ if get_hash(service.service_type) == hash
108
+ return UPnPScpd.to_xml_doc service.scpd
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+
115
+ def on_action_request(path, soap_req)
116
+ hash = path.split('/').reject(&:empty?).first
117
+ @devices.values.each do |device|
118
+ device.all_services.each do |service|
119
+ if get_hash(service.service_type) == hash
120
+ if @action_handler
121
+ return @action_handler.call service, soap_req
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ end
128
+
129
+ def set_property(device, service, props)
130
+ @subscriptions.values.each do |subscription|
131
+ if subscription.device.udn == device.udn and
132
+ subscription.service.service_type == service.service_type
133
+ subscription.callback_urls.each do |url|
134
+ url = URI.parse(url)
135
+ header = {
136
+ 'SID' => subscription.sid
137
+ }
138
+ Net::HTTP.start(url.host, url.port) do |http|
139
+ req = NotifyRequest.new url, initheader = header
140
+ req.body = UPnPEventProperty.to_xml_doc props
141
+ res = http.request req
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ def on_subscribe(path, timeout, callback_urls)
149
+ hash = path.split('/').reject(&:empty?).first
150
+ @devices.values.each do |device|
151
+ device.all_services.each do |service|
152
+ if get_hash(service.service_type) == hash
153
+ sid = 'uuid:' + SecureRandom.uuid
154
+ subscription = UPnPEventSubscription.new device, service, sid, timeout, callback_urls
155
+ @subscriptions[sid] = subscription
156
+ return sid
157
+ end
158
+ end
159
+ end
160
+ nil
161
+ end
162
+
163
+
164
+ def on_renew_subscription(sid)
165
+ subscription = @subscriptions[sid]
166
+ subscription.renew_timeout
167
+ end
168
+
169
+
170
+ def on_unsubscribe(sid)
171
+ @subscriptions.remove sid
172
+ end
173
+
174
+
175
+ def on_ssdp_header(ssdp_header)
176
+ if ssdp_header.msearch?
177
+ ret = on_msearch ssdp_header['st']
178
+ end
179
+ end
180
+
181
+
182
+ def on_msearch(st)
183
+
184
+ # ST can be one of
185
+ # - ssdp:all
186
+ # - upnp:rootdevice
187
+ # - udn
188
+ # - device type
189
+ # - service type
190
+
191
+ # HTTP/1.1 200 OK
192
+ # Cache-Control: max-age=1800
193
+ # HOST: 239.255.255.250:1900
194
+ # Location: http://172.17.0.2:9001/device.xml?udn=e399855c-7ecb-1fff-8000-000000000000
195
+ # ST: e399855c-7ecb-1fff-8000-000000000000
196
+ # Server: Cross-Platform/0.0 UPnP/1.0 App/0.0
197
+ # USN: e399855c-7ecb-1fff-8000-000000000000
198
+ # Ext:
199
+ # Date: Sat, 08 Sep 2018 13:47:14 GMT
200
+
201
+ response_list = []
202
+
203
+ if st == 'ssdp:all'
204
+ devices = @devices.values
205
+ devices.each do |root_device|
206
+
207
+ location = get_device_location root_device
208
+
209
+ response_list << msearch_response('upnp:rootdevice',
210
+ root_device.udn + '::upnp:rootdevice',
211
+ location)
212
+
213
+ root_device.all_devices.each do |device|
214
+ response_list << msearch_response(device.device_type,
215
+ device.usn,
216
+ location)
217
+ end
218
+
219
+ root_device.all_services.each do |service|
220
+ response_list << msearch_response(service.service_type,
221
+ root_device.udn + '::' + service.service_type,
222
+ location)
223
+ end
224
+ end
225
+ elsif st == 'upnp:rootdevice'
226
+ devices = @devices.values
227
+ devices.each do |root_device|
228
+
229
+ location = get_device_location root_device
230
+
231
+ response_list << msearch_response('upnp:rootdevice',
232
+ root_device.udn + '::upnp:rootdevice',
233
+ location)
234
+ end
235
+ else
236
+ @devices.values.each do |root_device|
237
+ location = get_device_location root_device
238
+ root_device.all_devices.each do |device|
239
+ if device.device_type == st
240
+ response_list << msearch_response(st, device.usn, location)
241
+ end
242
+ end
243
+ root_device.all_services.each do |service|
244
+ if service.service_type == st
245
+ response_list << msearch_response(st,
246
+ root_device.udn + '::' + service.service_type,
247
+ location)
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ return response_list
254
+ end
255
+
256
+ def get_device_location(device)
257
+ host = get_ip_address
258
+ port = @http_server.config[:Port]
259
+ return "http://#{host}:#{port}/#{get_hash(device.udn)}/device.xml"
260
+ end
261
+
262
+ def msearch_response(st, usn, location, ext_header = nil)
263
+ header = HttpHeader.new FirstLine.new ['HTTP/1.1', '200', 'OK']
264
+ fields = {
265
+ 'Cache-Control' => 'max-age=1800',
266
+ 'Location' => location,
267
+ 'ST' => st,
268
+ 'USN' => usn,
269
+ 'Ext' => '',
270
+ 'Date' => Time.now.httpdate,
271
+ }
272
+ header.update! fields
273
+ if ext_header != nil
274
+ header.update! ext_header
275
+ end
276
+ return header
277
+ end
278
+
279
+ def get_hash(udn)
280
+ Digest::MD5.hexdigest udn
281
+ end
282
+
283
+ def get_ip_address
284
+ Socket::getaddrinfo(Socket.gethostname, 'echo', Socket::AF_INET)[0][3]
285
+ end
286
+
287
+ def on_timer
288
+ @subscriptions.reject! {|key, value| value.expired?}
289
+ notify_alive_all
290
+ end
291
+
292
+ def timer_loop
293
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
294
+ while not @finishing
295
+ dur = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) - start
296
+ if dur >= @interval_timer
297
+ on_timer
298
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
299
+ end
300
+ end
301
+ end
302
+
303
+ def start
304
+ @finishing = false
305
+ @ssdp_listener_thread = Thread.new { @ssdp_listener.run }
306
+ @http_server_thread = Thread.new { @http_server.start }
307
+ @timer_thread = Thread.new { timer_loop }
308
+ end
309
+
310
+ def stop
311
+ @finishing = true
312
+ @http_server.shutdown
313
+ @http_server_thread.join
314
+ @timer_thread.join
315
+ end
316
+ end
data/lib/upnp_soap.rb ADDED
@@ -0,0 +1,96 @@
1
+ require 'nokogiri'
2
+ require_relative 'upnp_xml.rb'
3
+
4
+ class UPnPSoapRequest < Hash
5
+
6
+ def initialize(service_type = nil, action_name = nil)
7
+ @service_type = service_type
8
+ @action_name = action_name
9
+ end
10
+
11
+ attr_accessor :service_type, :action_name
12
+ def to_xml
13
+ tag = XmlTag.new 's:Envelope'
14
+ tag.attributes = {
15
+ 's:encodingStyle' => "http://schemas.xmlsoap.org/soap/encoding/",
16
+ 'xmlns:s' => "http://schemas.xmlsoap.org/soap/envelope/"
17
+ }
18
+ body = tag.append XmlTag.new('s:Body')
19
+ action = body.append XmlTag.new("u:#{@action_name}")
20
+ action.attributes = {
21
+ 'xmlns:u' => @service_type
22
+ }
23
+ self.each do |k,v|
24
+ prop = action.append XmlTag.new k
25
+ prop.append XmlText.new v
26
+ end
27
+
28
+ return tag.to_s
29
+ end
30
+ end
31
+
32
+ def UPnPSoapRequest.to_xml_doc(soap_req)
33
+ return '<?xml version="1.0" encoding="utf-8"?>' + "\n#{soap_req.to_xml}"
34
+ end
35
+
36
+ def UPnPSoapRequest.read(xml)
37
+ soap_req = UPnPSoapRequest.new
38
+ doc = Nokogiri::XML(xml)
39
+ action_elem = doc.root.first_element_child.first_element_child
40
+ soap_req.service_type = action_elem.namespace.href
41
+ soap_req.action_name = action_elem.name
42
+ action_elem.elements.each do |node|
43
+ name = node.name
44
+ value = node.text
45
+ soap_req[name] = value
46
+ end
47
+ soap_req
48
+ end
49
+
50
+
51
+ class UPnPSoapResponse < Hash
52
+
53
+ def initialize(service_type = nil, action_name = nil)
54
+ @service_type = service_type
55
+ @action_name = action_name
56
+ end
57
+
58
+ attr_accessor :service_type, :action_name
59
+
60
+ def to_xml
61
+ tag = XmlTag.new 's:Envelope'
62
+ tag.attributes = {
63
+ 's:encodingStyle' => "http://schemas.xmlsoap.org/soap/encoding/",
64
+ 'xmlns:s' => "http://schemas.xmlsoap.org/soap/envelope/"
65
+ }
66
+ body = tag.append XmlTag.new('s:Body')
67
+ action = body.append XmlTag.new("u:#{@action_name}Response")
68
+ action.attributes = {
69
+ 'xmlns:u' => @service_type
70
+ }
71
+ self.each do |k,v|
72
+ prop = action.append XmlTag.new(k)
73
+ prop.append XmlText.new(v)
74
+ end
75
+
76
+ return tag.to_s
77
+ end
78
+ end
79
+
80
+ def UPnPSoapResponse.to_xml_doc(soap_res)
81
+ return '<?xml version="1.0" encoding="utf-8"?>' + "\n#{soap_res.to_xml}"
82
+ end
83
+
84
+ def UPnPSoapResponse.read(xml)
85
+ soap_res = UPnPSoapResponse.new
86
+ doc = Nokogiri::XML(xml)
87
+ action_elem = doc.root.first_element_child.first_element_child
88
+ soap_res.service_type = action_elem.namespace.href
89
+ soap_res.action_name = action_elem.name[0..-'Response'.length]
90
+ action_elem.elements.each do |node|
91
+ name = node.name
92
+ value = node.text
93
+ soap_res[name] = value
94
+ end
95
+ soap_res
96
+ end
data/lib/upnp_xml.rb ADDED
@@ -0,0 +1,47 @@
1
+
2
+
3
+ class XmlTag
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ @children = []
8
+ @attributes = {}
9
+ end
10
+
11
+ attr_accessor :name, :attributes, :children
12
+
13
+ def append(tag)
14
+ @children.append(tag)
15
+ return tag
16
+ end
17
+
18
+ def to_s
19
+ elems = [@name] + @attributes.map {|k,v| "#{k}=\"#{v}\""}
20
+
21
+ if @children.any?
22
+ str = "<#{elems.join(' ')}>"
23
+ str += @children.each {|elem| "#{elem}"}.join("")
24
+ str += "</#{@name}>"
25
+ return str
26
+ else
27
+ return "<#{elems.join(' ')} />"
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+
34
+ class XmlText
35
+ def initialize(text)
36
+ @text = "#{text}"
37
+ @text = escape(@text)
38
+ end
39
+
40
+ def escape(text)
41
+ text.gsub('&', '&amp;').gsub('>', '&gt;').gsub('<', '&lt;')
42
+ end
43
+
44
+ def to_s
45
+ @text
46
+ end
47
+ end