lib/async/http/protocol/http11.rb in async-http-0.24.3 vs lib/async/http/protocol/http11.rb in async-http-0.25.0

- old
+ new

@@ -19,10 +19,11 @@ # THE SOFTWARE. require 'async/io/protocol/line' require_relative 'request_failed' +require_relative 'bad_request' require_relative '../request' require_relative '../response' require_relative '../headers' @@ -30,10 +31,14 @@ require_relative '../body/fixed' module Async module HTTP module Protocol + TRANSFER_ENCODING = 'transfer-encoding'.freeze + CONTENT_LENGTH = 'content-length'.freeze + CHUNKED = 'chunked'.freeze + # Implements basic HTTP/1.1 request/response. class HTTP11 < Async::IO::Protocol::Line CRLF = "\r\n".freeze CONNECTION = 'connection'.freeze HOST = 'host'.freeze @@ -78,29 +83,66 @@ else return true end end + def hijack + @persistent = false + + @stream.flush + + return @stream.io + end + + class Request < HTTP::Request + def initialize(protocol) + super(*protocol.read_request) + + @protocol = protocol + end + + attr :protocol + + def hijack? + true + end + + def hijack + @protocol.hijack + end + end + + def next_request + # The default is true. + return nil unless @persistent + + request = Request.new(self) + + unless persistent?(request.headers) + @persistent = false + end + + return request + rescue + # Bad Request + write_response(self.version, 400, {}, nil) + + raise + end + # Server loop. def receive_requests(task: Task.current) - while @persistent - request = Request.new(*read_request) - - unless persistent?(request.headers) - @persistent = false + while request = next_request + if response = yield(request, self) + write_response(response.version || self.version, response.status, response.headers, response.body) + request.finish + + # This ensures we yield at least once every iteration of the loop and allow other fibers to execute. + task.yield + else + break end - - response = yield request - - response.version ||= request.version - - write_response(response.version, response.status, response.headers, response.body) - - request.finish - - # This ensures we yield at least once every iteration of the loop and allow other fibers to execute. - task.yield end end def call(request) request.version ||= self.version @@ -116,43 +158,49 @@ end # 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. write_body(request.body) - return Response.new(*read_response) + return Response.new(*read_response(request)) rescue # This will ensure that #reusable? returns false. @stream.close raise end def write_request(authority, method, path, version, headers) @stream.write("#{method} #{path} #{version}\r\n") - @stream.write("Host: #{authority}\r\n") + @stream.write("host: #{authority}\r\n") write_headers(headers) @stream.flush end - def read_response + def read_response(request) version, status, reason = read_line.split(/\s+/, 3) + Async.logger.debug(self) {"#{version} #{status} #{reason}"} + headers = read_headers - body = read_body(headers) - @count += 1 - @persistent = persistent?(headers) + body = read_response_body(request, status, headers) + + @count += 1 + return version, Integer(status), reason, headers, body end def read_request method, path, version = read_line.split(/\s+/, 3) headers = read_headers - body = read_body(headers) + @persistent = persistent?(headers) + + body = read_request_body(headers) + @count += 1 return headers.delete(HOST), method, path, version, headers, body end @@ -165,19 +213,17 @@ end protected def write_persistent_header - @stream.write("Connection: close\r\n") unless @persistent + @stream.write("connection: close\r\n") unless @persistent end def write_headers(headers) headers.each do |name, value| @stream.write("#{name}: #{value}\r\n") end - - write_persistent_header end def read_headers fields = [] @@ -190,63 +236,157 @@ end return Headers.new(fields) end + def write_empty_body(body) + # Write empty body: + write_persistent_header + @stream.write("content-length: 0\r\n\r\n") + + body.read if body + + @stream.flush + end + + def write_fixed_length_body(body, length) + write_persistent_header + @stream.write("content-length: #{length}\r\n\r\n") + + body.each do |chunk| + @stream.write(chunk) + end + + @stream.flush + end + + def write_chunked_body(body) + write_persistent_header + @stream.write("transfer-encoding: chunked\r\n\r\n") + + body.each do |chunk| + next if chunk.size == 0 + + @stream.write("#{chunk.bytesize.to_s(16).upcase}\r\n") + @stream.write(chunk) + @stream.write(CRLF) + @stream.flush + end + + @stream.write("0\r\n\r\n") + @stream.flush + end + + def write_body_and_close(body) + # We can't be persistent because we don't know the data length: + @persistent = false + write_persistent_header + + @stream.write("\r\n") + + body.each do |chunk| + @stream.write(chunk) + @stream.flush + end + + @stream.io.close_write + end + def write_body(body, chunked = true) if body.nil? or body.empty? - @stream.write("Content-Length: 0\r\n\r\n") - body.read if body + write_empty_body(body) elsif length = body.length - @stream.write("Content-Length: #{length}\r\n\r\n") - - body.each do |chunk| - @stream.write(chunk) - end + write_fixed_length_body(body, length) elsif chunked - @stream.write("Transfer-Encoding: chunked\r\n\r\n") - - body.each do |chunk| - next if chunk.size == 0 - - @stream.write("#{chunk.bytesize.to_s(16).upcase}\r\n") - @stream.write(chunk) - @stream.write(CRLF) - @stream.flush - end - - @stream.write("0\r\n\r\n") + write_chunked_body(body) else - body = Body::Buffered.for(body) - - @stream.write("Content-Length: #{body.bytesize}\r\n\r\n") - - body.each do |chunk| - @stream.write(chunk) - end + write_body_and_close(body) end + end + + def read_response_body(request, status, headers) + # RFC 7230 3.3.3 + # 1. Any response to a HEAD request and any response with a 1xx + # (Informational), 204 (No Content), or 304 (Not Modified) status + # code is always terminated by the first empty line after the + # header fields, regardless of the header fields present in the + # message, and thus cannot contain a message body. + if request.head? or status == 204 or status == 304 + return nil + end - @stream.flush + # 2. Any 2xx (Successful) response to a CONNECT request implies that + # the connection will become a tunnel immediately after the empty + # line that concludes the header fields. A client MUST ignore any + # Content-Length or Transfer-Encoding header fields received in + # such a message. + if request.connect? and status == 200 + return Body::Remainder.new(@stream) + end + + if body = read_body(headers) + return body + else + # 7. Otherwise, this is a response message without a declared message + # body length, so the message body length is determined by the + # number of octets received prior to the server closing the + # connection. + return Body::Remainder.new(@stream) + end end - TRANSFER_ENCODING = 'transfer-encoding'.freeze - CONTENT_LENGTH = 'content-length'.freeze - CHUNKED = 'chunked'.freeze - - def chunked?(headers) - if transfer_encoding = headers[TRANSFER_ENCODING] - if transfer_encoding.count == 1 - return transfer_encoding.first == CHUNKED - end + def read_request_body(headers) + # 6. If this is a request message and none of the above are true, then + # the message body length is zero (no message body is present). + if body = read_body(headers) + return body end end def read_body(headers) - if chunked?(headers) - return Body::Chunked.new(self) - elsif content_length = headers[CONTENT_LENGTH] - if content_length != 0 - return Body::Fixed.new(@stream, Integer(content_length)) + # 3. If a Transfer-Encoding header field is present and the chunked + # transfer coding (Section 4.1) is the final encoding, the message + # body length is determined by reading and decoding the chunked + # data until the transfer coding indicates the data is complete. + if transfer_encoding = headers[TRANSFER_ENCODING] + # If a message is received with both a Transfer-Encoding and a + # Content-Length header field, the Transfer-Encoding overrides the + # Content-Length. Such a message might indicate an attempt to + # perform request smuggling (Section 9.5) or response splitting + # (Section 9.4) and ought to be handled as an error. A sender MUST + # remove the received Content-Length field prior to forwarding such + # a message downstream. + if headers[CONTENT_LENGTH] + raise BadRequest, "Message contains both transfer encoding and content length!" + end + + if transfer_encoding.last == CHUNKED + return Body::Chunked.new(self) + else + # If a Transfer-Encoding header field is present in a response and + # the chunked transfer coding is not the final encoding, the + # message body length is determined by reading the connection until + # it is closed by the server. If a Transfer-Encoding header field + # is present in a request and the chunked transfer coding is not + # the final encoding, the message body length cannot be determined + # reliably; the server MUST respond with the 400 (Bad Request) + # status code and then close the connection. + return Body::Remainder.new(@stream) + end + end + + # 5. If a valid Content-Length header field is present without + # Transfer-Encoding, its decimal value defines the expected message + # body length in octets. If the sender closes the connection or + # the recipient times out before the indicated number of octets are + # received, the recipient MUST consider the message to be + # incomplete and close the connection. + if content_length = headers[CONTENT_LENGTH] + length = Integer(content_length) + if length >= 0 + return Body::Fixed.new(@stream, length) + else + raise BadRequest, "Invalid content length: #{content_length}" end end end end end