rupnp 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- @response << resp
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
- subscribe_req = <<EOR
161
- SUSCRIBE #{uri.path} HTTP/1.1\r
162
- HOST: #{HOST_IP}:#{port}\r
163
- USER-AGENT: #{RUPNP::USER_AGENT}\r
164
- CALLBACK: #@callback_url\r
165
- NT: upnp:event
166
- TIMEOUT: Second-#{options[:timeout]}\r
167
- \r
168
- EOR
169
-
170
- server = uri.host
171
- port = (uri.port || 80).to_i
172
- ap @event_sub_url
173
- ap server
174
- ap port
175
- con = EM.connect(server, port, CP::EventSubscriber, subscribe_req)
176
-
177
- con.response.subscribe do |resp|
178
- if resp[:status_code] != 200
179
- log :warn, "Cannot subscribe to event #@event_sub_url: #{resp[:status]}"
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
- event = Event.new(resp[:sid], resp[:timeout].match(/(\d+)/)[1].to_i)
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(event, blk)
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
- # ease debug print
212
- @state_table.each { |s| s.each { |k, v| s[k] = v.to_s } }
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
- if params
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
- action[:arguments].map do |arg|
252
- if params arg[:direction] == 'out'
253
- process_soap_response name, response, arg
254
- end
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
@@ -3,7 +3,7 @@ module RUPNP
3
3
  # Listener class for listening for devices' notifications
4
4
  # @author Sylvain Daubert
5
5
  class SSDP::Listener < SSDP::MulticastConnection
6
- include SSDP::HTTP
6
+ include HTTP
7
7
 
8
8
  # Channel to receive notifications
9
9
  # @return [EM::Channel]
@@ -2,47 +2,50 @@ require 'socket'
2
2
  require 'ipaddr'
3
3
 
4
4
  module RUPNP
5
+ module SSDP
5
6
 
6
- # Base class for multicast connections (mainly SSDP search and listen)
7
- # @abstract
8
- class SSDP::MulticastConnection < EM::Connection
9
- include LogMixin
7
+ # Base class for multicast connections (mainly SSDP search and listen)
8
+ # @abstract
9
+ class MulticastConnection < EM::Connection
10
+ include LogMixin
10
11
 
11
- # @param [Integer] ttl
12
- def initialize(ttl=nil)
13
- @ttl = ttl || DEFAULT_TTL
14
- setup_multicast_socket
15
- end
12
+ # @param [Integer] ttl
13
+ def initialize(ttl=nil)
14
+ @ttl = ttl || DEFAULT_TTL
15
+ setup_multicast_socket
16
+ end
16
17
 
17
- # Get peer info
18
- # @return [Array] [port, hostname]
19
- def peer_info
20
- Socket.unpack_sockaddr_in(get_peername)
21
- end
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
- private
25
+ private
25
26
 
26
- def setup_multicast_socket
27
- set_membership IPAddr.new(MULTICAST_IP).hton + IPAddr.new('0.0.0.0').hton
28
- set_ttl
29
- set_reuse_addr
30
- end
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
- def set_membership(value)
33
- set_sock_opt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, value
34
- end
34
+ def set_membership(value)
35
+ set_sock_opt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, value
36
+ end
35
37
 
36
- def set_ttl
37
- value = [@ttl].pack('i')
38
- set_sock_opt Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, value
39
- set_sock_opt Socket::IPPROTO_IP, Socket::IP_TTL, value
40
- end
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
@@ -4,7 +4,7 @@ module RUPNP
4
4
  # Searcher class for searching devices
5
5
  # @author Sylvain Daubert
6
6
  class SSDP::Notifier < SSDP::MulticastConnection
7
- include SSDP::HTTP
7
+ include HTTP
8
8
 
9
9
  # Number of SEARCH datagrams to send
10
10
  DEFAULT_NOTIFY_TRY = 2
@@ -3,7 +3,7 @@ module RUPNP
3
3
  # M-SEARCH responder for M-SEARCH multicast requests from control points.
4
4
  # @author Sylvain Daubert
5
5
  module SSDP::SearchResponder
6
- include SSDP::HTTP
6
+ include HTTP
7
7
 
8
8
  def receive_data(data)
9
9
  port, ip = peer_info
@@ -4,7 +4,7 @@ module RUPNP
4
4
  # Searcher class for searching devices
5
5
  # @author Sylvain Daubert
6
6
  class SSDP::Searcher < SSDP::MulticastConnection
7
- include SSDP::HTTP
7
+ include HTTP
8
8
 
9
9
  # Number of SEARCH datagrams to send
10
10
  DEFAULT_M_SEARCH_TRY = 2
data/lib/rupnp/ssdp.rb CHANGED
@@ -1,4 +1,3 @@
1
- require_relative 'ssdp/http'
2
1
  require_relative 'ssdp/multicast_connection'
3
2
  require_relative 'ssdp/searcher'
4
3
  require_relative 'ssdp/listener'
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.1'
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'
@@ -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 => generate_xml_device_description(uuid1)
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 => generate_xml_device_description(uuid2)
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 => generate_xml_device_description(uuid)
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 => generate_xml_device_description(notify_options[:uuid])
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 => generate_xml_device_description(notify_options[:uuid])
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 => generate_xml_device_description(notify_options[:uuid])
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 => generate_xml_device_description(uuid))
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 = generate_xml_device_description(uuid)
78
+ desc = generate_device_description(uuid)
79
79
  em do
80
80
  stub_request(:get, location).
81
- to_return(:body => generate_xml_device_description(uuid),
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 => generate_xml_device_description(uuid))
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
- it "should not fail when a service cannot be extracted"
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 => generate_xml_device_description(uuid))
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 => generate_xml_device_description(uuid))
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 => generate_xml_device_description(uuid))
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 generate_xml_device_description(uuid, options={})
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="1">
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:</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.1
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-20 00:00:00.000000000 Z
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
@@ -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