require 'forwardable' require 'reel/request/body' require 'reel/request/info' require 'reel/request/parser' require 'reel/request/state_machine' require 'reel/response/writer' module Reel class Request extend Forwardable include RequestMixin def_delegators :@connection, :remote_addr, :respond def_delegator :@response_writer, :handle_response attr_reader :body # request_info is a RequestInfo object including the headers and # the url, method and http version. # # Access it through the RequestMixin methods. def initialize(request_info, connection = nil) @request_info = request_info @connection = connection @finished = false @buffer = "" @finished_read = false @websocket = nil @body = Request::Body.new(self) @response_writer = Response::Writer.new(connection.socket) end # Returns true if request fully finished reading def finished_reading?; @finished_read; end # When HTTP Parser marks the message parsing as complete, this will be set. def finish_reading! raise StateError, "already finished" if @finished_read @finished_read = true end # Fill the request buffer with data as it becomes available def fill_buffer(chunk) @buffer << chunk end # Read a number of bytes, looping until they are available or until # readpartial returns nil, indicating there are no more bytes to read def read(length = nil, buffer = nil) raise ArgumentError, "negative length #{length} given" if length && length < 0 return '' if length == 0 res = buffer.nil? ? '' : buffer.clear chunk_size = length.nil? ? @connection.buffer_size : length begin while chunk_size > 0 chunk = readpartial(chunk_size) break unless chunk res << chunk chunk_size = length - res.length unless length.nil? end rescue EOFError end return length && res.length == 0 ? nil : res end # Read a string up to the given number of bytes, blocking until some # data is available but returning immediately if some data is available def readpartial(length = nil) if length.nil? && @buffer.length > 0 slice = @buffer @buffer = "" else unless finished_reading? || (length && length <= @buffer.length) @connection.readpartial(length ? length - @buffer.length : @connection.buffer_size) end if length slice = @buffer.slice!(0, length) else slice = @buffer @buffer = "" end end slice && slice.length == 0 ? nil : slice end # Write body chunks directly to the connection def write(chunk) unless @connection.response_state == :chunked_body raise StateError, "not in chunked body mode" end @response_writer.write(chunk) end alias_method :<<, :write # Finish the response and reset the response state to header def finish_response raise StateError, "not in body state" if @connection.response_state != :chunked_body @response_writer.finish_response @connection.response_state = :headers end # Can the current request be upgraded to a WebSocket? def websocket?; @request_info.websocket_request?; end # Return a Reel::WebSocket for this request, hijacking the socket from # the underlying connection def websocket @websocket ||= begin raise StateError, "can't upgrade this request to a websocket" unless websocket? WebSocket.new(self, @connection) end end # Friendlier inspect def inspect "#<#{self.class} #{method} #{url} HTTP/#{version} @headers=#{headers.inspect}>" end end end