Class: Protocol::HTTP1::Connection
- Inherits:
-
Object
- Object
- Protocol::HTTP1::Connection
- Defined in:
- lib/protocol/http1/connection.rb
Overview
Represents a single HTTP/1.x connection, which may be used to send and receive multiple requests and responses.
Constant Summary collapse
- CRLF =
"\r\n"- HTTP10 =
The HTTP/1.0 version string.
"HTTP/1.0"- HTTP11 =
The HTTP/1.1 version string.
"HTTP/1.1"- HEAD =
The HTTP
HEADmethod. "HEAD"- CONNECT =
The HTTP
CONNECTmethod. "CONNECT"- VALID_CONTENT_LENGTH =
The pattern for valid content length values.
/\A\d+\z/
Instance Attribute Summary collapse
-
#count ⇒ Object
readonly
Returns the value of attribute count.
-
#persistent ⇒ Object
This determines what connection headers are sent in the response and whether the connection can be reused after the response is sent.
-
#state ⇒ Object
The current state of the connection.
-
#stream ⇒ Object
readonly
The underlying IO stream.
- #the number of requests and responses processed by this connection.(numberofrequests) ⇒ Object readonly
Instance Method Summary collapse
-
#close(error = nil) ⇒ Object
Close the connection and underlying stream and transition to the closed state.
-
#close!(error = nil) ⇒ Object
Transition to the closed state.
-
#close_read ⇒ Object
Close the read end of the connection and transition to the half-closed remote state (or closed if already in the half-closed local state).
-
#closed(error = nil) ⇒ Object
The connection (stream) was closed.
-
#closed? ⇒ Boolean
Whether the connection is in the closed state.
-
#extract_content_length(headers) ⇒ Object
Extract the content length from the headers, if possible.
-
#half_closed_local? ⇒ Boolean
Whether the connection is in the half-closed local state.
-
#half_closed_remote? ⇒ Boolean
Whether the connection is in the half-closed remote state.
-
#hijack! ⇒ Object
Hijack the connection - that is, take over the underlying IO and close the connection.
-
#hijacked? ⇒ Boolean
Indicates whether the connection has been hijacked meaning its IO has been handed over and is not usable anymore.
-
#idle? ⇒ Boolean
Whether the connection is in the idle state.
-
#initialize(stream, persistent: true, state: :idle, maximum_line_length: DEFAULT_MAXIMUM_LINE_LENGTH) ⇒ Connection
constructor
Initialize the connection with the given stream.
-
#interim_status?(status) ⇒ Boolean
Indicates whether the status code is an interim status code.
-
#open! ⇒ Object
Force a transition to the open state.
-
#open? ⇒ Boolean
Whether the connection is in the open state.
-
#persistent?(version, method, headers) ⇒ Boolean
Indicates whether the connection is persistent given the version, method, and headers.
-
#read(length) ⇒ Object
Read some data from the connection.
-
#read_body(headers, remainder = false) ⇒ Object
Read the body of the message.
-
#read_chunked_body(headers) ⇒ Object
Read the body, assuming it is using the chunked transfer encoding.
-
#read_fixed_body(length) ⇒ Object
Read the body, assuming it has a fixed length.
-
#read_head_body(length) ⇒ Object
Read the body, assuming that we are not receiving any actual data, but just the length.
-
#read_headers ⇒ Object
Read headers from the connection until an empty line is encountered.
-
#read_line ⇒ Object
Read a line from the connection.
-
#read_line? ⇒ Boolean
Read a line from the connection.
-
#read_remainder_body ⇒ Object
Read the body, assuming that we read until the connection is closed.
-
#read_request ⇒ Object
Read a request from the connection, including the request line and request headers, and prepares to read the request body.
-
#read_request_body(method, headers) ⇒ Object
Read the body of the request.
-
#read_request_line ⇒ Object
Read a request line from the connection.
-
#read_response(method) ⇒ Object
Read a response from the connection.
-
#read_response_body(method, status, headers) ⇒ Object
Read the body of the response.
-
#read_response_line ⇒ Object
Read a response line from the connection.
-
#read_tunnel_body ⇒ Object
Read the body, assuming it is a tunnel.
-
#read_upgrade_body ⇒ Object
Read the body, assuming it is an upgrade.
-
#readpartial(length) ⇒ Object
Read some data from the connection.
-
#receive_end_stream! ⇒ Object
Indicate that the end of the stream (body) has been received.
-
#send_end_stream! ⇒ Object
Transition to the half-closed local state, in other words, the connection is closed for writing.
-
#true if the connection is persistent.= ⇒ Object
This determines what connection headers are sent in the response and whether the connection can be reused after the response is sent.
-
#write_body(version, body, head = false, trailer = nil) ⇒ Object
Write a body to the connection.
-
#write_body_and_close(body, head) ⇒ Object
Write the body to the connection and close the connection.
-
#write_chunked_body(body, head, trailer = nil) ⇒ Object
Write a chunked body to the connection.
-
#write_connection_header(version) ⇒ Object
Write the appropriate header for connection persistence.
-
#write_empty_body(body = nil) ⇒ Object
Write an empty body to the connection.
-
#write_fixed_length_body(body, length, head) ⇒ Object
Write a fixed length body to the connection.
-
#write_headers(headers) ⇒ Object
Write headers to the connection.
-
#write_interim_response(version, status, headers, reason = nil) ⇒ Object
Write an interim response to the connection.
-
#write_request(authority, method, target, version, headers) ⇒ Object
Write a request to the connection.
-
#write_response(version, status, headers, reason = nil) ⇒ Object
Write a response to the connection.
-
#write_tunnel_body(version, body = nil) ⇒ Object
Write a tunnel body to the connection.
-
#write_upgrade_body(protocol, body = nil) ⇒ Object
Write an upgrade body to the connection.
-
#write_upgrade_header(upgrade) ⇒ Object
Write the appropriate header for connection upgrade.
Constructor Details
#initialize(stream, persistent: true, state: :idle, maximum_line_length: DEFAULT_MAXIMUM_LINE_LENGTH) ⇒ Connection
Initialize the connection with the given stream.
65 66 67 68 69 70 71 72 73 74 |
# File 'lib/protocol/http1/connection.rb', line 65 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 |
Instance Attribute Details
#count ⇒ Object (readonly)
Returns the value of attribute count.
149 150 151 |
# File 'lib/protocol/http1/connection.rb', line 149 def count @count end |
#persistent ⇒ Object
This determines what connection headers are sent in the response and whether the connection can be reused after the response is sent. This setting is automatically managed according to the nature of the request and response. Changing to false is safe. Changing to true from outside this class should generally be avoided and, depending on the response semantics, may be reset to false anyway.
82 83 84 |
# File 'lib/protocol/http1/connection.rb', line 82 def persistent @persistent end |
#state ⇒ Object
The current state of the connection.
┌────────┐
│ │
┌───────────────────────►│ idle │
│ │ │
│ └───┬────┘
│ │
│ │ send request /
│ │ receive request
│ │
│ ▼
│ ┌────────┐
│ recv ES │ │ send ES
│ ┌────────────┤ open ├────────────┐
│ │ │ │ │
│ ▼ └───┬────┘ ▼
│ ┌──────────┐ │ ┌──────────┐
│ │ half │ │ │ half │
│ │ closed │ │ send R / │ closed │
│ │ (remote) │ │ recv R │ (local) │
│ └────┬─────┘ │ └─────┬────┘
│ │ │ │
│ │ send ES / │ recv ES / │
│ │ close ▼ close │
│ │ ┌────────┐ │
│ └───────────►│ │◄───────────┘
│ │ closed │
└────────────────────────┤ │
persistent └────────┘
ES: the body was fully received or sent (end of stream).R: the connection was closed unexpectedly (reset).
State transition methods use a trailing "!".
121 122 123 |
# File 'lib/protocol/http1/connection.rb', line 121 def state @state end |
#stream ⇒ Object (readonly)
The underlying IO stream.
77 78 79 |
# File 'lib/protocol/http1/connection.rb', line 77 def stream @stream end |
#the number of requests and responses processed by this connection.(numberofrequests) ⇒ Object (readonly)
149 |
# File 'lib/protocol/http1/connection.rb', line 149 attr :count |
Instance Method Details
#close(error = nil) ⇒ Object
Close the connection and underlying stream and transition to the closed state.
225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/protocol/http1/connection.rb', line 225 def close(error = nil) @persistent = false if stream = @stream @stream = nil stream.close end unless closed? @state = :closed self.closed(error) end end |
#close!(error = nil) ⇒ Object
Transition to the closed state.
If no error occurred, and the connection is persistent, this will immediately transition to the idle state.
741 742 743 744 745 746 747 748 749 750 |
# File 'lib/protocol/http1/connection.rb', line 741 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 |
#close_read ⇒ Object
Close the read end of the connection and transition to the half-closed remote state (or closed if already in the half-closed local state).
216 217 218 219 220 221 222 |
# File 'lib/protocol/http1/connection.rb', line 216 def close_read unless @state == :closed @persistent = false @stream&.close_read self.receive_end_stream! end end |
#closed(error = nil) ⇒ Object
The connection (stream) was closed. It may now be in the idle state.
Sub-classes may override this method to perform additional cleanup.
733 734 |
# File 'lib/protocol/http1/connection.rb', line 733 def closed(error = nil) end |
#closed? ⇒ Boolean
Returns whether the connection is in the closed state.
144 145 146 |
# File 'lib/protocol/http1/connection.rb', line 144 def closed? @state == :closed end |
#extract_content_length(headers) ⇒ Object
Extract the content length from the headers, if possible.
876 877 878 879 880 881 882 883 884 |
# File 'lib/protocol/http1/connection.rb', line 876 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 |
#half_closed_local? ⇒ Boolean
Returns whether the connection is in the half-closed local state.
134 135 136 |
# File 'lib/protocol/http1/connection.rb', line 134 def half_closed_local? @state == :half_closed_local end |
#half_closed_remote? ⇒ Boolean
Returns whether the connection is in the half-closed remote state.
139 140 141 |
# File 'lib/protocol/http1/connection.rb', line 139 def half_closed_remote? @state == :half_closed_remote end |
#hijack! ⇒ Object
Hijack the connection - that is, take over the underlying IO and close the connection.
201 202 203 204 205 206 207 208 209 210 211 212 213 |
# File 'lib/protocol/http1/connection.rb', line 201 def hijack! @persistent = false if stream = @stream @stream = nil stream.flush @state = :hijacked self.closed return stream end end |
#hijacked? ⇒ Boolean
Indicates whether the connection has been hijacked meaning its IO has been handed over and is not usable anymore.
194 195 196 |
# File 'lib/protocol/http1/connection.rb', line 194 def hijacked? @stream.nil? end |
#idle? ⇒ Boolean
Returns whether the connection is in the idle state.
124 125 126 |
# File 'lib/protocol/http1/connection.rb', line 124 def idle? @state == :idle end |
#interim_status?(status) ⇒ Boolean
Indicates whether the status code is an interim status code.
451 452 453 |
# File 'lib/protocol/http1/connection.rb', line 451 def interim_status?(status) status != 101 and status >= 100 and status < 200 end |
#open! ⇒ Object
Force a transition to the open state.
242 243 244 245 246 247 248 249 250 |
# File 'lib/protocol/http1/connection.rb', line 242 def open! unless @state == :idle raise ProtocolError, "Cannot open connection in state: #{@state}!" end @state = :open return self end |
#open? ⇒ Boolean
Returns whether the connection is in the open state.
129 130 131 |
# File 'lib/protocol/http1/connection.rb', line 129 def open? @state == :open end |
#persistent?(version, method, headers) ⇒ Boolean
Indicates whether the connection is persistent given the version, method, and headers.
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/protocol/http1/connection.rb', line 157 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 |
#read(length) ⇒ Object
Read some data from the connection.
348 349 350 |
# File 'lib/protocol/http1/connection.rb', line 348 def read(length) @stream.read(length) end |
#read_body(headers, remainder = false) ⇒ Object
Read the body of the message.
- The
transfer-encodingheader is used to determine if the body is chunked. - Otherwise, if the
content-lengthis present, the body is read until the content length is reached. - Otherwise, if
remainderis true, the body is read until the connection is closed.
974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 |
# File 'lib/protocol/http1/connection.rb', line 974 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 |
#read_chunked_body(headers) ⇒ Object
Read the body, assuming it is using the chunked transfer encoding.
810 811 812 |
# File 'lib/protocol/http1/connection.rb', line 810 def read_chunked_body(headers) Body::Chunked.new(self, headers) end |
#read_fixed_body(length) ⇒ Object
Read the body, assuming it has a fixed length.
818 819 820 |
# File 'lib/protocol/http1/connection.rb', line 818 def read_fixed_body(length) Body::Fixed.new(self, length) end |
#read_head_body(length) ⇒ Object
Read the body, assuming that we are not receiving any actual data, but just the length.
834 835 836 837 838 839 |
# File 'lib/protocol/http1/connection.rb', line 834 def read_head_body(length) # We are not receiving any body: self.receive_end_stream! Protocol::HTTP::Body::Head.new(length) end |
#read_headers ⇒ Object
Read headers from the connection until an empty line is encountered.
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 |
# File 'lib/protocol/http1/connection.rb', line 497 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 |
#read_line ⇒ Object
Read a line from the connection.
380 381 382 |
# File 'lib/protocol/http1/connection.rb', line 380 def read_line read_line? or raise EOFError end |
#read_line? ⇒ Boolean
Read a line from the connection.
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 |
# File 'lib/protocol/http1/connection.rb', line 357 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 |
#read_remainder_body ⇒ Object
Read the body, assuming that we read until the connection is closed.
825 826 827 828 |
# File 'lib/protocol/http1/connection.rb', line 825 def read_remainder_body @persistent = false Body::Remainder.new(self) end |
#read_request ⇒ Object
Read a request from the connection, including the request line and request headers, and prepares to read the request body.
Transitions to the open state.
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 |
# File 'lib/protocol/http1/connection.rb', line 406 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 |
#read_request_body(method, headers) ⇒ Object
Read the body of the request.
- The
CONNECTmethod is used to establish a tunnel, so the body is read until the connection is closed. - The
UPGRADEmethod is used to upgrade the connection to a different protocol (typically WebSockets), so the body is read until the connection is closed. - Otherwise, the body is read according to #read_body.
945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 |
# File 'lib/protocol/http1/connection.rb', line 945 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 |
#read_request_line ⇒ Object
Read a request line from the connection.
387 388 389 390 391 392 393 394 395 396 397 |
# File 'lib/protocol/http1/connection.rb', line 387 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 |
#read_response(method) ⇒ Object
Read a response from the connection.
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 |
# File 'lib/protocol/http1/connection.rb', line 462 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 |
#read_response_body(method, status, headers) ⇒ Object
Read the body of the response.
- The
HEADmethod 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. - A 101 status code indicates that the connection will be upgraded, so #read_upgrade_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.
- The
CONNECTmethod is used to establish a tunnel, so #read_tunnel_body is invoked. - Otherwise, the body is read according to #read_body.
897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 |
# File 'lib/protocol/http1/connection.rb', line 897 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 |
#read_response_line ⇒ Object
Read a response line from the connection.
439 440 441 442 443 444 445 |
# File 'lib/protocol/http1/connection.rb', line 439 def read_response_line version, status, reason = read_line.split(/\s+/, 3) status = Integer(status) return version, status, reason end |
#read_tunnel_body ⇒ Object
Read the body, assuming it is a tunnel.
Invokes #read_remainder_body.
846 847 848 |
# File 'lib/protocol/http1/connection.rb', line 846 def read_tunnel_body read_remainder_body end |
#read_upgrade_body ⇒ Object
Read the body, assuming it is an upgrade.
Invokes #read_remainder_body.
855 856 857 858 859 |
# File 'lib/protocol/http1/connection.rb', line 855 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 |
#readpartial(length) ⇒ Object
Read some data from the connection.
341 342 343 |
# File 'lib/protocol/http1/connection.rb', line 341 def readpartial(length) @stream.readpartial(length) end |
#receive_end_stream! ⇒ Object
Indicate that the end of the stream (body) has been received.
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.
796 797 798 799 800 801 802 803 804 |
# File 'lib/protocol/http1/connection.rb', line 796 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 |
#send_end_stream! ⇒ Object
Transition to the half-closed local state, in other words, the connection is closed for writing.
If the connection is already in the half-closed remote state, it will transition to the closed state.
519 520 521 522 523 524 525 526 527 |
# File 'lib/protocol/http1/connection.rb', line 519 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 |
#true if the connection is persistent.= ⇒ Object
This determines what connection headers are sent in the response and whether the connection can be reused after the response is sent. This setting is automatically managed according to the nature of the request and response. Changing to false is safe. Changing to true from outside this class should generally be avoided and, depending on the response semantics, may be reset to false anyway.
82 |
# File 'lib/protocol/http1/connection.rb', line 82 attr_accessor :persistent |
#write_body(version, body, head = false, trailer = nil) ⇒ Object
Write a body to the connection.
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.
760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 |
# File 'lib/protocol/http1/connection.rb', line 760 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 |
#write_body_and_close(body, head) ⇒ Object
Write the body to the connection and close the connection.
701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 |
# File 'lib/protocol/http1/connection.rb', line 701 def write_body_and_close(body, head) # We can't be persistent because we don't know the data length: @persistent = false @stream.write("\r\n") unless head @stream.flush unless body.ready? while chunk = body.read @stream.write(chunk) @stream.flush unless body.ready? end end @stream.flush @stream.close_write 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 |
#write_chunked_body(body, head, trailer = nil) ⇒ Object
Write a chunked body to the connection.
If the request was a HEAD request, the body will be closed, and no data will be written.
If trailers are given, they will be written after the body.
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 |
# File 'lib/protocol/http1/connection.rb', line 659 def write_chunked_body(body, head, trailer = nil) @stream.write("transfer-encoding: chunked\r\n\r\n") if head @stream.flush else @stream.flush 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 @stream.write("#{chunk.bytesize.to_s(16).upcase}\r\n") @stream.write(chunk) @stream.write(CRLF) @stream.flush unless body.ready? end if trailer&.any? @stream.write("0\r\n") write_headers(trailer) @stream.write("\r\n") else @stream.write("0\r\n\r\n") end @stream.flush 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 |
#write_connection_header(version) ⇒ Object
Write the appropriate header for connection persistence.
178 179 180 181 182 183 184 |
# File 'lib/protocol/http1/connection.rb', line 178 def write_connection_header(version) if version == HTTP10 @stream.write("connection: keep-alive\r\n") if @persistent else @stream.write("connection: close\r\n") unless @persistent end end |
#write_empty_body(body = nil) ⇒ Object
Write an empty body to the connection.
If given, the body will be closed.
597 598 599 600 601 602 603 604 |
# File 'lib/protocol/http1/connection.rb', line 597 def write_empty_body(body = nil) @stream.write("content-length: 0\r\n\r\n") @stream.flush body&.close ensure self.send_end_stream! end |
#write_fixed_length_body(body, length, head) ⇒ Object
Write a fixed length body to the connection.
If the request was a HEAD request, the body will be closed, and no data will be written.
614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 |
# File 'lib/protocol/http1/connection.rb', line 614 def write_fixed_length_body(body, length, head) @stream.write("content-length: #{length}\r\n\r\n") if head @stream.flush else @stream.flush 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 @stream.write(chunk) @stream.flush unless body.ready? end @stream.flush 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 |
#write_headers(headers) ⇒ Object
Write headers to the connection.
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/protocol/http1/connection.rb', line 318 def write_headers(headers) headers.each do |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: @stream.write("#{name}: #{value}\r\n") end end |
#write_interim_response(version, status, headers, reason = nil) ⇒ Object
Write an interim response to the connection. It is expected you will eventually write the final response after this method.
299 300 301 302 303 304 305 306 307 308 309 310 311 312 |
# File 'lib/protocol/http1/connection.rb', line 299 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 @stream.write("#{version} #{status} #{reason}\r\n") write_headers(headers) @stream.write("\r\n") @stream.flush end |
#write_request(authority, method, target, version, headers) ⇒ Object
Write a request to the connection. It is expected you will write the body after this method.
Transitions to the open state.
262 263 264 265 266 267 268 269 270 271 |
# File 'lib/protocol/http1/connection.rb', line 262 def write_request(, method, target, version, headers) open! @stream.write("#{method} #{target} #{version}\r\n") @stream.write("host: #{}\r\n") if write_headers(headers) rescue raise ::Protocol::HTTP::RefusedError end |
#write_response(version, status, headers, reason = nil) ⇒ Object
Write a response to the connection. It is expected you will write the body after this method.
279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/protocol/http1/connection.rb', line 279 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: @stream.write("#{version} #{status} #{reason}\r\n") write_headers(headers) end |
#write_tunnel_body(version, body = nil) ⇒ Object
Write a tunnel body to the connection.
This writes the connection header and the body to the connection. If the body is nil, you should coordinate writing to the stream.
The connection will not be persistent after this method is called.
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 |
# File 'lib/protocol/http1/connection.rb', line 570 def write_tunnel_body(version, body = nil) @persistent = false write_connection_header(version) @stream.write("\r\n") @stream.flush # Don't remove me! if body body.each do |chunk| @stream.write(chunk) @stream.flush end @stream.close_write end return @stream ensure self.send_end_stream! end |
#write_upgrade_body(protocol, body = nil) ⇒ Object
Write an upgrade body to the connection.
This writes the upgrade header and the body to the connection. If the body is nil, you should coordinate writing to the stream.
The connection will not be persistent after this method is called.
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 |
# File 'lib/protocol/http1/connection.rb', line 538 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) @stream.write("\r\n") @stream.flush # Don't remove me! if body body.each do |chunk| @stream.write(chunk) @stream.flush end @stream.close_write end return @stream ensure self.send_end_stream! end |
#write_upgrade_header(upgrade) ⇒ Object
Write the appropriate header for connection upgrade.
187 188 189 |
# File 'lib/protocol/http1/connection.rb', line 187 def write_upgrade_header(upgrade) @stream.write("connection: upgrade\r\nupgrade: #{upgrade}\r\n") end |