require 'reel' require 'rack' module SnowmanIO class WebServer < Reel::Server::HTTP attr_reader :app class ConnectionHandler # include Celluloid def initialize(connection, app) @connection = connection @app = app # async.run run rescue Reel::SocketError @connection.close end def run @connection.each_request do |request| # TODO: support websockets if request.websocket? request.respond :bad_request, "WebSockets not supported" else route_request request end end end protected # Compile the regex once CONTENT_LENGTH_HEADER = %r{^content-length$}i def route_request(request) options = { :method => request.method, :input => request.body.to_s, "REMOTE_ADDR" => request.remote_addr }.merge(convert_headers(request.headers)) status, headers, body = @app.call ::Rack::MockRequest.env_for(request.url, options) if body.respond_to? :each # If Content-Length was specified we can send the response all at once if headers.keys.detect { |h| h =~ CONTENT_LENGTH_HEADER } # Can't use collect here because Rack::BodyProxy/Rack::Lint isn't a real Enumerable full_body = '' body.each { |b| full_body << b } request.respond status_symbol(status), headers, full_body else request.respond status_symbol(status), headers.merge(:transfer_encoding => :chunked) body.each { |chunk| request << chunk } request.finish_response end else SnowmanIO.logger.error("don't know how to render: #{body.inspect}") request.respond :internal_server_error, "An error occurred processing your request" end body.close if body.respond_to? :close end # Those headers must not start with 'HTTP_'. NO_PREFIX_HEADERS=%w[CONTENT_TYPE CONTENT_LENGTH].freeze def convert_headers(headers) Hash[headers.map { |key, value| header = key.upcase.gsub('-','_') if NO_PREFIX_HEADERS.member?(header) [header, value] else ['HTTP_' + header, value] end }] end def status_symbol(status) if status.is_a?(Fixnum) Http::Response::STATUS_CODES[status].downcase.gsub(/\s|-/, '_').to_sym else status.to_sym end end end def initialize(app, options) raise ArgumentError, "no host given" unless options[:host] raise ArgumentError, "no port given" unless options[:port] SnowmanIO.logger.info "Snowman WebServer started on http://#{options[:host]}:#{options[:port]}" # TODO: not sure, maybe should log without verbose option too app = Rack::CommonLogger.new(app, STDOUT) if options[:verbose] super(options[:host], options[:port], &method(:on_connection)) @app = app end def on_connection(connection) # connection.detach ConnectionHandler.new(connection, @app) end end end