rupnp 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/rupnp.rb +1 -2
- data/lib/rupnp/control_point.rb +1 -7
- data/lib/rupnp/cp/event_server.rb +89 -29
- data/lib/rupnp/cp/remote_service.rb +328 -257
- data/lib/rupnp/event.rb +4 -0
- data/spec/cp/event_server_spec.rb +140 -0
- data/spec/cp/remote_service_spec.rb +15 -3
- data/spec/spec_helper.rb +40 -0
- metadata +3 -3
- data/lib/rupnp/cp/event_subscriber.rb +0 -47
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.
|
9
|
+
VERSION = '0.3.0'
|
10
10
|
|
11
11
|
@logdev = STDERR
|
12
12
|
@log_level = :info
|
@@ -57,7 +57,6 @@ require_relative 'rupnp/cp/base'
|
|
57
57
|
require_relative 'rupnp/cp/remote_service'
|
58
58
|
require_relative 'rupnp/cp/remote_device'
|
59
59
|
require_relative 'rupnp/cp/event_server'
|
60
|
-
require_relative 'rupnp/cp/event_subscriber'
|
61
60
|
require_relative 'rupnp/ssdp'
|
62
61
|
|
63
62
|
require_relative 'rupnp/device'
|
data/lib/rupnp/control_point.rb
CHANGED
@@ -24,10 +24,6 @@ module RUPNP
|
|
24
24
|
# Get event listening port
|
25
25
|
# @return [Integer]
|
26
26
|
attr_reader :event_port
|
27
|
-
# Return channel to add event URL (URL to listen for a specific
|
28
|
-
# event)
|
29
|
-
# @return [EM::Channel]
|
30
|
-
attr_reader :add_event_url
|
31
27
|
# Return remote devices controlled by this control point
|
32
28
|
# @return [Array<CP::RemoteDevice>]
|
33
29
|
attr_reader :devices
|
@@ -48,7 +44,6 @@ module RUPNP
|
|
48
44
|
@devices = []
|
49
45
|
@new_device_channel = EM::Channel.new
|
50
46
|
@bye_device_channel = EM::Channel.new
|
51
|
-
@add_event_url = EM::Channel.new
|
52
47
|
end
|
53
48
|
|
54
49
|
# Start control point.
|
@@ -79,8 +74,7 @@ module RUPNP
|
|
79
74
|
# @return [void]
|
80
75
|
def start_event_server(port=EVENT_SUB_DEFAULT_PORT)
|
81
76
|
@event_port ||= port
|
82
|
-
@event_server ||= EM.start_server('0.0.0.0', port, CP::EventServer
|
83
|
-
@add_event_url)
|
77
|
+
@event_server ||= EM.start_server('0.0.0.0', port, CP::EventServer)
|
84
78
|
end
|
85
79
|
|
86
80
|
# Stop event server
|
@@ -7,44 +7,104 @@ module RUPNP
|
|
7
7
|
class CP::EventServer < EM::HttpServer::Server
|
8
8
|
include LogMixin
|
9
9
|
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
# @private
|
11
|
+
@@add_event = EM::Channel.new
|
12
|
+
# @private
|
13
|
+
@@events = []
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
@@add_event.subscribe do |url|
|
16
|
+
@@events << url
|
17
|
+
end
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
class << self
|
20
|
+
# Channel to add url for listening to
|
21
|
+
# @param [Event] event
|
22
|
+
# @return [void]
|
23
|
+
def add_event(event)
|
24
|
+
@@add_event << event
|
25
|
+
end
|
21
26
|
|
22
|
-
|
23
|
-
|
24
|
-
|
27
|
+
# Remove an event
|
28
|
+
# @param [Event] event
|
29
|
+
# @return [void]
|
30
|
+
def remove_event(event)
|
31
|
+
@@events.delete_if { |e| e == event }
|
25
32
|
end
|
26
33
|
end
|
27
34
|
|
35
|
+
|
36
|
+
# Channel to return received updated variables from events
|
37
|
+
# @return [EM::Channel]
|
38
|
+
attr_reader :events
|
39
|
+
|
40
|
+
|
28
41
|
# Process a HTTP request received from a service/device
|
29
42
|
def process_http_request
|
30
|
-
log :debug,
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
43
|
+
log :debug, "EventServer: receive request: #@http"
|
44
|
+
|
45
|
+
response = EM::DelegatedHttpResponse.new(self)
|
46
|
+
|
47
|
+
if @http_request_method != 'NOTIFY'
|
48
|
+
log :info, "EventServer: method #@http_request_method not allowed"
|
49
|
+
response.status = 405
|
50
|
+
response.headers['Allow'] = 'NOTIFY'
|
51
|
+
response.send_response
|
52
|
+
return
|
53
|
+
end
|
54
|
+
|
55
|
+
event = @@events.find { |e| e.callback_url == @http_request_uri }
|
56
|
+
|
57
|
+
if event.nil?
|
58
|
+
log :info, "EventServer: Requested URI #@http_request_uri unknown"
|
59
|
+
response.status = 404
|
60
|
+
response.send_response
|
61
|
+
return
|
62
|
+
end
|
63
|
+
|
64
|
+
if !event.is_a? EM::Channel
|
65
|
+
log :error, "EventServer: internal error!"
|
66
|
+
response.status = 500
|
67
|
+
response.send_response
|
68
|
+
return
|
69
|
+
end
|
70
|
+
|
71
|
+
if !@http.has_key?(:nt) or !@http.has_key?(:nts)
|
72
|
+
log :warn, 'EventServer: malformed NOTIFY event message'
|
73
|
+
log :debug, "#@http_headers\n#@http_content"
|
74
|
+
response.status = 400
|
75
|
+
response.send_response
|
76
|
+
return
|
77
|
+
end
|
78
|
+
|
79
|
+
if @http[:nt] != 'upnp:event' or @http[:nts] != 'upnp:propchange' or
|
80
|
+
!@http.has_key?(:sid) or @http[:sid] == ''
|
81
|
+
log :warn, 'EventServer: precondition failed'
|
82
|
+
log :debug, "#@http_headers\n#@http_content"
|
83
|
+
response.status = 412
|
84
|
+
response.send_response
|
85
|
+
return
|
86
|
+
end
|
87
|
+
|
88
|
+
if event.sid != @http[:sid]
|
89
|
+
log :warn, 'EventServer: precondition failed (unknown SID)'
|
90
|
+
log :debug, "#@http_headers\n#@http_content"
|
91
|
+
response.status = 412
|
92
|
+
response.send_response
|
93
|
+
return
|
47
94
|
end
|
95
|
+
|
96
|
+
seq = @http[:seq].nil? ? 0 : @http[:seq].to_i
|
97
|
+
notification = {
|
98
|
+
:seq => seq,
|
99
|
+
:content => Nori.new.parse(@http_content)
|
100
|
+
}
|
101
|
+
|
102
|
+
log :debug, "send notification #{notification.inspect}\n" +
|
103
|
+
" to #{event}"
|
104
|
+
event << notification
|
105
|
+
|
106
|
+
response_status = 200
|
107
|
+
response.send_response
|
48
108
|
end
|
49
109
|
|
50
110
|
end
|
@@ -3,304 +3,375 @@ require 'savon'
|
|
3
3
|
require_relative 'base'
|
4
4
|
|
5
5
|
module RUPNP
|
6
|
+
module CP
|
7
|
+
|
8
|
+
# Service class for device's services.
|
9
|
+
#
|
10
|
+
# ==Actions
|
11
|
+
# This class defines ruby methods from actions defined in
|
12
|
+
# service description, as provided by the device.
|
13
|
+
#
|
14
|
+
# By example, from this description:
|
15
|
+
# <action>
|
16
|
+
# <name>actionName</name>
|
17
|
+
# <argumentList>
|
18
|
+
# <argument>
|
19
|
+
# <name>argumentNameIn</name>
|
20
|
+
# <direction>in</direction>
|
21
|
+
# <relatedStateVariable>stateVariableName</relatedStateVariable>
|
22
|
+
# </argument>
|
23
|
+
# <argument>
|
24
|
+
# <name>argumentNameOut</name>
|
25
|
+
# <direction>out</direction>
|
26
|
+
# <relatedStateVariable>stateVariableName</relatedStateVariable>
|
27
|
+
# </argument>
|
28
|
+
# </action>
|
29
|
+
# a +#action_name+ method is created. This method requires a hash with
|
30
|
+
# an element named +argument_name_in+.
|
31
|
+
# If no <i>in</i> argument is required, an empty hash (<code>{}</code>)
|
32
|
+
# must be passed to the method. Hash keys may not be symbols.
|
33
|
+
#
|
34
|
+
# A Hash is returned, with a key for each <i>out</i> argument.
|
35
|
+
#
|
36
|
+
# ==Evented variables
|
37
|
+
# Some variables in state (see {#state_table}, +:@send_events variable
|
38
|
+
# attribute) are evented. Events to update these variables are received
|
39
|
+
# only after subscription. To subscribe, use {#subscribe_to_event}.
|
40
|
+
#
|
41
|
+
# After subscribing to events, state variables are automagically updated
|
42
|
+
# on events. Their value may be accessed through {#variables}.
|
43
|
+
#
|
44
|
+
# A block may be passed to {#subscribe_to_event} to do a user action
|
45
|
+
# on receiving events.
|
46
|
+
#
|
47
|
+
# service.subscribe_to_event do |msg|
|
48
|
+
# puts "receive #{msg}"
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# @author Sylvain Daubert
|
52
|
+
class RemoteService < Base
|
53
|
+
|
54
|
+
# @private
|
55
|
+
@@event_sub_count = 0
|
56
|
+
|
57
|
+
# Get event subscription count for all services
|
58
|
+
# (unique ID for subscription)
|
59
|
+
# @return [Integer]
|
60
|
+
def self.event_sub_count
|
61
|
+
@@event_sub_count += 1
|
62
|
+
end
|
6
63
|
|
7
|
-
# Service class for device's services.
|
8
|
-
#
|
9
|
-
# ==Actions
|
10
|
-
# This class defines ruby methods from actions defined in
|
11
|
-
# service description, as provided by the device.
|
12
|
-
#
|
13
|
-
# By example, from this description:
|
14
|
-
# <action>
|
15
|
-
# <name>actionName</name>
|
16
|
-
# <argumentList>
|
17
|
-
# <argument>
|
18
|
-
# <name>argumentNameIn</name>
|
19
|
-
# <direction>in</direction>
|
20
|
-
# <relatedStateVariable>stateVariableName</relatedStateVariable>
|
21
|
-
# </argument>
|
22
|
-
# <argument>
|
23
|
-
# <name>argumentNameOut</name>
|
24
|
-
# <direction>out</direction>
|
25
|
-
# <relatedStateVariable>stateVariableName</relatedStateVariable>
|
26
|
-
# </argument>
|
27
|
-
# </action>
|
28
|
-
# a +#action_name+ method is created. This method requires a hash with
|
29
|
-
# an element named +argument_name_in+.
|
30
|
-
# If no <i>in</i> argument is required, an empty hash (<code>{}</code>)
|
31
|
-
# must be passed to the method. Hash keys may not be symbols.
|
32
|
-
#
|
33
|
-
# A Hash is returned, with a key for each <i>out</i> argument.
|
34
|
-
#
|
35
|
-
# @author Sylvain Daubert
|
36
|
-
class CP::RemoteService < CP::Base
|
37
|
-
|
38
|
-
# @private
|
39
|
-
@@event_sub_count = 0
|
40
|
-
|
41
|
-
# Get event subscription count for all services
|
42
|
-
# (unique ID for subscription)
|
43
|
-
# @return [Integer]
|
44
|
-
def self.event_sub_count
|
45
|
-
@@event_sub_count += 1
|
46
|
-
end
|
47
|
-
|
48
|
-
|
49
|
-
# @private SOAP integer types
|
50
|
-
INTEGER_TYPES = %w(ui1 ui2 ui4 i1 i2 i4 int).freeze
|
51
|
-
# @private SOAP float types
|
52
|
-
FLOAT_TYPES = %w(r4 r8 number float).freeze
|
53
|
-
# @private SOAP string types
|
54
|
-
STRING_TYPES = %w(char string uuid).freeze
|
55
|
-
# @private SOAP true values
|
56
|
-
TRUE_TYPES = %w(1 true yes).freeze
|
57
|
-
# @private SOAP false values
|
58
|
-
FALSE_TYPES = %w(0 false no).freeze
|
59
|
-
|
60
|
-
# Get device to which this service belongs to
|
61
|
-
# @return [Device]
|
62
|
-
attr_reader :device
|
63
|
-
|
64
|
-
# Get service type
|
65
|
-
# @return [String]
|
66
|
-
attr_reader :type
|
67
|
-
# Get service id
|
68
|
-
# @return [String]
|
69
|
-
attr_reader :id
|
70
|
-
# URL for service description
|
71
|
-
# @return [String]
|
72
|
-
attr_reader :scpd_url
|
73
|
-
# URL for control
|
74
|
-
# @return [String]
|
75
|
-
attr_reader :control_url
|
76
|
-
# URL for eventing
|
77
|
-
# @return [String]
|
78
|
-
attr_reader :event_sub_url
|
79
|
-
|
80
|
-
# XML namespace for device description
|
81
|
-
# @return [String]
|
82
|
-
attr_reader :xmlns
|
83
|
-
# Define architecture on which the service is implemented
|
84
|
-
# @return [String]
|
85
|
-
attr_reader :spec_version
|
86
|
-
# Available actions on this service
|
87
|
-
# @return [Array<Hash>]
|
88
|
-
attr_reader :actions
|
89
|
-
# State table for the service
|
90
|
-
# @return [Array<Hash>]
|
91
|
-
attr_reader :state_table
|
92
|
-
|
93
|
-
# @param [Device] device
|
94
|
-
# @param [String] url_base
|
95
|
-
# @param [Hash] service
|
96
|
-
def initialize(device, url_base, service)
|
97
|
-
super()
|
98
|
-
@device = device
|
99
|
-
@description = service
|
100
|
-
|
101
|
-
@type = service[:service_type].to_s
|
102
|
-
@id = service[:service_id].to_s
|
103
|
-
@scpd_url = build_url(url_base, service[:scpdurl].to_s)
|
104
|
-
@control_url = build_url(url_base, service[:control_url].to_s)
|
105
|
-
@event_sub_url = build_url(url_base, service[:event_sub_url].to_s)
|
106
|
-
@actions = []
|
107
|
-
|
108
|
-
initialize_savon
|
109
|
-
end
|
110
64
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
65
|
+
# @private SOAP integer types
|
66
|
+
INTEGER_TYPES = %w(ui1 ui2 ui4 i1 i2 i4 int).freeze
|
67
|
+
# @private SOAP float types
|
68
|
+
FLOAT_TYPES = %w(r4 r8 number float).freeze
|
69
|
+
# @private SOAP string types
|
70
|
+
STRING_TYPES = %w(char string uuid).freeze
|
71
|
+
# @private SOAP true values
|
72
|
+
TRUE_TYPES = %w(1 true yes).freeze
|
73
|
+
# @private SOAP false values
|
74
|
+
FALSE_TYPES = %w(0 false no).freeze
|
75
|
+
|
76
|
+
# Get device to which this service belongs to
|
77
|
+
# @return [Device]
|
78
|
+
attr_reader :device
|
79
|
+
|
80
|
+
# Get service type
|
81
|
+
# @return [String]
|
82
|
+
attr_reader :type
|
83
|
+
# Get service id
|
84
|
+
# @return [String]
|
85
|
+
attr_reader :id
|
86
|
+
# URL for service description
|
87
|
+
# @return [String]
|
88
|
+
attr_reader :scpd_url
|
89
|
+
# URL for control
|
90
|
+
# @return [String]
|
91
|
+
attr_reader :control_url
|
92
|
+
# URL for eventing
|
93
|
+
# @return [String]
|
94
|
+
attr_reader :event_sub_url
|
95
|
+
|
96
|
+
# XML namespace for device description
|
97
|
+
# @return [String]
|
98
|
+
attr_reader :xmlns
|
99
|
+
# Define architecture on which the service is implemented
|
100
|
+
# @return [String]
|
101
|
+
attr_reader :spec_version
|
102
|
+
# Available actions on this service
|
103
|
+
# @return [Array<Hash>]
|
104
|
+
attr_reader :actions
|
105
|
+
# State table for the service
|
106
|
+
# @return [Array<Hash>]
|
107
|
+
attr_reader :state_table
|
108
|
+
# Variables from state table
|
109
|
+
# @return [Hash]
|
110
|
+
attr_reader :variables
|
111
|
+
|
112
|
+
# @param [Device] device
|
113
|
+
# @param [String] url_base
|
114
|
+
# @param [Hash] service
|
115
|
+
def initialize(device, url_base, service)
|
116
|
+
super()
|
117
|
+
@device = device
|
118
|
+
@description = service
|
119
|
+
|
120
|
+
@type = service[:service_type].to_s
|
121
|
+
@id = service[:service_id].to_s
|
122
|
+
@scpd_url = build_url(url_base, service[:scpdurl].to_s)
|
123
|
+
@control_url = build_url(url_base, service[:control_url].to_s)
|
124
|
+
@event_sub_url = build_url(url_base, service[:event_sub_url].to_s)
|
125
|
+
@actions = []
|
126
|
+
@variables = {}
|
127
|
+
@update_variables = EM::Channel.new
|
128
|
+
|
129
|
+
@update_variables.subscribe do |var, value|
|
130
|
+
@variables[var] = value
|
131
|
+
log :debug, "varibale #{var} set to #{value}"
|
132
|
+
end
|
115
133
|
|
116
|
-
|
117
|
-
fail "cannot get SCPD from #@scpd_url"
|
118
|
-
next
|
134
|
+
initialize_savon
|
119
135
|
end
|
120
136
|
|
121
|
-
|
122
|
-
|
123
|
-
|
137
|
+
# Get service from its description
|
138
|
+
# @return [void]
|
139
|
+
def fetch
|
140
|
+
scpd_getter = EM::DefaultDeferrable.new
|
141
|
+
|
142
|
+
scpd_getter.errback do
|
143
|
+
fail "cannot get SCPD from #@scpd_url"
|
124
144
|
next
|
125
145
|
end
|
126
146
|
|
127
|
-
|
128
|
-
|
147
|
+
scpd_getter.callback do |scpd|
|
148
|
+
if bad_description?(scpd)
|
149
|
+
fail 'not a UPNP 1.0/1.1 SCPD'
|
150
|
+
next
|
151
|
+
end
|
129
152
|
|
130
|
-
|
131
|
-
|
153
|
+
extract_service_state_table scpd
|
154
|
+
extract_actions scpd
|
132
155
|
|
133
|
-
|
134
|
-
|
156
|
+
succeed self
|
157
|
+
end
|
135
158
|
|
136
|
-
|
137
|
-
# @param [Hash] options
|
138
|
-
# @option options [Integer] timeout
|
139
|
-
# @yieldparam [Event] event event received
|
140
|
-
# @yieldparam [Object] msg message received
|
141
|
-
# @return [Integer] subscribe id. May be used to unsubscribe on event
|
142
|
-
def subscribe_to_event(options={}, &blk)
|
143
|
-
cp = device.control_point
|
144
|
-
|
145
|
-
cp.start_event_server
|
146
|
-
|
147
|
-
port = cp.event_port
|
148
|
-
num = self.class.event_sub_count
|
149
|
-
@callback_url = "http://#{HOST_IP}:#{port}/event#{num}}"
|
150
|
-
|
151
|
-
uri = URI(@event_sub_url)
|
152
|
-
options[:timeout] ||= EVENT_SUB_DEFAULT_TIMEOUT
|
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}"
|
159
|
+
get_description @scpd_url, scpd_getter
|
165
160
|
end
|
166
161
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
162
|
+
# Subscribe to event
|
163
|
+
# @param [Hash] options
|
164
|
+
# @option options [Integer] timeout
|
165
|
+
# @yieldparam [Event] event event received
|
166
|
+
# @yieldparam [Object] msg message received
|
167
|
+
# @return [Integer] subscribe id. May be used to unsubscribe on event
|
168
|
+
# @since 0.3.0
|
169
|
+
def subscribe_to_event(options={}, &blk)
|
170
|
+
cp = device.control_point
|
171
|
+
|
172
|
+
cp.start_event_server
|
173
|
+
|
174
|
+
port = cp.event_port
|
175
|
+
num = self.class.event_sub_count
|
176
|
+
@callback_url = "http://#{HOST_IP}:#{port}/events/#{num}"
|
177
|
+
|
178
|
+
uri = URI(@event_sub_url)
|
179
|
+
options[:timeout] ||= EVENT_SUB_DEFAULT_TIMEOUT
|
180
|
+
|
181
|
+
log :info, "send SUBSCRIBE request to #{uri}"
|
182
|
+
con = EM::HttpRequest.new(@event_sub_url)
|
183
|
+
http = con.setup_request(:subscribe, :head => {
|
184
|
+
'HOST' => "#{uri.host}:#{uri.port}",
|
185
|
+
'USER-AGENT' => RUPNP::USER_AGENT,
|
186
|
+
'CALLBACK' => @callback_url,
|
187
|
+
'NT' => 'upnp:event',
|
188
|
+
'TIMEOUT' => "Second-#{options[:timeout]}"})
|
189
|
+
|
190
|
+
http.errback do |client|
|
191
|
+
log :warn, "Cannot subscribe to event: #{client.error}"
|
192
|
+
end
|
193
|
+
|
194
|
+
http.callback do
|
195
|
+
log :debug, 'Close connection to subscribe event URL'
|
196
|
+
con.close
|
197
|
+
if http.response_header.status != 200
|
198
|
+
log :warn, "Cannot subscribe to event #@event_sub_url:" +
|
199
|
+
" #{http.response_header.http_reason}"
|
200
|
+
else
|
201
|
+
timeout = http.response_header['TIMEOUT'].match(/(\d+)/)[1] || 1800
|
202
|
+
event = Event.new(@event_sub_url, URI(@callback_url).path,
|
203
|
+
http.response_header['SID'], timeout.to_i)
|
204
|
+
EventServer.add_event event
|
205
|
+
log :info, 'event subscribtion registered'
|
206
|
+
log :debug, "event: #{event.inspect}"
|
207
|
+
|
208
|
+
event.subscribe do |msg|
|
209
|
+
log :debug, "event #{event} received"
|
210
|
+
if msg.is_a? Hash and msg[:content].is_a? Hash
|
211
|
+
msg[:content].each do |k, v|
|
212
|
+
if @variables.has_key? k
|
213
|
+
log :info, "update evented variable #{k}"
|
214
|
+
type = get_data_type_from_table(k)
|
215
|
+
@update_variables << [k, get_value_from_type(type, v)]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
log :debug, "call user block"
|
220
|
+
blk.call(msg) if blk
|
221
|
+
end
|
222
|
+
end
|
179
223
|
end
|
180
224
|
end
|
181
|
-
end
|
182
225
|
|
183
226
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
227
|
+
private
|
228
|
+
|
229
|
+
def bad_description?(scpd)
|
230
|
+
if scpd[:scpd]
|
231
|
+
bd = false
|
232
|
+
@xmlns = scpd[:scpd][:@xmlns]
|
233
|
+
bd |= @xmlns != "urn:schemas-upnp-org:service-1-0"
|
234
|
+
bd |= scpd[:scpd][:spec_version][:major].to_i != 1
|
235
|
+
@spec_version = scpd[:scpd][:spec_version][:major] + '.'
|
236
|
+
@spec_version += scpd[:scpd][:spec_version][:minor]
|
237
|
+
bd |= !scpd[:scpd][:service_state_table]
|
238
|
+
bd | scpd[:scpd][:service_state_table].empty?
|
239
|
+
else
|
240
|
+
true
|
241
|
+
end
|
198
242
|
end
|
199
|
-
end
|
200
243
|
|
201
|
-
|
202
|
-
|
203
|
-
|
244
|
+
def extract_service_state_table(scpd)
|
245
|
+
if scpd[:scpd][:service_state_table][:state_variable]
|
246
|
+
@state_table = scpd[:scpd][:service_state_table][:state_variable]
|
247
|
+
|
248
|
+
if @state_table.is_a? Hash
|
249
|
+
@state_table = [@state_table]
|
250
|
+
end
|
204
251
|
|
205
|
-
|
206
|
-
|
252
|
+
@state_table.each do |var|
|
253
|
+
name = var[:name]
|
254
|
+
if INTEGER_TYPES.include? var[:data_type]
|
255
|
+
value = var[:default_value] ||
|
256
|
+
var[:allowed_value_range][:minimum] || 0
|
257
|
+
elsif FLOAT_TYPES.include? var[:data_type]
|
258
|
+
value = var[:default_value] ||
|
259
|
+
var[:allowed_value_range][:minimum] || 0
|
260
|
+
elsif STRING_TYPES.include? var[:data_type]
|
261
|
+
value = var[:default_value] ||
|
262
|
+
var[:allowed_value_list][:allowed_value].first || ''
|
263
|
+
else
|
264
|
+
value = nil
|
265
|
+
end
|
266
|
+
|
267
|
+
value = get_value_from_type(var[:data_type], value)
|
268
|
+
@update_variables << [name, value]
|
269
|
+
end
|
207
270
|
end
|
208
271
|
end
|
209
|
-
end
|
210
272
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
273
|
+
def extract_actions(scpd)
|
274
|
+
if scpd[:scpd][:action_list] and scpd[:scpd][:action_list][:action]
|
275
|
+
log :info, "extract actions for service #@type"
|
276
|
+
@actions = scpd[:scpd][:action_list][:action]
|
277
|
+
@actions = [@actions] unless @actions.is_a? Array
|
278
|
+
@actions.each do |action|
|
279
|
+
action[:arguments] = action[:argument_list][:argument]
|
280
|
+
action.delete :argument_list
|
281
|
+
define_method_from_action action
|
282
|
+
end
|
220
283
|
end
|
221
284
|
end
|
222
|
-
end
|
223
285
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
286
|
+
def define_method_from_action(action)
|
287
|
+
action[:name] = action[:name].to_s
|
288
|
+
action_name = action[:name]
|
289
|
+
name = snake_case(action_name).to_sym
|
228
290
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
291
|
+
define_singleton_method(name) do |params|
|
292
|
+
if params
|
293
|
+
unless params.is_a? Hash
|
294
|
+
raise ArgumentError, 'only hash arguments are accepted'
|
295
|
+
end
|
233
296
|
end
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
locals.message params
|
239
|
-
end
|
240
|
-
|
241
|
-
if action[:arguments].is_a? Hash
|
242
|
-
log :debug, 'only one argument in argument list'
|
243
|
-
if action[:arguments][:direction] == 'out'
|
244
|
-
process_soap_response name, response, action[:arguments]
|
297
|
+
response = @soap.call(action_name) do |locals|
|
298
|
+
locals.attributes 'xmlns:u' => @type
|
299
|
+
locals.soap_action "#{type}##{action_name}"
|
300
|
+
locals.message params
|
245
301
|
end
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
302
|
+
|
303
|
+
if action[:arguments].is_a? Hash
|
304
|
+
log :debug, 'only one argument in argument list'
|
305
|
+
if action[:arguments][:direction] == 'out'
|
306
|
+
process_soap_response name, response, action[:arguments]
|
307
|
+
end
|
308
|
+
else
|
309
|
+
log :debug, 'true argument list'
|
310
|
+
hsh = {}
|
311
|
+
outer = action[:arguments].select { |arg| arg[:direction] == 'out' }
|
312
|
+
outer.each do |arg|
|
313
|
+
hsh.merge! process_soap_response(name, response, arg)
|
314
|
+
end
|
315
|
+
hsh
|
252
316
|
end
|
253
|
-
hsh
|
254
317
|
end
|
255
318
|
end
|
256
|
-
end
|
257
319
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
320
|
+
def process_soap_response(action, resp, out_arg)
|
321
|
+
if resp.success? and resp.to_xml.empty?
|
322
|
+
log :debug, 'Successful SOAP request but empty response'
|
323
|
+
return {}
|
324
|
+
end
|
325
|
+
|
326
|
+
state_var = @state_table.find do |h|
|
327
|
+
h[:name] == out_arg[:related_state_variable]
|
328
|
+
end
|
329
|
+
|
330
|
+
action_response = "#{action}_response".to_sym
|
331
|
+
out_arg_name = snake_case(out_arg[:name]).to_sym
|
332
|
+
value = resp.hash[:envelope][:body][action_response][out_arg_name]
|
333
|
+
|
334
|
+
{ out_arg_name => get_value_from_type(state_var[:data_type], value) }
|
262
335
|
end
|
263
336
|
|
264
|
-
|
265
|
-
|
337
|
+
def get_value_from_type(type, value)
|
338
|
+
transform_method = if INTEGER_TYPES.include? type
|
339
|
+
:to_i
|
340
|
+
elsif FLOAT_TYPES.include? type
|
341
|
+
:to_f
|
342
|
+
elsif STRING_TYPES.include? type
|
343
|
+
:to_s
|
344
|
+
end
|
345
|
+
if transform_method
|
346
|
+
value.send(transform_method)
|
347
|
+
elsif TRUE_TYPES.include? type
|
348
|
+
true
|
349
|
+
elsif FALSE_TYPES.include? type
|
350
|
+
false
|
351
|
+
else
|
352
|
+
log :warn, "SOAP response has an unknown type: #{type}"
|
353
|
+
nil
|
354
|
+
end
|
266
355
|
end
|
267
356
|
|
268
|
-
|
269
|
-
|
270
|
-
value = resp.hash[:envelope][:body][action_response][out_arg_name]
|
271
|
-
|
272
|
-
transform_method = if INTEGER_TYPES.include? state_var[:data_type]
|
273
|
-
:to_i
|
274
|
-
elsif FLOAT_TYPES.include? state_var[:data_type]
|
275
|
-
:to_f
|
276
|
-
elsif STRING_TYPES.include? state_var[:data_type]
|
277
|
-
:to_s
|
278
|
-
end
|
279
|
-
if transform_method
|
280
|
-
{ out_arg_name => value.send(transform_method) }
|
281
|
-
elsif TRUE_TYPES.include? state_var[:data_type]
|
282
|
-
{ out_arg_name => true }
|
283
|
-
elsif FALSE_TYPES.include? state_var[:data_type]
|
284
|
-
{ out_arg_name => false }
|
285
|
-
else
|
286
|
-
log :warn, "SOAP response has an unknown type: #{state_var[:data_type]}"
|
287
|
-
{}
|
357
|
+
def get_data_type_from_table(var_name)
|
358
|
+
@state_table.find { |v| v[:name] == var_name }[:data_type]
|
288
359
|
end
|
289
|
-
end
|
290
360
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
361
|
+
def initialize_savon
|
362
|
+
@soap = Savon.client do |globals|
|
363
|
+
globals.log_level :error
|
364
|
+
globals.endpoint @control_url
|
365
|
+
globals.namespace @type
|
366
|
+
globals.convert_request_keys_to :camel_case
|
367
|
+
globals.log true
|
368
|
+
globals.headers :HOST => "#{HOST_IP}"
|
369
|
+
globals.env_namespace 's'
|
370
|
+
globals.namespace_identifier 'u'
|
371
|
+
end
|
301
372
|
end
|
373
|
+
|
302
374
|
end
|
303
375
|
|
304
376
|
end
|
305
|
-
|
306
377
|
end
|
data/lib/rupnp/event.rb
CHANGED
@@ -8,6 +8,7 @@ module RUPNP
|
|
8
8
|
# Get service ID
|
9
9
|
# @return [Integer]
|
10
10
|
attr_reader :sid
|
11
|
+
attr_reader :callback_url
|
11
12
|
|
12
13
|
# @param [String] event_suburl Event subscription URL
|
13
14
|
# @param [String] callback_url Callback URL to receive events
|
@@ -16,17 +17,20 @@ module RUPNP
|
|
16
17
|
def initialize(event_suburl, callback_url, sid, timeout)
|
17
18
|
super()
|
18
19
|
@event_suburl = event_suburl
|
20
|
+
@callback_url = callback_url
|
19
21
|
@sid, @timeout = sid, timeout
|
20
22
|
|
21
23
|
@timeout_timer = EM.add_timer(@timeout) { self << :timeout }
|
22
24
|
end
|
23
25
|
|
24
26
|
# Renew subscription to event
|
27
|
+
# @todo
|
25
28
|
def renew_subscription
|
26
29
|
raise NotImplementedError
|
27
30
|
end
|
28
31
|
|
29
32
|
# Cancel subscription to event
|
33
|
+
# @todo
|
30
34
|
def cancel_subscription
|
31
35
|
raise NotImplementedError
|
32
36
|
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
def start_server
|
4
|
+
EM.start_server('127.0.0.1', RUPNP::EVENT_SUB_DEFAULT_PORT,
|
5
|
+
RUPNP::CP::EventServer)
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
module RUPNP
|
10
|
+
module CP
|
11
|
+
|
12
|
+
describe EventServer do
|
13
|
+
include EM::SpecHelper
|
14
|
+
|
15
|
+
let(:timeout) { 2 }
|
16
|
+
let(:sid) { "uuid:#{UUID.generate}" }
|
17
|
+
let(:event_uri) { '/event/1' }
|
18
|
+
let(:event) { Event.new('', event_uri, sid, timeout) }
|
19
|
+
let(:port) { RUPNP::EVENT_SUB_DEFAULT_PORT }
|
20
|
+
let(:req) {EM::HttpRequest.new("http://127.0.0.1:#{port}#{event_uri}")}
|
21
|
+
|
22
|
+
it 'should return 404 error on bad HTTP method URI' do
|
23
|
+
em do
|
24
|
+
start_server
|
25
|
+
|
26
|
+
req = EM::HttpRequest.new("http://127.0.0.1:#{port}/unknown")
|
27
|
+
http = send_notify_request(req)
|
28
|
+
http.errback { fail 'must not fail!' }
|
29
|
+
http.callback do
|
30
|
+
expect(http.response_header.status).to eq(404)
|
31
|
+
done
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should return 405 error on bad HTTP method' do
|
37
|
+
em do
|
38
|
+
start_server
|
39
|
+
|
40
|
+
http = req.get
|
41
|
+
http.callback do
|
42
|
+
expect(http.response_header.status).to eq(405)
|
43
|
+
done
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should return 400 error on malformed request' do
|
49
|
+
em do
|
50
|
+
req2 = req.dup
|
51
|
+
EventServer.add_event event
|
52
|
+
start_server
|
53
|
+
|
54
|
+
http = send_notify_request(req, :delete => 'NT')
|
55
|
+
http.errback { fail 'must not fail!' }
|
56
|
+
http.callback do
|
57
|
+
expect(http.response_header.status).to eq(400)
|
58
|
+
http2 = send_notify_request(req2, :delete => 'NTS')
|
59
|
+
http2.callback do
|
60
|
+
expect(http2.response_header.status).to eq(400)
|
61
|
+
done
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
EventServer.remove_event event
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should return 412 error on bad request' do
|
69
|
+
em do
|
70
|
+
EventServer.add_event event
|
71
|
+
start_server
|
72
|
+
http = send_notify_request(req, 'SID' => "uuid:#{UUID.generate}")
|
73
|
+
http.errback { fail 'must not fail!' }
|
74
|
+
http.callback do
|
75
|
+
expect(http.response_header.status).to eq(412)
|
76
|
+
done
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
em do
|
81
|
+
start_server
|
82
|
+
http = send_notify_request(req, 'NT' => "upnp:other")
|
83
|
+
http.errback { fail 'must not fail!' }
|
84
|
+
http.callback do
|
85
|
+
expect(http.response_header.status).to eq(412)
|
86
|
+
done
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
em do
|
92
|
+
start_server
|
93
|
+
http = send_notify_request(req, 'NTS' => "upnp:other")
|
94
|
+
http.errback { fail 'must not fail!' }
|
95
|
+
http.callback do
|
96
|
+
expect(http.response_header.status).to eq(412)
|
97
|
+
done
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
em do
|
102
|
+
start_server
|
103
|
+
http = send_notify_request(req, :delete => 'SID')
|
104
|
+
http.errback { fail 'must not fail!' }
|
105
|
+
http.callback do
|
106
|
+
expect(http.response_header.status).to eq(412)
|
107
|
+
done
|
108
|
+
end
|
109
|
+
end
|
110
|
+
EventServer.remove_event event
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should receive a NOTIFY request' do
|
114
|
+
em do
|
115
|
+
EventServer.add_event event
|
116
|
+
start_server
|
117
|
+
http = send_notify_request(req, 'SID' => sid)
|
118
|
+
http.errback { fail 'must not fail!' }
|
119
|
+
http_ok = event_ok = false
|
120
|
+
http.callback do
|
121
|
+
expect(http.response_header.status).to eq(200)
|
122
|
+
http_ok = true
|
123
|
+
done if event_ok
|
124
|
+
end
|
125
|
+
event.subscribe do |h|
|
126
|
+
expect(h[:seq]).to be_a(Integer)
|
127
|
+
expect(h[:content]).to be_a(Hash)
|
128
|
+
event_ok = true
|
129
|
+
done if http_ok
|
130
|
+
end
|
131
|
+
end
|
132
|
+
EventServer.remove_event event
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should return updated variables through 'events' channel"
|
136
|
+
it 'should serve multiple URLs'
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
@@ -37,12 +37,19 @@ module RUPNP
|
|
37
37
|
em do
|
38
38
|
rs.errback { fail 'RemoteService#fetch should work' }
|
39
39
|
rs.callback do
|
40
|
-
expect(rs.state_table).to have(
|
40
|
+
expect(rs.state_table).to have(5).items
|
41
41
|
expect(rs.state_table[0][:name]).to eq('X_variableName1')
|
42
42
|
expect(rs.state_table[1][:data_type]).to eq('ui4')
|
43
43
|
expect(rs.state_table[2][:default_value]).to eq('2')
|
44
44
|
expect(rs.state_table[3][:allowed_value_range][:maximum]).
|
45
45
|
to eq('255')
|
46
|
+
expect(rs.state_table[4][:data_type]).to eq('string')
|
47
|
+
expect(rs.state_table[4][:default_value]).to eq('none')
|
48
|
+
|
49
|
+
expect(rs.variables['X_variableName1']).to be_a(Fixnum)
|
50
|
+
4.times do |i|
|
51
|
+
expect(rs.variables["X_variableName#{i+1}"]).to eq(i)
|
52
|
+
end
|
46
53
|
done
|
47
54
|
end
|
48
55
|
rs.fetch
|
@@ -147,14 +154,19 @@ module RUPNP
|
|
147
154
|
em do
|
148
155
|
rs.errback { fail 'RemoteService#fetch should work' }
|
149
156
|
rs.callback do
|
157
|
+
expect(rs.variables['X_variableName1']).to eq(0)
|
150
158
|
rs.subscribe_to_event do |msg|
|
159
|
+
expect(rs.variables['X_variableName1']).to eq(12)
|
151
160
|
done
|
152
161
|
end
|
153
162
|
end
|
154
163
|
rs.fetch
|
155
164
|
|
156
|
-
|
157
|
-
|
165
|
+
EM.add_timer(1) do
|
166
|
+
event = class EventServer; @@events.last; end
|
167
|
+
url = "http://127.0.0.1:8080#{event.callback_url}"
|
168
|
+
conn = EM::HttpRequest.new(url)
|
169
|
+
send_notify_request(conn, 'SID' => event.sid)
|
158
170
|
end
|
159
171
|
end
|
160
172
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -10,6 +10,7 @@ require 'webmock/rspec'
|
|
10
10
|
|
11
11
|
RUPNP.log_level = :failure
|
12
12
|
|
13
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
13
14
|
|
14
15
|
class FakeMulticast < RUPNP::SSDP::MulticastConnection
|
15
16
|
attr_reader :handshake_response, :packets
|
@@ -151,6 +152,17 @@ EOAL
|
|
151
152
|
</stateVariable>
|
152
153
|
EOSV
|
153
154
|
end
|
155
|
+
scpd << <<EOSV2
|
156
|
+
<stateVariable sendEvents="#{opt[:send_event] ? 'yes' : 'no'}">
|
157
|
+
<name>X_variableStr</name>
|
158
|
+
<dataType>string</dataType>
|
159
|
+
<defaultValue>none</defaultValue>
|
160
|
+
<allowedValueList>
|
161
|
+
<allowedValue>none</allowedValue>
|
162
|
+
<allowedValue>another value</allowedValue>
|
163
|
+
</allowedValueList>
|
164
|
+
</stateVariable>
|
165
|
+
EOSV2
|
154
166
|
scpd << " </serviceStateTable>\n</scpd>\n"
|
155
167
|
end
|
156
168
|
|
@@ -175,6 +187,34 @@ EOD
|
|
175
187
|
end
|
176
188
|
|
177
189
|
|
190
|
+
def event_body
|
191
|
+
<<EOD
|
192
|
+
<?xml version="1.0"?>
|
193
|
+
<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
|
194
|
+
<e:property>
|
195
|
+
<X_variableName1>12</variableName>
|
196
|
+
</e:property>
|
197
|
+
</e:propertyset>
|
198
|
+
EOD
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
def send_notify_request(req, options={})
|
203
|
+
delete = options.delete(:delete)
|
204
|
+
headers = {
|
205
|
+
'HOST' => "127.0.0.1:1234",
|
206
|
+
'USER-AGENT' => RUPNP::USER_AGENT,
|
207
|
+
'CONTENT-TYPE' => 'text/xml; charset="utf-8"',
|
208
|
+
'NT' => 'upnp:event',
|
209
|
+
'NTS' => 'upnp:propchange',
|
210
|
+
'SID' => "uuid:#{UUID.generate}",
|
211
|
+
'SEQ' => 0 }.merge(options)
|
212
|
+
headers.delete delete if delete
|
213
|
+
|
214
|
+
req.setup_request(:notify, :head => headers, :body => event_body)
|
215
|
+
end
|
216
|
+
|
217
|
+
|
178
218
|
NOTIFY_REGEX = {
|
179
219
|
:common => [
|
180
220
|
/^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.
|
4
|
+
version: 0.3.0
|
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-02-
|
12
|
+
date: 2014-02-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: uuid
|
@@ -187,6 +187,7 @@ files:
|
|
187
187
|
- spec/ssdp/searcher_spec.rb
|
188
188
|
- spec/cp/remote_service_spec.rb
|
189
189
|
- spec/cp/remote_device_spec.rb
|
190
|
+
- spec/cp/event_server_spec.rb
|
190
191
|
- spec/control_point_spec.rb
|
191
192
|
- spec/spec_helper.rb
|
192
193
|
- lib/rupnp.rb
|
@@ -203,7 +204,6 @@ files:
|
|
203
204
|
- lib/rupnp/cp/base.rb
|
204
205
|
- lib/rupnp/cp/event_server.rb
|
205
206
|
- lib/rupnp/cp/remote_device.rb
|
206
|
-
- lib/rupnp/cp/event_subscriber.rb
|
207
207
|
- lib/rupnp/discover.rb
|
208
208
|
- lib/rupnp/ssdp.rb
|
209
209
|
- lib/rupnp/log_mixin.rb
|
@@ -1,47 +0,0 @@
|
|
1
|
-
module RUPNP
|
2
|
-
|
3
|
-
# Event subscriber to an event's service
|
4
|
-
# @author Sylvain Daubert
|
5
|
-
class CP::EventSubscriber < EM::Connection
|
6
|
-
include LogMixin
|
7
|
-
|
8
|
-
# Response from device
|
9
|
-
# @return [EM::Channel]
|
10
|
-
attr_reader :response
|
11
|
-
|
12
|
-
|
13
|
-
# @param [String] msg message to send for subscribing
|
14
|
-
def initialize(msg)
|
15
|
-
@msg = msg
|
16
|
-
@response = EM::Channel.new
|
17
|
-
end
|
18
|
-
|
19
|
-
# @return [void]
|
20
|
-
def post_init
|
21
|
-
log :debug, "send event subscribe request:\n#@msg"
|
22
|
-
send_data @msg
|
23
|
-
end
|
24
|
-
|
25
|
-
# Receive response from device and send it through {#response}
|
26
|
-
# @param [String] data
|
27
|
-
# @return [void]
|
28
|
-
def receive_data(data)
|
29
|
-
log :debug, "receive data from subscribe event action:\n#{data}"
|
30
|
-
resp = {}
|
31
|
-
io = StringIO.new(data)
|
32
|
-
|
33
|
-
status = io.readline
|
34
|
-
|
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
|
43
|
-
end
|
44
|
-
|
45
|
-
end
|
46
|
-
|
47
|
-
end
|