rbupnptools 0.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.
- checksums.yaml +7 -0
- data/lib/http_header.rb +84 -0
- data/lib/ssdp.rb +168 -0
- data/lib/upnp_control_point.rb +197 -0
- data/lib/upnp_event.rb +86 -0
- data/lib/upnp_model.rb +419 -0
- data/lib/upnp_server.rb +316 -0
- data/lib/upnp_soap.rb +96 -0
- data/lib/upnp_xml.rb +47 -0
- data/lib/usn.rb +31 -0
- metadata +52 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 03fbd519c8cb1e74cc7a96cbbb204a48792f4bf792872c59c8df39ace6b8458b
|
4
|
+
data.tar.gz: 96fa311850190c03e6f9adc502f078cfc7f28cfd6b436cd3dd1c3d8de7057f48
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3a1c31bf6a73f12fc09d226b4ada2c6246dcc17d2fe168b269333b51be3c876e9d6265645c0f0675fdbba35065a55d6bbe5c1ae5ef34e9c58f5b72ce6189fc23
|
7
|
+
data.tar.gz: 1e804d391a806988ef463240f4eb0420a7338d935bfa2c7e7e52d3955bc1cb39f88548333a1d00bf31c671433685a914f06e69663fcb5806badd6afa789d4d7f
|
data/lib/http_header.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
class FirstLine
|
2
|
+
def initialize(tokens)
|
3
|
+
@tokens = tokens
|
4
|
+
end
|
5
|
+
|
6
|
+
def [](key)
|
7
|
+
@tokens[key]
|
8
|
+
end
|
9
|
+
|
10
|
+
def []=(key, value)
|
11
|
+
@tokens[key] = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@tokens.join(' ')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def FirstLine.read(line)
|
20
|
+
FirstLine.new line.split(%r{\s+}, 3)
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
class HttpHeader < Hash
|
25
|
+
|
26
|
+
def initialize(firstline)
|
27
|
+
@name_table = Hash.new
|
28
|
+
@firstline = firstline
|
29
|
+
end
|
30
|
+
|
31
|
+
def firstline
|
32
|
+
@firstline
|
33
|
+
end
|
34
|
+
|
35
|
+
def org_keys
|
36
|
+
@name_table.values
|
37
|
+
end
|
38
|
+
|
39
|
+
def [](key)
|
40
|
+
super _issensitive(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
def []=(key, value)
|
44
|
+
@name_table[_issensitive(key)] = key
|
45
|
+
super _issensitive(key), value
|
46
|
+
end
|
47
|
+
|
48
|
+
def update!(hash)
|
49
|
+
hash.each { |k,v| self[k] = v }
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
s = "#{firstline}\r\n"
|
54
|
+
self.each do |key, value|
|
55
|
+
s << "#{@name_table[key]}: #{value}\r\n"
|
56
|
+
end
|
57
|
+
s << "\r\n"
|
58
|
+
return s
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_str
|
62
|
+
self.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def _issensitive(key)
|
68
|
+
key.respond_to?(:upcase) ? key.upcase : key
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def HttpHeader.read(text)
|
74
|
+
firstline = text.lines[0].strip
|
75
|
+
fields = text.lines[1..-1]
|
76
|
+
header = HttpHeader.new FirstLine.read(firstline)
|
77
|
+
fields.each do |line|
|
78
|
+
tokens = line.split ':', 2
|
79
|
+
name = tokens[0].strip
|
80
|
+
value = tokens[1].strip
|
81
|
+
header[name] = value
|
82
|
+
end
|
83
|
+
header
|
84
|
+
end
|
data/lib/ssdp.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'logger'
|
3
|
+
require_relative 'http_header.rb'
|
4
|
+
|
5
|
+
$logger = Logger.new(STDOUT)
|
6
|
+
|
7
|
+
module SSDP
|
8
|
+
|
9
|
+
MCAST_HOST = '239.255.255.250'
|
10
|
+
MCAST_PORT = 1900
|
11
|
+
|
12
|
+
|
13
|
+
class SsdpHeader < HttpHeader
|
14
|
+
def initialize(http_header)
|
15
|
+
@http_header = http_header
|
16
|
+
end
|
17
|
+
|
18
|
+
def notify?
|
19
|
+
@http_header.firstline[0] == 'NOTIFY'
|
20
|
+
end
|
21
|
+
|
22
|
+
def notify_alive?
|
23
|
+
self.notify? and @http_header['nts'] == 'ssdp:alive'
|
24
|
+
end
|
25
|
+
|
26
|
+
def notify_byebye?
|
27
|
+
self.notify? and @http_header['nts'] == 'ssdp:byebye'
|
28
|
+
end
|
29
|
+
|
30
|
+
def msearch?
|
31
|
+
@http_header.firstline[0] == 'M-SEARCH'
|
32
|
+
end
|
33
|
+
|
34
|
+
def http_response?
|
35
|
+
@http_header.firstline[0].start_with? 'HTTP'
|
36
|
+
end
|
37
|
+
|
38
|
+
def usn
|
39
|
+
self['usn']
|
40
|
+
end
|
41
|
+
|
42
|
+
def location
|
43
|
+
self['location']
|
44
|
+
end
|
45
|
+
|
46
|
+
def [](key)
|
47
|
+
@http_header[key]
|
48
|
+
end
|
49
|
+
|
50
|
+
def []=(key, value)
|
51
|
+
@http_header[key] = value
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
@http_header.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_str
|
59
|
+
@http_header.to_str
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
class SsdpHandler
|
65
|
+
def initialize(func = nil)
|
66
|
+
@func = func
|
67
|
+
end
|
68
|
+
|
69
|
+
def on_ssdp_header(ssdp_header)
|
70
|
+
if @func
|
71
|
+
return @func.(ssdp_header)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
class SsdpListener
|
78
|
+
def initialize(port = 1900)
|
79
|
+
@host = '0.0.0.0'
|
80
|
+
@port = port
|
81
|
+
@handler = nil
|
82
|
+
@finishing = false
|
83
|
+
end
|
84
|
+
|
85
|
+
attr_reader :port
|
86
|
+
attr_accessor :handler
|
87
|
+
|
88
|
+
def finish
|
89
|
+
@finishing = true
|
90
|
+
end
|
91
|
+
|
92
|
+
def run
|
93
|
+
@finishing = false
|
94
|
+
socket = UDPSocket.new
|
95
|
+
membership = IPAddr.new(MCAST_HOST).hton + IPAddr.new(@host).hton
|
96
|
+
socket.setsockopt(:IPPROTO_IP, :IP_ADD_MEMBERSHIP, membership)
|
97
|
+
socket.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, 1)
|
98
|
+
socket.setsockopt(:SOL_SOCKET, :SO_REUSEPORT, 1)
|
99
|
+
socket.bind(@host, @port)
|
100
|
+
fds = [socket]
|
101
|
+
$logger.debug 'listen...'
|
102
|
+
while @finishing == false do
|
103
|
+
timeout = 1
|
104
|
+
if ios = select(fds, [], [], timeout)
|
105
|
+
data, addr = socket.recvfrom(4096)
|
106
|
+
ssdp_header = SsdpHeader.new HttpHeader.read(data.chomp)
|
107
|
+
if @handler
|
108
|
+
responses = @handler.on_ssdp_header(ssdp_header)
|
109
|
+
if responses != nil
|
110
|
+
responses.each do |response|
|
111
|
+
socket.send response.to_s, 0, addr[2], addr[1]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
socket.close
|
118
|
+
end
|
119
|
+
|
120
|
+
def start
|
121
|
+
@run_thread = Thread.new { self.run }
|
122
|
+
end
|
123
|
+
|
124
|
+
def stop
|
125
|
+
@finishing = true
|
126
|
+
@run_thread.join
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
def self.send_msearch(st, mx, handler = nil)
|
132
|
+
socket = UDPSocket.new
|
133
|
+
socket.setsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL, 1)
|
134
|
+
payload = "M-SEARCH * HTTP/1.1\r\n" \
|
135
|
+
"HOST: #{MCAST_HOST}:#{MCAST_PORT}\r\n" \
|
136
|
+
"MAN: \"ssdp:discover\"\r\n" \
|
137
|
+
"MX: 3\r\n" \
|
138
|
+
"ST: ssdp:all\r\n" \
|
139
|
+
"\r\n"
|
140
|
+
|
141
|
+
socket.send(payload, 0, MCAST_HOST, MCAST_PORT)
|
142
|
+
|
143
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
144
|
+
|
145
|
+
fds = [socket]
|
146
|
+
|
147
|
+
lst = []
|
148
|
+
|
149
|
+
while true
|
150
|
+
cur = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
151
|
+
if (cur - start) >= mx
|
152
|
+
break
|
153
|
+
end
|
154
|
+
if ios = select(fds, [], [], 1)
|
155
|
+
data, addr = socket.recvfrom(4096)
|
156
|
+
ssdp_header = SsdpHeader.new HttpHeader.read(data.chomp)
|
157
|
+
if handler
|
158
|
+
handler.call(ssdp_header)
|
159
|
+
end
|
160
|
+
lst << ssdp_header
|
161
|
+
end
|
162
|
+
end
|
163
|
+
socket.close
|
164
|
+
return lst
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'active_support/core_ext/hash/conversions'
|
2
|
+
require "webrick"
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
require 'logger'
|
6
|
+
require_relative "ssdp.rb"
|
7
|
+
require_relative "upnp_model.rb"
|
8
|
+
require_relative "usn.rb"
|
9
|
+
require_relative "upnp_soap.rb"
|
10
|
+
require_relative "upnp_event.rb"
|
11
|
+
|
12
|
+
|
13
|
+
$logger = Logger.new STDOUT
|
14
|
+
|
15
|
+
|
16
|
+
class UPnPDeviceListener
|
17
|
+
def initialize(on_device_added = nil, on_device_removed = nil)
|
18
|
+
@on_device_added = on_device_added
|
19
|
+
@on_device_removed = on_device_removed
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_device_added(device)
|
23
|
+
if @on_device_added
|
24
|
+
@on_device_added.call(device)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def on_device_removed(device)
|
29
|
+
if @on_device_removed
|
30
|
+
@on_device_removed.call(device)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
class EventNotifyServlet < WEBrick::HTTPServlet::AbstractServlet
|
37
|
+
def do_NOTIFY(req, res)
|
38
|
+
cp = @options[0]
|
39
|
+
if req.path == '/event'
|
40
|
+
cp.on_event_notify req['SID'], req.body
|
41
|
+
end
|
42
|
+
res.status = 200
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
class UPnPControlPoint
|
48
|
+
|
49
|
+
def initialize(host = '0.0.0.0', port = 0)
|
50
|
+
@finishing = false
|
51
|
+
@ssdp_listener = SSDP::SsdpListener.new
|
52
|
+
@ssdp_listener.handler = self
|
53
|
+
@http_server = WEBrick::HTTPServer.new :BindAddress => host, :Port => port
|
54
|
+
@http_server.mount '/', EventNotifyServlet, self
|
55
|
+
@devices = {}
|
56
|
+
@subscriptions = {}
|
57
|
+
@interval_timer = 10
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_accessor :device_listener, :event_listener, :subscriptions
|
61
|
+
|
62
|
+
|
63
|
+
def on_event_notify(sid, body)
|
64
|
+
props = UPnPEventProperty.read(body)
|
65
|
+
if @event_listener
|
66
|
+
@event_listener.on_event_notify sid, props
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def on_ssdp_header(ssdp_header)
|
72
|
+
if ssdp_header.notify_alive? or ssdp_header.http_response?
|
73
|
+
usn = Usn.read(ssdp_header.usn)
|
74
|
+
if not @devices.key? usn.udn
|
75
|
+
xml = self.build_device ssdp_header
|
76
|
+
if xml.to_s.empty?
|
77
|
+
return
|
78
|
+
end
|
79
|
+
device = UPnPDevice.read xml
|
80
|
+
device.base_url = ssdp_header.location
|
81
|
+
@devices[usn.udn] = device
|
82
|
+
if @device_listener
|
83
|
+
@device_listener.on_device_added device
|
84
|
+
end
|
85
|
+
end
|
86
|
+
elsif ssdp_header.notify_byebye?
|
87
|
+
usn = Usn.read(ssdp_header['usn'])
|
88
|
+
device = @devices[usn.udn]
|
89
|
+
if device
|
90
|
+
if @device_listener
|
91
|
+
@device_listener.on_device_removed device
|
92
|
+
end
|
93
|
+
@devices.delete usn.udn
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
def send_msearch(st, mx = 3)
|
100
|
+
$logger.debug("send msearch / #{st}")
|
101
|
+
SSDP.send_msearch st, mx, lambda {
|
102
|
+
|ssdp_header| on_ssdp_header ssdp_header}
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
def build_device(ssdp_header)
|
107
|
+
uri = URI(ssdp_header.location)
|
108
|
+
xml = Net::HTTP.get(uri)
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def get_ip_address
|
113
|
+
Socket::getaddrinfo(Socket.gethostname, 'echo', Socket::AF_INET)[0][3]
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
def subscribe(device, service)
|
118
|
+
host = self.get_ip_address
|
119
|
+
port = @http_server.config[:Port]
|
120
|
+
headers = {
|
121
|
+
'NT' => 'upnp:event',
|
122
|
+
'CALLBACK' => "<http://#{host}:#{port}/event>",
|
123
|
+
'TIMEOUT' => 'Second-1800'
|
124
|
+
}
|
125
|
+
url = URI::join(device.base_url, service['eventSubURL'])
|
126
|
+
Net::HTTP.start(url.host, url.port) { |http|
|
127
|
+
req = SubscribeRequest.new url, initheader = headers
|
128
|
+
res = http.request req
|
129
|
+
sid = res['sid']
|
130
|
+
timeout = res['timeout'].split('-')[-1]
|
131
|
+
subscription = UPnPEventSubscription.new device, service, sid, timeout
|
132
|
+
@subscriptions[sid] = subscription
|
133
|
+
return subscription
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
def unsubscribe(subscription)
|
139
|
+
headers = {
|
140
|
+
'SID' => subscription.sid,
|
141
|
+
}
|
142
|
+
url = URI::join(subscription.device.base_url, subscription.service.scpdurl)
|
143
|
+
Net::HTTP.start(url.host, url.port) { |http|
|
144
|
+
req = UnsubscribeRequest.new url, initheader = headers
|
145
|
+
res = http.request req
|
146
|
+
$logger.debug("response : #{res.code}'")
|
147
|
+
}
|
148
|
+
@subscriptions.delete(subscription.sid)
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
def invoke_action(device, service, action_name, params)
|
153
|
+
url = URI::join(device.base_url, service['controlURL'])
|
154
|
+
soap_req = UPnPSoapRequest.new service.service_type, action_name
|
155
|
+
soap_req.merge! params
|
156
|
+
header = {
|
157
|
+
'SOAPACTION' => "#{service.service_type}##{action_name}",
|
158
|
+
'Content-Type' => 'text/xml; charset="utf-8'
|
159
|
+
}
|
160
|
+
http = Net::HTTP.new(url.host, url.port)
|
161
|
+
req = Net::HTTP::Post.new(url.request_uri, header)
|
162
|
+
req.body = soap_req.to_xml
|
163
|
+
res = http.request(req)
|
164
|
+
UPnPSoapResponse.read res.body
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
def on_timer
|
169
|
+
@devices.reject! {|key, value| value.expired? }
|
170
|
+
end
|
171
|
+
|
172
|
+
def timer_loop
|
173
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
174
|
+
while not @finishing
|
175
|
+
dur = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) - start
|
176
|
+
if dur >= @interval_timer
|
177
|
+
on_timer
|
178
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def start
|
184
|
+
@finishing = false
|
185
|
+
@ssdp_listener_thread = Thread.new { @ssdp_listener.run }
|
186
|
+
@http_server_thread = Thread.new { @http_server.start }
|
187
|
+
@timer_thread = Thread.new { timer_loop }
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
def stop
|
192
|
+
@finishing = true
|
193
|
+
@http_server.shutdown
|
194
|
+
@http_server_thread.join
|
195
|
+
@timer_thread.join
|
196
|
+
end
|
197
|
+
end
|