class Protocol::HTTP1::Connection
Represents a single HTTP/1.x connection, which may be used to send and receive multiple requests and responses.
def close(error = nil)
def close(error = nil) @persistent = false if stream = @stream @stream = nil stream.close end unless closed? @state = :closed self.closed(error) end end
def close!(error = nil)
If no error occurred, and the connection is persistent, this will immediately transition to the idle state.
Transition to the closed state.
def close!(error = nil) if @persistent and !error # If there was no error, and the connection is persistent, we can reuse it: @state = :idle else @state = :closed end self.closed(error) end
def close_read
def close_read unless @state == :closed @persistent = false @stream&.close_read self.receive_end_stream! end end
def close_write_stream
def close_write_stream @stream.close_write rescue Errno::EPIPE, Errno::ECONNRESET raise HTTP::RemoteError, "Remote connection closed during close_write!" end
def closed(error = nil)
Sub-classes may override this method to perform additional cleanup.
The connection (stream) was closed. It may now be in the idle state.
def closed(error = nil) end
def closed?
-
(Boolean)- whether the connection is in the closed state.
def closed? @state == :closed end
def extract_content_length(headers)
@parameter length [Integer] the content length.
@yields {|length| ...} if a content length is found.
@parameter headers [Hash] the headers.
Extract the content length from the headers, if possible.
def extract_content_length(headers) if content_length = headers.delete(CONTENT_LENGTH) if content_length =~ VALID_CONTENT_LENGTH yield Integer(content_length, 10) else raise BadRequest, "Invalid content length: #{content_length.inspect}" end end end
def flush_stream
def flush_stream @stream.flush rescue Errno::EPIPE, Errno::ECONNRESET raise HTTP::RemoteError, "Remote connection closed during flush!" end
def half_closed_local?
-
(Boolean)- whether the connection is in the half-closed local state.
def half_closed_local? @state == :half_closed_local end
def half_closed_remote?
-
(Boolean)- whether the connection is in the half-closed remote state.
def half_closed_remote? @state == :half_closed_remote end
def hijack!
Hijack the connection - that is, take over the underlying IO and close the connection.
def hijack! @persistent = false if stream = @stream @stream = nil stream.flush @state = :hijacked self.closed return stream end end
def hijacked?
Indicates whether the connection has been hijacked meaning its IO has been handed over and is not usable anymore.
def hijacked? @stream.nil? end
def idle?
-
(Boolean)- whether the connection is in the idle state.
def idle? @state == :idle end
def initialize(stream, persistent: true, state: :idle, maximum_line_length: DEFAULT_MAXIMUM_LINE_LENGTH)
@parameter persistent [Boolean] whether the connection is persistent.
@parameter stream [IO] the stream to read and write data from.
Initialize the connection with the given stream.
def initialize(stream, persistent: true, state: :idle, maximum_line_length: DEFAULT_MAXIMUM_LINE_LENGTH) @stream = stream @persistent = persistent @state = state @count = 0 @maximum_line_length = maximum_line_length end
def interim_status?(status)
@parameter status [Integer] the status code.
Indicates whether the status code is an interim status code.
def interim_status?(status) status != 101 and status >= 100 and status < 200 end
def open!
Force a transition to the open state.
def open! unless @state == :idle raise ProtocolError, "Cannot open connection in state: #{@state}!" end @state = :open return self end
def open?
-
(Boolean)- whether the connection is in the open state.
def open? @state == :open end
def persistent?(version, method, headers)
-
(Boolean)- whether the connection can be persistent.
def persistent?(version, method, headers) if method == HTTP::Methods::CONNECT return false end if version == HTTP10 if connection = headers[CONNECTION] return connection.keep_alive? else return false end else # HTTP/1.1+ if connection = headers[CONNECTION] return !connection.close? else return true end end end
def read(length)
Read some data from the connection.
def read(length) @stream.read(length) rescue Errno::EPIPE, Errno::ECONNRESET raise HTTP::RemoteError, "Remote connection closed during read!" end
def read_body(headers, remainder = false)
@parameter remainder [Boolean] whether to read the remainder of the body.
@parameter headers [Hash] the headers of the message.
- Otherwise, if `remainder` is true, the body is read until the connection is closed.
- Otherwise, if the `content-length` is present, the body is read until the content length is reached.
- The `transfer-encoding` header is used to determine if the body is chunked.
Read the body of the message.
def read_body(headers, remainder = false) # 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.delete(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 read_chunked_body(headers) 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 read_remainder_body 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. extract_content_length(headers) do |length| if length > 0 return read_fixed_body(length) else return nil end end # http://tools.ietf.org/html/rfc2068#section-19.7.1.1 if remainder # 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 read_remainder_body end end
def read_chunked_body(headers)
@parameters headers [Hash] the headers of the request.
Read the body, assuming it is using the chunked transfer encoding.
def read_chunked_body(headers) Body::Chunked.new(self, headers) end
def read_fixed_body(length)
@parameters length [Integer] the length of the body.
Read the body, assuming it has a fixed length.
def read_fixed_body(length) Body::Fixed.new(self, length) end
def read_head_body(length)
@parameters length [Integer] the length of the body.
Read the body, assuming that we are not receiving any actual data, but just the length.
def read_head_body(length) # We are not receiving any body: self.receive_end_stream! Protocol::HTTP::Body::Head.new(length) end
def read_headers
@raises [EOFError] if the connection is closed.
@returns [HTTP::Headers] the headers read.
Read headers from the connection until an empty line is encountered.
def read_headers fields = [] while line = read_line # Empty line indicates end of headers: break if line.empty? if match = line.match(HEADER) fields << [match[1], match[2] || ""] else raise BadHeader, "Could not parse header: #{line.inspect}" end end return HTTP::Headers.new(fields) end
def read_line
@raises [EOFError] if a line could not be read.
Read a line from the connection.
def read_line read_line? or raise EOFError end
def read_line?
@raises [LineLengthError] if the line is too long.
@returns [String | Nil] the line read, or nil if the connection is closed.
Read a line from the connection.
def read_line? if line = @stream.gets(CRLF, @maximum_line_length) unless line.chomp!(CRLF) if line.bytesize == @maximum_line_length # This basically means that the request line, response line, header, or chunked length line is too long: raise LineLengthError, "Line too long!" else # This means the line was not terminated properly, which is a protocol violation: raise ProtocolError, "Line not terminated properly!" end end end return line # If a connection is shut down abruptly, we treat it as EOF, but only specifically in `read_line?`. rescue Errno::ECONNRESET return nil end
def read_remainder_body
Read the body, assuming that we read until the connection is closed.
def read_remainder_body @persistent = false Body::Remainder.new(self) end
def read_request
@returns [Tuple(String, String, String, String, HTTP::Headers, Protocol::HTTP1::Body) | Nil] the host, method, path, version, headers, and body of the request, or `nil` if the connection is closed.
@yields {|host, method, path, version, headers, body| ...} if a block is given.
Transitions to the open state.
Read a request from the connection, including the request line and request headers, and prepares to read the request body.
def read_request open! method, path, version = read_request_line return unless method headers = read_headers # If we are not persistent, we can't become persistent even if the request might allow it: if @persistent # In other words, `@persistent` can only transition from true to false. @persistent = persistent?(version, method, headers) end body = read_request_body(method, headers) unless body self.receive_end_stream! end @count += 1 if block_given? yield headers.delete(HOST), method, path, version, headers, body else return headers.delete(HOST), method, path, version, headers, body end end
def read_request_body(method, headers)
@parameter method [String] the HTTP method.
- Otherwise, the body is read according to {read_body}.
- The `UPGRADE` method is used to upgrade the connection to a different protocol (typically WebSockets), so the body is read until the connection is closed.
- The `CONNECT` method is used to establish a tunnel, so the body is read until the connection is closed.
Read the body of the request.
def read_request_body(method, headers) # 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 method == HTTP::Methods::CONNECT return read_tunnel_body end # A successful upgrade response implies that the connection will become a tunnel immediately after the empty line that concludes the header fields. if headers[UPGRADE] return read_upgrade_body end # 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). return read_body(headers) end
def read_request_line
Read a request line from the connection.
def read_request_line return unless line = read_line? if match = line.match(REQUEST_LINE) _, method, path, version = *match else raise InvalidRequest, line.inspect end return method, path, version end
def read_response(method)
@raises [ProtocolError] if the connection is not in the open or half-closed local state.
@returns [Tuple(String, Integer, String, HTTP::Headers, Protocol::HTTP1::Body)] the version, status, reason, headers, and body of the response.
@yields {|version, status, reason, headers, body| ...} if a block is given.
@parameter method [String] the HTTP method.
Read a response from the connection.
def read_response(method) unless @state == :open or @state == :half_closed_local raise ProtocolError, "Cannot read response in state: #{@state}!" end version, status, reason = read_response_line headers = read_headers if @persistent @persistent = persistent?(version, method, headers) end unless interim_status?(status) body = read_response_body(method, status, headers) unless body self.receive_end_stream! end @count += 1 end if block_given? yield version, status, reason, headers, body else return version, status, reason, headers, body end end
def read_response_body(method, status, headers)
@parameter status [Integer] the HTTP status code.
@parameter method [String] the HTTP method.
- Otherwise, the body is read according to {read_body}.
- The `CONNECT` method is used to establish a tunnel, so {read_tunnel_body} is invoked.
- Interim status codes (1xx), no content (204) and not modified (304) status codes do not have a body, so nil is returned.
- A 101 status code indicates that the connection will be upgraded, so {read_upgrade_body} is invoked.
- The `HEAD` method is used to retrieve the headers of the response without the body, so {read_head_body} is invoked if there is a content length, otherwise nil is returned.
Read the body of the response.
def read_response_body(method, 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 method == HTTP::Methods::HEAD extract_content_length(headers) do |length| if length > 0 return read_head_body(length) else return nil end end # There is no body for a HEAD request if there is no content length: return nil end if status == 101 return read_upgrade_body end if (status >= 100 and status < 200) or status == 204 or status == 304 return nil end # 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 method == HTTP::Methods::CONNECT and status == 200 return read_tunnel_body end return read_body(headers, true) end
def read_response_line
@returns [Tuple(String, Integer, String)] the version, status, and reason of the response.
Read a response line from the connection.
def read_response_line version, status, reason = read_line.split(/\s+/, 3) status = Integer(status) return version, status, reason end
def read_tunnel_body
Invokes {read_remainder_body}.
Read the body, assuming it is a tunnel.
def read_tunnel_body read_remainder_body end
def read_upgrade_body
Invokes {read_remainder_body}.
Read the body, assuming it is an upgrade.
def read_upgrade_body # When you have an incoming upgrade request body, we must be extremely careful not to start reading it until the upgrade has been confirmed, otherwise if the upgrade was rejected and we started forwarding the incoming request body, it would desynchronize the connection (potential security issue). # We mitigate this issue by setting @persistent to false, which will prevent the connection from being reused, even if the upgrade fails (potential performance issue). read_remainder_body end
def readpartial(length)
Read some data from the connection.
def readpartial(length) @stream.readpartial(length) rescue Errno::EPIPE, Errno::ECONNRESET raise HTTP::RemoteError, "Remote connection closed during readpartial!" end
def receive_end_stream!
This will transition to the half-closed remote state if the connection is open, or the closed state if the connection is half-closed local.
Indicate that the end of the stream (body) has been received.
def receive_end_stream! if @state == :open @state = :half_closed_remote elsif @state == :half_closed_local self.close! else raise ProtocolError, "Cannot receive end stream in state: #{@state}!" end end
def send_end_stream!
If the connection is already in the half-closed remote state, it will transition to the closed state.
Transition to the half-closed local state, in other words, the connection is closed for writing.
def send_end_stream! if @state == :open @state = :half_closed_local elsif @state == :half_closed_remote self.close! else raise ProtocolError, "Cannot send end stream in state: #{@state}!" end end
def write_body(version, body, head = false, trailer = nil)
@parameter head [Boolean] whether the request was a `HEAD` request.
@parameter body [Object] the body to write.
@parameter version [String] the HTTP version.
The behavior of this method is determined by the HTTP version, the body, and the request method. We try to choose the best approach possible, given the constraints, connection persistence, whether the length is known, etc.
Write a body to the connection.
def write_body(version, body, head = false, trailer = nil) # HTTP/1.0 cannot in any case handle trailers. if version == HTTP10 # or te: trailers was not present (strictly speaking not required.) trailer = nil end # While writing the body, we don't know if trailers will be added. We must choose a different body format depending on whether there is the chance of trailers, even if trailer.any? is currently false. # # Below you notice `and trailer.nil?`. I tried this but content-length is more important than trailers. if body.nil? write_connection_header(version) write_empty_body(body) elsif length = body.length # and trailer.nil? write_connection_header(version) write_fixed_length_body(body, length, head) elsif body.empty? # Even thought this code is the same as the first clause `body.nil?`, HEAD responses have an empty body but still carry a content length. `write_fixed_length_body` takes care of this appropriately. write_connection_header(version) write_empty_body(body) elsif version == HTTP11 write_connection_header(version) # We specifically ensure that non-persistent connections do not use chunked response, so that hijacking works as expected. write_chunked_body(body, head, trailer) else @persistent = false write_connection_header(version) write_body_and_close(body, head) end end
def write_body_and_close(body, head)
@parameter body [Object] the body to write.
Write the body to the connection and close the connection.
def write_body_and_close(body, head) # We can't be persistent because we don't know the data length: @persistent = false write_stream("\r\n") unless head flush_stream unless body.ready? while chunk = body.read write_stream(chunk) flush_stream unless body.ready? end end flush_stream close_write_stream rescue => error raise ensure # Close the body after the stream is fully written and half-closed, so that completion callbacks run after the client has received the full response: body.close(error) self.send_end_stream! end
def write_chunked_body(body, head, trailer = nil)
@parameter head [Boolean] whether the request was a `HEAD` request.
@parameter body [Object] the body to write.
If trailers are given, they will be written after the body.
If the request was a `HEAD` request, the body will be closed, and no data will be written.
Write a chunked body to the connection.
def write_chunked_body(body, head, trailer = nil) write_stream("transfer-encoding: chunked\r\n\r\n") if head flush_stream else flush_stream unless body.ready? # Use a manual read loop (not body.each) so that body.close runs after the terminal chunk is written. With body.each, the ensure { close } fires before the terminal "0\r\n\r\n" is sent, delaying the client. while chunk = body.read next if chunk.size == 0 write_stream("#{chunk.bytesize.to_s(16).upcase}\r\n") write_stream(chunk) write_stream(CRLF) flush_stream unless body.ready? end if trailer&.any? write_stream("0\r\n") write_headers(trailer) write_stream("\r\n") else write_stream("0\r\n\r\n") end flush_stream end rescue => error raise ensure # Close the body after the complete chunked response (including terminal chunk) is flushed, so that completion callbacks don't block the client from seeing the response as complete: body.close(error) self.send_end_stream! end
def write_connection_header(version)
def write_connection_header(version) if version == HTTP10 write_stream("connection: keep-alive\r\n") if @persistent else write_stream("connection: close\r\n") unless @persistent end end
def write_empty_body(body = nil)
If given, the body will be closed.
Write an empty body to the connection.
def write_empty_body(body = nil) write_stream("content-length: 0\r\n\r\n") flush_stream body&.close ensure self.send_end_stream! end
def write_fixed_length_body(body, length, head)
@parameter head [Boolean] whether the request was a `HEAD` request.
@parameter length [Integer] the length of the body.
@parameter body [Object] the body to write.
If the request was a `HEAD` request, the body will be closed, and no data will be written.
Write a fixed length body to the connection.
def write_fixed_length_body(body, length, head) write_stream("content-length: #{length}\r\n\r\n") if head flush_stream else flush_stream unless body.ready? chunk_length = 0 # Use a manual read loop (not body.each) so that body.close runs after the response is fully written and flushed. This ensures completion callbacks (e.g. rack.response_finished) don't delay the client. while chunk = body.read chunk_length += chunk.bytesize if chunk_length > length raise ContentLengthError, "Trying to write #{chunk_length} bytes, but content length was #{length} bytes!" end write_stream(chunk) flush_stream unless body.ready? end flush_stream if chunk_length != length raise ContentLengthError, "Wrote #{chunk_length} bytes, but content length was #{length} bytes!" end end rescue => error raise ensure # Close the body after the response is fully flushed, so that completion callbacks run after the client has received the response: body.close(error) self.send_end_stream! end
def write_header(name, value)
@parameter value [String] the header value.
@parameter name [String] the header name.
Write a header to the connection, rejecting CR, LF, and NUL in its value.
def write_header(name, value) # Convert it to a string: name = name.to_s value = value.to_s # Validate it: unless name.match?(VALID_FIELD_NAME) raise BadHeader, "Invalid header name: #{name.inspect}" end unless value.match?(VALID_FIELD_VALUE) raise BadHeader, "Invalid header value for #{name}: #{value.inspect}" end # Write it: write_stream("#{name}: #{value}\r\n") end
def write_headers(headers)
@parameter headers [Hash] the headers to write.
Write headers to the connection.
def write_headers(headers) headers.each do |name, value| write_header(name, value) end end
def write_interim_response(version, status, headers, reason = nil)
@parameter reason [String] the reason phrase, defaults to the standard reason phrase for the status code.
@parameter headers [Hash] the HTTP headers.
@parameter status [Integer] the HTTP status code.
@parameter version [String] the HTTP version.
Write an interim response to the connection. It is expected you will eventually write the final response after this method.
def write_interim_response(version, status, headers, reason = nil) reason ||= Protocol::HTTP::Status.description(status) unless @state == :open or @state == :half_closed_remote raise ProtocolError, "Cannot write interim response in state: #{@state}!" end write_stream("#{version} #{status} #{reason}\r\n") write_headers(headers) write_stream("\r\n") flush_stream end
def write_request(authority, method, target, version, headers)
@parameter headers [Hash] the HTTP headers.
@parameter version [String] the HTTP version.
@parameter target [String] the request target.
@parameter method [String] the HTTP method.
@parameter authority [String] the authority of the request.
Requires the method to be an HTTP token and rejects CR, LF, and NUL bytes in the authority and target before writing any request bytes.
Transitions to the open state.
Write a request to the connection. It is expected you will write the body after this method.
def write_request(authority, method, target, version, headers) method = method.to_s unless method.b.match?(VALID_METHOD) raise InvalidRequest, "Invalid request method: #{method.inspect}" end target = target.to_s unless target.b.match?(VALID_FIELD_VALUE) raise InvalidRequest, "Invalid request target: #{target.inspect}" end if authority authority = authority.to_s unless authority.b.match?(VALID_FIELD_VALUE) raise InvalidRequest, "Invalid request authority: #{authority.inspect}" end end open! write_stream("#{method} #{target} #{version}\r\n") write_stream("host: #{authority}\r\n") if authority write_headers(headers) rescue raise ::Protocol::HTTP::RefusedError end
def write_response(version, status, headers, reason = nil)
@parameter headers [Hash] the HTTP headers.
@parameter status [Integer] the HTTP status code.
@parameter version [String] the HTTP version.
Write a response to the connection. It is expected you will write the body after this method.
def write_response(version, status, headers, reason = nil) reason ||= Protocol::HTTP::Status.description(status) unless @state == :open or @state == :half_closed_remote raise ProtocolError, "Cannot write response in state: #{@state}!" end # Safari WebSockets break if no reason is given: write_stream("#{version} #{status} #{reason}\r\n") write_headers(headers) end
def write_stream(data)
def write_stream(data) @stream.write(data) rescue Errno::EPIPE, Errno::ECONNRESET raise HTTP::RemoteError, "Remote connection closed during write!" end
def write_tunnel_body(version, body = nil)
@parameter body [Object | Nil] the body to write.
@parameter version [String] the HTTP version.
The connection will not be persistent after this method is called.
This writes the connection header and the body to the connection. If the body is `nil`, you should coordinate writing to the stream.
Write a tunnel body to the connection.
def write_tunnel_body(version, body = nil) @persistent = false write_connection_header(version) write_stream("\r\n") flush_stream # Don't remove me! if body body.each do |chunk| write_stream(chunk) flush_stream end close_write_stream end return @stream ensure self.send_end_stream! end
def write_upgrade_body(protocol, body = nil)
@parameter body [Object | Nil] the body to write.
@parameter protocol [String] the protocol to upgrade to.
The connection will not be persistent after this method is called.
This writes the upgrade header and the body to the connection. If the body is `nil`, you should coordinate writing to the stream.
Write an upgrade body to the connection.
def write_upgrade_body(protocol, body = nil) # Once we upgrade the connection, it can no longer handle other requests: @persistent = false write_upgrade_header(protocol) write_stream("\r\n") flush_stream # Don't remove me! if body body.each do |chunk| write_stream(chunk) flush_stream end close_write_stream end return @stream ensure self.send_end_stream! end
def write_upgrade_header(upgrade)
def write_upgrade_header(upgrade) write_header(CONNECTION, "upgrade") write_header(UPGRADE, upgrade) end