excon 0.29.0 → 0.30.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of excon might be problematic. Click here for more details.
- data/Gemfile +2 -1
- data/Gemfile.lock +209 -1
- data/changelog.txt +25 -0
- data/excon.gemspec +5 -2
- data/lib/excon.rb +6 -3
- data/lib/excon/connection.rb +23 -9
- data/lib/excon/constants.rb +1 -5
- data/lib/excon/errors.rb +2 -0
- data/lib/excon/middlewares/decompress.rb +23 -6
- data/lib/excon/middlewares/redirect_follower.rb +5 -5
- data/lib/excon/middlewares/response_parser.rb +18 -0
- data/lib/excon/response.rb +58 -31
- data/lib/excon/socket.rb +2 -2
- data/lib/excon/ssl_socket.rb +6 -8
- data/lib/excon/unix_socket.rb +26 -10
- data/lib/excon/utils.rb +16 -0
- data/tests/middlewares/canned_response_tests.rb +27 -0
- data/tests/middlewares/decompress_tests.rb +133 -12
- data/tests/middlewares/redirect_follower_tests.rb +33 -0
- data/tests/requests_tests.rb +28 -0
- data/tests/response_tests.rb +220 -0
- data/tests/servers/good.rb +313 -0
- data/tests/test_helper.rb +14 -0
- metadata +6 -3
@@ -30,3 +30,36 @@ Shindo.tests('Excon redirector support') do
|
|
30
30
|
|
31
31
|
env_restore
|
32
32
|
end
|
33
|
+
|
34
|
+
Shindo.tests('Excon redirect support for relative Location headers') do
|
35
|
+
env_init
|
36
|
+
|
37
|
+
connection = Excon.new(
|
38
|
+
'http://127.0.0.1:9292',
|
39
|
+
:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower],
|
40
|
+
:mock => true
|
41
|
+
)
|
42
|
+
|
43
|
+
Excon.stub(
|
44
|
+
{ :path => '/old' },
|
45
|
+
{
|
46
|
+
:headers => { 'Location' => '/new' },
|
47
|
+
:body => 'old',
|
48
|
+
:status => 301
|
49
|
+
}
|
50
|
+
)
|
51
|
+
|
52
|
+
Excon.stub(
|
53
|
+
{ :path => '/new' },
|
54
|
+
{
|
55
|
+
:body => 'new',
|
56
|
+
:status => 200
|
57
|
+
}
|
58
|
+
)
|
59
|
+
|
60
|
+
tests("request(:method => :get, :path => '/old').body").returns('new') do
|
61
|
+
connection.request(:method => :get, :path => '/old').body
|
62
|
+
end
|
63
|
+
|
64
|
+
env_restore
|
65
|
+
end
|
data/tests/requests_tests.rb
CHANGED
@@ -42,4 +42,32 @@ Shindo.tests('requests should succeed') do
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
end
|
45
|
+
|
46
|
+
with_server('good') do
|
47
|
+
|
48
|
+
tests('sets transfer-coding and connection options') do
|
49
|
+
|
50
|
+
tests('without a :response_block') do
|
51
|
+
request = Marshal.load(
|
52
|
+
Excon.get('http://127.0.0.1:9292/echo/request').body
|
53
|
+
)
|
54
|
+
returns('trailers, deflate, gzip') { request[:headers]['TE'] }
|
55
|
+
returns('TE') { request[:headers]['Connection'] }
|
56
|
+
end
|
57
|
+
|
58
|
+
tests('with a :response_block') do
|
59
|
+
captures = capture_response_block do |block|
|
60
|
+
Excon.get('http://127.0.0.1:9292/echo/request',
|
61
|
+
:response_block => block)
|
62
|
+
end
|
63
|
+
data = captures.map {|capture| capture[0] }.join
|
64
|
+
request = Marshal.load(data)
|
65
|
+
|
66
|
+
returns('trailers') { request[:headers]['TE'] }
|
67
|
+
returns('TE') { request[:headers]['Connection'] }
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
45
73
|
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
Shindo.tests('Excon Response Parsing') do
|
2
|
+
env_init
|
3
|
+
|
4
|
+
with_server('good') do
|
5
|
+
|
6
|
+
tests('responses with chunked transfer-encoding') do
|
7
|
+
|
8
|
+
tests('simple response').returns('hello world') do
|
9
|
+
Excon.get('http://127.0.0.1:9292/chunked/simple').body
|
10
|
+
end
|
11
|
+
|
12
|
+
tests('with :response_block') do
|
13
|
+
|
14
|
+
tests('simple response').
|
15
|
+
returns([['hello ', nil, nil], ['world', nil, nil]]) do
|
16
|
+
capture_response_block do |block|
|
17
|
+
Excon.get('http://127.0.0.1:9292/chunked/simple',
|
18
|
+
:response_block => block,
|
19
|
+
:chunk_size => 5) # not used
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
tests('with expected response status').
|
24
|
+
returns([['hello ', nil, nil], ['world', nil, nil]]) do
|
25
|
+
capture_response_block do |block|
|
26
|
+
Excon.get('http://127.0.0.1:9292/chunked/simple',
|
27
|
+
:response_block => block,
|
28
|
+
:expects => 200)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
tests('with unexpected response status').returns('hello world') do
|
33
|
+
begin
|
34
|
+
Excon.get('http://127.0.0.1:9292/chunked/simple',
|
35
|
+
:response_block => Proc.new { raise 'test failed' },
|
36
|
+
:expects => 500)
|
37
|
+
rescue Excon::Errors::HTTPStatusError => err
|
38
|
+
err.response[:body]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
tests('merges trailers into headers').
|
45
|
+
returns('one, two, three, four, five, six') do
|
46
|
+
Excon.get('http://127.0.0.1:9292/chunked/trailers').headers['Test-Header']
|
47
|
+
end
|
48
|
+
|
49
|
+
tests("removes 'chunked' from Transfer-Encoding").returns('') do
|
50
|
+
Excon.get('http://127.0.0.1:9292/chunked/simple').headers['Transfer-Encoding']
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
tests('responses with content-length') do
|
56
|
+
|
57
|
+
tests('simple response').returns('hello world') do
|
58
|
+
Excon.get('http://127.0.0.1:9292/content-length/simple').body
|
59
|
+
end
|
60
|
+
|
61
|
+
tests('with :response_block') do
|
62
|
+
|
63
|
+
tests('simple response').
|
64
|
+
returns([['hello', 6, 11], [' worl', 1, 11], ['d', 0, 11]]) do
|
65
|
+
capture_response_block do |block|
|
66
|
+
Excon.get('http://127.0.0.1:9292/content-length/simple',
|
67
|
+
:response_block => block,
|
68
|
+
:chunk_size => 5)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
tests('with expected response status').
|
73
|
+
returns([['hello', 6, 11], [' worl', 1, 11], ['d', 0, 11]]) do
|
74
|
+
capture_response_block do |block|
|
75
|
+
Excon.get('http://127.0.0.1:9292/content-length/simple',
|
76
|
+
:response_block => block,
|
77
|
+
:chunk_size => 5,
|
78
|
+
:expects => 200)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
tests('with unexpected response status').returns('hello world') do
|
83
|
+
begin
|
84
|
+
Excon.get('http://127.0.0.1:9292/content-length/simple',
|
85
|
+
:response_block => Proc.new { raise 'test failed' },
|
86
|
+
:chunk_size => 5,
|
87
|
+
:expects => 500)
|
88
|
+
rescue Excon::Errors::HTTPStatusError => err
|
89
|
+
err.response[:body]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
tests('responses with unknown length') do
|
98
|
+
|
99
|
+
tests('simple response').returns('hello world') do
|
100
|
+
Excon.get('http://127.0.0.1:9292/unknown/simple').body
|
101
|
+
end
|
102
|
+
|
103
|
+
tests('with :response_block') do
|
104
|
+
|
105
|
+
tests('simple response').
|
106
|
+
returns([['hello', nil, nil], [' worl', nil, nil], ['d', nil, nil]]) do
|
107
|
+
capture_response_block do |block|
|
108
|
+
Excon.get('http://127.0.0.1:9292/unknown/simple',
|
109
|
+
:response_block => block,
|
110
|
+
:chunk_size => 5)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
tests('with expected response status').
|
115
|
+
returns([['hello', nil, nil], [' worl', nil, nil], ['d', nil, nil]]) do
|
116
|
+
capture_response_block do |block|
|
117
|
+
Excon.get('http://127.0.0.1:9292/unknown/simple',
|
118
|
+
:response_block => block,
|
119
|
+
:chunk_size => 5,
|
120
|
+
:expects => 200)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
tests('with unexpected response status').returns('hello world') do
|
125
|
+
begin
|
126
|
+
Excon.get('http://127.0.0.1:9292/unknown/simple',
|
127
|
+
:response_block => Proc.new { raise 'test failed' },
|
128
|
+
:chunk_size => 5,
|
129
|
+
:expects => 500)
|
130
|
+
rescue Excon::Errors::HTTPStatusError => err
|
131
|
+
err.response[:body]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
tests('header continuation') do
|
140
|
+
|
141
|
+
tests('proper continuation').returns('one, two, three, four, five, six') do
|
142
|
+
resp = Excon.get('http://127.0.0.1:9292/unknown/header_continuation')
|
143
|
+
resp.headers['Test-Header']
|
144
|
+
end
|
145
|
+
|
146
|
+
tests('malformed header').raises(Excon::Errors::SocketError) do
|
147
|
+
Excon.get('http://127.0.0.1:9292/bad/malformed_header')
|
148
|
+
end
|
149
|
+
|
150
|
+
tests('malformed header continuation').raises(Excon::Errors::SocketError) do
|
151
|
+
Excon.get('http://127.0.0.1:9292/bad/malformed_header_continuation')
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
tests('Transfer-Encoding') do
|
157
|
+
|
158
|
+
tests('used with chunked response') do
|
159
|
+
resp = Excon.post(
|
160
|
+
'http://127.0.0.1:9292/echo/transfer-encoded/chunked',
|
161
|
+
:body => 'hello world'
|
162
|
+
)
|
163
|
+
|
164
|
+
tests('server sent transfer-encoding').returns('gzip, chunked') do
|
165
|
+
resp[:headers]['Transfer-Encoding-Sent']
|
166
|
+
end
|
167
|
+
|
168
|
+
tests('processed encodings removed from header').returns('') do
|
169
|
+
resp[:headers]['Transfer-Encoding']
|
170
|
+
end
|
171
|
+
|
172
|
+
tests('response body decompressed').returns('hello world') do
|
173
|
+
resp[:body]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
tests('used with non-chunked response') do
|
178
|
+
resp = Excon.post(
|
179
|
+
'http://127.0.0.1:9292/echo/transfer-encoded',
|
180
|
+
:body => 'hello world'
|
181
|
+
)
|
182
|
+
|
183
|
+
tests('server sent transfer-encoding').returns('gzip') do
|
184
|
+
resp[:headers]['Transfer-Encoding-Sent']
|
185
|
+
end
|
186
|
+
|
187
|
+
tests('processed encoding removed from header').returns('') do
|
188
|
+
resp[:headers]['Transfer-Encoding']
|
189
|
+
end
|
190
|
+
|
191
|
+
tests('response body decompressed').returns('hello world') do
|
192
|
+
resp[:body]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# sends TE header without gzip/deflate accepted (see requests_tests)
|
197
|
+
tests('with a :response_block') do
|
198
|
+
resp = nil
|
199
|
+
captures = capture_response_block do |block|
|
200
|
+
resp = Excon.post('http://127.0.0.1:9292/echo/transfer-encoded/chunked',
|
201
|
+
:body => 'hello world',
|
202
|
+
:response_block => block)
|
203
|
+
end
|
204
|
+
|
205
|
+
tests('server does not compress').returns('chunked') do
|
206
|
+
resp[:headers]['Transfer-Encoding-Sent']
|
207
|
+
end
|
208
|
+
|
209
|
+
tests('block receives uncompressed response').returns('hello world') do
|
210
|
+
captures.map {|capture| capture[0] }.join
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
env_restore
|
220
|
+
end
|
@@ -0,0 +1,313 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'stringio'
|
5
|
+
require 'uri'
|
6
|
+
require 'zlib'
|
7
|
+
|
8
|
+
module GoodServer
|
9
|
+
# This method will be called with each request received.
|
10
|
+
#
|
11
|
+
# request = {
|
12
|
+
# :method => method,
|
13
|
+
# :uri => URI.parse(uri),
|
14
|
+
# :headers => {},
|
15
|
+
# :body => ''
|
16
|
+
# }
|
17
|
+
#
|
18
|
+
# Each connection to this server is persistent unless the client sends
|
19
|
+
# "Connection: close" in the request. If a response requires the connection
|
20
|
+
# to be closed, it should set `@persistent = false` and send "Connection: close".
|
21
|
+
def send_response(request)
|
22
|
+
type, path = request[:uri].path.split('/', 3)[1, 2]
|
23
|
+
case type
|
24
|
+
when 'echo'
|
25
|
+
case path
|
26
|
+
when 'request'
|
27
|
+
data = Marshal.dump(request)
|
28
|
+
send_data "HTTP/1.1 200 OK\r\n"
|
29
|
+
send_data "Content-Length: #{ data.size }\r\n"
|
30
|
+
send_data "\r\n"
|
31
|
+
send_data data
|
32
|
+
|
33
|
+
when /(content|transfer)-encoded\/?(.*)/
|
34
|
+
if (encoding_type = $1) == 'content'
|
35
|
+
accept_header = 'Accept-Encoding'
|
36
|
+
encoding_header = 'Content-Encoding'
|
37
|
+
else
|
38
|
+
accept_header = 'TE'
|
39
|
+
encoding_header = 'Transfer-Encoding'
|
40
|
+
end
|
41
|
+
chunked = $2 == 'chunked'
|
42
|
+
|
43
|
+
encodings = parse_encodings(request[:headers][accept_header])
|
44
|
+
while encoding = encodings.pop
|
45
|
+
break if ['gzip', 'deflate'].include?(encoding)
|
46
|
+
end
|
47
|
+
|
48
|
+
case encoding
|
49
|
+
when 'gzip'
|
50
|
+
io = (Zlib::GzipWriter.new(StringIO.new) << request[:body]).finish
|
51
|
+
io.rewind
|
52
|
+
body = io.read
|
53
|
+
when 'deflate'
|
54
|
+
# drops the zlib header
|
55
|
+
deflator = Zlib::Deflate.new(nil, -Zlib::MAX_WBITS)
|
56
|
+
body = deflator.deflate(request[:body], Zlib::FINISH)
|
57
|
+
deflator.close
|
58
|
+
else
|
59
|
+
body = request[:body]
|
60
|
+
end
|
61
|
+
|
62
|
+
# simulate server pre/post content encoding
|
63
|
+
encodings = [
|
64
|
+
request[:headers]["#{ encoding_header }-Pre"],
|
65
|
+
encoding,
|
66
|
+
request[:headers]["#{ encoding_header }-Post"],
|
67
|
+
]
|
68
|
+
if chunked && encoding_type == 'transfer'
|
69
|
+
encodings << 'chunked'
|
70
|
+
end
|
71
|
+
encodings = encodings.compact.join(', ')
|
72
|
+
|
73
|
+
send_data "HTTP/1.1 200 OK\r\n"
|
74
|
+
# let the test know what the server sent
|
75
|
+
send_data "#{ encoding_header }-Sent: #{ encodings }\r\n"
|
76
|
+
send_data "#{ encoding_header }: #{ encodings }\r\n" unless encodings.empty?
|
77
|
+
if chunked
|
78
|
+
if encoding_type == 'content'
|
79
|
+
send_data "Transfer-Encoding: chunked\r\n"
|
80
|
+
end
|
81
|
+
send_data "\r\n"
|
82
|
+
send_data chunks_for(body)
|
83
|
+
send_data "\r\n"
|
84
|
+
else
|
85
|
+
send_data "Content-Length: #{ body.size }\r\n"
|
86
|
+
send_data "\r\n"
|
87
|
+
send_data body
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
when 'chunked'
|
92
|
+
case path
|
93
|
+
when 'simple'
|
94
|
+
send_data "HTTP/1.1 200 OK\r\n"
|
95
|
+
send_data "Transfer-Encoding: chunked\r\n"
|
96
|
+
send_data "\r\n"
|
97
|
+
# chunk-extension is currently ignored.
|
98
|
+
# this works because "6; chunk-extension".to_i => "6"
|
99
|
+
send_data "6; chunk-extension\r\n"
|
100
|
+
send_data "hello \r\n"
|
101
|
+
send_data "5; chunk-extension\r\n"
|
102
|
+
send_data "world\r\n"
|
103
|
+
send_data "0; chunk-extension\r\n" # last-chunk
|
104
|
+
send_data "\r\n"
|
105
|
+
|
106
|
+
# merged trailers also support continuations
|
107
|
+
when 'trailers'
|
108
|
+
send_data "HTTP/1.1 200 OK\r\n"
|
109
|
+
send_data "Transfer-Encoding: chunked\r\n"
|
110
|
+
send_data "Test-Header: one, two\r\n"
|
111
|
+
send_data "\r\n"
|
112
|
+
send_data chunks_for('hello world')
|
113
|
+
send_data "Test-Header: three, four,\r\n"
|
114
|
+
send_data "\tfive, six\r\n"
|
115
|
+
send_data "\r\n"
|
116
|
+
end
|
117
|
+
|
118
|
+
when 'content-length'
|
119
|
+
case path
|
120
|
+
when 'simple'
|
121
|
+
send_data "HTTP/1.1 200 OK\r\n"
|
122
|
+
send_data "Content-Length: 11\r\n"
|
123
|
+
send_data "\r\n"
|
124
|
+
send_data "hello world"
|
125
|
+
end
|
126
|
+
|
127
|
+
when 'unknown'
|
128
|
+
@persistent = false
|
129
|
+
case path
|
130
|
+
when 'simple'
|
131
|
+
send_data "HTTP/1.1 200 OK\r\n"
|
132
|
+
send_data "Connection: close\r\n"
|
133
|
+
send_data "\r\n"
|
134
|
+
send_data "hello world"
|
135
|
+
|
136
|
+
when 'header_continuation'
|
137
|
+
send_data "HTTP/1.1 200 OK\r\n"
|
138
|
+
send_data "Connection: close\r\n"
|
139
|
+
send_data "Test-Header: one, two\r\n"
|
140
|
+
send_data "Test-Header: three, four,\r\n"
|
141
|
+
send_data " five, six\r\n"
|
142
|
+
send_data "\r\n"
|
143
|
+
send_data "hello world"
|
144
|
+
end
|
145
|
+
|
146
|
+
when 'bad'
|
147
|
+
# Excon will close these connections due to the errors.
|
148
|
+
case path
|
149
|
+
when 'malformed_header'
|
150
|
+
send_data "HTTP/1.1 200 OK\r\n"
|
151
|
+
send_data "Bad-Header\r\n" # no ':'
|
152
|
+
send_data "\r\n"
|
153
|
+
send_data "hello world"
|
154
|
+
|
155
|
+
when 'malformed_header_continuation'
|
156
|
+
send_data "HTTP/1.1 200 OK\r\n"
|
157
|
+
send_data " Bad-Header: one, two\r\n" # no previous header
|
158
|
+
send_data "\r\n"
|
159
|
+
send_data "hello world"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
close_connection(true) unless @persistent
|
164
|
+
end
|
165
|
+
|
166
|
+
def post_init
|
167
|
+
@buffer = StringIO.new
|
168
|
+
@buffer.set_encoding('BINARY') if @buffer.respond_to?(:set_encoding)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Receives a String of +data+ sent from the client.
|
172
|
+
# +data+ may only be a portion of what the client sent.
|
173
|
+
# The data is buffered, then processed and removed from the buffer
|
174
|
+
# as data becomes available until the @request is complete.
|
175
|
+
def receive_data(data)
|
176
|
+
@buffer.write(data)
|
177
|
+
|
178
|
+
parse_headers unless @request
|
179
|
+
parse_body if @request
|
180
|
+
|
181
|
+
if @request_complete
|
182
|
+
send_response(@request)
|
183
|
+
sync_buffer
|
184
|
+
@request = nil
|
185
|
+
@request_complete = false
|
186
|
+
end
|
187
|
+
|
188
|
+
@buffer.seek(0, IO::SEEK_END) # wait for more data
|
189
|
+
end
|
190
|
+
|
191
|
+
# Removes the processed portion of the buffer
|
192
|
+
# by replacing the buffer with it's contents from the current pos.
|
193
|
+
def sync_buffer
|
194
|
+
@buffer.string = @buffer.read
|
195
|
+
end
|
196
|
+
|
197
|
+
def parse_headers
|
198
|
+
@buffer.rewind
|
199
|
+
# wait until buffer contains the end of the headers
|
200
|
+
if /\sHTTP\/\d+\.\d+\r\n.*?\r\n\r\n/m =~ @buffer.read
|
201
|
+
@buffer.rewind
|
202
|
+
# For persistent connections, the buffer could start with the
|
203
|
+
# \r\n chunked-message terminator from the previous request.
|
204
|
+
# This will discard anything up to the request-line.
|
205
|
+
until m = /^(\w+)\s(.*)\sHTTP\/\d+\.\d+$/.match(@buffer.readline.chop!); end
|
206
|
+
method, uri = m[1, 2]
|
207
|
+
|
208
|
+
headers = {}
|
209
|
+
last_key = nil
|
210
|
+
until (line = @buffer.readline.chop!).empty?
|
211
|
+
if !line.lstrip!.nil?
|
212
|
+
headers[last_key] << ' ' << line.rstrip
|
213
|
+
else
|
214
|
+
key, value = line.split(':', 2)
|
215
|
+
headers[key] = ([headers[key]] << value.strip).compact.join(', ')
|
216
|
+
last_key = key
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
sync_buffer
|
221
|
+
|
222
|
+
@chunked = headers['Transfer-Encoding'] =~ /chunked/i
|
223
|
+
@content_length = headers['Content-Length'].to_i
|
224
|
+
@persistent = headers['Connection'] !~ /close/i
|
225
|
+
@request = {
|
226
|
+
:method => method,
|
227
|
+
:uri => URI.parse(uri),
|
228
|
+
:headers => headers,
|
229
|
+
:body => ''
|
230
|
+
}
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def parse_body
|
235
|
+
if @chunked
|
236
|
+
@buffer.rewind
|
237
|
+
until @request_complete || @buffer.eof?
|
238
|
+
unless @chunk_size
|
239
|
+
# in case buffer only contains a portion of the chunk-size line
|
240
|
+
if (line = @buffer.readline) =~ /\r\n\z/
|
241
|
+
@chunk_size = line.to_i(16)
|
242
|
+
if @chunk_size > 0
|
243
|
+
sync_buffer
|
244
|
+
else # last-chunk
|
245
|
+
@buffer.read(2) # the final \r\n may or may not be in the buffer
|
246
|
+
@chunk_size = nil
|
247
|
+
@body_pos = nil
|
248
|
+
@request_complete = true
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
if @chunk_size
|
253
|
+
if @buffer.size >= @chunk_size + 2
|
254
|
+
@request[:body] << @buffer.read(@chunk_size + 2).chop!
|
255
|
+
@chunk_size = nil
|
256
|
+
sync_buffer
|
257
|
+
else
|
258
|
+
break # wait for more data
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
elsif @content_length > 0
|
263
|
+
@buffer.rewind
|
264
|
+
unless @buffer.eof? # buffer only contained the headers
|
265
|
+
@request[:body] << @buffer.read(@content_length - @request[:body].size)
|
266
|
+
if @request[:body].size == @content_length
|
267
|
+
@request_complete = true
|
268
|
+
else
|
269
|
+
sync_buffer
|
270
|
+
end
|
271
|
+
end
|
272
|
+
else
|
273
|
+
# no body
|
274
|
+
@request_complete = true
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def chunks_for(str)
|
279
|
+
chunks = ''
|
280
|
+
str.force_encoding('BINARY') if str.respond_to?(:force_encoding)
|
281
|
+
chunk_size = str.size / 2
|
282
|
+
until (chunk = str.slice!(0, chunk_size)).empty?
|
283
|
+
chunks << chunk.size.to_s(16) << "\r\n"
|
284
|
+
chunks << chunk << "\r\n"
|
285
|
+
end
|
286
|
+
chunks << "0\r\n" # last-chunk
|
287
|
+
end
|
288
|
+
|
289
|
+
# only supports a single quality parameter for tokens
|
290
|
+
def parse_encodings(encodings)
|
291
|
+
return [] if encodings.nil?
|
292
|
+
split_header_value(encodings).map do |value|
|
293
|
+
token, q_val = /^(.*?)(?:;q=(.*))?$/.match(value.strip)[1, 2]
|
294
|
+
if q_val && q_val.to_f == 0
|
295
|
+
nil
|
296
|
+
else
|
297
|
+
[token, (q_val || 1).to_f]
|
298
|
+
end
|
299
|
+
end.compact.sort_by {|_, q_val| q_val }.map {|token, _| token }
|
300
|
+
end
|
301
|
+
|
302
|
+
# Splits a header value +str+ according to HTTP specification.
|
303
|
+
def split_header_value(str)
|
304
|
+
return [] if str.nil?
|
305
|
+
str.strip.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+)
|
306
|
+
(?:,\s*|\Z)'xn).flatten
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
EM.run do
|
311
|
+
EM.start_server("127.0.0.1", 9292, GoodServer)
|
312
|
+
$stderr.puts "ready"
|
313
|
+
end
|