rbupnptools 0.1.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
+ 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