1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
|
require "rack"
require "ftw"
require "ftw/protocol"
require "ftw/crlf"
require "socket"
require "cabin"
# FTW cannot fully respect the Rack 1.1 specification due to technical
# limitations in the Rack design, specifically:
#
# * rack.input must be buffered, to support IO#rewind, for the duration of each
# request. This is not safe if that request is an HTTP Upgrade or a long
# upload.
#
# FTW::Connection does not implement #rewind. Need it? File a ticket.
#
# To support HTTP Upgrade, CONNECT, and protocol-switching features, this
# server handler will set "ftw.connection" to the FTW::Connection related
# to this request.
#
# The above data is based on the response to this ticket:
# https://github.com/rack/rack/issues/347
class Rack::Handler::FTW
include FTW::Protocol
include FTW::CRLF
# The version of the rack specification supported by this handler.
RACK_VERSION = [1,1]
# A string constant value (used to avoid typos).
REQUEST_METHOD = "REQUEST_METHOD".freeze
# A string constant value (used to avoid typos).
SCRIPT_NAME = "SCRIPT_NAME".freeze
# A string constant value (used to avoid typos).
PATH_INFO = "PATH_INFO".freeze
# A string constant value (used to avoid typos).
QUERY_STRING = "QUERY_STRING".freeze
# A string constant value (used to avoid typos).
SERVER_NAME = "SERVER_NAME".freeze
# A string constant value (used to avoid typos).
SERVER_PORT = "SERVER_PORT".freeze
# A string constant value (used to avoid typos).
RACK_DOT_VERSION = "rack.version".freeze
# A string constant value (used to avoid typos).
RACK_DOT_URL_SCHEME = "rack.url_scheme".freeze
# A string constant value (used to avoid typos).
RACK_DOT_INPUT = "rack.input".freeze
# A string constant value (used to avoid typos).
RACK_DOT_ERRORS = "rack.errors".freeze
# A string constant value (used to avoid typos).
RACK_DOT_MULTITHREAD = "rack.multithread".freeze
# A string constant value (used to avoid typos).
RACK_DOT_MULTIPROCESS = "rack.multiprocess".freeze
# A string constant value (used to avoid typos).
RACK_DOT_RUN_ONCE = "rack.run_once".freeze
# A string constant value (used to avoid typos).
RACK_DOT_LOGGER = "rack.logger".freeze
# A string constant value (used to avoid typos).
FTW_DOT_CONNECTION = "ftw.connection".freeze
# This method is invoked when rack starts this as the server.
def self.run(app, config)
#@logger.subscribe(STDOUT)
server = self.new(app, config)
server.run
end # def self.run
private
# setup a new rack server
def initialize(app, config)
@app = app
@config = config
@threads = []
end # def initialize
# Run the server.
#
# Connections are farmed out to threads.
def run
# {:environment=>"development", :pid=>nil, :Port=>9292, :Host=>"0.0.0.0",
# :AccessLog=>[], :config=>"/home/jls/projects/ruby-ftw/examples/test.ru",
# :server=>"FTW"}
#
# listen, pass connections off
#
#
# """A Rack application is an Ruby object (not a class) that responds to
# call. It takes exactly one argument, the environment and returns an
# Array of exactly three values: The status, the headers, and the body."""
#
logger.info("Starting server", :config => @config)
@server = FTW::Server.new([@config[:Host], @config[:Port]].join(":"))
@server.each_connection do |connection|
# The rack specification insists that 'rack.input' objects support
# #rewind. Bleh. Just lie about it and monkeypatch it in.
# This is required for Sinatra to accept 'post' requests, otherwise
# it barfs.
class << connection
def rewind(*args)
# lolrack, nothing to do here.
end
end
@threads << Thread.new do
handle_connection(connection)
end
end
end # def run
def stop
@server.stop unless @server.nil?
@threads.each(&:join)
end # def stop
# Handle a new connection.
#
# This method parses http requests and passes them on to #handle_request
#
# @param connection The FTW::Connection being handled.
def handle_connection(connection)
while true
begin
request = read_http_message(connection)
rescue IOError, EOFError, Errno::EPIPE, Errno::ECONNRESET, HTTP::Parser::Error
# Connection EOF'd or errored before we finished reading a full HTTP
# message, shut it down.
break
rescue ArgumentError
# Invalid http request sent
break
end
begin
handle_request(request, connection)
rescue => e
puts e.inspect
puts e.backtrace
raise e
end
end
ensure
connection.disconnect("Closing...")
end # def handle_connection
# Handle a request. This will set up the rack 'env' and invoke the
# application associated with this handler.
def handle_request(request, connection)
path, query = request.path.split("?", 2)
env = {
# CGI-like environment as required by the Rack SPEC version 1.1
REQUEST_METHOD => request.method,
SCRIPT_NAME => "/", # TODO(sissel): not totally sure what this really should be
PATH_INFO => path,
QUERY_STRING => query.nil? ? "" : query,
SERVER_NAME => "hahaha, no", # TODO(sissel): Set this
SERVER_PORT => "", # TODO(sissel): Set this
# Rack-specific environment, also required by Rack SPEC version 1.1
RACK_DOT_VERSION => RACK_VERSION,
RACK_DOT_URL_SCHEME => "http", # TODO(sissel): support https
RACK_DOT_INPUT => connection,
RACK_DOT_ERRORS => STDERR,
RACK_DOT_MULTITHREAD => true,
RACK_DOT_MULTIPROCESS => false,
RACK_DOT_RUN_ONCE => false,
RACK_DOT_LOGGER => logger,
# Extensions, not in Rack v1.1.
# ftw.connection lets you access the connection involved in this request.
# It should be used when you need to hijack the connection for use
# in proxying, HTTP CONNECT, websockets, SPDY(maybe?), etc.
FTW_DOT_CONNECTION => connection
} # env
request.headers.each do |name, value|
# The Rack spec says:
# """ Variables corresponding to the client-supplied HTTP request headers
# (i.e., variables whose names begin with HTTP_). The presence or
# absence of these variables should correspond with the presence or
# absence of the appropriate HTTP header in the request. """
#
# It doesn't specify how to translate the header names into this hash syntax.
# I looked at what Thin does, and it capitalizes and replaces dashes with
# underscores, so I'll just copy that behavior. The specific code that implements
# this in thin is here:
# https://github.com/macournoyer/thin/blob/2e9db13e414ae7425/ext/thin_parser/thin.c#L89-L95
#
# The Rack spec also doesn't describe what should be done for headers
# with multiple values.
#
env["HTTP_#{name.upcase.gsub("-", "_")}"] = value
end # request.headers.each
# Invoke the application in this rack app
status, headers, body = @app.call(env)
# The application is done handling this request, respond to the client.
response = FTW::Response.new
response.status = status.to_i
response.version = request.version
headers.each do |name, value|
response.headers.add(name, value)
end
response.body = body
begin
connection.write(response.to_s + CRLF)
write_http_body(body, connection, response["Transfer-Encoding"] == "chunked")
rescue => e
@logger.error(e)
connection.disconnect(e.inspect)
end
end # def handle_request
# Get the logger.
def logger
if @logger.nil?
@logger = Cabin::Channel.get
end
return @logger
end # def logger
public(:run, :initialize, :stop)
end
|