rupnp 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/rupnp/control_point.rb +1 -1
- data/lib/rupnp/cp/event_subscriber.rb +8 -10
- data/lib/rupnp/cp/remote_device.rb +4 -0
- data/lib/rupnp/cp/remote_service.rb +45 -47
- data/lib/rupnp/event.rb +5 -1
- data/lib/rupnp/http.rb +43 -0
- data/lib/rupnp/ssdp/listener.rb +1 -1
- data/lib/rupnp/ssdp/multicast_connection.rb +34 -31
- data/lib/rupnp/ssdp/notifier.rb +1 -1
- data/lib/rupnp/ssdp/search_responder.rb +1 -1
- data/lib/rupnp/ssdp/searcher.rb +1 -1
- data/lib/rupnp/ssdp.rb +0 -1
- data/lib/rupnp.rb +2 -1
- data/spec/control_point_spec.rb +9 -6
- data/spec/cp/remote_device_spec.rb +48 -10
- data/spec/cp/remote_service_spec.rb +197 -0
- data/spec/spec_helper.rb +85 -4
- metadata +4 -3
- data/lib/rupnp/ssdp/http.rb +0 -45
data/lib/rupnp/control_point.rb
CHANGED
@@ -48,6 +48,7 @@ module RUPNP
|
|
48
48
|
@devices = []
|
49
49
|
@new_device_channel = EM::Channel.new
|
50
50
|
@bye_device_channel = EM::Channel.new
|
51
|
+
@add_event_url = EM::Channel.new
|
51
52
|
end
|
52
53
|
|
53
54
|
# Start control point.
|
@@ -78,7 +79,6 @@ module RUPNP
|
|
78
79
|
# @return [void]
|
79
80
|
def start_event_server(port=EVENT_SUB_DEFAULT_PORT)
|
80
81
|
@event_port ||= port
|
81
|
-
@add_event_url ||= EM::Channel.new
|
82
82
|
@event_server ||= EM.start_server('0.0.0.0', port, CP::EventServer,
|
83
83
|
@add_event_url)
|
84
84
|
end
|
@@ -31,17 +31,15 @@ module RUPNP
|
|
31
31
|
io = StringIO.new(data)
|
32
32
|
|
33
33
|
status = io.readline
|
34
|
-
status =~ /HTTP\/1\.1 (\d+) (.+)/
|
35
|
-
resp[:status] = $2
|
36
|
-
resp[:status_code] = $1
|
37
|
-
|
38
|
-
io.each_line do |line|
|
39
|
-
if line =~ /(\w+):\s*(.*)/
|
40
|
-
resp[$1.downcase.to_sym] = $2.chomp
|
41
|
-
end
|
42
|
-
end
|
43
34
|
|
44
|
-
|
35
|
+
if status =~ /HTTP\/1\.1 (\d+) (.+)/
|
36
|
+
resp[:status] = $2
|
37
|
+
resp[:status_code] = $1
|
38
|
+
|
39
|
+
resp.merge!(get_http_headers(io))
|
40
|
+
|
41
|
+
@response << resp
|
42
|
+
end
|
45
43
|
end
|
46
44
|
|
47
45
|
end
|
@@ -248,6 +248,10 @@ module RUPNP
|
|
248
248
|
@description[:root][:device][:service_list][:service]
|
249
249
|
sl = @description[:root][:device][:service_list][:service]
|
250
250
|
|
251
|
+
if sl.is_a? Hash
|
252
|
+
sl = [sl]
|
253
|
+
end
|
254
|
+
|
251
255
|
proc_each = Proc.new do |s, iter|
|
252
256
|
service = CP::RemoteService.new(self, @url_base, s)
|
253
257
|
|
@@ -28,7 +28,7 @@ module RUPNP
|
|
28
28
|
# a +#action_name+ method is created. This method requires a hash with
|
29
29
|
# an element named +argument_name_in+.
|
30
30
|
# If no <i>in</i> argument is required, an empty hash (<code>{}</code>)
|
31
|
-
# must be passed to the method.
|
31
|
+
# must be passed to the method. Hash keys may not be symbols.
|
32
32
|
#
|
33
33
|
# A Hash is returned, with a key for each <i>out</i> argument.
|
34
34
|
#
|
@@ -111,23 +111,14 @@ module RUPNP
|
|
111
111
|
# Get service from its description
|
112
112
|
# @return [void]
|
113
113
|
def fetch
|
114
|
-
if @scpd_url.empty?
|
115
|
-
fail 'no SCPD URL'
|
116
|
-
return
|
117
|
-
end
|
118
|
-
|
119
114
|
scpd_getter = EM::DefaultDeferrable.new
|
120
115
|
|
121
116
|
scpd_getter.errback do
|
122
117
|
fail "cannot get SCPD from #@scpd_url"
|
118
|
+
next
|
123
119
|
end
|
124
120
|
|
125
121
|
scpd_getter.callback do |scpd|
|
126
|
-
if !scpd or scpd.empty?
|
127
|
-
fail "SCPD from #@scpd_url is empty"
|
128
|
-
next
|
129
|
-
end
|
130
|
-
|
131
122
|
if bad_description?(scpd)
|
132
123
|
fail 'not a UPNP 1.0/1.1 SCPD'
|
133
124
|
next
|
@@ -146,6 +137,8 @@ module RUPNP
|
|
146
137
|
# @param [Hash] options
|
147
138
|
# @option options [Integer] timeout
|
148
139
|
# @yieldparam [Event] event event received
|
140
|
+
# @yieldparam [Object] msg message received
|
141
|
+
# @return [Integer] subscribe id. May be used to unsubscribe on event
|
149
142
|
def subscribe_to_event(options={}, &blk)
|
150
143
|
cp = device.control_point
|
151
144
|
|
@@ -157,33 +150,33 @@ module RUPNP
|
|
157
150
|
|
158
151
|
uri = URI(@event_sub_url)
|
159
152
|
options[:timeout] ||= EVENT_SUB_DEFAULT_TIMEOUT
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
153
|
+
|
154
|
+
log :info, "send SUBSCRIBE request to #{uri}"
|
155
|
+
con = EM::HttpRequest.new(@event_sub_url)
|
156
|
+
http = con.setup_request(:subscribe, :head => {
|
157
|
+
'HOST' => "#{uri.host}:#{uri.port}",
|
158
|
+
'USER-AGENT' => RUPNP::USER_AGENT,
|
159
|
+
'CALLBACK' => @callback_url,
|
160
|
+
'NT' => 'upnp:event',
|
161
|
+
'TIMEOUT' => "Second-#{options[:timeout]}"})
|
162
|
+
|
163
|
+
http.errback do |client|
|
164
|
+
log :warn, "Cannot subscribe to event: #{client.error}"
|
165
|
+
end
|
166
|
+
|
167
|
+
http.callback do
|
168
|
+
log :debug, 'Close connection to subscribe event URL'
|
169
|
+
con.close
|
170
|
+
if http.response_header.status != 200
|
171
|
+
log :warn, "Cannot subscribe to event #@event_sub_url:" +
|
172
|
+
" #{http.response_header.http_reason}"
|
180
173
|
else
|
181
|
-
|
174
|
+
timeout = http.response_header['TIMEOUT'].match(/(\d+)/)[1] || 1800
|
175
|
+
event = Event.new(@event_sub_url, @callback_url,
|
176
|
+
http.response_header['SID'], timeout.to_i)
|
182
177
|
cp.add_event_url << ["/event#{num}", event]
|
183
|
-
event.subscribe
|
178
|
+
event.subscribe &blk
|
184
179
|
end
|
185
|
-
log :info, 'Close connection to subscribe event URL'
|
186
|
-
con.close_connection
|
187
180
|
end
|
188
181
|
end
|
189
182
|
|
@@ -208,8 +201,10 @@ EOR
|
|
208
201
|
def extract_service_state_table(scpd)
|
209
202
|
if scpd[:scpd][:service_state_table][:state_variable]
|
210
203
|
@state_table = scpd[:scpd][:service_state_table][:state_variable]
|
211
|
-
|
212
|
-
@state_table.
|
204
|
+
|
205
|
+
if @state_table.is_a? Hash
|
206
|
+
@state_table = [@state_table]
|
207
|
+
end
|
213
208
|
end
|
214
209
|
end
|
215
210
|
|
@@ -217,6 +212,7 @@ EOR
|
|
217
212
|
if scpd[:scpd][:action_list] and scpd[:scpd][:action_list][:action]
|
218
213
|
log :info, "extract actions for service #@type"
|
219
214
|
@actions = scpd[:scpd][:action_list][:action]
|
215
|
+
@actions = [@actions] unless @actions.is_a? Array
|
220
216
|
@actions.each do |action|
|
221
217
|
action[:arguments] = action[:argument_list][:argument]
|
222
218
|
action.delete :argument_list
|
@@ -229,16 +225,17 @@ EOR
|
|
229
225
|
action[:name] = action[:name].to_s
|
230
226
|
action_name = action[:name]
|
231
227
|
name = snake_case(action_name).to_sym
|
228
|
+
|
232
229
|
define_singleton_method(name) do |params|
|
230
|
+
if params
|
231
|
+
unless params.is_a? Hash
|
232
|
+
raise ArgumentError, 'only hash arguments are accepted'
|
233
|
+
end
|
234
|
+
end
|
233
235
|
response = @soap.call(action_name) do |locals|
|
234
236
|
locals.attributes 'xmlns:u' => @type
|
235
237
|
locals.soap_action "#{type}##{action_name}"
|
236
|
-
|
237
|
-
unless params.is_a? Hash
|
238
|
-
raise ArgumentError, 'only hash arguments are accepted'
|
239
|
-
end
|
240
|
-
locals.message params
|
241
|
-
end
|
238
|
+
locals.message params
|
242
239
|
end
|
243
240
|
|
244
241
|
if action[:arguments].is_a? Hash
|
@@ -248,11 +245,12 @@ EOR
|
|
248
245
|
end
|
249
246
|
else
|
250
247
|
log :debug, 'true argument list'
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
248
|
+
hsh = {}
|
249
|
+
outer = action[:arguments].select { |arg| arg[:direction] == 'out' }
|
250
|
+
outer.each do |arg|
|
251
|
+
hsh.merge! process_soap_response(name, response, arg)
|
255
252
|
end
|
253
|
+
hsh
|
256
254
|
end
|
257
255
|
end
|
258
256
|
end
|
data/lib/rupnp/event.rb
CHANGED
@@ -9,9 +9,13 @@ module RUPNP
|
|
9
9
|
# @return [Integer]
|
10
10
|
attr_reader :sid
|
11
11
|
|
12
|
+
# @param [String] event_suburl Event subscription URL
|
13
|
+
# @param [String] callback_url Callback URL to receive events
|
12
14
|
# @param [#to_i] sid
|
13
15
|
# @param [Integer] timeout for event (in seconds)
|
14
|
-
def initialize(sid, timeout)
|
16
|
+
def initialize(event_suburl, callback_url, sid, timeout)
|
17
|
+
super()
|
18
|
+
@event_suburl = event_suburl
|
15
19
|
@sid, @timeout = sid, timeout
|
16
20
|
|
17
21
|
@timeout_timer = EM.add_timer(@timeout) { self << :timeout }
|
data/lib/rupnp/http.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module RUPNP
|
2
|
+
|
3
|
+
# HTTP module to provide some helper methods
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
module HTTP
|
6
|
+
|
7
|
+
# Return status from HTTP response
|
8
|
+
# @param [IO] sock
|
9
|
+
# @return [Booelan]
|
10
|
+
def is_http_status_ok?(sock)
|
11
|
+
sock.readline =~ /\s*HTTP\/1.1 200 OK\r\n\z/i
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get HTTP headers from response
|
15
|
+
# @param [IO] sock
|
16
|
+
# @return [Hash] keys are downcase header name strings
|
17
|
+
def get_http_headers(sock)
|
18
|
+
headers = {}
|
19
|
+
sock.each_line do |l|
|
20
|
+
l =~ /([\w\.-]+):\s*(.*)/
|
21
|
+
if $1
|
22
|
+
headers[$1.downcase] = $2.strip
|
23
|
+
end
|
24
|
+
end
|
25
|
+
headers
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get HTTP verb from HTTP request
|
29
|
+
# @param [IO] sock
|
30
|
+
# @return [nil,Hash] keys are +:verb+, +:path+, +:http_version+ and
|
31
|
+
# +:cmd+ (all line)
|
32
|
+
def get_http_verb(sock)
|
33
|
+
str = sock.readline
|
34
|
+
if str =~ /([\w-]+)\s+(.*)\s+HTTP\/(\d\.\d)/
|
35
|
+
{:verb => $1, :path => $2, :http_version => $3, :cmd => str}
|
36
|
+
else
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/lib/rupnp/ssdp/listener.rb
CHANGED
@@ -2,47 +2,50 @@ require 'socket'
|
|
2
2
|
require 'ipaddr'
|
3
3
|
|
4
4
|
module RUPNP
|
5
|
+
module SSDP
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
# Base class for multicast connections (mainly SSDP search and listen)
|
8
|
+
# @abstract
|
9
|
+
class MulticastConnection < EM::Connection
|
10
|
+
include LogMixin
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
# @param [Integer] ttl
|
13
|
+
def initialize(ttl=nil)
|
14
|
+
@ttl = ttl || DEFAULT_TTL
|
15
|
+
setup_multicast_socket
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
# Get peer info
|
19
|
+
# @return [Array] [port, hostname]
|
20
|
+
def peer_info
|
21
|
+
Socket.unpack_sockaddr_in(get_peername)
|
22
|
+
end
|
22
23
|
|
23
24
|
|
24
|
-
|
25
|
+
private
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
def setup_multicast_socket
|
28
|
+
set_membership(IPAddr.new(MULTICAST_IP).hton +
|
29
|
+
IPAddr.new('0.0.0.0').hton)
|
30
|
+
set_ttl
|
31
|
+
set_reuse_addr
|
32
|
+
end
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
|
34
|
+
def set_membership(value)
|
35
|
+
set_sock_opt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, value
|
36
|
+
end
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
def set_ttl
|
39
|
+
value = [@ttl].pack('i')
|
40
|
+
set_sock_opt Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, value
|
41
|
+
set_sock_opt Socket::IPPROTO_IP, Socket::IP_TTL, value
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_reuse_addr
|
45
|
+
set_sock_opt Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true
|
46
|
+
end
|
41
47
|
|
42
|
-
def set_reuse_addr
|
43
|
-
set_sock_opt Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true
|
44
48
|
end
|
45
49
|
|
46
50
|
end
|
47
|
-
|
48
51
|
end
|
data/lib/rupnp/ssdp/notifier.rb
CHANGED
data/lib/rupnp/ssdp/searcher.rb
CHANGED
data/lib/rupnp/ssdp.rb
CHANGED
data/lib/rupnp.rb
CHANGED
@@ -6,7 +6,7 @@ require 'eventmachine-le'
|
|
6
6
|
module RUPNP
|
7
7
|
|
8
8
|
# RUPNP version
|
9
|
-
VERSION = '0.2.
|
9
|
+
VERSION = '0.2.2'
|
10
10
|
|
11
11
|
@logdev = STDERR
|
12
12
|
@log_level = :info
|
@@ -49,6 +49,7 @@ end
|
|
49
49
|
|
50
50
|
require_relative 'rupnp/constants'
|
51
51
|
require_relative 'rupnp/tools'
|
52
|
+
require_relative 'rupnp/http'
|
52
53
|
require_relative 'rupnp/log_mixin'
|
53
54
|
require_relative 'rupnp/event'
|
54
55
|
require_relative 'rupnp/control_point'
|
data/spec/control_point_spec.rb
CHANGED
@@ -33,10 +33,10 @@ module RUPNP
|
|
33
33
|
|
34
34
|
stub_request(:get, '127.0.0.1:1234').to_return :headers => {
|
35
35
|
'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'
|
36
|
-
}, :body =>
|
36
|
+
}, :body => generate_device_description(uuid1)
|
37
37
|
stub_request(:get, '127.0.0.1:1235').to_return :headers => {
|
38
38
|
'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'
|
39
|
-
}, :body =>
|
39
|
+
}, :body => generate_device_description(uuid2)
|
40
40
|
|
41
41
|
cp.send meth
|
42
42
|
|
@@ -53,7 +53,7 @@ module RUPNP
|
|
53
53
|
uuid = UUID.generate
|
54
54
|
stub_request(:get, '127.0.0.1:1234').to_return :headers => {
|
55
55
|
'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'
|
56
|
-
}, :body =>
|
56
|
+
}, :body => generate_device_description(uuid)
|
57
57
|
|
58
58
|
cp.search_only
|
59
59
|
|
@@ -74,7 +74,7 @@ module RUPNP
|
|
74
74
|
stub_request(:get, '127.0.0.1:1234/root_description.xml').
|
75
75
|
to_return :headers => {
|
76
76
|
'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'
|
77
|
-
}, :body =>
|
77
|
+
}, :body => generate_device_description(notify_options[:uuid])
|
78
78
|
|
79
79
|
EM.add_timer(2) do
|
80
80
|
expect(cp.devices).to be_empty
|
@@ -94,7 +94,7 @@ module RUPNP
|
|
94
94
|
stub_request(:get, '127.0.0.1:1234/root_description.xml').
|
95
95
|
to_return :headers => {
|
96
96
|
'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'
|
97
|
-
}, :body =>
|
97
|
+
}, :body => generate_device_description(notify_options[:uuid])
|
98
98
|
|
99
99
|
EM.add_timer(2) do
|
100
100
|
expect(cp.devices).to be_empty
|
@@ -118,7 +118,7 @@ module RUPNP
|
|
118
118
|
stub_request(:get, '127.0.0.1:1234/root_description.xml').
|
119
119
|
to_return :headers => {
|
120
120
|
'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'
|
121
|
-
}, :body =>
|
121
|
+
}, :body => generate_device_description(notify_options[:uuid])
|
122
122
|
|
123
123
|
EM.add_timer(2) do
|
124
124
|
expect(cp.devices).to be_empty
|
@@ -147,6 +147,9 @@ module RUPNP
|
|
147
147
|
expect(cp.find_device_by_udn(uuid2)).to eq(cp.devices[1])
|
148
148
|
expect(cp.find_device_by_udn(uuid3)).to be_nil
|
149
149
|
end
|
150
|
+
|
151
|
+
it '#start_event_server should only start server if not already running'
|
152
|
+
it '#stopt_event_server should stop event server'
|
150
153
|
end
|
151
154
|
|
152
155
|
end
|
@@ -14,7 +14,7 @@ module RUPNP
|
|
14
14
|
'<?xml version="1.0"?><root xmlns="urn:schemas-upnp-org:device-1-0" configId="1"><spec_version></spec_version></root>',
|
15
15
|
'<?xml version="1.0"?><root xmlns="urn:schemas-upnp-org:device-1-0" configId="1"><spec_version><major>0</major><minor>9</minor></spec_version></root>',
|
16
16
|
'<?xml version="1.0"?><root xmlns="urn:schemas-upnp-org:device-1-0" configId="1"><spec_version><major>1</major><minor>9</minor></spec_version></root>',
|
17
|
-
'<?xml version="1.0"?><root xmlns="urn:schemas-upnp-org:device-1-0" configId="1"><spec_version><major>1</major><minor>9</minor></spec_version><device></device></root>'
|
17
|
+
'<?xml version="1.0"?><root xmlns="urn:schemas-upnp-org:device-1-0" configId="1"><spec_version><major>1</major><minor>9</minor></spec_version><device></device></root>']
|
18
18
|
|
19
19
|
|
20
20
|
let(:location) { 'http://127.0.0.1:1234/root_description.xml' }
|
@@ -54,7 +54,7 @@ module RUPNP
|
|
54
54
|
em do
|
55
55
|
stub_request(:get, location).
|
56
56
|
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.0 TEST/1.0'},
|
57
|
-
:body =>
|
57
|
+
:body => generate_device_description(uuid))
|
58
58
|
rd.errback { fail 'RemoteDevice#fetch should work' }
|
59
59
|
rd.callback { done }
|
60
60
|
rd.fetch
|
@@ -75,10 +75,10 @@ module RUPNP
|
|
75
75
|
end
|
76
76
|
|
77
77
|
it "should fail when description header is not a UPnP 1.x response" do
|
78
|
-
desc =
|
78
|
+
desc = generate_device_description(uuid)
|
79
79
|
em do
|
80
80
|
stub_request(:get, location).
|
81
|
-
to_return(:body =>
|
81
|
+
to_return(:body => generate_device_description(uuid),
|
82
82
|
:headers => { 'SERVER' => 'Linux/1.2 Apache/1.0' },
|
83
83
|
:body => desc)
|
84
84
|
|
@@ -124,7 +124,7 @@ module RUPNP
|
|
124
124
|
em do
|
125
125
|
stub_request(:get, location).
|
126
126
|
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
127
|
-
:body =>
|
127
|
+
:body => generate_device_description(uuid))
|
128
128
|
rd.errback { fail 'RemoteDevice#fetch should work' }
|
129
129
|
rd.callback do
|
130
130
|
done
|
@@ -133,8 +133,46 @@ module RUPNP
|
|
133
133
|
end
|
134
134
|
end
|
135
135
|
|
136
|
-
it "should extract services if any"
|
137
|
-
|
136
|
+
it "should extract services if any" do
|
137
|
+
dev_desc = generate_device_description(uuid, :device_type => 'MediaServer')
|
138
|
+
scpd = generate_scpd
|
139
|
+
em do
|
140
|
+
stub_request(:get, location).
|
141
|
+
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
142
|
+
:body => dev_desc)
|
143
|
+
stub_request(:get, 'http://127.0.0.1:1234/cd/description.xml').
|
144
|
+
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
145
|
+
:body => scpd)
|
146
|
+
stub_request(:get, 'http://127.0.0.1:1234/cd/control').
|
147
|
+
to_return(:status => 404)
|
148
|
+
|
149
|
+
rd.errback { |d, msg| fail msg }
|
150
|
+
rd.callback do
|
151
|
+
expect(rd.services).to have(1).item
|
152
|
+
done
|
153
|
+
end
|
154
|
+
rd.fetch
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should not fail when a service cannot be extracted" do
|
159
|
+
dev_desc = generate_device_description(uuid, :device_type => 'MediaServer')
|
160
|
+
em do
|
161
|
+
stub_request(:get, location).
|
162
|
+
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
163
|
+
:body => dev_desc)
|
164
|
+
stub_request(:get, 'http://127.0.0.1:1234/cd/description.xml').
|
165
|
+
to_return(:status => 404)
|
166
|
+
|
167
|
+
rd.errback { |d, msg| fail msg }
|
168
|
+
rd.callback do
|
169
|
+
expect(rd.services).to have(0).item
|
170
|
+
done
|
171
|
+
end
|
172
|
+
rd.fetch
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
138
176
|
end
|
139
177
|
|
140
178
|
context "#update" do
|
@@ -142,7 +180,7 @@ module RUPNP
|
|
142
180
|
em do
|
143
181
|
stub_request(:get, location).
|
144
182
|
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
145
|
-
:body =>
|
183
|
+
:body => generate_device_description(uuid))
|
146
184
|
rd.errback { fail 'RemoteDevice#fetch should work' }
|
147
185
|
rd.callback do
|
148
186
|
not2 = notification.dup
|
@@ -164,7 +202,7 @@ module RUPNP
|
|
164
202
|
em do
|
165
203
|
stub_request(:get, location).
|
166
204
|
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0' },
|
167
|
-
:body =>
|
205
|
+
:body => generate_device_description(uuid))
|
168
206
|
rd.errback { fail 'RemoteDevice#fetch should work' }
|
169
207
|
rd.callback do
|
170
208
|
not2 = notification.merge('nextbootid.upnp.org' => 15)
|
@@ -180,7 +218,7 @@ module RUPNP
|
|
180
218
|
em do
|
181
219
|
stub_request(:get, location).
|
182
220
|
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0' },
|
183
|
-
:body =>
|
221
|
+
:body => generate_device_description(uuid))
|
184
222
|
rd.errback { fail 'RemoteDevice#fetch should work' }
|
185
223
|
rd.callback do
|
186
224
|
not2 = notification.merge('configid.upnp.org' => 47)
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
module RUPNP
|
3
|
+
module CP
|
4
|
+
|
5
|
+
describe RemoteService do
|
6
|
+
include EM::SpecHelper
|
7
|
+
include RUPNP::Tools
|
8
|
+
|
9
|
+
let(:url_base) { 'http://127.0.0.1:1234/' }
|
10
|
+
let(:sd) { {
|
11
|
+
:service_type => 'urn:schemas-upnp-org:service:Service:1-0',
|
12
|
+
:service_id => 'urn:upnp-org:serviceId:Service',
|
13
|
+
:scpdurl => '/service/description.xml',
|
14
|
+
:control_url => '/service/control',
|
15
|
+
:event_sub_url => '/service/event' } }
|
16
|
+
let(:rs){ RemoteService.new(double('rdevice'), url_base, sd)}
|
17
|
+
|
18
|
+
context '#fetch' do
|
19
|
+
|
20
|
+
it 'should fetch SCPD' do
|
21
|
+
stub_request(:get, build_url(url_base, sd[:scpdurl])).
|
22
|
+
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
23
|
+
:body => generate_scpd)
|
24
|
+
em do
|
25
|
+
rs.errback { fail 'RemoteService#fetch should work' }
|
26
|
+
rs.callback do
|
27
|
+
done
|
28
|
+
end
|
29
|
+
rs.fetch
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should set state table' do
|
34
|
+
stub_request(:get, build_url(url_base, sd[:scpdurl])).
|
35
|
+
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
36
|
+
:body => generate_scpd(:nb_state_var => 4))
|
37
|
+
em do
|
38
|
+
rs.errback { fail 'RemoteService#fetch should work' }
|
39
|
+
rs.callback do
|
40
|
+
expect(rs.state_table).to have(4).items
|
41
|
+
expect(rs.state_table[0][:name]).to eq('X_variableName1')
|
42
|
+
expect(rs.state_table[1][:data_type]).to eq('ui4')
|
43
|
+
expect(rs.state_table[2][:default_value]).to eq('2')
|
44
|
+
expect(rs.state_table[3][:allowed_value_range][:maximum]).
|
45
|
+
to eq('255')
|
46
|
+
done
|
47
|
+
end
|
48
|
+
rs.fetch
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should define actions as methods from description' do
|
53
|
+
stub_request(:get, build_url(url_base, sd[:scpdurl])).
|
54
|
+
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
55
|
+
:body => generate_scpd(:nb_state_var => 2,
|
56
|
+
:define_action => true))
|
57
|
+
stub_request(:post, build_url(url_base, sd[:control_url])).
|
58
|
+
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
59
|
+
:body => action_response(var2: 1))
|
60
|
+
em do
|
61
|
+
rs.errback { fail 'RemoteService#fetch should work' }
|
62
|
+
rs.callback do
|
63
|
+
expect(rs).to respond_to(:test_action)
|
64
|
+
res = rs.test_action('var1' => 10)
|
65
|
+
expect(res[:var2]).to eq(1)
|
66
|
+
done
|
67
|
+
end
|
68
|
+
rs.fetch
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should fail when SCPDURL is unreachable' do
|
73
|
+
stub_request(:get, build_url(url_base, sd[:scpdurl])).to_timeout
|
74
|
+
|
75
|
+
em do
|
76
|
+
rs.errback do |msg|
|
77
|
+
expect(msg).to match(/cannot get SCPD/)
|
78
|
+
done
|
79
|
+
end
|
80
|
+
rs.callback { fail 'RemoteService#fetch should not work' }
|
81
|
+
rs.fetch
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should fail when SCPDURL does not return a SCPD' do
|
86
|
+
stub_request(:get, build_url(url_base, sd[:scpdurl])).
|
87
|
+
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
88
|
+
:body => 'This is only text!')
|
89
|
+
|
90
|
+
em do
|
91
|
+
rs.errback do |msg|
|
92
|
+
expect(msg).to match(/not a UPNP .* SCPD/)
|
93
|
+
done
|
94
|
+
end
|
95
|
+
rs.callback { fail 'RemoteService#fetch should not work' }
|
96
|
+
rs.fetch
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should fail when SCPD does not conform to UPnP spec" do
|
101
|
+
stub_request(:get, build_url(url_base, sd[:scpdurl])).
|
102
|
+
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
103
|
+
:body => generate_scpd(:version_major => 2))
|
104
|
+
|
105
|
+
em do
|
106
|
+
rs.errback do |msg|
|
107
|
+
expect(msg).to match(/not a UPNP .* SCPD/)
|
108
|
+
done
|
109
|
+
end
|
110
|
+
rs.callback { fail 'RemoteService#fetch should not work' }
|
111
|
+
rs.fetch
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context '#subscribe_to_event' do
|
117
|
+
|
118
|
+
before(:each) do
|
119
|
+
stub_request(:get, build_url(url_base, sd[:scpdurl])).
|
120
|
+
to_return(:headers => { 'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0'},
|
121
|
+
:body => generate_scpd)
|
122
|
+
@webstub = stub_request(:subscribe,
|
123
|
+
build_url(url_base, sd[:event_sub_url])).
|
124
|
+
with(:headers => { 'NT' => 'upnp:event'}).
|
125
|
+
to_return(:headers => {
|
126
|
+
'SERVER' => 'OS/1.0 UPnP/1.1 TEST/1.0',
|
127
|
+
'SID' => "uuid:#{UUID.generate}",
|
128
|
+
'CONTENT-LENGTH' => 0,
|
129
|
+
'TIMEOUT' => 'Second-1800'})
|
130
|
+
rs.device.stub(:control_point => ControlPoint.new(:root))
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should start event server' do
|
134
|
+
em do
|
135
|
+
rs.errback { fail 'RemoteService#fetch should work' }
|
136
|
+
rs.callback do
|
137
|
+
expect(rs.device.control_point.event_port).to be_nil
|
138
|
+
rs.subscribe_to_event {}
|
139
|
+
expect(rs.device.control_point.event_port).to be_a(Integer)
|
140
|
+
done
|
141
|
+
end
|
142
|
+
rs.fetch
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should subscribe to an event' do
|
147
|
+
em do
|
148
|
+
rs.errback { fail 'RemoteService#fetch should work' }
|
149
|
+
rs.callback do
|
150
|
+
rs.subscribe_to_event do |msg|
|
151
|
+
done
|
152
|
+
end
|
153
|
+
end
|
154
|
+
rs.fetch
|
155
|
+
|
156
|
+
rs.device.control_point.add_event_url.subscribe do |url, event|
|
157
|
+
EM.add_timer(0) { event << 'message' }
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'should not fail if subscribtion is not possible' do
|
163
|
+
rd_io, wr_io = IO.pipe
|
164
|
+
begin
|
165
|
+
RUPNP.logdev = wr_io
|
166
|
+
RUPNP.log_level = :warn
|
167
|
+
|
168
|
+
remove_request_stub(@webstub)
|
169
|
+
stub_request(:subscribe, build_url(url_base, sd[:event_sub_url])).
|
170
|
+
with(:headers => { 'NT' => 'upnp:event'}).
|
171
|
+
to_timeout.then.
|
172
|
+
to_return(:status => 404)
|
173
|
+
|
174
|
+
em do
|
175
|
+
rs.errback { fail 'RemoteService#fetch should work' }
|
176
|
+
rs.callback do
|
177
|
+
expect { rs.subscribe_to_event { fail } }.to_not raise_error
|
178
|
+
expect { rs.subscribe_to_event { fail } }.to_not raise_error
|
179
|
+
EM.add_timer(1) do
|
180
|
+
expect(rd_io.readline).to match(/timeout/)
|
181
|
+
expect(rd_io.readline).to match(/Not Found/)
|
182
|
+
done
|
183
|
+
end
|
184
|
+
end
|
185
|
+
rs.fetch
|
186
|
+
end
|
187
|
+
ensure
|
188
|
+
rd_io.close
|
189
|
+
wr_io.close
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -58,7 +58,7 @@ EOR
|
|
58
58
|
end
|
59
59
|
|
60
60
|
|
61
|
-
def
|
61
|
+
def generate_device_description(uuid, options={})
|
62
62
|
opt = {
|
63
63
|
:version_major => 1,
|
64
64
|
:version_minor => 1,
|
@@ -67,7 +67,7 @@ def generate_xml_device_description(uuid, options={})
|
|
67
67
|
|
68
68
|
desc=<<EOD
|
69
69
|
<?xml version="1.0"?>
|
70
|
-
<root xmlns="urn:schemas-upnp-org:device-1-0" configId="
|
70
|
+
<root xmlns="urn:schemas-upnp-org:device-1-0" configId="23">
|
71
71
|
<specVersion>
|
72
72
|
<major>#{opt[:version_major]}</major>
|
73
73
|
<minor>#{opt[:version_minor]}</minor>
|
@@ -84,8 +84,10 @@ EOD
|
|
84
84
|
<serviceList>
|
85
85
|
<service>
|
86
86
|
<serviceType>usrn:schemas-upnp-org:service:ContentDirectory:1</serviceType>
|
87
|
-
<serviceId>urn:upnp-org:serviceId
|
88
|
-
|
87
|
+
<serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId>
|
88
|
+
<SCPDURL>/cd/description.xml</SCPDURL>
|
89
|
+
<ControlURL>/cd/control</ControlURL>
|
90
|
+
<EventURL></EventURL>
|
89
91
|
</service>
|
90
92
|
</serviceList>
|
91
93
|
EOD
|
@@ -94,6 +96,85 @@ EOD
|
|
94
96
|
end
|
95
97
|
|
96
98
|
|
99
|
+
def generate_scpd(options={})
|
100
|
+
opt = {
|
101
|
+
:version_major => 1,
|
102
|
+
:version_minor => 1,
|
103
|
+
:nb_state_var => 1,
|
104
|
+
:define_action => false,
|
105
|
+
:send_event => false
|
106
|
+
}.merge(options)
|
107
|
+
|
108
|
+
scpd=<<EOD
|
109
|
+
<?xml version="1.0"?>
|
110
|
+
<scpd xmlns="urn:schemas-upnp-org:service-1-0" configId="23">
|
111
|
+
<specVersion>
|
112
|
+
<major>#{opt[:version_major]}</major>
|
113
|
+
<minor>#{opt[:version_minor]}</minor>
|
114
|
+
</specVersion>
|
115
|
+
EOD
|
116
|
+
|
117
|
+
if opt[:define_action]
|
118
|
+
scpd << <<EOAL
|
119
|
+
<actionList>
|
120
|
+
<action>
|
121
|
+
<name>testAction</name>
|
122
|
+
<argumentList>
|
123
|
+
<argument>
|
124
|
+
<name>var1</name>
|
125
|
+
<direction>in</direction>
|
126
|
+
<relatedStateVariable>X_variableName1</relatedStateVariable>
|
127
|
+
</argument>
|
128
|
+
<argument>
|
129
|
+
<name>var2</name>
|
130
|
+
<direction>out</direction>
|
131
|
+
<retval/>
|
132
|
+
<relatedStateVariable>X_variableName2</relatedStateVariable>
|
133
|
+
</argument>
|
134
|
+
</argumentList>
|
135
|
+
</action>
|
136
|
+
</actionList>
|
137
|
+
EOAL
|
138
|
+
end
|
139
|
+
|
140
|
+
scpd << ' <serviceStateTable>'
|
141
|
+
opt[:nb_state_var].times do |i|
|
142
|
+
scpd << <<EOSV
|
143
|
+
<stateVariable sendEvents="#{opt[:send_event] ? 'yes' : 'no'}">
|
144
|
+
<name>X_variableName#{i+1}</name>
|
145
|
+
<dataType>ui4</dataType>
|
146
|
+
<defaultValue>#{i}</defaultValue>
|
147
|
+
<allowedValueRange>
|
148
|
+
<minimum>0</minimum>
|
149
|
+
<maximum>#{64*(i+1) - 1}</maximum>
|
150
|
+
</allowedValueRange>
|
151
|
+
</stateVariable>
|
152
|
+
EOSV
|
153
|
+
end
|
154
|
+
scpd << " </serviceStateTable>\n</scpd>\n"
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
def action_response(hash)
|
159
|
+
r = <<EOD
|
160
|
+
<?xml version="1.0"?>
|
161
|
+
<s:Envelope
|
162
|
+
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
|
163
|
+
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
164
|
+
<s:Body>
|
165
|
+
<u:testActionResponse xmlns:u="urn:schemas-upnp-org:service:service:1">
|
166
|
+
EOD
|
167
|
+
|
168
|
+
hash.each { |k, v| r << " <#{k}>#{v}</#{k}>\n"}
|
169
|
+
|
170
|
+
r << <<EOD
|
171
|
+
</u:testActionResponse>
|
172
|
+
</s:Body>
|
173
|
+
</s:Envelope>
|
174
|
+
EOD
|
175
|
+
end
|
176
|
+
|
177
|
+
|
97
178
|
NOTIFY_REGEX = {
|
98
179
|
:common => [
|
99
180
|
/^NOTIFY \* HTTP\/1.1\r\n/,
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rupnp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-01
|
12
|
+
date: 2014-02-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: uuid
|
@@ -185,6 +185,7 @@ files:
|
|
185
185
|
- spec/ssdp/listener_spec.rb
|
186
186
|
- spec/ssdp/notifier_spec.rb
|
187
187
|
- spec/ssdp/searcher_spec.rb
|
188
|
+
- spec/cp/remote_service_spec.rb
|
188
189
|
- spec/cp/remote_device_spec.rb
|
189
190
|
- spec/control_point_spec.rb
|
190
191
|
- spec/spec_helper.rb
|
@@ -197,7 +198,6 @@ files:
|
|
197
198
|
- lib/rupnp/ssdp/msearch_responder.rb
|
198
199
|
- lib/rupnp/ssdp/search_responder.rb
|
199
200
|
- lib/rupnp/ssdp/notifier.rb
|
200
|
-
- lib/rupnp/ssdp/http.rb
|
201
201
|
- lib/rupnp/control_point.rb
|
202
202
|
- lib/rupnp/cp/remote_service.rb
|
203
203
|
- lib/rupnp/cp/base.rb
|
@@ -208,6 +208,7 @@ files:
|
|
208
208
|
- lib/rupnp/ssdp.rb
|
209
209
|
- lib/rupnp/log_mixin.rb
|
210
210
|
- lib/rupnp/tools.rb
|
211
|
+
- lib/rupnp/http.rb
|
211
212
|
- lib/rupnp/event.rb
|
212
213
|
- bin/discover
|
213
214
|
- tasks/spec.rake
|
data/lib/rupnp/ssdp/http.rb
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
module RUPNP
|
2
|
-
module SSDP
|
3
|
-
|
4
|
-
# HTTP module to provide some helper methods
|
5
|
-
# @author Sylvain Daubert
|
6
|
-
module SSDP::HTTP
|
7
|
-
|
8
|
-
# Return status from HTTP response
|
9
|
-
# @param [IO] sock
|
10
|
-
# @return [Booelan]
|
11
|
-
def is_http_status_ok?(sock)
|
12
|
-
sock.readline =~ /\s*HTTP\/1.1 200 OK\r\n\z/i
|
13
|
-
end
|
14
|
-
|
15
|
-
# Get HTTP headers from response
|
16
|
-
# @param [IO] sock
|
17
|
-
# @return [Hash] keys are downcase header name strings
|
18
|
-
def get_http_headers(sock)
|
19
|
-
headers = {}
|
20
|
-
sock.each_line do |l|
|
21
|
-
l =~ /([\w\.-]+):\s*(.*)/
|
22
|
-
if $1
|
23
|
-
headers[$1.downcase] = $2.strip
|
24
|
-
end
|
25
|
-
end
|
26
|
-
headers
|
27
|
-
end
|
28
|
-
|
29
|
-
# Get HTTP verb from HTTP request
|
30
|
-
# @param [IO] sock
|
31
|
-
# @return [nil,Hash] keys are +:verb+, +:path+, +:http_version+ and
|
32
|
-
# +:cmd+ (all line)
|
33
|
-
def get_http_verb(sock)
|
34
|
-
str = sock.readline
|
35
|
-
if str =~ /([\w-]+)\s+(.*)\s+HTTP\/(\d\.\d)/
|
36
|
-
{:verb => $1, :path => $2, :http_version => $3, :cmd => str}
|
37
|
-
else
|
38
|
-
nil
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
end
|
43
|
-
|
44
|
-
end
|
45
|
-
end
|