async-http 0.30.4 → 0.31.1
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.
- checksums.yaml +4 -4
- data/async-http.gemspec +2 -2
- data/lib/async/http/body/file.rb +1 -0
- data/lib/async/http/client.rb +1 -0
- data/lib/async/http/pool.rb +2 -2
- data/lib/async/http/protocol.rb +24 -0
- data/lib/async/http/protocol/http1.rb +19 -28
- data/lib/async/http/protocol/http1/client.rb +55 -0
- data/lib/async/http/protocol/http1/connection.rb +94 -0
- data/lib/async/http/protocol/http1/request.rb +45 -0
- data/lib/async/http/protocol/http1/response.rb +37 -0
- data/lib/async/http/protocol/http1/server.rb +72 -0
- data/lib/async/http/protocol/http10.rb +9 -24
- data/lib/async/http/protocol/http10/client.rb +36 -0
- data/lib/async/http/protocol/http10/server.rb +36 -0
- data/lib/async/http/protocol/http11.rb +9 -405
- data/lib/async/http/protocol/http11/client.rb +36 -0
- data/lib/async/http/protocol/http11/server.rb +36 -0
- data/lib/async/http/protocol/http2/client.rb +1 -0
- data/lib/async/http/protocol/http2/connection.rb +1 -1
- data/lib/async/http/protocol/http2/request.rb +1 -1
- data/lib/async/http/protocol/http2/server.rb +2 -1
- data/lib/async/http/reference.rb +4 -165
- data/lib/async/http/server.rb +1 -2
- data/lib/async/http/version.rb +1 -1
- metadata +16 -7
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright,
|
1
|
+
# Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the "Software"), to deal
|
@@ -18,36 +18,21 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
require_relative '
|
21
|
+
require_relative 'http10/client'
|
22
|
+
require_relative 'http10/server'
|
22
23
|
|
23
24
|
module Async
|
24
25
|
module HTTP
|
25
26
|
module Protocol
|
26
|
-
|
27
|
-
|
28
|
-
KEEP_ALIVE = 'keep-alive'.freeze
|
27
|
+
module HTTP10
|
28
|
+
VERSION = "HTTP/1.0"
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
def version
|
33
|
-
VERSION
|
34
|
-
end
|
35
|
-
|
36
|
-
def persistent?(headers)
|
37
|
-
if connection = headers[CONNECTION]
|
38
|
-
return connection.include?(KEEP_ALIVE)
|
39
|
-
else
|
40
|
-
return false
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def write_persistent_header
|
45
|
-
@stream.write("connection: keep-alive\r\n") if @persistent
|
30
|
+
def self.client(stream)
|
31
|
+
Client.new(stream)
|
46
32
|
end
|
47
33
|
|
48
|
-
def
|
49
|
-
|
50
|
-
super(body, chunked)
|
34
|
+
def self.server(stream)
|
35
|
+
Server.new(stream)
|
51
36
|
end
|
52
37
|
end
|
53
38
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'http/protocol/http10/connection'
|
22
|
+
|
23
|
+
require_relative '../http1/connection'
|
24
|
+
require_relative '../http1/client'
|
25
|
+
|
26
|
+
module Async
|
27
|
+
module HTTP
|
28
|
+
module Protocol
|
29
|
+
module HTTP10
|
30
|
+
class Client < ::HTTP::Protocol::HTTP10::Connection
|
31
|
+
include HTTP1::Connection, HTTP1::Client
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'http/protocol/http10/connection'
|
22
|
+
|
23
|
+
require_relative '../http1/connection'
|
24
|
+
require_relative '../http1/server'
|
25
|
+
|
26
|
+
module Async
|
27
|
+
module HTTP
|
28
|
+
module Protocol
|
29
|
+
module HTTP10
|
30
|
+
class Server < ::HTTP::Protocol::HTTP10::Connection
|
31
|
+
include HTTP1::Connection, HTTP1::Server
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright,
|
1
|
+
# Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the "Software"), to deal
|
@@ -18,417 +18,21 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
require_relative 'request'
|
24
|
-
require_relative 'response'
|
25
|
-
|
26
|
-
require_relative '../body/chunked'
|
27
|
-
require_relative '../body/fixed'
|
21
|
+
require_relative 'http11/client'
|
22
|
+
require_relative 'http11/server'
|
28
23
|
|
29
24
|
module Async
|
30
25
|
module HTTP
|
31
26
|
module Protocol
|
32
|
-
|
33
|
-
|
34
|
-
CHUNKED = 'chunked'.freeze
|
35
|
-
|
36
|
-
# Implements basic HTTP/1.1 request/response.
|
37
|
-
class HTTP11 < Async::IO::Protocol::Line
|
38
|
-
CRLF = "\r\n".freeze
|
39
|
-
CONNECTION = 'connection'.freeze
|
40
|
-
HOST = 'host'.freeze
|
41
|
-
CLOSE = 'close'.freeze
|
42
|
-
VERSION = "HTTP/1.1".freeze
|
43
|
-
|
44
|
-
def initialize(stream)
|
45
|
-
super(stream, CRLF)
|
46
|
-
|
47
|
-
@persistent = true
|
48
|
-
@count = 0
|
49
|
-
end
|
50
|
-
|
51
|
-
def peer
|
52
|
-
@stream.io
|
53
|
-
end
|
54
|
-
|
55
|
-
attr :count
|
56
|
-
|
57
|
-
# Only one simultaneous connection at a time.
|
58
|
-
def multiplex
|
59
|
-
1
|
60
|
-
end
|
61
|
-
|
62
|
-
# Can we use this connection to make requests?
|
63
|
-
def good?
|
64
|
-
@stream.connected?
|
65
|
-
end
|
66
|
-
|
67
|
-
def reusable?
|
68
|
-
@persistent && !@stream.closed?
|
69
|
-
end
|
70
|
-
|
71
|
-
class << self
|
72
|
-
alias server new
|
73
|
-
alias client new
|
74
|
-
end
|
75
|
-
|
76
|
-
def version
|
77
|
-
VERSION
|
78
|
-
end
|
79
|
-
|
80
|
-
def persistent?(headers)
|
81
|
-
if connection = headers[CONNECTION]
|
82
|
-
return !connection.include?(CLOSE)
|
83
|
-
else
|
84
|
-
return true
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
# @return [Async::Wrapper] the underlying non-blocking IO.
|
89
|
-
def hijack
|
90
|
-
@persistent = false
|
91
|
-
|
92
|
-
@stream.flush
|
93
|
-
|
94
|
-
return @stream.io
|
95
|
-
end
|
96
|
-
|
97
|
-
class Request < Protocol::Request
|
98
|
-
def initialize(protocol)
|
99
|
-
super(*protocol.read_request)
|
100
|
-
|
101
|
-
@protocol = protocol
|
102
|
-
end
|
103
|
-
|
104
|
-
def hijack?
|
105
|
-
true
|
106
|
-
end
|
107
|
-
|
108
|
-
def hijack
|
109
|
-
@protocol.hijack
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def next_request
|
114
|
-
# The default is true.
|
115
|
-
return nil unless @persistent
|
116
|
-
|
117
|
-
request = Request.new(self)
|
118
|
-
|
119
|
-
unless persistent?(request.headers)
|
120
|
-
@persistent = false
|
121
|
-
end
|
122
|
-
|
123
|
-
return request
|
124
|
-
rescue
|
125
|
-
# Bad Request
|
126
|
-
write_response(self.version, 400, {}, nil)
|
127
|
-
|
128
|
-
raise
|
129
|
-
end
|
130
|
-
|
131
|
-
# Server loop.
|
132
|
-
def receive_requests(task: Task.current)
|
133
|
-
while request = next_request
|
134
|
-
response = yield(request, self)
|
135
|
-
|
136
|
-
return if @stream.closed?
|
137
|
-
|
138
|
-
if response
|
139
|
-
write_response(self.version, response.status, response.headers, response.body, request.head?)
|
140
|
-
else
|
141
|
-
# If the request failed to generate a response, it was an internal server error:
|
142
|
-
write_response(self.version, 500, {}, nil)
|
143
|
-
end
|
144
|
-
|
145
|
-
# Gracefully finish reading the request body if it was not already done so.
|
146
|
-
request.finish
|
147
|
-
|
148
|
-
# This ensures we yield at least once every iteration of the loop and allow other fibers to execute.
|
149
|
-
task.yield
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
class Response < Protocol::Response
|
154
|
-
def initialize(protocol, request)
|
155
|
-
super(*protocol.read_response(request))
|
156
|
-
|
157
|
-
@protocol = protocol
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
# Used by the client to send requests to the remote server.
|
162
|
-
def call(request)
|
163
|
-
Async.logger.debug(self) {"#{request.method} #{request.path} #{request.headers.inspect}"}
|
164
|
-
|
165
|
-
# We carefully interpret https://tools.ietf.org/html/rfc7230#section-6.3.1 to implement this correctly.
|
166
|
-
begin
|
167
|
-
write_request(request.authority, request.method, request.path, self.version, request.headers)
|
168
|
-
rescue
|
169
|
-
# If we fail to fully write the request and body, we can retry this request.
|
170
|
-
raise RequestFailed.new
|
171
|
-
end
|
172
|
-
|
173
|
-
# Once we start writing the body, we can't recover if the request fails. That's because the body might be generated dynamically, streaming, etc.
|
174
|
-
write_body(request.body)
|
175
|
-
|
176
|
-
return Response.new(self, request)
|
177
|
-
rescue
|
178
|
-
# This will ensure that #reusable? returns false.
|
179
|
-
@stream.close
|
180
|
-
|
181
|
-
raise
|
182
|
-
end
|
27
|
+
module HTTP11
|
28
|
+
VERSION = "HTTP/1.1"
|
183
29
|
|
184
|
-
def
|
185
|
-
|
186
|
-
@stream.write("host: #{authority}\r\n")
|
187
|
-
write_headers(headers)
|
188
|
-
|
189
|
-
@stream.flush
|
30
|
+
def self.client(stream)
|
31
|
+
Client.new(stream)
|
190
32
|
end
|
191
33
|
|
192
|
-
def
|
193
|
-
|
194
|
-
Async.logger.debug(self) {"#{version} #{status} #{reason}"}
|
195
|
-
|
196
|
-
headers = read_headers
|
197
|
-
|
198
|
-
@persistent = persistent?(headers)
|
199
|
-
|
200
|
-
body = read_response_body(request, status, headers)
|
201
|
-
|
202
|
-
@count += 1
|
203
|
-
|
204
|
-
return version, Integer(status), reason, headers, body
|
205
|
-
end
|
206
|
-
|
207
|
-
def read_request
|
208
|
-
method, path, version = read_line.split(/\s+/, 3)
|
209
|
-
headers = read_headers
|
210
|
-
|
211
|
-
@persistent = persistent?(headers)
|
212
|
-
|
213
|
-
body = read_request_body(headers)
|
214
|
-
|
215
|
-
@count += 1
|
216
|
-
|
217
|
-
return headers.delete(HOST), method, path, version, headers, body
|
218
|
-
end
|
219
|
-
|
220
|
-
def write_response(version, status, headers, body = nil, head = false)
|
221
|
-
@stream.write("#{version} #{status}\r\n")
|
222
|
-
write_headers(headers)
|
223
|
-
|
224
|
-
if head
|
225
|
-
write_body_head(body)
|
226
|
-
else
|
227
|
-
write_body(body)
|
228
|
-
end
|
229
|
-
|
230
|
-
@stream.flush
|
231
|
-
end
|
232
|
-
|
233
|
-
protected
|
234
|
-
|
235
|
-
def write_persistent_header
|
236
|
-
@stream.write("connection: close\r\n") unless @persistent
|
237
|
-
end
|
238
|
-
|
239
|
-
def write_headers(headers)
|
240
|
-
headers.each do |name, value|
|
241
|
-
@stream.write("#{name}: #{value}\r\n")
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
def read_headers
|
246
|
-
fields = []
|
247
|
-
|
248
|
-
each_line do |line|
|
249
|
-
if line =~ /^([a-zA-Z\-\d]+):\s*(.+?)\s*$/
|
250
|
-
fields << [$1, $2]
|
251
|
-
else
|
252
|
-
break
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
return Headers.new(fields)
|
257
|
-
end
|
258
|
-
|
259
|
-
def write_empty_body(body)
|
260
|
-
# Write empty body:
|
261
|
-
write_persistent_header
|
262
|
-
@stream.write("content-length: 0\r\n\r\n")
|
263
|
-
|
264
|
-
body.read if body
|
265
|
-
|
266
|
-
@stream.flush
|
267
|
-
end
|
268
|
-
|
269
|
-
def write_fixed_length_body(body, length)
|
270
|
-
write_persistent_header
|
271
|
-
@stream.write("content-length: #{length}\r\n\r\n")
|
272
|
-
|
273
|
-
chunk_length = 0
|
274
|
-
body.each do |chunk|
|
275
|
-
chunk_length += chunk.bytesize
|
276
|
-
|
277
|
-
if chunk_length > length
|
278
|
-
raise ArgumentError, "Trying to write #{chunk_length} bytes, but content length was #{length} bytes!"
|
279
|
-
end
|
280
|
-
|
281
|
-
@stream.write(chunk)
|
282
|
-
end
|
283
|
-
|
284
|
-
@stream.flush
|
285
|
-
|
286
|
-
if chunk_length != length
|
287
|
-
raise ArgumentError, "Wrote #{chunk_length} bytes, but content length was #{length} bytes!"
|
288
|
-
end
|
289
|
-
end
|
290
|
-
|
291
|
-
def write_chunked_body(body)
|
292
|
-
write_persistent_header
|
293
|
-
@stream.write("transfer-encoding: chunked\r\n\r\n")
|
294
|
-
|
295
|
-
body.each do |chunk|
|
296
|
-
next if chunk.size == 0
|
297
|
-
|
298
|
-
@stream.write("#{chunk.bytesize.to_s(16).upcase}\r\n")
|
299
|
-
@stream.write(chunk)
|
300
|
-
@stream.write(CRLF)
|
301
|
-
@stream.flush
|
302
|
-
end
|
303
|
-
|
304
|
-
@stream.write("0\r\n\r\n")
|
305
|
-
@stream.flush
|
306
|
-
end
|
307
|
-
|
308
|
-
def write_body_and_close(body)
|
309
|
-
# We can't be persistent because we don't know the data length:
|
310
|
-
@persistent = false
|
311
|
-
write_persistent_header
|
312
|
-
|
313
|
-
@stream.write("\r\n")
|
314
|
-
|
315
|
-
body.each do |chunk|
|
316
|
-
@stream.write(chunk)
|
317
|
-
@stream.flush
|
318
|
-
end
|
319
|
-
|
320
|
-
@stream.io.close_write
|
321
|
-
end
|
322
|
-
|
323
|
-
def write_body(body, chunked = true)
|
324
|
-
if body.nil? or body.empty?
|
325
|
-
write_empty_body(body)
|
326
|
-
elsif length = body.length
|
327
|
-
write_fixed_length_body(body, length)
|
328
|
-
elsif chunked
|
329
|
-
write_chunked_body(body)
|
330
|
-
else
|
331
|
-
write_body_and_close(body)
|
332
|
-
end
|
333
|
-
end
|
334
|
-
|
335
|
-
def write_body_head(body)
|
336
|
-
write_persistent_header
|
337
|
-
|
338
|
-
if body.nil? or body.empty?
|
339
|
-
@stream.write("content-length: 0\r\n\r\n")
|
340
|
-
elsif length = body.length
|
341
|
-
@stream.write("content-length: #{length}\r\n\r\n")
|
342
|
-
else
|
343
|
-
@stream.write("\r\n")
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
def read_response_body(request, status, headers)
|
348
|
-
# RFC 7230 3.3.3
|
349
|
-
# 1. Any response to a HEAD request and any response with a 1xx
|
350
|
-
# (Informational), 204 (No Content), or 304 (Not Modified) status
|
351
|
-
# code is always terminated by the first empty line after the
|
352
|
-
# header fields, regardless of the header fields present in the
|
353
|
-
# message, and thus cannot contain a message body.
|
354
|
-
if request.head? or status == 204 or status == 304
|
355
|
-
return nil
|
356
|
-
end
|
357
|
-
|
358
|
-
# 2. Any 2xx (Successful) response to a CONNECT request implies that
|
359
|
-
# the connection will become a tunnel immediately after the empty
|
360
|
-
# line that concludes the header fields. A client MUST ignore any
|
361
|
-
# Content-Length or Transfer-Encoding header fields received in
|
362
|
-
# such a message.
|
363
|
-
if request.connect? and status == 200
|
364
|
-
return Body::Remainder.new(@stream)
|
365
|
-
end
|
366
|
-
|
367
|
-
if body = read_body(headers)
|
368
|
-
return body
|
369
|
-
else
|
370
|
-
# 7. Otherwise, this is a response message without a declared message
|
371
|
-
# body length, so the message body length is determined by the
|
372
|
-
# number of octets received prior to the server closing the
|
373
|
-
# connection.
|
374
|
-
return Body::Remainder.new(@stream)
|
375
|
-
end
|
376
|
-
end
|
377
|
-
|
378
|
-
def read_request_body(headers)
|
379
|
-
# 6. If this is a request message and none of the above are true, then
|
380
|
-
# the message body length is zero (no message body is present).
|
381
|
-
if body = read_body(headers)
|
382
|
-
return body
|
383
|
-
end
|
384
|
-
end
|
385
|
-
|
386
|
-
def read_body(headers)
|
387
|
-
# 3. If a Transfer-Encoding header field is present and the chunked
|
388
|
-
# transfer coding (Section 4.1) is the final encoding, the message
|
389
|
-
# body length is determined by reading and decoding the chunked
|
390
|
-
# data until the transfer coding indicates the data is complete.
|
391
|
-
if transfer_encoding = headers[TRANSFER_ENCODING]
|
392
|
-
# If a message is received with both a Transfer-Encoding and a
|
393
|
-
# Content-Length header field, the Transfer-Encoding overrides the
|
394
|
-
# Content-Length. Such a message might indicate an attempt to
|
395
|
-
# perform request smuggling (Section 9.5) or response splitting
|
396
|
-
# (Section 9.4) and ought to be handled as an error. A sender MUST
|
397
|
-
# remove the received Content-Length field prior to forwarding such
|
398
|
-
# a message downstream.
|
399
|
-
if headers[CONTENT_LENGTH]
|
400
|
-
raise BadRequest, "Message contains both transfer encoding and content length!"
|
401
|
-
end
|
402
|
-
|
403
|
-
if transfer_encoding.last == CHUNKED
|
404
|
-
return Body::Chunked.new(self)
|
405
|
-
else
|
406
|
-
# If a Transfer-Encoding header field is present in a response and
|
407
|
-
# the chunked transfer coding is not the final encoding, the
|
408
|
-
# message body length is determined by reading the connection until
|
409
|
-
# it is closed by the server. If a Transfer-Encoding header field
|
410
|
-
# is present in a request and the chunked transfer coding is not
|
411
|
-
# the final encoding, the message body length cannot be determined
|
412
|
-
# reliably; the server MUST respond with the 400 (Bad Request)
|
413
|
-
# status code and then close the connection.
|
414
|
-
return Body::Remainder.new(@stream)
|
415
|
-
end
|
416
|
-
end
|
417
|
-
|
418
|
-
# 5. If a valid Content-Length header field is present without
|
419
|
-
# Transfer-Encoding, its decimal value defines the expected message
|
420
|
-
# body length in octets. If the sender closes the connection or
|
421
|
-
# the recipient times out before the indicated number of octets are
|
422
|
-
# received, the recipient MUST consider the message to be
|
423
|
-
# incomplete and close the connection.
|
424
|
-
if content_length = headers[CONTENT_LENGTH]
|
425
|
-
length = Integer(content_length)
|
426
|
-
if length >= 0
|
427
|
-
return Body::Fixed.new(@stream, length)
|
428
|
-
else
|
429
|
-
raise BadRequest, "Invalid content length: #{content_length}"
|
430
|
-
end
|
431
|
-
end
|
34
|
+
def self.server(stream)
|
35
|
+
Server.new(stream)
|
432
36
|
end
|
433
37
|
end
|
434
38
|
end
|