From e26b858d969d073ecc36799182e2cc8d08c4bbe7 Mon Sep 17 00:00:00 2001 From: macournoyer Date: Mon, 6 Aug 2012 22:25:58 -0400 Subject: [PATCH] Remove :protocol option. --- examples/thin.conf.rb | 9 - lib/thin/chunked_body.rb | 28 ++ lib/thin/connection.rb | 289 +++++++++++++++++ lib/thin/listener.rb | 27 +- lib/thin/protocols/http.rb | 292 ------------------ lib/thin/protocols/http/chunked_body.rb | 32 -- lib/thin/protocols/http/request.rb | 186 ----------- lib/thin/protocols/http/response.rb | 186 ----------- lib/thin/request.rb | 182 +++++++++++ lib/thin/response.rb | 184 +++++++++++ lib/thin/server.rb | 21 +- man/thin-conf.5.ronn | 3 - man/thin.1.ronn | 2 - test/integration/big_request_test.rb | 6 +- test/integration/custom_protocol_test.rb | 15 - .../unit/{http_test.rb => connection_test.rb} | 8 +- test/unit/{http => }/request_test.rb | 6 +- test/unit/{http => }/response_test.rb | 10 +- v2.todo | 2 +- 19 files changed, 702 insertions(+), 786 deletions(-) create mode 100644 lib/thin/chunked_body.rb create mode 100644 lib/thin/connection.rb delete mode 100644 lib/thin/protocols/http.rb delete mode 100644 lib/thin/protocols/http/chunked_body.rb delete mode 100644 lib/thin/protocols/http/request.rb delete mode 100644 lib/thin/protocols/http/response.rb create mode 100644 lib/thin/request.rb create mode 100644 lib/thin/response.rb delete mode 100644 test/integration/custom_protocol_test.rb rename test/unit/{http_test.rb => connection_test.rb} (91%) rename test/unit/{http => }/request_test.rb (94%) rename test/unit/{http => }/response_test.rb (86%) diff --git a/examples/thin.conf.rb b/examples/thin.conf.rb index 923a4488..80260534 100644 --- a/examples/thin.conf.rb +++ b/examples/thin.conf.rb @@ -29,15 +29,6 @@ listen "[::]:8081" # IPv6 listen "/tmp/thin.sock" # UNIX domain socket -# Custom protocol -class Echo < EventMachine::Connection - def receive_data(data) - send_data data - close_connection_after_writing - end -end -listen 3001, :protocol => Echo - # Callbacks before_fork do |server| puts "Preparing to fork a new worker ..." diff --git a/lib/thin/chunked_body.rb b/lib/thin/chunked_body.rb new file mode 100644 index 00000000..f0817615 --- /dev/null +++ b/lib/thin/chunked_body.rb @@ -0,0 +1,28 @@ +module Thin + # Same as Rack::Chunked::Body, but doesn't send the tail automaticaly. + class ChunkedBody + TERM = "\r\n" + TAIL = "0#{TERM}#{TERM}" + + include Rack::Utils + + def initialize(body) + @body = body + end + + def each + term = TERM + @body.each do |chunk| + size = bytesize(chunk) + next if size == 0 + + chunk = chunk.dup.force_encoding(Encoding::BINARY) if chunk.respond_to?(:force_encoding) + yield [size.to_s(16), term, chunk, term].join + end + end + + def close + @body.close if @body.respond_to?(:close) + end + end +end \ No newline at end of file diff --git a/lib/thin/connection.rb b/lib/thin/connection.rb new file mode 100644 index 00000000..fa92547f --- /dev/null +++ b/lib/thin/connection.rb @@ -0,0 +1,289 @@ +require "rack" +require "http/parser" + +require "thin/request" +require "thin/response" +require "thin/chunked_body" + +module Thin + # EventMachine connection. + # Supports: + # * Rack specifications v1.1: http://rack.rubyforge.org/doc/SPEC.html + # * Asynchronous responses with chunked encoding, via the env['async.callback'] or throw :async. + # * Keep-alive. + # * File streaming. + # * Calling the Rack app from pooled threads. + class Connection < EM::Connection + attr_accessor :server + attr_accessor :listener + attr_accessor :can_keep_alive + + # For tests + attr_reader :request, :response + + + def on_close(&block) + @on_close = block + end + + + # == EM callback methods + + # Get the connection ready to process a request. + def post_init + @parser = HTTP::Parser.new(self) + end + + # Called when data is received from the client. + def receive_data(data) + puts data if $DEBUG + @parser << data + rescue HTTP::Parser::Error => e + $stderr.puts "Parse error: #{e}" + send_response_and_reset Response.error(400) # Bad Request + end + + # Called when the connection is unbinded from the socket + # and can no longer be used to process requests. + def unbind + close_request_and_response + @on_close.call if @on_close + end + + + # == Parser callback methods + + def on_message_begin + @request = Request.new + end + + def on_headers_complete(headers) + @request.multithread = server.threaded? + @request.multiprocess = server.prefork? + @request.remote_address = socket_address + @request.http_version = "HTTP/%d.%d" % @parser.http_version + @request.method = @parser.http_method + @request.path = @parser.request_path + @request.fragment = @parser.fragment + @request.query_string = @parser.query_string + @request.keep_alive = @parser.keep_alive? + @request.headers = headers + end + + def on_body(chunk) + @request << chunk + end + + def on_message_complete + @request.finish + process + end + + + # == Request processing methods + + # Starts the processing of the current request in @request. + def process + if server.threaded? + EM.defer(method(:call_app), method(:process_response)) + else + if response = call_app + process_response(response) + end + end + end + + # Calls the Rack app in server.app. + # Returns a Rack response: [status, {headers}, [body]] + # or +nil+ if there was an error. + # The app can return [-1, ...] or throw :async to short-circuit request processing. + def call_app + # Connection may be closed unless the App#call response was a [-1, ...] + # It should be noted that connection objects will linger until this + # callback is no longer referenced, so be tidy! + @request.async_callback = method(:process_async_response) + + # Call the Rack application + response = Response::ASYNC # `throw :async` will result in this response + catch(:async) do + response = @server.app.call(@request.env) + end + + response + + rescue Exception + handle_error + nil # Signals that the request could not be processed + end + + def prepare_response(response) + return unless response + + Response.new(*response) + end + + # Process the response returns by +call_app+. + def process_response(response) + @response = prepare_response(response) + + # We're going to respond later (async). + return if @response.async? + + # Close the resources used by the request as soon as possible. + @request.close + + # Send the response. + send_response_and_reset + + rescue Exception + handle_error + end + + # Process the response sent asynchronously via body.call. + # The response will automatically be send using chunked encoding under + # HTTP 1.1 protocol. + def process_async_response(response) + @response = prepare_response(response) + + # Terminate the connection on callback from the response's body. + @response.body_callback = method(:terminate_async_response) + + # Use chunked encoding if available. + if @request.support_encoding_chunked? + @response.chunked_encoding! + @response.body = ChunkedBody.new(@response.body) + end + + # Send the response. + send_response + + rescue Exception + handle_error + end + + # Called after an asynchronous response is done sending the body. + def terminate_async_response + if @request.support_encoding_chunked? + # Send tail chunk. 0 length signals we're done w/ HTTP chunked encoding. + send_chunk ChunkedBody::TAIL + end + + reset + + rescue Exception + handle_error + end + + # Reset the connection and prepare for another request if keep-alive is + # requested. + # Else, closes the connection. + def reset + if @response && @response.keep_alive? + # Prepare the connection for another request if the client + # requested a persistent connection (keep-alive). + post_init + else + close_connection_after_writing + end + + close_request_and_response + end + + + # == Response sending methods + + # Send the HTTP response back to the client. + def send_response(response=@response) + @response = response + + if @request + # Keep connection alive if requested by the client. + @response.keep_alive! if @can_keep_alive && @request.keep_alive? + @response.http_version = @request.http_version + end + + # Prepare the response for sending. + @response.finish + + if @response.file? + send_file + return + end + + @response.each(&method(:send_chunk)) + puts if $DEBUG + + rescue Exception => e + # In case there's an error sending the response, we give up and just + # close the connection to prevent recursion and consuming too much + # resources. + $stderr.puts "Error sending response: #{e}" + close_connection + end + + def send_response_and_reset(response=@response) + send_response(response) + reset + end + + # Sending a file using EM streaming and HTTP 1.1 style chunked-encoding if + # supported by the client. + def send_file + # Use HTTP 1.1 style chunked-encoding to send the file if supported + if @request.support_encoding_chunked? + @response.chunked_encoding! + send_chunk @response.head + deferrable = stream_file_data @response.filename, :http_chunks => true + else + send_chunk @response.head + deferrable = stream_file_data @response.filename + end + + deferrable.callback(&method(:reset)) + deferrable.errback(&method(:reset)) + + if $DEBUG + puts "" + puts + end + end + + def send_chunk(data) + print data if $DEBUG + send_data data + end + + private + # == Support methods + + def close_request_and_response + if @request + @request.close + @request = nil + end + if @response + @response.close + @response = nil + end + end + + # Returns IP address of peer as a string. + def socket_address + if listener.unix? + "" + else + Socket.unpack_sockaddr_in(get_peername)[1] + end + rescue Exception => e + $stderr.puts "Can't get socket address: #{e}" + "" + end + + # Output the error to stderr and sends back a 500 error. + def handle_error(e=$!) + $stderr.puts "[ERROR] #{e}" + $stderr.puts "\t" + e.backtrace.join("\n\t") if $DEBUG + send_response_and_reset Response.error(500) # Internal Server Error + end + end +end diff --git a/lib/thin/listener.rb b/lib/thin/listener.rb index d6258620..a1472130 100644 --- a/lib/thin/listener.rb +++ b/lib/thin/listener.rb @@ -12,9 +12,6 @@ class Listener # UNIX domain socket the socket will bind to. attr_reader :socket_file - # Ruby class of the EventMachine::Connection class used to process connections. - attr_accessor :protocol_class - def initialize(address, options={}) case address when Integer @@ -38,8 +35,6 @@ def initialize(address, options={}) # Default values options = { - :protocol => :http, - # Same defaults as Unicorn :tcp_no_delay => true, :tcp_no_push => false, @@ -48,7 +43,6 @@ def initialize(address, options={}) }.merge(options) @backlog = options[:backlog] - self.protocol = options[:protocol] self.tcp_no_delay = options[:tcp_nodelay] || options[:tcp_no_delay] self.tcp_no_push = options[:tcp_nopush] || options[:tcp_no_push] self.ipv6_only = options[:ipv6_only] @@ -93,24 +87,6 @@ def tcp_no_push=(value) end end - def protocol=(name_or_class) - case name_or_class - when Class - @protocol_class = name_or_class - when String - @protocol_class = Object.const_get(name_or_class) - when Symbol - require "thin/protocols/#{name_or_class}" - @protocol_class = Thin::Protocols.const_get(name_or_class.to_s.capitalize) - else - raise ArgumentError, "invalid protocol, use a Class, String or Symbol." - end - end - - def protocol - @protocol_class.name.split(":").last - end - def listen delete_socket_file! @@ -126,8 +102,7 @@ def close end def to_s - protocol + " on " + (unix? ? @socket_file : - "#{@host}:#{@port}") + unix? ? @socket_file : "#{@host}:#{@port}" end private diff --git a/lib/thin/protocols/http.rb b/lib/thin/protocols/http.rb deleted file mode 100644 index 78590204..00000000 --- a/lib/thin/protocols/http.rb +++ /dev/null @@ -1,292 +0,0 @@ -require "rack" -require "http/parser" - -module Thin - module Protocols - # EventMachine HTTP protocol. - # Supports: - # * Rack specifications v1.1: http://rack.rubyforge.org/doc/SPEC.html - # * Asynchronous responses with chunked encoding, via the env['async.callback'] or throw :async. - # * Keep-alive. - # * File streaming. - # * Calling the Rack app from pooled threads. - class Http < EM::Connection - # Http class has to be defined before requiring those. - require "thin/protocols/http/request" - require "thin/protocols/http/response" - require "thin/protocols/http/chunked_body" - - attr_accessor :server - attr_accessor :listener - attr_accessor :can_keep_alive - - # For tests - attr_reader :request, :response - - - def on_close(&block) - @on_close = block - end - - - # == EM callback methods - - # Get the connection ready to process a request. - def post_init - @parser = HTTP::Parser.new(self) - end - - # Called when data is received from the client. - def receive_data(data) - puts data if $DEBUG - @parser << data - rescue HTTP::Parser::Error => e - $stderr.puts "Parse error: #{e}" - send_response_and_reset Response.error(400) # Bad Request - end - - # Called when the connection is unbinded from the socket - # and can no longer be used to process requests. - def unbind - close_request_and_response - @on_close.call if @on_close - end - - - # == Parser callback methods - - def on_message_begin - @request = Request.new - end - - def on_headers_complete(headers) - @request.multithread = server.threaded? - @request.multiprocess = server.prefork? - @request.remote_address = socket_address - @request.http_version = "HTTP/%d.%d" % @parser.http_version - @request.method = @parser.http_method - @request.path = @parser.request_path - @request.fragment = @parser.fragment - @request.query_string = @parser.query_string - @request.keep_alive = @parser.keep_alive? - @request.headers = headers - end - - def on_body(chunk) - @request << chunk - end - - def on_message_complete - @request.finish - process - end - - - # == Request processing methods - - # Starts the processing of the current request in @request. - def process - if server.threaded? - EM.defer(method(:call_app), method(:process_response)) - else - if response = call_app - process_response(response) - end - end - end - - # Calls the Rack app in server.app. - # Returns a Rack response: [status, {headers}, [body]] - # or +nil+ if there was an error. - # The app can return [-1, ...] or throw :async to short-circuit request processing. - def call_app - # Connection may be closed unless the App#call response was a [-1, ...] - # It should be noted that connection objects will linger until this - # callback is no longer referenced, so be tidy! - @request.async_callback = method(:process_async_response) - - # Call the Rack application - response = Response::ASYNC # `throw :async` will result in this response - catch(:async) do - response = @server.app.call(@request.env) - end - - response - - rescue Exception - handle_error - nil # Signals that the request could not be processed - end - - def prepare_response(response) - return unless response - - Response.new(*response) - end - - # Process the response returns by +call_app+. - def process_response(response) - @response = prepare_response(response) - - # We're going to respond later (async). - return if @response.async? - - # Close the resources used by the request as soon as possible. - @request.close - - # Send the response. - send_response_and_reset - - rescue Exception - handle_error - end - - # Process the response sent asynchronously via body.call. - # The response will automatically be send using chunked encoding under - # HTTP 1.1 protocol. - def process_async_response(response) - @response = prepare_response(response) - - # Terminate the connection on callback from the response's body. - @response.body_callback = method(:terminate_async_response) - - # Use chunked encoding if available. - if @request.support_encoding_chunked? - @response.chunked_encoding! - @response.body = ChunkedBody.new(@response.body) - end - - # Send the response. - send_response - - rescue Exception - handle_error - end - - # Called after an asynchronous response is done sending the body. - def terminate_async_response - if @request.support_encoding_chunked? - # Send tail chunk. 0 length signals we're done w/ HTTP chunked encoding. - send_chunk ChunkedBody::TAIL - end - - reset - - rescue Exception - handle_error - end - - # Reset the connection and prepare for another request if keep-alive is - # requested. - # Else, closes the connection. - def reset - if @response && @response.keep_alive? - # Prepare the connection for another request if the client - # requested a persistent connection (keep-alive). - post_init - else - close_connection_after_writing - end - - close_request_and_response - end - - - # == Response sending methods - - # Send the HTTP response back to the client. - def send_response(response=@response) - @response = response - - if @request - # Keep connection alive if requested by the client. - @response.keep_alive! if @can_keep_alive && @request.keep_alive? - @response.http_version = @request.http_version - end - - # Prepare the response for sending. - @response.finish - - if @response.file? - send_file - return - end - - @response.each(&method(:send_chunk)) - puts if $DEBUG - - rescue Exception => e - # In case there's an error sending the response, we give up and just - # close the connection to prevent recursion and consuming too much - # resources. - $stderr.puts "Error sending response: #{e}" - close_connection - end - - def send_response_and_reset(response=@response) - send_response(response) - reset - end - - # Sending a file using EM streaming and HTTP 1.1 style chunked-encoding if - # supported by the client. - def send_file - # Use HTTP 1.1 style chunked-encoding to send the file if supported - if @request.support_encoding_chunked? - @response.chunked_encoding! - send_chunk @response.head - deferrable = stream_file_data @response.filename, :http_chunks => true - else - send_chunk @response.head - deferrable = stream_file_data @response.filename - end - - deferrable.callback(&method(:reset)) - deferrable.errback(&method(:reset)) - - if $DEBUG - puts "" - puts - end - end - - def send_chunk(data) - print data if $DEBUG - send_data data - end - - private - # == Support methods - - def close_request_and_response - if @request - @request.close - @request = nil - end - if @response - @response.close - @response = nil - end - end - - # Returns IP address of peer as a string. - def socket_address - if listener.unix? - "" - else - Socket.unpack_sockaddr_in(get_peername)[1] - end - rescue Exception => e - $stderr.puts "Can't get socket address: #{e}" - "" - end - - # Output the error to stderr and sends back a 500 error. - def handle_error(e=$!) - $stderr.puts "[ERROR] #{e}" - $stderr.puts "\t" + e.backtrace.join("\n\t") if $DEBUG - send_response_and_reset Response.error(500) # Internal Server Error - end - end - end -end diff --git a/lib/thin/protocols/http/chunked_body.rb b/lib/thin/protocols/http/chunked_body.rb deleted file mode 100644 index f84cab91..00000000 --- a/lib/thin/protocols/http/chunked_body.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Thin - module Protocols - class Http - # Same as Rack::Chunked::Body, but doesn't send the tail automaticaly. - class ChunkedBody - TERM = "\r\n" - TAIL = "0#{TERM}#{TERM}" - - include Rack::Utils - - def initialize(body) - @body = body - end - - def each - term = TERM - @body.each do |chunk| - size = bytesize(chunk) - next if size == 0 - - chunk = chunk.dup.force_encoding(Encoding::BINARY) if chunk.respond_to?(:force_encoding) - yield [size.to_s(16), term, chunk, term].join - end - end - - def close - @body.close if @body.respond_to?(:close) - end - end - end - end -end \ No newline at end of file diff --git a/lib/thin/protocols/http/request.rb b/lib/thin/protocols/http/request.rb deleted file mode 100644 index 677d859a..00000000 --- a/lib/thin/protocols/http/request.rb +++ /dev/null @@ -1,186 +0,0 @@ -require "stringio" -require "tempfile" - -module Thin - module Protocols - class Http - # 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 - - INITIAL_BODY = '' - # Force external_encoding of request's body to ASCII_8BIT - INITIAL_BODY.encode!(Encoding::ASCII_8BIT) if INITIAL_BODY.respond_to?(:encode!) - - # Freeze some HTTP header names & values - SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze - SERVER_NAME = 'SERVER_NAME'.freeze - SERVER_PORT = 'SERVER_PORT'.freeze - DEFAULT_PORT = '80'.freeze - HTTP_HOST = 'HTTP_HOST'.freeze - LOCALHOST = 'localhost'.freeze - HTTP_VERSION = 'HTTP_VERSION'.freeze - SERVER_PROTOCOL = 'SERVER_PROTOCOL'.freeze - HTTP_1_0 = 'HTTP/1.0'.freeze - REMOTE_ADDR = 'REMOTE_ADDR'.freeze - CONTENT_TYPE = 'CONTENT_TYPE'.freeze - CONTENT_TYPE_L = 'Content-Type'.freeze - CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze - CONTENT_LENGTH_L = 'Content-Length'.freeze - SCRIPT_NAME = 'SCRIPT_NAME'.freeze - QUERY_STRING = 'QUERY_STRING'.freeze - PATH_INFO = 'PATH_INFO'.freeze - REQUEST_METHOD = 'REQUEST_METHOD'.freeze - FRAGMENT = 'FRAGMENT'.freeze - HTTP = 'http'.freeze - EMPTY = ''.freeze - KEEP_ALIVE_REGEXP = /\bkeep-alive\b/i.freeze - CLOSE_REGEXP = /\bclose\b/i.freeze - - # Freeze some Rack header names - RACK_INPUT = 'rack.input'.freeze - RACK_VERSION = 'rack.version'.freeze - RACK_ERRORS = 'rack.errors'.freeze - RACK_URL_SCHEME = 'rack.url_scheme'.freeze - RACK_MULTITHREAD = 'rack.multithread'.freeze - RACK_MULTIPROCESS = 'rack.multiprocess'.freeze - RACK_RUN_ONCE = 'rack.run_once'.freeze - ASYNC_CALLBACK = 'async.callback'.freeze - - # CGI-like request environment variables - attr_reader :env - - # Request body - attr_reader :body - - def initialize - @body = StringIO.new(INITIAL_BODY) - @env = { - SERVER_SOFTWARE => SERVER, - SERVER_NAME => LOCALHOST, - SCRIPT_NAME => EMPTY, - - # Rack stuff - RACK_INPUT => @body, - RACK_URL_SCHEME => HTTP, - - RACK_VERSION => VERSION::RACK, - RACK_ERRORS => $stderr, - - RACK_RUN_ONCE => false - } - @keep_alive = false - end - - def headers=(headers) - # TODO benchmark & optimize - headers.each_pair do |k, v| - # Convert to Rack headers - if k == CONTENT_TYPE_L - @env[CONTENT_TYPE] = v - elsif k == CONTENT_LENGTH_L - @env[CONTENT_LENGTH] = v - else - @env["HTTP_" + k.upcase.tr("-", "_")] = v - end - end - - host, port = @env[HTTP_HOST].split(":") if @env.key?(HTTP_HOST) - @env[SERVER_NAME] = host || LOCALHOST - @env[SERVER_PORT] = port || DEFAULT_PORT - end - - # Expected size of the body - def content_length - @env[CONTENT_LENGTH].to_i - end - - def remote_address=(address) - @env[REMOTE_ADDR] = address - end - - def method=(method) - @env[REQUEST_METHOD] = method - end - - def http_version=(string) - @env[HTTP_VERSION] = string - @env[SERVER_PROTOCOL] = string - end - - def http_version - @env[HTTP_VERSION] - end - - def path=(path) - @env[PATH_INFO] = path - end - - def query_string=(string) - @env[QUERY_STRING] = string - end - - def fragment=(string) - @env[FRAGMENT] = string - end - - def keep_alive=(bool) - @keep_alive = bool - end - - def multithread=(bool) - @env[RACK_MULTITHREAD] = bool - end - - def multiprocess=(bool) - @env[RACK_MULTIPROCESS] = bool - end - - # Returns +true+ if the client expect the connection to be kept alive. - def keep_alive? - @keep_alive - end - - def <<(data) - @body << data - - # Transfert to a tempfile if body is very big. - move_body_to_tempfile if content_length > MAX_BODY - - @body - end - - def async_callback=(callback) - @env[ASYNC_CALLBACK] = callback - end - - def support_encoding_chunked? - @env[HTTP_VERSION] != HTTP_1_0 - end - - # Called when we're done processing the request. - def finish - @body.rewind - end - - # Close any resource used by the request - 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 - end -end diff --git a/lib/thin/protocols/http/response.rb b/lib/thin/protocols/http/response.rb deleted file mode 100644 index 9eb3bea5..00000000 --- a/lib/thin/protocols/http/response.rb +++ /dev/null @@ -1,186 +0,0 @@ -module Thin - module Protocols - class Http - # A response sent to the client. - class Response - # Template async response. - ASYNC = [-1, {}, []].freeze - - # Store HTTP header name-value pairs direcly to a string - # and allow duplicated entries on some names. - class Headers - HEADER_FORMAT = "%s: %s\r\n".freeze - ALLOWED_DUPLICATES = %w(Set-Cookie Set-Cookie2 Warning WWW-Authenticate).freeze - - def initialize - @sent = {} - @out = [] - end - - # Add key: value pair to the headers. - # Ignore if already sent and no duplicates are allowed - # for this +key+. - def []=(key, value) - if !@sent.has_key?(key) || ALLOWED_DUPLICATES.include?(key) - @sent[key] = true - value = case value - when Time - value.httpdate - when NilClass - return - else - value.to_s - end - @out << HEADER_FORMAT % [key, value] - end - end - - def has_key?(key) - @sent[key] - end - - def to_s - @out.join - end - end - - CONNECTION = 'Connection'.freeze - CLOSE = 'close'.freeze - KEEP_ALIVE = 'keep-alive'.freeze - SERVER = 'Server'.freeze - CONTENT_LENGTH = 'Content-Length'.freeze - - KEEP_ALIVE_STATUSES = [100, 101].freeze - - # Status code - attr_accessor :status - - # Response body, must respond to +each+. - attr_accessor :body - - # Headers key-value hash - attr_reader :headers - - attr_reader :http_version - - def initialize(status=200, headers=nil, body=nil) - @headers = Headers.new - @status = status - @keep_alive = false - @body = body - @http_version = "HTTP/1.1" - - self.headers = headers if headers - end - - if System.ruby_18? - - # Ruby 1.8 implementation. - # Respects Rack specs. - # - # See http://rack.rubyforge.org/doc/files/SPEC.html - def headers=(key_value_pairs) - key_value_pairs.each do |k, vs| - vs.each { |v| @headers[k] = v.chomp } if vs - end if key_value_pairs - end - - else - - # Ruby 1.9 doesn't have a String#each anymore. - # Rack spec doesn't take care of that yet, for now we just use - # +each+ but fallback to +each_line+ on strings. - # I wish we could remove that condition. - # To be reviewed when a new Rack spec comes out. - def headers=(key_value_pairs) - key_value_pairs.each do |k, vs| - next unless vs - if vs.is_a?(String) - vs.each_line { |v| @headers[k] = v.chomp } - else - vs.each { |v| @headers[k] = v.chomp } - end - end if key_value_pairs - end - - end - - # Finish preparing the response. - def finish - @headers[CONNECTION] = keep_alive? ? KEEP_ALIVE : CLOSE - @headers[SERVER] = Thin::SERVER - end - - # Top header of the response, - # containing the status code and response headers. - def head - status_message = Rack::Utils::HTTP_STATUS_CODES[@status.to_i] - "#{@http_version} #{@status} #{status_message}\r\n#{@headers.to_s}\r\n" - end - - # Close any resource used by the response - def close - @body.fail if @body.respond_to?(:fail) - @body.close if @body.respond_to?(:close) - end - - # Yields each chunk of the response. - # To control the size of each chunk - # define your own +each+ method on +body+. - def each - yield head - if @body.is_a?(String) - yield @body - else - @body.each { |chunk| yield chunk } - end - end - - # Tell the client the connection should stay open - def keep_alive! - @keep_alive = true - end - - # Persistent connection must be requested as keep-alive - # from the server and have a Content-Length, or the response - # status must require that the connection remain open. - def keep_alive? - (@keep_alive && @headers.has_key?(CONTENT_LENGTH)) || KEEP_ALIVE_STATUSES.include?(@status) - end - - def async? - @status == ASYNC.first - end - - def file? - @body.respond_to?(:to_path) - end - - def filename - @body.to_path - end - - def body_callback=(proc) - @body.callback(&proc) if @body.respond_to?(:callback) - @body.errback(&proc) if @body.respond_to?(:errback) - end - - def chunked_encoding! - @headers['Transfer-Encoding'] = 'chunked' - end - - def http_version=(string) - return unless string && string == "HTTP/1.1" || string == "HTTP/1.0" - @http_version = string - end - - def self.error(status=500, message=Rack::Utils::HTTP_STATUS_CODES[status]) - new status, - { "Content-Type" => "text/plain", - "Content-Length" => Rack::Utils.bytesize(message).to_s }, - [message] - end - end - end - end -end diff --git a/lib/thin/request.rb b/lib/thin/request.rb new file mode 100644 index 00000000..8c0213e6 --- /dev/null +++ b/lib/thin/request.rb @@ -0,0 +1,182 @@ +require "stringio" +require "tempfile" + +module Thin + # 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 + + INITIAL_BODY = '' + # Force external_encoding of request's body to ASCII_8BIT + INITIAL_BODY.encode!(Encoding::ASCII_8BIT) if INITIAL_BODY.respond_to?(:encode!) + + # Freeze some HTTP header names & values + SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze + SERVER_NAME = 'SERVER_NAME'.freeze + SERVER_PORT = 'SERVER_PORT'.freeze + DEFAULT_PORT = '80'.freeze + HTTP_HOST = 'HTTP_HOST'.freeze + LOCALHOST = 'localhost'.freeze + HTTP_VERSION = 'HTTP_VERSION'.freeze + SERVER_PROTOCOL = 'SERVER_PROTOCOL'.freeze + HTTP_1_0 = 'HTTP/1.0'.freeze + REMOTE_ADDR = 'REMOTE_ADDR'.freeze + CONTENT_TYPE = 'CONTENT_TYPE'.freeze + CONTENT_TYPE_L = 'Content-Type'.freeze + CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze + CONTENT_LENGTH_L = 'Content-Length'.freeze + SCRIPT_NAME = 'SCRIPT_NAME'.freeze + QUERY_STRING = 'QUERY_STRING'.freeze + PATH_INFO = 'PATH_INFO'.freeze + REQUEST_METHOD = 'REQUEST_METHOD'.freeze + FRAGMENT = 'FRAGMENT'.freeze + HTTP = 'http'.freeze + EMPTY = ''.freeze + KEEP_ALIVE_REGEXP = /\bkeep-alive\b/i.freeze + CLOSE_REGEXP = /\bclose\b/i.freeze + + # Freeze some Rack header names + RACK_INPUT = 'rack.input'.freeze + RACK_VERSION = 'rack.version'.freeze + RACK_ERRORS = 'rack.errors'.freeze + RACK_URL_SCHEME = 'rack.url_scheme'.freeze + RACK_MULTITHREAD = 'rack.multithread'.freeze + RACK_MULTIPROCESS = 'rack.multiprocess'.freeze + RACK_RUN_ONCE = 'rack.run_once'.freeze + ASYNC_CALLBACK = 'async.callback'.freeze + + # CGI-like request environment variables + attr_reader :env + + # Request body + attr_reader :body + + def initialize + @body = StringIO.new(INITIAL_BODY) + @env = { + SERVER_SOFTWARE => SERVER, + SERVER_NAME => LOCALHOST, + SCRIPT_NAME => EMPTY, + + # Rack stuff + RACK_INPUT => @body, + RACK_URL_SCHEME => HTTP, + + RACK_VERSION => VERSION::RACK, + RACK_ERRORS => $stderr, + + RACK_RUN_ONCE => false + } + @keep_alive = false + end + + def headers=(headers) + # TODO benchmark & optimize + headers.each_pair do |k, v| + # Convert to Rack headers + if k == CONTENT_TYPE_L + @env[CONTENT_TYPE] = v + elsif k == CONTENT_LENGTH_L + @env[CONTENT_LENGTH] = v + else + @env["HTTP_" + k.upcase.tr("-", "_")] = v + end + end + + host, port = @env[HTTP_HOST].split(":") if @env.key?(HTTP_HOST) + @env[SERVER_NAME] = host || LOCALHOST + @env[SERVER_PORT] = port || DEFAULT_PORT + end + + # Expected size of the body + def content_length + @env[CONTENT_LENGTH].to_i + end + + def remote_address=(address) + @env[REMOTE_ADDR] = address + end + + def method=(method) + @env[REQUEST_METHOD] = method + end + + def http_version=(string) + @env[HTTP_VERSION] = string + @env[SERVER_PROTOCOL] = string + end + + def http_version + @env[HTTP_VERSION] + end + + def path=(path) + @env[PATH_INFO] = path + end + + def query_string=(string) + @env[QUERY_STRING] = string + end + + def fragment=(string) + @env[FRAGMENT] = string + end + + def keep_alive=(bool) + @keep_alive = bool + end + + def multithread=(bool) + @env[RACK_MULTITHREAD] = bool + end + + def multiprocess=(bool) + @env[RACK_MULTIPROCESS] = bool + end + + # Returns +true+ if the client expect the connection to be kept alive. + def keep_alive? + @keep_alive + end + + def <<(data) + @body << data + + # Transfert to a tempfile if body is very big. + move_body_to_tempfile if content_length > MAX_BODY + + @body + end + + def async_callback=(callback) + @env[ASYNC_CALLBACK] = callback + end + + def support_encoding_chunked? + @env[HTTP_VERSION] != HTTP_1_0 + end + + # Called when we're done processing the request. + def finish + @body.rewind + end + + # Close any resource used by the request + 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 diff --git a/lib/thin/response.rb b/lib/thin/response.rb new file mode 100644 index 00000000..af244afe --- /dev/null +++ b/lib/thin/response.rb @@ -0,0 +1,184 @@ +require "rack" + +module Thin + # A response sent to the client. + class Response + # Template async response. + ASYNC = [-1, {}, []].freeze + + # Store HTTP header name-value pairs direcly to a string + # and allow duplicated entries on some names. + class Headers + HEADER_FORMAT = "%s: %s\r\n".freeze + ALLOWED_DUPLICATES = %w(Set-Cookie Set-Cookie2 Warning WWW-Authenticate).freeze + + def initialize + @sent = {} + @out = [] + end + + # Add key: value pair to the headers. + # Ignore if already sent and no duplicates are allowed + # for this +key+. + def []=(key, value) + if !@sent.has_key?(key) || ALLOWED_DUPLICATES.include?(key) + @sent[key] = true + value = case value + when Time + value.httpdate + when NilClass + return + else + value.to_s + end + @out << HEADER_FORMAT % [key, value] + end + end + + def has_key?(key) + @sent[key] + end + + def to_s + @out.join + end + end + + CONNECTION = 'Connection'.freeze + CLOSE = 'close'.freeze + KEEP_ALIVE = 'keep-alive'.freeze + SERVER = 'Server'.freeze + CONTENT_LENGTH = 'Content-Length'.freeze + + KEEP_ALIVE_STATUSES = [100, 101].freeze + + # Status code + attr_accessor :status + + # Response body, must respond to +each+. + attr_accessor :body + + # Headers key-value hash + attr_reader :headers + + attr_reader :http_version + + def initialize(status=200, headers=nil, body=nil) + @headers = Headers.new + @status = status + @keep_alive = false + @body = body + @http_version = "HTTP/1.1" + + self.headers = headers if headers + end + + if System.ruby_18? + + # Ruby 1.8 implementation. + # Respects Rack specs. + # + # See http://rack.rubyforge.org/doc/files/SPEC.html + def headers=(key_value_pairs) + key_value_pairs.each do |k, vs| + vs.each { |v| @headers[k] = v.chomp } if vs + end if key_value_pairs + end + + else + + # Ruby 1.9 doesn't have a String#each anymore. + # Rack spec doesn't take care of that yet, for now we just use + # +each+ but fallback to +each_line+ on strings. + # I wish we could remove that condition. + # To be reviewed when a new Rack spec comes out. + def headers=(key_value_pairs) + key_value_pairs.each do |k, vs| + next unless vs + if vs.is_a?(String) + vs.each_line { |v| @headers[k] = v.chomp } + else + vs.each { |v| @headers[k] = v.chomp } + end + end if key_value_pairs + end + + end + + # Finish preparing the response. + def finish + @headers[CONNECTION] = keep_alive? ? KEEP_ALIVE : CLOSE + @headers[SERVER] = Thin::SERVER + end + + # Top header of the response, + # containing the status code and response headers. + def head + status_message = Rack::Utils::HTTP_STATUS_CODES[@status.to_i] + "#{@http_version} #{@status} #{status_message}\r\n#{@headers.to_s}\r\n" + end + + # Close any resource used by the response + def close + @body.fail if @body.respond_to?(:fail) + @body.close if @body.respond_to?(:close) + end + + # Yields each chunk of the response. + # To control the size of each chunk + # define your own +each+ method on +body+. + def each + yield head + if @body.is_a?(String) + yield @body + else + @body.each { |chunk| yield chunk } + end + end + + # Tell the client the connection should stay open + def keep_alive! + @keep_alive = true + end + + # Persistent connection must be requested as keep-alive + # from the server and have a Content-Length, or the response + # status must require that the connection remain open. + def keep_alive? + (@keep_alive && @headers.has_key?(CONTENT_LENGTH)) || KEEP_ALIVE_STATUSES.include?(@status) + end + + def async? + @status == ASYNC.first + end + + def file? + @body.respond_to?(:to_path) + end + + def filename + @body.to_path + end + + def body_callback=(proc) + @body.callback(&proc) if @body.respond_to?(:callback) + @body.errback(&proc) if @body.respond_to?(:errback) + end + + def chunked_encoding! + @headers['Transfer-Encoding'] = 'chunked' + end + + def http_version=(string) + return unless string && string == "HTTP/1.1" || string == "HTTP/1.0" + @http_version = string + end + + def self.error(status=500, message=Rack::Utils::HTTP_STATUS_CODES[status]) + new status, + { "Content-Type" => "text/plain", + "Content-Length" => Rack::Utils.bytesize(message).to_s }, + [message] + end + end +end diff --git a/lib/thin/server.rb b/lib/thin/server.rb index 4010d7dc..7dab3bba 100644 --- a/lib/thin/server.rb +++ b/lib/thin/server.rb @@ -2,6 +2,7 @@ require "thin/system" require "thin/listener" +require "thin/connection" require "thin/backends/prefork" require "thin/backends/single_process" @@ -15,23 +16,6 @@ module Thin # server.listen 3000 # server.start # - # == Using a custom protocol - # You can implement your own protocol handler that will parse and process requests. This is simply done by - # implementing a typical EventMachine +Connection+ class or module. - # - # class Echo < EventMachine::Connection - # attr_accessor :server - # - # def receive_data(data) - # send_data data - # close_connection_after_writing - # end - # end - # - # server = Thin::Server.new - # server.listen 3000, :protocol => Echo - # server.start - # # == Preforking and workers # If fork(2) is available on your system, Thin will try to start workers to process requests. # By default the number of workers will be based on the number of processors on your system. @@ -171,7 +155,6 @@ def backend # @option options [Boolean] :tcp_no_delay (true) Disables the Nagle algorithm for send coalescing. # @option options [Boolean] :ipv6_only (false) do not listen on IPv4 interface. # @option options [Integer] :backlog (1024) Maximum number of clients in the listening backlog. - # @option options [Symbol, String, Class] :protocol (:http) Protocol name or class to use to process connections. def listen(address, options={}) @listeners << Listener.new(address, options) end @@ -201,7 +184,7 @@ def start(daemonize=false) @app = @app_loader.call unless @preload_app @listeners.each do |listener| - EM.attach_server(listener.socket, listener.protocol_class) do |connection| + EM.attach_server(listener.socket, Connection) do |connection| connection.comm_inactivity_timeout = @timeout connection.server = self connection.listener = listener diff --git a/man/thin-conf.5.ronn b/man/thin-conf.5.ronn index 7390be38..f40bb8b4 100644 --- a/man/thin-conf.5.ronn +++ b/man/thin-conf.5.ronn @@ -51,9 +51,6 @@ Listen for incoming connections on a given address. * `backlog`: Maximum number of clients in the listening backlog. Default: `1024` - * `protocol`: - Protocol name or class to use to process connections. Default: `:http` - ## PRELOAD APP (#preload_app) Set to `true` to load the app before forking to workers. Default: `false`. If the Ruby interpreter garbage collector doesn't support copy on write, set this option to false which will prevent loading your application in the master process. If the interpreter does support copy on write, setting this to `true` will help save memory. diff --git a/man/thin.1.ronn b/man/thin.1.ronn index cb9c7c42..b53495e3 100644 --- a/man/thin.1.ronn +++ b/man/thin.1.ronn @@ -10,8 +10,6 @@ thin(1) -- high performance Ruby server Thin is a high performance Ruby server. Thin is generally used as an app server for serving [Rack applications](http://rack.rubyforge.org/doc/SPEC.html) behind full blown HTTP servers such as [nginx](http://nginx.org). -Since version 2, Thin can serve any protocol defined with EventMachine. - ## FILES The `thin` command will load the Rack application defined in a rackup config file. The default behaviour is to look for a file named `config.ru` that lives at the root of the application if the argument is omitted. This file is evaluated in the context of a [Rack::Builder](http://rack.rubyforge.org/doc/Rack/Builder.html) class. diff --git a/test/integration/big_request_test.rb b/test/integration/big_request_test.rb index 14115a14..fd86739b 100644 --- a/test/integration/big_request_test.rb +++ b/test/integration/big_request_test.rb @@ -1,5 +1,5 @@ require 'test_helper' -require "thin/protocols/http" +require "thin/request" class BigRequestTest < IntegrationTestCase def setup @@ -7,14 +7,14 @@ def setup end def test_big_body_is_stored_in_tempfile - post "/eval?code=request.body.class", :big => "X" * (Thin::Protocols::Http::Request::MAX_BODY + 1) + post "/eval?code=request.body.class", :big => "X" * (Thin::Request::MAX_BODY + 1) assert_status 200 assert_response_equals "Tempfile" end def test_big_body_is_read_from_tempfile - size = Thin::Protocols::Http::Request::MAX_BODY + 1 + size = Thin::Request::MAX_BODY + 1 post "/eval?code=request.body.read", :big => "X" * size assert_status 200 diff --git a/test/integration/custom_protocol_test.rb b/test/integration/custom_protocol_test.rb deleted file mode 100644 index 26cbd895..00000000 --- a/test/integration/custom_protocol_test.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'test_helper' - -class CustomProtocolTest < IntegrationTestCase - def test_echo - thin do - require File.expand_path("../../fixtures/echo", __FILE__) - listen PORT, :protocol => "Echo" - end - - socket do |s| - s.write "hi" - assert_equal "hi", s.read - end - end -end \ No newline at end of file diff --git a/test/unit/http_test.rb b/test/unit/connection_test.rb similarity index 91% rename from test/unit/http_test.rb rename to test/unit/connection_test.rb index 4df5121e..d7c1aa7a 100644 --- a/test/unit/http_test.rb +++ b/test/unit/connection_test.rb @@ -1,9 +1,9 @@ require 'test_helper' -require 'thin/protocols/http' +require 'thin/connection' -class HttpTest < Test::Unit::TestCase +class ConnectionTest < Test::Unit::TestCase def setup - @connection = Thin::Protocols::Http.new(nil) + @connection = Thin::Connection.new(nil) @connection.server = self @connection.post_init @@ -89,6 +89,6 @@ def test_parse_post_request def test_async_response_do_not_send_response @connection.expects(:send_response).never - @connection.process_response(Thin::Protocols::Http::Response::ASYNC) + @connection.process_response(Thin::Response::ASYNC) end end diff --git a/test/unit/http/request_test.rb b/test/unit/request_test.rb similarity index 94% rename from test/unit/http/request_test.rb rename to test/unit/request_test.rb index 876748a2..e4007b4b 100644 --- a/test/unit/http/request_test.rb +++ b/test/unit/request_test.rb @@ -1,10 +1,10 @@ require 'test_helper' -require "thin/protocols/http" +require "thin/request" require "rack" -class HttpRequestTest < Test::Unit::TestCase +class RequestTest < Test::Unit::TestCase def setup - @request = Thin::Protocols::Http::Request.new + @request = Thin::Request.new end def test_env_contains_requires_rack_variables diff --git a/test/unit/http/response_test.rb b/test/unit/response_test.rb similarity index 86% rename from test/unit/http/response_test.rb rename to test/unit/response_test.rb index 2c26da36..198cdf08 100644 --- a/test/unit/http/response_test.rb +++ b/test/unit/response_test.rb @@ -1,9 +1,9 @@ require 'test_helper' -require 'thin/protocols/http' +require 'thin/response' -class HttpResponseTest < Test::Unit::TestCase +class ResponseTest < Test::Unit::TestCase def setup - @response = Thin::Protocols::Http::Response.new + @response = Thin::Response.new @response.headers['Content-Type'] = 'text/html' @response.headers['Content-Length'] = '0' @response.body = '' @@ -11,7 +11,7 @@ def setup end def test_initialize_with_values - @response = Thin::Protocols::Http::Response.new(201, {"Content-Type" => "text/plain"}, ["hi"]) + @response = Thin::Response.new(201, {"Content-Type" => "text/plain"}, ["hi"]) assert_equal 201, @response.status assert_match "Content-Type: text/plain", @response.headers.to_s assert_equal ["hi"], @response.body @@ -85,6 +85,6 @@ def test_close end def test_async - assert Thin::Protocols::Http::Response.new(*Thin::Protocols::Http::Response::ASYNC).async? + assert Thin::Response.new(*Thin::Response::ASYNC).async? end end diff --git a/v2.todo b/v2.todo index 3c3430ae..103bfc1d 100644 --- a/v2.todo +++ b/v2.todo @@ -14,8 +14,8 @@ x Transfer-Encoding: chunked x Threading - Change user:group after bind - SSL -- Drop custom protocol? - Graceful stop +- Daemonizing Optimizations: - Stock 200 OK response (store in frozen const) \ No newline at end of file