File: ftw.rb

package info (click to toggle)
ruby-ftw 0.0.49-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 548 kB
  • sloc: ruby: 1,922; makefile: 5
file content (227 lines) | stat: -rw-r--r-- 7,801 bytes parent folder | download | duplicates (4)
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