rupnp 0.2.1 → 0.2.2
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.
- 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
|