falcon 0.34.5 → 0.35.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -2
  3. data/Gemfile +2 -0
  4. data/bin/falcon +1 -1
  5. data/bin/falcon-host +1 -1
  6. data/examples/beer/config.ru +25 -23
  7. data/examples/beer/falcon.rb +2 -0
  8. data/examples/hello/config.ru +1 -1
  9. data/examples/hello/falcon.rb +14 -2
  10. data/examples/hello/preload.rb +6 -0
  11. data/examples/trailers/config.ru +33 -0
  12. data/examples/trailers/falcon.rb +7 -0
  13. data/falcon.gemspec +3 -1
  14. data/lib/falcon.rb +0 -4
  15. data/lib/falcon/adapters/response.rb +2 -2
  16. data/lib/falcon/command.rb +3 -53
  17. data/lib/falcon/command/host.rb +22 -39
  18. data/lib/falcon/command/paths.rb +45 -0
  19. data/lib/falcon/{host.rb → command/proxy.rb} +39 -45
  20. data/lib/falcon/command/redirect.rb +72 -0
  21. data/lib/falcon/command/serve.rb +28 -58
  22. data/lib/falcon/command/supervisor.rb +5 -5
  23. data/lib/falcon/command/top.rb +79 -0
  24. data/lib/falcon/command/virtual.rb +18 -53
  25. data/lib/falcon/configuration.rb +1 -1
  26. data/lib/falcon/{configurations/host.rb → configuration/application.rb} +13 -11
  27. data/lib/falcon/{configurations → configuration}/lets_encrypt_tls.rb +0 -0
  28. data/lib/falcon/{configurations → configuration}/proxy.rb +2 -2
  29. data/lib/falcon/{configurations → configuration}/rack.rb +2 -2
  30. data/lib/falcon/{configurations → configuration}/self_signed_tls.rb +0 -0
  31. data/lib/falcon/{configurations → configuration}/supervisor.rb +2 -2
  32. data/lib/falcon/{configurations → configuration}/tls.rb +0 -0
  33. data/lib/falcon/controller/host.rb +58 -0
  34. data/lib/falcon/controller/proxy.rb +102 -0
  35. data/lib/falcon/{service.rb → controller/redirect.rb} +37 -24
  36. data/lib/falcon/controller/serve.rb +112 -0
  37. data/lib/falcon/controller/virtual.rb +89 -0
  38. data/lib/falcon/middleware/proxy.rb +143 -0
  39. data/lib/falcon/{redirection.rb → middleware/redirect.rb} +31 -29
  40. data/lib/falcon/proxy_endpoint.rb +1 -1
  41. data/lib/falcon/service/application.rb +113 -0
  42. data/lib/falcon/service/generic.rb +53 -0
  43. data/lib/falcon/service/supervisor.rb +95 -0
  44. data/lib/falcon/services.rb +32 -5
  45. data/lib/falcon/version.rb +1 -1
  46. data/lib/rack/handler/falcon.rb +2 -1
  47. data/logo-square.afdesign +0 -0
  48. metadata +43 -17
  49. data/lib/falcon/hosts.rb +0 -135
  50. data/lib/falcon/proxy.rb +0 -141
  51. data/lib/falcon/supervisor.rb +0 -106
