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 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
@@ -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