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 +4 -4
- data/README.md +13 -3
- data/falcon.gemspec +2 -2
- data/lib/falcon/adapters/rack.rb +64 -37
- data/lib/falcon/adapters/rewindable.rb +3 -1
- data/lib/falcon/command.rb +4 -2
- data/lib/falcon/command/serve.rb +2 -2
- data/lib/falcon/command/virtual.rb +126 -0
- data/lib/falcon/hosts.rb +105 -2
- data/lib/falcon/proxy.rb +28 -6
- data/lib/falcon/redirection.rb +58 -0
- data/lib/falcon/version.rb +1 -1
- data/lib/rack/handler/falcon.rb +3 -1
- data/server.rb +3 -62
- data/tasks/benchmark.rake +1 -1
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 16eb6bd0bff8f6177b4f110a0d7fc07da2a1b190a4c0e0aa77bbe86978bf9331
|
4
|
+
data.tar.gz: b3520e014ed9f70fe6f87ba9d5cefbe69d27b173bc79c1d3cf4cc36e9960c967
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
55
|
+
#### Thread Safety
|
55
56
|
|
56
|
-
With older
|
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
|
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
|
|
data/falcon.gemspec
CHANGED
@@ -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.
|
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.
|
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")
|
data/lib/falcon/adapters/rack.rb
CHANGED
@@ -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
|
-
|
92
|
+
::Rack::RACK_VERSION => [2, 0, 0],
|
54
93
|
|
55
|
-
|
56
|
-
|
94
|
+
::Rack::RACK_INPUT => Input.new(request.body),
|
95
|
+
::Rack::RACK_ERRORS => $stderr,
|
57
96
|
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
111
|
+
::Rack::QUERY_STRING => query_string || '',
|
73
112
|
|
74
|
-
# The server protocol
|
75
|
-
|
76
|
-
|
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
|
-
|
80
|
-
|
120
|
+
::Rack::SERVER_NAME => server_name || '',
|
121
|
+
::Rack::SERVER_PORT => server_port || '',
|
81
122
|
}
|
82
123
|
|
83
|
-
|
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[
|
127
|
+
env[::Rack::RACK_IS_HIJACK] = true
|
102
128
|
|
103
|
-
env[
|
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
|
-
|
136
|
+
# This is implicitly returned:
|
137
|
+
env[::Rack::RACK_HIJACK_IO] = io
|
111
138
|
end
|
112
139
|
else
|
113
|
-
env[
|
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
|
-
|
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
|
-
|
61
|
+
return super
|
60
62
|
end
|
61
63
|
end
|
62
64
|
end
|
data/lib/falcon/command.rb
CHANGED
@@ -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
|
-
|
45
|
+
'virtual' => Virtual
|
46
|
+
}, default: 'serve'
|
45
47
|
|
46
48
|
def verbose?
|
47
49
|
@options[:logging] == :verbose
|
data/lib/falcon/command/serve.rb
CHANGED
@@ -90,7 +90,7 @@ module Falcon
|
|
90
90
|
end
|
91
91
|
|
92
92
|
def run(verbose = false)
|
93
|
-
app,
|
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
|
data/lib/falcon/hosts.rb
CHANGED
@@ -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
|
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
|
data/lib/falcon/proxy.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/falcon/version.rb
CHANGED
data/lib/rack/handler/falcon.rb
CHANGED
@@ -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.
|
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
|
data/tasks/benchmark.rake
CHANGED
@@ -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.
|
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-
|
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.
|
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.
|
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.
|
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.
|
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
|