@@ -0,0 +1,143 @@
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 'protocol/http/headers'
23
+ require 'protocol/http/middleware'
24
+
25
+ module Falcon
26
+ module Middleware
27
+ module BadRequest
28
+ def self.call(request)
29
+ return Protocol::HTTP::Response[400, {}, []]
30
+ end
31
+
32
+ def self.close
33
+ end
34
+ end
35
+
36
+ class Proxy < Protocol::HTTP::Middleware
37
+ FORWARDED = 'forwarded'.freeze
38
+ X_FORWARDED_FOR = 'x-forwarded-for'.freeze
39
+ X_FORWARDED_PROTO = 'x-forwarded-proto'.freeze
40
+
41
+ VIA = 'via'.freeze
42
+ CONNECTION = 'connection'.freeze
43
+
44
+ HOP_HEADERS = [
45
+ 'connection',
46
+ 'keep-alive',
47
+ 'public',
48
+ 'proxy-authenticate',
49
+ 'transfer-encoding',
50
+ 'upgrade',
51
+ ]
52
+
53
+ def initialize(app, hosts)
54
+ super(app)
55
+
56
+ @server_context = nil
57
+
58
+ @hosts = hosts
59
+ @clients = {}
60
+
61
+ @count = 0
62
+ end
63
+
64
+ attr :count
65
+
66
+ def close
67
+ @clients.each_value(&:close)
68
+
69
+ super
70
+ end
71
+
72
+ def connect(endpoint)
73
+ @clients[endpoint] ||= Async::HTTP::Client.new(endpoint)
74
+ end
75
+
76
+ def lookup(request)
77
+ # Trailing dot and port is ignored/normalized.
78
+ if authority = request.authority&.sub(/(\.)?(:\d+)?$/, '')
79
+ return @hosts[authority]
80
+ end
81
+ end
82
+
83
+ def prepare_headers(headers)
84
+ if connection = headers[CONNECTION]
85
+ headers.extract(connection)
86
+ end
87
+
88
+ headers.extract(HOP_HEADERS)
89
+ end
90
+
91
+ def prepare_request(request, host)
92
+ forwarded = []
93
+
94
+ Async.logger.debug(self) do |buffer|
95
+ buffer.puts "Request authority: #{request.authority}"
96
+ buffer.puts "Host authority: #{host.authority}"
97
+ buffer.puts "Request: #{request.method} #{request.path} #{request.version}"
98
+ buffer.puts "Request headers: #{request.headers.inspect}"
99
+ end
100
+
101
+ # The authority of the request must match the authority of the endpoint we are proxying to, otherwise SNI and other things won't work correctly.
102
+ request.authority = host.authority
103
+
104
+ if address = request.remote_address
105
+ request.headers.add(X_FORWARDED_FOR, address.ip_address)
106
+ forwarded << "for=#{address.ip_address}"
107
+ end
108
+
109
+ if scheme = request.scheme
110
+ request.headers.add(X_FORWARDED_PROTO, scheme)
111
+ forwarded << "proto=#{scheme}"
112
+ end
113
+
114
+ unless forwarded.empty?
115
+ request.headers.add(FORWARDED, forwarded.join(';'))
116
+ end
117
+
118
+ request.headers.add(VIA, "#{request.version} #{self.class}")
119
+
120
+ self.prepare_headers(request.headers)
121
+
122
+ return request
123
+ end
124
+
125
+ def call(request)
126
+ if host = lookup(request)
127
+ @count += 1
128
+
129
+ request = self.prepare_request(request, host)
130
+
131
+ client = connect(host.endpoint)
132
+
133
+ client.call(request)
134
+ else
135
+ super
136
+ end
137
+ rescue
138
+ Async.logger.error(self) {$!}
139
+ return Protocol::HTTP::Response[502, {'content-type' => 'text/plain'}, ["#{$!.inspect}: #{$!.backtrace.join("\n")}"]]
140
+ end
141
+ end
142
+ end
143
+ end
@@ -21,41 +21,43 @@
21
21
  require 'async/http/client'
22
22
 
23
23
  module Falcon
24
- module NotFound
25
- def self.call(request)
26
- return Protocol::HTTP::Response[404, {}, []]
27
- end
28
-
29
- def self.close
30
- end
31
- end
32
-
33
- class Redirection < Protocol::HTTP::Middleware
34
- def initialize(app, hosts, endpoint)
35
- super(app)
24
+ module Middleware
25
+ module NotFound
26
+ def self.call(request)
27
+ return Protocol::HTTP::Response[404, {}, []]
28
+ end
36
29
 
37
- @hosts = hosts
38
- @endpoint = endpoint
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]
30
+ def self.close
45
31
  end
46
32
  end
47
33
 
48
- def call(request)
49
- if host = lookup(request)
50
- if @endpoint.default_port?
51
- location = "#{@endpoint.scheme}://#{host.authority}#{request.path}"
34
+ class Redirect < Protocol::HTTP::Middleware
35
+ def initialize(app, hosts, endpoint)
36
+ super(app)
37
+
38
+ @hosts = hosts
39
+ @endpoint = endpoint
40
+ end
41
+
42
+ def lookup(request)
43
+ # Trailing dot and port is ignored/normalized.
44
+ if authority = request.authority&.sub(/(\.)?(:\d+)?$/, '')
45
+ return @hosts[authority]
46
+ end
47
+ end
48
+
49
+ def call(request)
50
+ if host = lookup(request)
51
+ if @endpoint.default_port?
52
+ location = "#{@endpoint.scheme}://#{host.authority}#{request.path}"
53
+ else
54
+ location = "#{@endpoint.scheme}://#{host.authority}:#{@endpoint.port}#{request.path}"
55
+ end
56
+
57
+ return Protocol::HTTP::Response[301, [['location', location]], []]
52
58
  else
53
- location = "#{@endpoint.scheme}://#{host.authority}:#{@endpoint.port}#{request.path}"
59
+ super
54
60
  end
55
-
56
- return Protocol::HTTP::Response[301, [['location', location]], []]
57
- else
58
- super
59
61
  end
60
62
  end
61
63
  end
@@ -58,7 +58,7 @@ module Falcon
58
58
  return to_enum unless block_given?
59
59
 
60
60
  @endpoint.each do |endpoint|
61
- yield self.class.new(endpoint, @options)
61
+ yield self.class.new(endpoint, **@options)
62
62
  end
63
63
  end
64
64
 
