lib/reel/connection.rb in reel-0.4.0.pre vs lib/reel/connection.rb in reel-0.4.0.pre2

- old
+ new

@@ -8,22 +8,24 @@ CONNECTION = 'Connection'.freeze TRANSFER_ENCODING = 'Transfer-Encoding'.freeze KEEP_ALIVE = 'Keep-Alive'.freeze CLOSE = 'close'.freeze - CHUNKED = 'chunked'.freeze attr_reader :socket, :parser # Attempt to read this much data BUFFER_SIZE = 16384 + attr_reader :buffer_size - def initialize(socket) + def initialize(socket, buffer_size = nil) @attached = true @socket = socket @keepalive = true - @parser = Request::Parser.new + @parser = Request::Parser.new(socket, self) + @writer = Response::Writer.new(socket, self) + @buffer_size = buffer_size.nil? ? BUFFER_SIZE : buffer_size reset_request @response_state = :header end @@ -38,78 +40,59 @@ @attached = false self end # Reset the current request state - def reset_request(state = :header) + def reset_request(state = :ready) @request_state = state - @header_buffer = "" # Buffer headers in case of an upgrade request + @current_request = nil @parser.reset end + def readpartial(size = @buffer_size) + raise StateError, "can't read in the '#{@request_state}' request state" unless @request_state == :ready + @parser.readpartial(size) + end + + def current_request + @current_request + end + # Read a request object from the connection def request - return if @request_state == :websocket - req = Request.read(self) + raise StateError, "already processing a request" if current_request - case req - when Request - @request_state = :body - @keepalive = false if req[CONNECTION] == CLOSE || req.version == HTTP_VERSION_1_0 - when WebSocket - @request_state = @response_state = :websocket - @socket = nil - else raise "unexpected request type: #{req.class}" - end + req = @parser.current_request + @request_state = :ready + @keepalive = false if req[CONNECTION] == CLOSE || req.version == HTTP_VERSION_1_0 + @current_request = req req rescue IOError, Errno::ECONNRESET, Errno::EPIPE # The client is disconnected @request_state = :closed @keepalive = false nil end - # Read a chunk from the request - def readpartial(size = BUFFER_SIZE) - raise StateError, "can't read in the `#{@request_state}' state" unless @request_state == :body + # Enumerate the requests from this connection, since we might receive + # many if the client is using keep-alive + def each_request + while req = request + yield req - chunk = @parser.chunk - unless chunk || @parser.finished? - @parser << @socket.readpartial(size) - chunk = @parser.chunk + # Websockets upgrade the connection to the Websocket protocol + # Once we have finished processing a Websocket, we can't handle + # additional requests + break if req.websocket? end - - chunk end - # read length bytes from request body - 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? ? 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 - # Send a response back to the client # Response can be a symbol indicating the status code or a Reel::Response def respond(response, headers_or_body = {}, body = nil) - raise StateError "not in header state" if @response_state != :header + raise StateError, "not in header state" if @response_state != :header if headers_or_body.is_a? Hash headers = headers_or_body else headers = {} @@ -127,46 +110,66 @@ response = Response.new(response, headers, body) when Response else raise TypeError, "invalid response: #{response.inspect}" end - response.render(@socket) + @writer.handle_response(response) # Enable streaming mode - if response.headers[TRANSFER_ENCODING] == CHUNKED and response.body.nil? + if response.chunked? and response.body.nil? @response_state = :chunked_body end rescue IOError, Errno::ECONNRESET, Errno::EPIPE # The client disconnected early @keepalive = false ensure if @keepalive - reset_request(:header) + reset_request(:ready) else @socket.close unless @socket.closed? reset_request(:closed) end end # Write body chunks directly to the connection def write(chunk) raise StateError, "not in chunked body mode" unless @response_state == :chunked_body - chunk_header = chunk.bytesize.to_s(16) - @socket << chunk_header + Response::CRLF - @socket << chunk + Response::CRLF + @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 @response_state != :chunked_body - @socket << "0#{Response::CRLF * 2}" + @writer.finish_response @response_state = :header end # Close the connection def close + raise StateError, "socket has been hijacked from this connection" unless @socket + @keepalive = false @socket.close unless @socket.closed? + end + + # Hijack the socket from the connection + def hijack_socket + # FIXME: this doesn't do a great job of ensuring we can hijack the socket + # in its current state. Improve the state detection. + if @request_state != :ready && @response_state != :header + raise StateError, "connection is not in a hijackable state" + end + + @request_state = @response_state = :hijacked + socket = @socket + @socket = nil + socket + end + + # Raw access to the underlying socket + def socket + raise StateError, "socket has already been hijacked" unless @socket + @socket end end end