require 'thin_parser'
require 'tempfile'
module Thin
# Raised when an incoming request is not valid
# and the server can not process it.
class InvalidRequest < IOError; end
# A request sent by the client to the server.
class Request
# Maximum request body size before it is moved out of memory
# and into a tempfile for reading.
MAX_BODY = 1024 * (80 + 32)
BODY_TMPFILE = 'thin-body'.freeze
# Freeze some HTTP header names
SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
REMOTE_ADDR = 'REMOTE_ADDR'.freeze
FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR'.freeze
CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze
# Freeze some Rack header names
RACK_INPUT = 'rack.input'.freeze
RACK_VERSION = 'rack.version'.freeze
RACK_ERRORS = 'rack.errors'.freeze
RACK_MULTITHREAD = 'rack.multithread'.freeze
RACK_MULTIPROCESS = 'rack.multiprocess'.freeze
RACK_RUN_ONCE = 'rack.run_once'.freeze
# CGI-like request environment variables
attr_reader :env
# Unparsed data of the request
attr_reader :data
# Request body
attr_reader :body
def initialize
@parser = HttpParser.new
@data = ''
@nparsed = 0
@body = StringIO.new
@env = {
SERVER_SOFTWARE => SERVER,
# Rack stuff
RACK_INPUT => @body,
RACK_VERSION => [0, 2],
RACK_ERRORS => STDERR,
RACK_MULTITHREAD => false,
RACK_MULTIPROCESS => false,
RACK_RUN_ONCE => false
}
end
# Parse a chunk of data into the request environment
# Raises a +InvalidRequest+ if invalid.
# Returns +true+ if the parsing is complete.
def parse(data)
@data << data
if @parser.finished? # Header finished, can only be some more body
body << data
else # Parse more header using the super parser
@nparsed = @parser.execute(@env, @data, @nparsed)
# Transfert to a tempfile if body is very big
move_body_to_tempfile if @parser.finished? && content_length > MAX_BODY
end
if finished? # Check if header and body are complete
@body.rewind
true # Request is fully parsed
else
false # Not finished, need more data
end
end
# +true+ if headers and body are finished parsing
def finished?
@parser.finished? && @body.size >= content_length
end
# Expected size of the body
def content_length
@env[CONTENT_LENGTH].to_i
end
# Close any resource used by the response
def close
@body.delete if @body.class == Tempfile
end
private
def move_body_to_tempfile
current_body = @body
current_body.rewind
@body = Tempfile.new(BODY_TMPFILE)
@body.binmode
@body << current_body.read
@env[RACK_INPUT] = @body
end
end
end