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