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.
@@ -1,4 +1,4 @@
1
- # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
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 'http11'
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
- # Implements basic HTTP/1.1 request/response.
27
- class HTTP10 < HTTP11
28
- KEEP_ALIVE = 'keep-alive'.freeze
27
+ module HTTP10
28
+ VERSION = "HTTP/1.0"
29
29
 
30
- VERSION = "HTTP/1.0".freeze
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 write_body(body, chunked = false)
49
- # We don't support chunked encoding.
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, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
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
- require 'async/io/protocol/line'
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
- TRANSFER_ENCODING = 'transfer-encoding'.freeze
33
- CONTENT_LENGTH = 'content-length'.freeze
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 write_request(authority, method, path, version, headers)
185
- @stream.write("#{method} #{path} #{version}\r\n")
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 read_response(request)
193
- version, status, reason = read_line.split(/\s+/, 3)
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