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