falcon 0.18.14 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00d78cf28ce29545aadf35ae46f9c74d9a17d735104a0ae14e89e65d4971b9cf
4
- data.tar.gz: bb8ebf365e2c1b509e02d59e5f3777f2afd7dc93ba36766093115a12276f73f7
3
+ metadata.gz: 16eb6bd0bff8f6177b4f110a0d7fc07da2a1b190a4c0e0aa77bbe86978bf9331
4
+ data.tar.gz: b3520e014ed9f70fe6f87ba9d5cefbe69d27b173bc79c1d3cf4cc36e9960c967
5
5
  SHA512:
6
- metadata.gz: 5815df38ef80aec5070a315513e90507c1e6e012d8f4ae72b3346fe04f2370f3bcc54d9b58f33d5159c5360abb2a4768561593a153477cf3cc93874a947e0c28
7
- data.tar.gz: 1ebfc78ee21151b7e7f4c119b910bac160e2c33e178a4d706bb8367c73964b1d6247cc3bf54304ebdc7b520f4775f269f70f6791c4c66fc3506b58a4e1bed792
6
+ metadata.gz: 14f8a9402e0f4080d9cb1aa4cd2ea545a62b1b986c1e23306f3a56fc64a3125883f50225abc14029c13370ef628b1c7fecf06f525a5868d70fa34e7224d6cbd4
7
+ data.tar.gz: ab8782501460ae14988977be03af1209e6d45a37eadaa42d3f67b3a4196bfd1499fac05efd7360f84ca65d77cad522db83ae6cf7ddedd1a3d43c4bad3161e19d
data/README.md CHANGED
@@ -5,6 +5,7 @@ Falcon is a multi-process, multi-fiber rack-compatible HTTP server built on top
5
5
  [![Build Status](https://secure.travis-ci.org/socketry/falcon.svg)](http://travis-ci.org/socketry/falcon)
6
6
  [![Code Climate](https://codeclimate.com/github/socketry/falcon.svg)](https://codeclimate.com/github/socketry/falcon)
7
7
  [![Coverage Status](https://coveralls.io/repos/socketry/falcon/badge.svg)](https://coveralls.io/r/socketry/falcon)
8
+ [![Gitter](https://badges.gitter.im/join.svg)](https://gitter.im/socketry/falcon)
8
9
 
9
10
  [async]: https://github.com/socketry/async
10
11
  [async-io]: https://github.com/socketry/async-io
@@ -51,9 +52,13 @@ Falcon works perfectly with `rails` apps.
51
52
 
52
53
  Alternatively run `RACK_HANDLER=falcon rails server` to start the server (at least, until [rack#181](https://github.com/rack/rack/pull/1181) is merged).
53
54
 
54
- #### Thread Safety (Rails < 5.x)
55
+ #### Thread Safety
55
56
 
56
- With older versons of Rails, the `Rack::Lock` middleware is inserted into your app unless you explicitly add `config.threadsafe!`. `Rack::Lock` will cause both poor performance and deadlocks due to the highly concurrent nature of `falcon`. Therefore, please ensure you specify `config.threadsafe!` in your `config/application.rb`:
57
+ With older versions of Rails, the `Rack::Lock` middleware can be inserted into your app by Rails. `Rack::Lock` will cause both poor performance and deadlocks due to the highly concurrent nature of `falcon`. Other web frameworks are generally unaffected.
58
+
59
+ ##### Rails 3.x (and older)
60
+
61
+ Please ensure you specify `config.threadsafe!` in your `config/application.rb`:
57
62
 
58
63
  ```ruby
59
64
  module MySite
@@ -65,8 +70,13 @@ module MySite
65
70
  end
66
71
  end
67
72
  ```
73
+ ##### Rails 4.x
74
+
75
+ Please ensure you have `config.allow_concurrency = true` in your configuration.
76
+
77
+ ##### Rails 5.x+
68
78
 
69
- This became the default in Rails 5 so no change is necessary in this version. Other web frameworks are generally unaffected.
79
+ This became the default in Rails 5 so no change is necessary unless you explicitly disabled concurrency, in which case you should remove that configuration.
70
80
 
71
81
  ### WebSockets
72
82
 
@@ -16,10 +16,10 @@ Gem::Specification.new do |spec|
16
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
17
  spec.require_paths = ["lib"]
18
18
 
19
- spec.add_dependency("http-protocol", "~> 0.6.0")
19
+ spec.add_dependency("http-protocol", "~> 0.8.0")
20
20
 
21
21
  spec.add_dependency("async-io", "~> 1.9")
22
- spec.add_dependency("async-http", "~> 0.34.0")
22
+ spec.add_dependency("async-http", "~> 0.36.0")
23
23
  spec.add_dependency("async-container", "~> 0.6.0")
24
24
 
25
25
  spec.add_dependency("rack", ">= 1.0")
@@ -26,6 +26,12 @@ require 'async/logger'
26
26
  module Falcon
27
27
  module Adapters
28
28
  class Rack
29
+ HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'.freeze
30
+ REMOTE_ADDR = 'REMOTE_ADDR'.freeze
31
+
32
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
33
+ CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze
34
+
29
35
  def initialize(app, logger = Async.logger)
30
36
  @app = app
31
37
 
@@ -45,76 +51,98 @@ module Falcon
45
51
  end
46
52
  end
47
53
 
54
+ # Process the incoming request into a valid rack env.
55
+ def unwrap_request(request, env)
56
+ if content_type = request.headers.delete('content-type')
57
+ env[CONTENT_TYPE] = content_type
58
+ end
59
+
60
+ # In some situations we don't know the content length, e.g. when using chunked encoding, or when decompressing the body.
61
+ if body = request.body and length = body.length
62
+ env[CONTENT_LENGTH] = length.to_s
63
+ end
64
+
65
+ self.unwrap_headers(request.headers, env)
66
+
67
+ # HTTP/2 prefers `:authority` over `host`, so we do this for backwards compatibility.
68
+ env[::Rack::HTTP_HOST] ||= request.authority
69
+
70
+ # This is the HTTP/1 header for the scheme of the request and is used by Rack.
71
+ # Technically it should use the Forwarded header but this is not common yet.
72
+ # https://tools.ietf.org/html/rfc7239#section-5.4
73
+ # https://github.com/rack/rack/issues/1310
74
+ env[HTTP_X_FORWARDED_PROTO] ||= request.scheme
75
+
76
+ if remote_address = request.remote_address
77
+ env[REMOTE_ADDR] = remote_address.ip_address if remote_address.ip?
78
+ end
79
+ end
80
+
81
+ def make_response(request, status, headers, body)
82
+ @logger.debug(request) {"Rack response: #{status} #{headers.inspect} #{body.class}"}
83
+
84
+ return Response.wrap(status, headers, body)
85
+ end
86
+
48
87
  def call(request)
49
88
  request_path, query_string = request.path.split('?', 2)
50
89
  server_name, server_port = (request.authority || '').split(':', 2)
51
90
 
52
91
  env = {
53
- 'rack.version' => [2, 0, 0],
92
+ ::Rack::RACK_VERSION => [2, 0, 0],
54
93
 
55
- 'rack.input' => Input.new(request.body),
56
- 'rack.errors' => $stderr,
94
+ ::Rack::RACK_INPUT => Input.new(request.body),
95
+ ::Rack::RACK_ERRORS => $stderr,
57
96
 
58
- 'rack.multithread' => true,
59
- 'rack.multiprocess' => true,
60
- 'rack.run_once' => false,
97
+ ::Rack::RACK_MULTITHREAD => true,
98
+ ::Rack::RACK_MULTIPROCESS => true,
99
+ ::Rack::RACK_RUNONCE => false,
61
100
 
62
101
  # The HTTP request method, such as “GET” or “POST”. This cannot ever be an empty string, and so is always required.
63
- 'REQUEST_METHOD' => request.method,
102
+ ::Rack::REQUEST_METHOD => request.method,
64
103
 
65
104
  # The initial portion of the request URL's “path” that corresponds to the application object, so that the application knows its virtual “location”. This may be an empty string, if the application corresponds to the “root” of the server.
66
- 'SCRIPT_NAME' => '',
105
+ ::Rack::SCRIPT_NAME => '',
67
106
 
68
107
  # The remainder of the request URL's “path”, designating the virtual “location” of the request's target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash. This value may be percent-encoded when originating from a URL.
69
- 'PATH_INFO' => request_path,
108
+ ::Rack::PATH_INFO => request_path,
70
109
 
71
110
  # The portion of the request URL that follows the ?, if any. May be empty, but is always required!
72
- 'QUERY_STRING' => query_string || '',
111
+ ::Rack::QUERY_STRING => query_string || '',
73
112
 
74
- # The server protocol, e.g. HTTP/1.1
75
- 'SERVER_PROTOCOL' => request.version,
76
- 'rack.url_scheme' => 'http',
113
+ # The server protocol (e.g. HTTP/1.1):
114
+ ::Rack::SERVER_PROTOCOL => request.version,
115
+
116
+ # The request scheme:
117
+ ::Rack::RACK_URL_SCHEME => request.scheme,
77
118
 
78
119
  # I'm not sure what sane defaults should be here:
79
- 'SERVER_NAME' => server_name || '',
80
- 'SERVER_PORT' => server_port || '',
120
+ ::Rack::SERVER_NAME => server_name || '',
121
+ ::Rack::SERVER_PORT => server_port || '',
81
122
  }
82
123
 
83
- if content_type = request.headers.delete('content-type')
84
- env['CONTENT_TYPE'] = content_type
85
- end
86
-
87
- if content_length = request.headers.delete('content-length')
88
- env['CONTENT_LENGTH'] = content_length
89
- end
90
-
91
- self.unwrap_headers(request.headers, env)
92
-
93
- # HTTP/2 prefers `:authority` over `host`, so we do this for backwards compatibility.
94
- env['HTTP_HOST'] ||= request.authority
95
-
96
- if remote_address = request.remote_address
97
- env['REMOTE_ADDR'] = remote_address.ip_address if remote_address.ip?
98
- end
124
+ self.unwrap_request(request, env)
99
125
 
100
126
  if request.hijack?
101
- env['rack.hijack?'] = true
127
+ env[::Rack::RACK_IS_HIJACK] = true
102
128
 
103
- env['rack.hijack'] = lambda do
129
+ env[::Rack::RACK_HIJACK] = lambda do
104
130
  wrapper = request.hijack
105
131
 
106
132
  # We dup this as it might be taken out of the normal control flow, and the io will be closed shortly after returning from this method.
107
133
  io = wrapper.io.dup
108
134
  wrapper.close
109
135
 
110
- env['rack.hijack_io'] = io
136
+ # This is implicitly returned:
137
+ env[::Rack::RACK_HIJACK_IO] = io
111
138
  end
112
139
  else
113
- env['rack.hijack?'] = false
140
+ env[::Rack::RACK_IS_HIJACK] = false
114
141
  end
115
142
 
116
143
  status, headers, body = @app.call(env)
117
144
 
145
+ # Partial hijack is not supported/tested.
118
146
  # if hijack = headers.delete('rack.hijack')
119
147
  # body = Async::HTTP::Body::Writable.new
120
148
  #
@@ -128,8 +156,7 @@ module Falcon
128
156
  # return nil
129
157
  # end
130
158
 
131
- @logger.debug(request) {"Rack response: #{status} #{headers.inspect} #{body.class}"}
132
- return Response.wrap(status, headers, body)
159
+ return make_response(request, status, headers, body)
133
160
  rescue => exception
134
161
  @logger.error "#{exception.class}: #{exception.message}\n\t#{$!.backtrace.join("\n\t")}"
135
162
 
@@ -51,12 +51,14 @@ module Falcon
51
51
  return false
52
52
  end
53
53
 
54
+ # Wrap the request body in a rewindable buffer.
55
+ # @return [Async::HTTP::Response] the response.
54
56
  def call(request)
55
57
  if body = request.body and needs_rewind?(request)
56
58
  request.body = Async::HTTP::Body::Rewindable.new(body)
57
59
  end
58
60
 
59
- response = super
61
+ return super
60
62
  end
61
63
  end
62
64
  end
@@ -19,6 +19,7 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require_relative 'command/serve'
22
+ require_relative 'command/virtual'
22
23
 
23
24
  require_relative 'version'
24
25
 
@@ -39,9 +40,10 @@ module Falcon
39
40
  option '-v/--version', "Print out the application version."
40
41
  end
41
42
 
42
- nested '<command>',
43
+ nested '<command>', {
43
44
  'serve' => Serve,
44
- default: 'serve'
45
+ 'virtual' => Virtual
46
+ }, default: 'serve'
45
47
 
46
48
  def verbose?
47
49
  @options[:logging] == :verbose
@@ -90,7 +90,7 @@ module Falcon
90
90
  end
91
91
 
92
92
  def run(verbose = false)
93
- app, options = load_app(verbose)
93
+ app, _ = load_app(verbose)
94
94
 
95
95
  endpoint = Endpoint.parse(@options[:bind], **endpoint_options)
96
96
 
@@ -112,7 +112,7 @@ module Falcon
112
112
  end
113
113
  end
114
114
 
115
- server = Falcon::Server.new(app, bound_endpoint, endpoint.protocol)
115
+ server = Falcon::Server.new(app, bound_endpoint, endpoint.protocol, endpoint.scheme)
116
116
 
117
117
  server.run
118
118
 
@@ -0,0 +1,126 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative '../server'
22
+ require_relative '../endpoint'
23
+ require_relative '../hosts'
24
+
25
+ require 'async/container'
26
+ require 'async/container/controller'
27
+
28
+ require 'async/io/host_endpoint'
29
+ require 'async/io/shared_endpoint'
30
+ require 'async/io/ssl_endpoint'
31
+
32
+ require 'samovar'
33
+
34
+ require 'rack/builder'
35
+ require 'rack/server'
36
+
37
+ module Falcon
38
+ module Command
39
+ class Virtual < Samovar::Command
40
+ self.description = "Run an HTTP server with one or more virtual hosts."
41
+
42
+ options do
43
+ option '--bind-insecure <address>', "Bind redirection to the given hostname/address", default: "http://localhost"
44
+ option '--bind-secure <address>', "Bind proxy to the given hostname/address", default: "https://localhost"
45
+
46
+ option '--self-signed', "Use self-signed SSL", default: false
47
+ end
48
+
49
+ many :sites
50
+
51
+ CONFIG_RU = "config.ru"
52
+
53
+ def load_app(path, verbose)
54
+ config = File.join(path, CONFIG_RU)
55
+
56
+ rack_app, options = Rack::Builder.parse_file(config)
57
+
58
+ return Server.middleware(rack_app, verbose: verbose), options
59
+ end
60
+
61
+ def client
62
+ Async::HTTP::Client.new(client_endpoint)
63
+ end
64
+
65
+ def run(verbose = false)
66
+ hosts = Falcon::Hosts.new
67
+ root = Dir.pwd
68
+
69
+ sites.each do |path|
70
+ name = File.basename(path)
71
+
72
+ hosts.add(name) do |host|
73
+ host.app_root = File.expand_path(path, root)
74
+
75
+ if @options[:self_signed]
76
+ host.self_signed!(name)
77
+ else
78
+ host.ssl_certificate_path = File.join(path, "ssl", "fullchain.pem")
79
+ host.ssl_key_path = File.join(path, "ssl", "privkey.pem")
80
+ end
81
+ end
82
+ end
83
+
84
+ controller = Async::Container::Controller.new
85
+
86
+ hosts.each do |name, host|
87
+ if container = host.start
88
+ controller << container
89
+ end
90
+ end
91
+
92
+ controller << Async::Container::Forked.new do |task|
93
+ proxy = hosts.proxy
94
+ secure_endpoint = Async::HTTP::URLEndpoint.parse(@options[:bind_secure], ssl_context: hosts.ssl_context)
95
+
96
+ Process.setproctitle("Falcon Proxy")
97
+
98
+ proxy_server = Falcon::Server.new(proxy, secure_endpoint)
99
+
100
+ proxy_server.run
101
+ end
102
+
103
+ controller << Async::Container::Forked.new do |task|
104
+ redirection = hosts.redirection
105
+ insecure_endpoint = Async::HTTP::URLEndpoint.parse(@options[:bind_insecure])
106
+
107
+ Process.setproctitle("Falcon Redirector")
108
+
109
+ redirection_server = Falcon::Server.new(redirection, insecure_endpoint)
110
+
111
+ redirection_server.run
112
+ end
113
+
114
+ Process.setproctitle("Falcon Controller")
115
+
116
+ return controller
117
+ end
118
+
119
+ def invoke(parent)
120
+ container = run(parent.verbose?)
121
+
122
+ container.wait
123
+ end
124
+ end
125
+ end
126
+ end
@@ -20,10 +20,18 @@
20
20
 
21
21
  require 'async/io/endpoint'
22
22
 
23
+ require_relative 'proxy'
24
+ require_relative 'redirection'
25
+
26
+ require 'async/container/forked'
27
+
23
28
  module Falcon
24
29
  class Host
25
30
  def initialize
26
31
  @app = nil
32
+ @app_root = nil
33
+ @config_path = "config.ru"
34
+
27
35
  @endpoint = nil
28
36
 
29
37
  @ssl_certificate = nil
@@ -33,6 +41,8 @@ module Falcon
33
41
  end
34
42
 
35
43
  attr_accessor :app
44
+ attr_accessor :app_root
45
+ attr_accessor :config_path
36
46
 
37
47
  attr_accessor :endpoint
38
48
 
@@ -49,6 +59,40 @@ module Falcon
49
59
  super
50
60
  end
51
61
 
62
+ def app?
63
+ @app || @config_path
64
+ end
65
+
66
+ def load_app(verbose = false)
67
+ return @app if @app
68
+
69
+ if @config_path
70
+ rack_app, options = Rack::Builder.parse_file(@config_path)
71
+
72
+ return Server.middleware(rack_app, verbose: verbose)
73
+ end
74
+ end
75
+
76
+ def self_signed!(hostname)
77
+ authority = Localhost::Authority.fetch(hostname)
78
+
79
+ @ssl_context = authority.server_context.tap do |context|
80
+ context.alpn_select_cb = lambda do |protocols|
81
+ if protocols.include? "h2"
82
+ return "h2"
83
+ elsif protocols.include? "http/1.1"
84
+ return "http/1.1"
85
+ elsif protocols.include? "http/1.0"
86
+ return "http/1.0"
87
+ else
88
+ return nil
89
+ end
90
+ end
91
+
92
+ context.session_id_context = "falcon"
93
+ end
94
+ end
95
+
52
96
  def ssl_certificate_path= path
53
97
  @ssl_certificate = OpenSSL::X509::Certificate.new(File.read(path))
54
98
  end
@@ -70,9 +114,13 @@ module Falcon
70
114
  end
71
115
  end
72
116
 
73
- def start
74
- if app = self.app
117
+ def start(*args)
118
+ if self.app?
75
119
  Async::Container::Forked.new do
120
+ Dir.chdir(@app_root) if @app_root
121
+
122
+ app = self.load_app(*args)
123
+
76
124
  server = Falcon::Server.new(app, self.server_endpoint)
77
125
 
78
126
  server.run
@@ -143,5 +191,60 @@ module Falcon
143
191
  def proxy
144
192
  Proxy.new(Falcon::BadRequest, self.client_endpoints)
145
193
  end
194
+
195
+ def redirection
196
+ Redirection.new(Falcon::BadRequest, self.client_endpoints)
197
+ end
198
+
199
+ def call(controller)
200
+ self.each do |name, host|
201
+ if container = host.start
202
+ controller << container
203
+ end
204
+ end
205
+
206
+ proxy = hosts.proxy
207
+ debug_trap = Async::IO::Trap.new(:USR1)
208
+
209
+ profile = RubyProf::Profile.new(merge_fibers: true)
210
+
211
+ controller << Async::Container::Forked.new do |task|
212
+ Process.setproctitle("Falcon Proxy")
213
+
214
+ server = Falcon::Server.new(
215
+ proxy,
216
+ Async::HTTP::URLEndpoint.parse(
217
+ 'https://0.0.0.0',
218
+ reuse_address: true,
219
+ ssl_context: hosts.ssl_context
220
+ )
221
+ )
222
+
223
+ Async::Reactor.run do |task|
224
+ task.async do
225
+ debug_trap.install!
226
+ $stderr.puts "Send `kill -USR1 #{Process.pid}` for detailed status :)"
227
+
228
+ debug_trap.trap do
229
+ task.reactor.print_hierarchy($stderr)
230
+ # Async.logger.level = Logger::DEBUG
231
+ end
232
+ end
233
+
234
+ task.async do |task|
235
+ start_time = Async::Clock.now
236
+
237
+ while true
238
+ task.sleep(600)
239
+ duration = Async::Clock.now - start_time
240
+ puts "Handled #{proxy.count} requests; #{(proxy.count.to_f / duration.to_f).round(1)} requests per second."
241
+ end
242
+ end
243
+
244
+ $stderr.puts "Starting server"
245
+ server.run
246
+ end
247
+ end
248
+ end
146
249
  end
147
250
  end
@@ -32,7 +32,10 @@ module Falcon
32
32
  end
33
33
 
34
34
  class Proxy < Async::HTTP::Middleware
35
+ FORWARDED = 'forwarded'.freeze
35
36
  X_FORWARDED_FOR = 'x-forwarded-for'.freeze
37
+ X_FORWARDED_PROTO = 'x-forwarded-proto'.freeze
38
+
36
39
  VIA = 'via'.freeze
37
40
  CONNECTION = ::HTTP::Protocol::CONNECTION
38
41
 
@@ -83,19 +86,38 @@ module Falcon
83
86
  headers.slice!(HOP_HEADERS)
84
87
  end
85
88
 
89
+ def prepare_request(request)
90
+ forwarded = []
91
+
92
+ if address = request.remote_address
93
+ request.headers.add(X_FORWARDED_FOR, address.ip_address)
94
+ forwarded << "for=#{address.ip_address}"
95
+ end
96
+
97
+ if scheme = request.scheme
98
+ request.headers.add(X_FORWARDED_PROTO, scheme)
99
+ forwarded << "proto=#{scheme}"
100
+ end
101
+
102
+ unless forwarded.empty?
103
+ request.headers.add(FORWARDED, forwarded.join(';'))
104
+ end
105
+
106
+ request.headers.add(VIA, "#{request.version} #{self.class}")
107
+
108
+ self.prepare_headers(request.headers)
109
+
110
+ return request
111
+ end
112
+
86
113
  def call(request)
87
114
  if endpoint = lookup(request)
88
115
  @count += 1
89
116
 
90
- if address = request.remote_address
91
- request.headers.add(X_FORWARDED_FOR, address.ip_address)
92
- end
93
-
94
- request.headers.add(VIA, "#{request.version} #{self.class}")
117
+ request = self.prepare_request(request)
95
118
 
96
119
  client = connect(endpoint)
97
120
 
98
- prepare_headers(request.headers)
99
121
  client.call(request)
100
122
  else
101
123
  super
@@ -0,0 +1,58 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'async/http/client'
22
+ require 'http/protocol/headers'
23
+
24
+ module Falcon
25
+ module NotFound
26
+ def self.call(request)
27
+ return Async::HTTP::Response[404, {}, []]
28
+ end
29
+
30
+ def self.close
31
+ end
32
+ end
33
+
34
+ class Redirection < Async::HTTP::Middleware
35
+ def initialize(app, hosts)
36
+ super(app)
37
+
38
+ @hosts = hosts
39
+ end
40
+
41
+ def lookup(request)
42
+ # Trailing dot and port is ignored/normalized.
43
+ if authority = request.authority.sub(/(\.)?(:\d+)?$/, '')
44
+ return @hosts[authority]
45
+ end
46
+ end
47
+
48
+ def call(request)
49
+ if endpoint = lookup(request)
50
+ location = "https://#{request.authority}#{request.path}"
51
+
52
+ return Async::HTTP::Response[301, {'location' => location}, []]
53
+ else
54
+ super
55
+ end
56
+ end
57
+ end
58
+ end
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Falcon
22
- VERSION = "0.18.14"
22
+ VERSION = "0.19.0"
23
23
  end
@@ -8,6 +8,8 @@ require 'async/io/host_endpoint'
8
8
  module Rack
9
9
  module Handler
10
10
  module Falcon
11
+ SCHEME = "http".freeze
12
+
11
13
  def self.endpoint_for(**options)
12
14
  host = options[:Host] || 'localhost'
13
15
  port = Integer(options[:Port] || 9292)
@@ -21,7 +23,7 @@ module Rack
21
23
  app = ::Falcon::Adapters::Rack.new(app)
22
24
  app = ::Falcon::Adapters::Rewindable.new(app)
23
25
 
24
- server = ::Falcon::Server.new(app, endpoint)
26
+ server = ::Falcon::Server.new(app, endpoint, Async::HTTP::Protocol::HTTP1, SCHEME)
25
27
 
26
28
  Async::Reactor.run do
27
29
  server.run
data/server.rb CHANGED
@@ -30,66 +30,7 @@ end
30
30
 
31
31
  controller = Async::Container::Controller.new
32
32
 
33
- hosts.each do |name, host|
34
- if container = host.start
35
- controller << container
36
- end
37
- end
38
-
39
- proxy = hosts.proxy
40
- debug_trap = Async::IO::Trap.new(:USR1)
41
-
42
- profile = RubyProf::Profile.new(merge_fibers: true)
43
-
44
- #controller << Async::Container::Forked.new do |task|
45
- Process.setproctitle("Falcon Proxy")
46
-
47
- server = Falcon::Server.new(
48
- proxy,
49
- Async::HTTP::URLEndpoint.parse(
50
- 'https://0.0.0.0',
51
- reuse_address: true,
52
- ssl_context: hosts.ssl_context
53
- )
54
- )
55
-
56
- begin
57
- #profile.start
58
-
59
- Async::Reactor.run do |task|
60
- task.async do
61
- debug_trap.install!
62
- $stderr.puts "Send `kill -USR1 #{Process.pid}` for detailed status :)"
63
-
64
- debug_trap.trap do
65
- task.reactor.print_hierarchy($stderr)
66
- # Async.logger.level = Logger::DEBUG
67
- end
68
- end
69
-
70
- task.async do |task|
71
- start_time = Async::Clock.now
72
-
73
- while true
74
- task.sleep(600)
75
- duration = Async::Clock.now - start_time
76
- puts "Handled #{proxy.count} requests; #{(proxy.count.to_f / duration.to_f).round(1)} requests per second."
77
- end
78
- end
79
-
80
- $stderr.puts "Starting server"
81
- server.run
82
- end
83
- ensure
84
- if profile.running?
85
- profile.stop
86
-
87
- # print a flat profile to text
88
- printer = RubyProf::FlatPrinter.new(profile)
89
- printer.print($stdout)
90
- end
91
- end
92
-
93
- #Process.setproctitle("Falcon Controller")
94
- #controller.wait
33
+ hosts.call(controller)
95
34
 
35
+ Process.setproctitle("Falcon Controller")
36
+ controller.wait
@@ -42,7 +42,7 @@ namespace :benchmark do
42
42
 
43
43
  socket = endpoint.connect
44
44
 
45
- request = Async::HTTP::Request.new("localhost", "GET", "/small")
45
+ request = Async::HTTP::Request.new("http", "localhost", "GET", "/small")
46
46
  stream = Async::IO::Stream.new(socket)
47
47
  protocol = Async::HTTP::Protocol::HTTP1.client(stream)
48
48
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: falcon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.14
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-17 00:00:00.000000000 Z
11
+ date: 2018-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-protocol
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.6.0
19
+ version: 0.8.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.6.0
26
+ version: 0.8.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: async-io
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.34.0
47
+ version: 0.36.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 0.34.0
54
+ version: 0.36.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: async-container
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -208,9 +208,11 @@ files:
208
208
  - lib/falcon/adapters/rewindable.rb
209
209
  - lib/falcon/command.rb
210
210
  - lib/falcon/command/serve.rb
211
+ - lib/falcon/command/virtual.rb
211
212
  - lib/falcon/endpoint.rb
212
213
  - lib/falcon/hosts.rb
213
214
  - lib/falcon/proxy.rb
215
+ - lib/falcon/redirection.rb
214
216
  - lib/falcon/server.rb
215
217
  - lib/falcon/verbose.rb
216
218
  - lib/falcon/version.rb