@@ -0,0 +1,113 @@
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 'generic'
22
+
23
+ require 'async/http/endpoint'
24
+ require 'async/io/shared_endpoint'
25
+
26
+ module Falcon
27
+ module Service
28
+ class Application < Generic
29
+ def initialize(environment)
30
+ super
31
+
32
+ @bound_endpoint = nil
33
+ end
34
+
35
+ def name
36
+ "#{self.class} for #{self.authority}"
37
+ end
38
+
39
+ def authority
40
+ @evaluator.authority
41
+ end
42
+
43
+ def endpoint
44
+ @evaluator.endpoint
45
+ end
46
+
47
+ def ssl_context
48
+ @evaluator.ssl_context
49
+ end
50
+
51
+ def root
52
+ @evaluator.root
53
+ end
54
+
55
+ def middleware
56
+ @evaluator.middleware
57
+ end
58
+
59
+ def protocol
60
+ endpoint.protocol
61
+ end
62
+
63
+ def scheme
64
+ endpoint.scheme
65
+ end
66
+
67
+ def preload!
68
+ if scripts = @evaluator.preload
69
+ scripts.each do |path|
70
+ Async.logger.info(self) {"Preloading #{path}..."}
71
+ full_path = File.expand_path(path, self.root)
72
+ load(full_path)
73
+ end
74
+ end
75
+ end
76
+
77
+ def to_s
78
+ "#{self.class} #{@evaluator.authority}"
79
+ end
80
+
81
+ def start
82
+ Async.logger.info(self) {"Binding to #{self.endpoint}..."}
83
+
84
+ @bound_endpoint = Async::Reactor.run do
85
+ Async::IO::SharedEndpoint.bound(self.endpoint)
86
+ end.wait
87
+
88
+ preload!
89
+ end
90
+
91
+ def setup(container)
92
+ container.run(name: self.name, restart: true) do |instance|
93
+ Async(logger: logger) do |task|
94
+ Async.logger.info(self) {"Starting application server for #{self.root}..."}
95
+
96
+ server = Server.new(self.middleware, @bound_endpoint, self.protocol, self.scheme)
97
+
98
+ server.run
99
+
100
+ instance.ready!
101
+
102
+ task.children.each(&:wait)
103
+ end
104
+ end
105
+ end
106
+
107
+ def stop
108
+ @bound_endpoint&.close
109
+ @bound_endpoint = nil
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,53 @@
1
+ # Copyright, 201, 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
+ module Falcon
22
+ module Service
23
+ class Generic
24
+ def self.wrap(environment)
25
+ evaluator = environment.evaluator
26
+ service = evaluator.service || self
27
+
28
+ return service.new(environment)
29
+ end
30
+
31
+ def initialize(environment)
32
+ @environment = environment
33
+ @evaluator = @environment.evaluator
34
+ end
35
+
36
+ def include?(keys)
37
+ keys.all?{|key| @environment.include?(key)}
38
+ end
39
+
40
+ def name
41
+ @evaluator.name
42
+ end
43
+
44
+ def logger
45
+ return Async.logger # .with(name: name)
46
+ end
47
+
48
+ def to_s
49
+ self.class.name
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,95 @@
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/io/endpoint'
22
+ require 'process/metrics'
23
+ require 'json'
24
+
25
+ module Falcon
26
+ module Service
27
+ class Supervisor < Generic
28
+ def initialize(environment)
29
+ super
30
+
31
+ @bound_endpoint = nil
32
+ end
33
+
34
+ def endpoint
35
+ @evaluator.endpoint
36
+ end
37
+
38
+ def do_restart(message)
39
+ # Tell the parent of this process group to spin up a new process group/container.
40
+ # Wait for that to start accepting new connections.
41
+ # Stop accepting connections.
42
+ # Wait for existing connnections to drain.
43
+ # Terminate this process group.
44
+
45
+ signal = message[:signal] || :INT
46
+
47
+ # Sepukku:
48
+ Process.kill(signal, -Process.getpgrp)
49
+ end
50
+
51
+ def do_metrics(message)
52
+ Process::Metrics.capture(pid: Process.ppid, ppid: Process.ppid)
53
+ end
54
+
55
+ def handle(message)
56
+ case message[:please]
57
+ when 'restart'
58
+ self.do_restart(message)
59
+ when 'metrics'
60
+ self.do_metrics(message)
61
+ end
62
+ end
63
+
64
+ def start
65
+ Async.logger.info(self) {"Binding to #{self.endpoint}..."}
66
+
67
+ @bound_endpoint = Async::Reactor.run do
68
+ Async::IO::SharedEndpoint.bound(self.endpoint)
69
+ end.wait
70
+ end
71
+
72
+ def setup(container)
73
+ container.run(name: self.name, restart: true, count: 1) do |instance|
74
+ Async do
75
+ @bound_endpoint.accept do |peer|
76
+ stream = Async::IO::Stream.new(peer)
77
+
78
+ while message = stream.gets("\0")
79
+ response = handle(JSON.parse(message, symbolize_names: true))
80
+ stream.puts(response.to_json, separator: "\0")
81
+ end
82
+ end
83
+
84
+ instance.ready!
85
+ end
86
+ end
87
+ end
88
+
89
+ def stop
90
+ @bound_endpoint&.close
91
+ @bound_endpoint = nil
92
+ end
93
+ end
94
+ end
95
+ end