goliath-rack_proxy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fe3f590845b04b15e9d4c493890f3abb0128dbd4
4
+ data.tar.gz: 732ca21c6467a237ff05d39fb22e9fced483b051
5
+ SHA512:
6
+ metadata.gz: 4dd788993d89ccf180919686374de9436c2623a2638def3b132bbdf7e885b7c79d261afb5ef7f9f2e90d25937289fc6a11b12f5eae1f0e6b0b74565408996ced
7
+ data.tar.gz: b48dd539d8afb09d0ff4e3966e5e73eac27f29d952408fc9429b1abb3d748c260352c32f8825010a16a75e11a357a8bb732c1ebc16bec9cc2177edc979aeec76
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Janko Marohnić
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,108 @@
1
+ # Goliath::RackProxy
2
+
3
+ Allows you to use [Goliath] as a web server for your Rack app, giving you streaming requests and responses.
4
+
5
+ ## Motivation
6
+
7
+ While developing [tus-ruby-server], a Rack application that handles large
8
+ uploads and large downloads, I wanted to find an appropriate web server to
9
+ recommend. I needed a web server that supports **streaming uploads**, allowing
10
+ the Rack application to start processing the request while the request body is
11
+ still being received, and that way giving it the ability to save whatever data
12
+ it received before possible request interruptions. I also needed support for
13
+ **streaming downloads**, sending response body in chunks back to the client.
14
+
15
+ The only web server I found that supported all of this was [Unicorn]. However,
16
+ Unicorn needs to spawn a whole process for serving each concurrent request,
17
+ which isn't the most efficent use of server resources.
18
+
19
+ Then I found [Goliath], which I found to provide the most flexibility in
20
+ handling requests. It's built on top of [EventMachine], so it uses threads for
21
+ serving requests, but it can also be run in hybrid mode. However, Goliath is
22
+ more meant to be used standalone than in tandem with another Rack app.
23
+
24
+ So I created a `Goliath::API` subclass that proxies incoming requests to the
25
+ specified Rack app in a streaming fashion, making it act like a web server for
26
+ the app.
27
+
28
+ ## Installation
29
+
30
+ ```rb
31
+ gem "goliath-rack_proxy"
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ Create a file where you will initialize the Goliath Rack proxy:
37
+
38
+ ```rb
39
+ # app.rb
40
+ require "goliath/rack_proxy"
41
+
42
+ class MyGoliathApp < Goliath::RackProxy
43
+ rack_app MyRackApp # provide your #call-able Rack application
44
+ end
45
+ ```
46
+
47
+ You can then run the server by running that Ruby file:
48
+
49
+ ```sh
50
+ $ ruby app.rb
51
+ ```
52
+
53
+ Any command-line arguments passed after the file will be forwarded to the
54
+ Goliath server (see [list of available options][goliath server options]):
55
+
56
+ ```sh
57
+ $ ruby app.rb --port 3000 --stdout
58
+ ```
59
+
60
+ This will run a single EventMachine process, which by default uses a pool of
61
+ 20 threads. You can increase the number of threads EventMachine uses:
62
+
63
+ ```rb
64
+ EventMachine.threadpool_size = 100
65
+ ```
66
+
67
+ You can also spawn multiple EventMachine processes using [Einhorn]:
68
+
69
+ ```sh
70
+ $ einhorn -n COUNT -b 127.0.0.1:3000 ruby app.rb --einhorn
71
+ ```
72
+
73
+ By default `Goliath::RackProxy` will use a rewindable `rack.input`, which means
74
+ the data received from the client will be cached onto disk for the duration of
75
+ the request. If you don't need the `rack.input` to be rewindable and want to
76
+ save on disk I/O, you can disable caching:
77
+
78
+ ```rb
79
+ class MyGoliathApp < Goliath::RackProxy
80
+ rack_app MyRackApp
81
+ rewindable_input false
82
+ end
83
+ ```
84
+
85
+ If you want to report any exceptions that might occur with the Rack app, you can
86
+ override `Goliath::RackProxy#log_exception`:
87
+
88
+ ```rb
89
+ class MyGoliathApp < Goliath::RackProxy
90
+ rack_app MyRackApp
91
+
92
+ def log_exception(exception, env)
93
+ super
94
+ Airbrake.notify(exception)
95
+ end
96
+ end
97
+ ```
98
+
99
+ ## License
100
+
101
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
102
+
103
+ [Goliath]: https://github.com/postrank-labs/goliath
104
+ [EventMachine]: https://github.com/eventmachine/eventmachine
105
+ [tus-ruby-server]: https://github.com/janko-m/tus-ruby-server
106
+ [Unicorn]: https://github.com/defunkt/unicorn
107
+ [goliath server options]: https://github.com/postrank-labs/goliath/wiki/Server
108
+ [Einhorn]: https://github.com/stripe/einhorn
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = "goliath-rack_proxy"
3
+ gem.version = "0.1.0"
4
+
5
+ gem.required_ruby_version = ">= 2.1"
6
+
7
+ gem.summary = "Allows you to use Goliath as a web server for your Rack app, giving you streaming requests and responses."
8
+
9
+ gem.homepage = "https://github.com/janko-m/goliath-rack_proxy"
10
+ gem.authors = ["Janko Marohnić"]
11
+ gem.email = ["janko.marohnic@gmail.com"]
12
+ gem.license = "MIT"
13
+
14
+ gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "*.gemspec"]
15
+ gem.require_path = "lib"
16
+
17
+ gem.add_dependency "goliath", ">= 1.0.5", "< 2"
18
+
19
+ gem.add_development_dependency "rake", "~> 11.1"
20
+ gem.add_development_dependency "minitest", "~> 5.8"
21
+ gem.add_development_dependency "http"
22
+ gem.add_development_dependency "rack", "~> 2.0"
23
+ end
@@ -0,0 +1,240 @@
1
+ # frozen-string-literal: true
2
+ require "goliath/rack_proxy/rack_2_compatibility"
3
+ require "goliath"
4
+ require "tempfile"
5
+
6
+ module Goliath
7
+ class RackProxy < Goliath::API
8
+ # Rack app to proxy the incoming requests to.
9
+ def self.rack_app(app)
10
+ rack_proxy_options[:rack_app] = app
11
+ end
12
+
13
+ # Whether the input should be rewindable, i.e. cached onto disk.
14
+ def self.rewindable_input(value)
15
+ rack_proxy_options[:rewindable_input] = value
16
+ end
17
+
18
+ # Custom user-defined options.
19
+ def self.rack_proxy_options
20
+ @rack_proxy_options ||= {}
21
+ end
22
+
23
+ # Called when request headers were parsed.
24
+ def on_headers(env, headers)
25
+ # assign a streaming input that acts as a bidirectional pipe
26
+ env["rack_proxy.input"] = RackInput.new(rewindable: self.class.rack_proxy_options.fetch(:rewindable_input, true))
27
+ rack_app = self.class.rack_proxy_options.fetch(:rack_app)
28
+
29
+ # start the rack request asynchronously with the created rack input
30
+ async_rack_call rack_app, env.merge("rack.input" => env["rack_proxy.input"])
31
+ end
32
+
33
+ # Called on each request body chunk received from the client.
34
+ def on_body(env, data)
35
+ # write data to the input, which will be read by the Rack app
36
+ env["rack_proxy.input"].write(data)
37
+ end
38
+
39
+ # Called at the end of the request (after #response) or on client disconnect.
40
+ def on_close(env)
41
+ # reading the request body has finished, so we close write end of the input
42
+ env["rack_proxy.input"].close_write
43
+ end
44
+
45
+ # Called after all the data has been received from the client.
46
+ def response(env)
47
+ # reading the request body has finished, so we close write end of the input
48
+ env["rack_proxy.input"].close_write
49
+
50
+ # prevent Goliath from sending a response, we will send it once the
51
+ # asynchronous request to the rack app finishes
52
+ nil
53
+ end
54
+
55
+ private
56
+
57
+ # Spawns a thread and initiates the call to the Rack application, which
58
+ # will be reading from Rack input that is being written to in #on_body. Once
59
+ # the request has finished, we stream the response back to the client.
60
+ def async_rack_call(rack_app, env)
61
+ env["goliath.request"] = env["stream.start"].binding.receiver # https://github.com/postrank-labs/goliath/pull/341
62
+
63
+ # spawn a thread for the request
64
+ EM.defer do
65
+ rack_response = make_request(rack_app, env)
66
+
67
+ # wait for client to stop sending data before sending the response
68
+ env["goliath.request"].callback do
69
+ # spawn a thread for the response
70
+ EM.defer { send_response(rack_response, env) }
71
+ end
72
+ end
73
+ end
74
+
75
+ def make_request(rack_app, env)
76
+ # call the rack app with some patches
77
+ rack_app.call env.merge(
78
+ "rack.url_scheme" => env["options"][:ssl] ? "https" : "http", # https://github.com/postrank-labs/goliath/issues/210
79
+ "async.callback " => nil, # prevent Roda/Sinatra from calling EventMachine while streaming the response
80
+ )
81
+ rescue Exception => exception
82
+ # log the exception that occurred
83
+ log_exception(exception, env)
84
+
85
+ # return a generic error message on production, or a more detailed one otherwise
86
+ body = Goliath.env?(:production) ? ["An error occurred"] : [exception.inspect]
87
+ headers = {"Content-Length" => body[0].bytesize.to_s}
88
+
89
+ [500, headers, body]
90
+ ensure
91
+ # request has finished, so we close the read end of the rack input
92
+ env["rack.input"].close_read
93
+ end
94
+
95
+ # Streams the response to the client.
96
+ def send_response(rack_response, env)
97
+ request = env["goliath.request"]
98
+ connection = request.conn
99
+ response = request.response
100
+
101
+ response.status, response.headers, response.body = rack_response
102
+ response.each { |data| connection.send_data(data) }
103
+
104
+ connection.terminate_request(keep_alive?(env))
105
+ rescue Exception => exception
106
+ # log the exception that occurred
107
+ log_exception(exception, env)
108
+
109
+ # communicate that sending response failed and close the connection
110
+ connection.send_data("HTTP/1.1 500 Internal Server Error\r\n\r\n")
111
+ connection.terminate_request(false)
112
+ ensure
113
+ # log the response information
114
+ log_response(response, env)
115
+ end
116
+
117
+ # Returns whether the TCP connection should be kept alive.
118
+ def keep_alive?(env)
119
+ if env["HTTP_VERSION"] >= "1.1"
120
+ # HTTP 1.1: all requests are persistent requests, client must
121
+ # send a "Connection: close" header to indicate otherwise
122
+ env["HTTP_CONNECTION"].to_s.downcase != "close"
123
+ elsif env["HTTP_VERSION"] == "1.0"
124
+ # HTTP 1.0: all requests are non keep-alive, client must
125
+ # send a "Connection: Keep-Alive" to indicate otherwise
126
+ env["HTTP_CONNECTION"].to_s.downcase == "keep-alive"
127
+ end
128
+ end
129
+
130
+ # Logs the response in the Rack::CommonLogger format.
131
+ def log_response(response, env)
132
+ length = response.headers["Content-Length"]
133
+ length = nil if length.to_s == "0"
134
+
135
+ # log the response as Goliath would log it
136
+ env.logger.info '%s - %s [%s] "%s %s%s %s" %d %s %0.4f' % [
137
+ env["HTTP_X_FORWARDED_FOR"] || env["REMOTE_ADDR"] || "-",
138
+ env["REMOTE_USER"] || "-",
139
+ Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
140
+ env["REQUEST_METHOD"],
141
+ env["PATH_INFO"],
142
+ env["QUERY_STRING"].empty? ? "" : "?#{env["QUERY_STRING"]}",
143
+ env["HTTP_VERSION"],
144
+ response.status,
145
+ length || "-",
146
+ Time.now.to_f - env[:start_time],
147
+ ]
148
+ end
149
+
150
+ # Logs the exception and adds it to the env hash.
151
+ def log_exception(exception, env)
152
+ # mimic how Ruby would display the error
153
+ stderr = "#{exception.backtrace[0]}: #{exception.message} (#{exception.class})\n".dup
154
+ exception.backtrace[1..-1].each do |line|
155
+ stderr << " from #{line}\n"
156
+ end
157
+ env.logger.error(stderr)
158
+
159
+ # save the exception in the env hash
160
+ env["rack.exception"] = exception
161
+ end
162
+
163
+ # IO-like object that acts as a bidirectional pipe, which returns the data
164
+ # that has been written to it.
165
+ class RackInput
166
+ def initialize(rewindable: true)
167
+ @data_queue = Queue.new
168
+ @cache = Tempfile.new("goliath-rack_input") if rewindable
169
+ @buffer = nil
170
+ end
171
+
172
+ # Pops chunks of data from the queue and implements
173
+ # `IO#read(length = nil, outbuf = nil)` semantics.
174
+ def read(length = nil, outbuf = nil)
175
+ data = outbuf.clear if outbuf
176
+ data = @cache.read(length, outbuf) if @cache && !@cache.eof?
177
+
178
+ loop do
179
+ remaining_length = length - data.bytesize if data && length
180
+
181
+ break if remaining_length == 0
182
+
183
+ @buffer = @data_queue.pop or break if @buffer.nil?
184
+
185
+ buffered_data = if remaining_length && remaining_length < @buffer.bytesize
186
+ @buffer.byteslice(0, remaining_length)
187
+ else
188
+ @buffer
189
+ end
190
+
191
+ if data
192
+ data << buffered_data
193
+ else
194
+ data = buffered_data
195
+ end
196
+
197
+ @cache.write(buffered_data) if @cache
198
+
199
+ if buffered_data.bytesize < @buffer.bytesize
200
+ @buffer = @buffer.byteslice(buffered_data.bytesize..-1)
201
+ else
202
+ @buffer = nil
203
+ end
204
+ end
205
+
206
+ data.to_s unless length && (data.nil? || data.empty?)
207
+ end
208
+
209
+ # Pushes data to the queue, which is then popped in #read.
210
+ def write(data)
211
+ @data_queue.push(data) unless @data_queue.closed?
212
+ end
213
+
214
+ # Rewinds the cache IO if it's configured, otherwise raises Errno::ESPIPE
215
+ # exception, which mimics the behaviour of caling #rewind on
216
+ # non-rewindable IOs such as pipes, sockets, and ttys.
217
+ def rewind
218
+ raise Errno::ESPIPE if @cache.nil? # raised by other non-rewindable IOs
219
+ @cache.rewind
220
+ end
221
+
222
+ # Closes the queue and deletes the cache IO.
223
+ def close_read
224
+ @data_queue.close
225
+ @cache.close! if @cache
226
+ end
227
+
228
+ # Closes the queue, which prevents fruther pushing, but #read can still
229
+ # pop remaining chunks from it.
230
+ def close_write
231
+ @data_queue.close
232
+ end
233
+
234
+ # Conforming to the Rack specification.
235
+ def close
236
+ # no-op
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,19 @@
1
+ require "rack"
2
+
3
+ # Async-rack attempts to require files that exist only in Rack 1.x even on Rack
4
+ # 2.x, so we patch that behaviour to allow users to use this gem with Rack 2.x apps.
5
+ module Kernel
6
+ if Rack.release >= "2.0.0"
7
+ alias original_rubygems_require require
8
+
9
+ def require(file)
10
+ case file
11
+ when "rack/commonlogger" then original_rubygems_require("rack/common_logger")
12
+ when "rack/conditionalget" then original_rubygems_require("rack/conditional_get")
13
+ when "rack/showstatus" then original_rubygems_require("rack/show_status")
14
+ else
15
+ original_rubygems_require(file)
16
+ end
17
+ end
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: goliath-rack_proxy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Janko Marohnić
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: goliath
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.5
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.5
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2'
33
+ - !ruby/object:Gem::Dependency
34
+ name: rake
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '11.1'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '11.1'
47
+ - !ruby/object:Gem::Dependency
48
+ name: minitest
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '5.8'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '5.8'
61
+ - !ruby/object:Gem::Dependency
62
+ name: http
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rack
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.0'
89
+ description:
90
+ email:
91
+ - janko.marohnic@gmail.com
92
+ executables: []
93
+ extensions: []
94
+ extra_rdoc_files: []
95
+ files:
96
+ - LICENSE.txt
97
+ - README.md
98
+ - goliath-rack_proxy.gemspec
99
+ - lib/goliath/rack_proxy.rb
100
+ - lib/goliath/rack_proxy/rack_2_compatibility.rb
101
+ homepage: https://github.com/janko-m/goliath-rack_proxy
102
+ licenses:
103
+ - MIT
104
+ metadata: {}
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '2.1'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project:
121
+ rubygems_version: 2.6.11
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: Allows you to use Goliath as a web server for your Rack app, giving you streaming
125
+ requests and responses.
126
+ test_files: []