1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
|
# frozen_string_literal: true
require "http/headers"
module HTTP
class Request
class Writer
# CRLF is the universal HTTP delimiter
CRLF = "\r\n"
# Chunked data termintaor.
ZERO = "0"
# Chunked transfer encoding
CHUNKED = "chunked"
# End of a chunked transfer
CHUNKED_END = "#{ZERO}#{CRLF}#{CRLF}"
def initialize(socket, body, headers, headline)
@body = body
@socket = socket
@headers = headers
@request_header = [headline]
end
# Adds headers to the request header from the headers array
def add_headers
@headers.each do |field, value|
@request_header << "#{field}: #{value}"
end
end
# Stream the request to a socket
def stream
add_headers
add_body_type_headers
send_request
end
# Send headers needed to connect through proxy
def connect_through_proxy
add_headers
write(join_headers)
end
# Adds the headers to the header array for the given request body we are working
# with
def add_body_type_headers
return if @headers[Headers::CONTENT_LENGTH] || chunked?
@request_header << "#{Headers::CONTENT_LENGTH}: #{@body.size}"
end
# Joins the headers specified in the request into a correctly formatted
# http request header string
def join_headers
# join the headers array with crlfs, stick two on the end because
# that ends the request header
@request_header.join(CRLF) + CRLF * 2
end
# Writes HTTP request data into the socket.
def send_request
each_chunk { |chunk| write chunk }
rescue Errno::EPIPE
# server doesn't need any more data
nil
end
# Yields chunks of request data that should be sent to the socket.
#
# It's important to send the request in a single write call when possible
# in order to play nicely with Nagle's algorithm. Making two writes in a
# row triggers a pathological case where Nagle is expecting a third write
# that never happens.
def each_chunk
data = join_headers
@body.each do |chunk|
data << encode_chunk(chunk)
yield data
data.clear
end
yield data unless data.empty?
yield CHUNKED_END if chunked?
end
# Returns the chunk encoded for to the specified "Transfer-Encoding" header.
def encode_chunk(chunk)
if chunked?
chunk.bytesize.to_s(16) << CRLF << chunk << CRLF
else
chunk
end
end
# Returns true if the request should be sent in chunked encoding.
def chunked?
@headers[Headers::TRANSFER_ENCODING] == CHUNKED
end
private
def write(data)
until data.empty?
length = @socket.write(data)
break unless data.bytesize > length
data = data.byteslice(length..-1)
end
rescue Errno::EPIPE
raise
rescue IOError, SocketError, SystemCallError => ex
raise ConnectionError, "error writing to socket: #{ex}", ex.backtrace
end
end
end
end
|