bowtie-io-rack-streaming-proxy 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +95 -0
- data/README.txt +78 -0
- data/Rakefile +1 -0
- data/dev/client.rb +24 -0
- data/dev/proxy.ru +13 -0
- data/dev/streamer.ru +54 -0
- data/lib/rack/streaming_proxy.rb +2 -0
- data/lib/rack/streaming_proxy/errors.rb +5 -0
- data/lib/rack/streaming_proxy/proxy.rb +120 -0
- data/lib/rack/streaming_proxy/request.rb +80 -0
- data/lib/rack/streaming_proxy/response.rb +79 -0
- data/lib/rack/streaming_proxy/session.rb +119 -0
- data/lib/rack/streaming_proxy/version.rb +5 -0
- data/rack-streaming-proxy.gemspec +34 -0
- data/spec/app.ru +55 -0
- data/spec/proxy.ru +9 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/streaming_proxy_spec.rb +230 -0
- metadata +201 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cea9bd8a7ee62c0e0606871db98bba8f5de3e3e8
|
4
|
+
data.tar.gz: 7c5cfcf36d77ed6e53a6f478a2fce63f9b7037bd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5e1f9c1c0f8471331bdd2baead76a7754d1a88aad4691b15757bdf892814760faa2118135c5a9cb1caf065f931490f3e2a54f4eed2a8bf66dcec6a5fd20ab1e0
|
7
|
+
data.tar.gz: 1e78c5b5686d47e41d933658c8af6b813ea10dc7a2ceeaec74c74b5273569cee33f7719fab178f677a4fe68baaf5bcb9f05c8c030455fbb8fa01630ba002d7b2
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2009-2013 Fred Ngo, Nathan Witmer
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# Rack::StreamingProxy
|
2
|
+
|
3
|
+
A transparent streaming proxy to be used as rack middleware.
|
4
|
+
|
5
|
+
* Streams the response from the downstream server to minimize memory usage
|
6
|
+
* Handles chunked encoding if used
|
7
|
+
* Proxies GET/PUT/POST/DELETE, XHR, and cookies
|
8
|
+
|
9
|
+
Now updated to be compatible with Rails 3 and 4, and fixes major concurrency issues that were present in 1.0.
|
10
|
+
|
11
|
+
Use Rack::StreamingProxy when you need to have the response streamed back to the client, for example when handling large file requests that could be proxied directly but need to be authenticated against the rest of your middleware stack.
|
12
|
+
|
13
|
+
Note that this will not work well with EventMachine. EM buffers the entire rack response before sending it to the client. When testing, try Unicorn or Passenger rather than the EM-based Thin (See [discussion](http://groups.google.com/group/thin-ruby/browse_thread/thread/4762f8f851b965f6)).
|
14
|
+
|
15
|
+
A simple streamer app has been included for testing and development.
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
To use inside a Rails app, add a `config/initializers/streaming_proxy.rb` initialization file, and place in it:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
require 'rack/streaming_proxy'
|
23
|
+
|
24
|
+
YourRailsApp::Application.configure do
|
25
|
+
config.streaming_proxy.logger = Rails.logger # stdout by default
|
26
|
+
config.streaming_proxy.log_verbosity = Rails.env.production? ? :low : :high # :low or :high, :low by default
|
27
|
+
config.streaming_proxy.num_retries_on_5xx = 5 # 0 by default
|
28
|
+
config.streaming_proxy.raise_on_5xx = true # false by default
|
29
|
+
|
30
|
+
# Will be inserted at the end of the middleware stack by default.
|
31
|
+
config.middleware.use Rack::StreamingProxy::Proxy do |request|
|
32
|
+
|
33
|
+
# Inside the request block, return the full URI to redirect the request to,
|
34
|
+
# or nil/false if the request should continue on down the middleware stack.
|
35
|
+
if request.path.start_with?('/search')
|
36
|
+
"http://www.some-other-service.com/search?#{request.query}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
To use as a Rack app:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require 'rack/streaming_proxy'
|
46
|
+
|
47
|
+
use Rack::StreamingProxy::Proxy do |request|
|
48
|
+
if request.path.start_with?('/proxy')
|
49
|
+
# You probably want to get rid of the '/proxy' in the path, when requesting from the destination.
|
50
|
+
proxy_path = request.path.sub %r{^/proxy}, ''
|
51
|
+
"http://www.another-server.com#{proxy_path}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
## Installation
|
57
|
+
|
58
|
+
Add this line to your application's Gemfile:
|
59
|
+
|
60
|
+
gem 'rack-streaming-proxy'
|
61
|
+
|
62
|
+
And then execute:
|
63
|
+
|
64
|
+
$ bundle
|
65
|
+
|
66
|
+
Or install it yourself as:
|
67
|
+
|
68
|
+
$ gem install rack-streaming-proxy
|
69
|
+
|
70
|
+
## Requirements
|
71
|
+
|
72
|
+
* Ruby = 1.9.3
|
73
|
+
* rack >= 1.4
|
74
|
+
* servolux ~> 0.10
|
75
|
+
|
76
|
+
These requirements (other than Ruby) will be automatically installed via Bundler.
|
77
|
+
|
78
|
+
This gem has not been tested with versions lower than those indicated.
|
79
|
+
|
80
|
+
This gem works with Ubuntu 10.04. It has not been tested with later versions of Ubuntu or other Linuxes, but it should work just fine. It has not been tested with OS X but should work as well. However, I doubt it will work on any version of Windows, as it does process-based stuff. You are most welcome to try it and report back.
|
81
|
+
|
82
|
+
## Contributing
|
83
|
+
|
84
|
+
1. Fork it
|
85
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
86
|
+
3. Implement your changes, and make sure to add tests!
|
87
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
88
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
89
|
+
6. Create new Pull Request
|
90
|
+
|
91
|
+
## Thanks To
|
92
|
+
|
93
|
+
* [Nathan Witmer](http://github.com/zerowidth) for the 1.0 implementation of [Rack::StreamingProxy](http://github.com/zerowidth/rack-streaming-proxy)
|
94
|
+
* [Tom Lea](http://github.com/cwninja) for [Rack::Proxy](http://gist.github.com/207938), which inspired Rack::StreamingProxy.
|
95
|
+
* [Tim Pease](http://github.com/TwP) for [Servolux](https://github.com/Twp/servolux)
|
data/README.txt
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
rack-streaming-proxy
|
2
|
+
by Nathan Witmer <nwitmer@gmail.com>
|
3
|
+
http://github.com/zerowidth/rack-streaming-proxy
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn.
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
Provides a transparent streaming proxy to be used as rack middleware.
|
12
|
+
|
13
|
+
* Streams the response from the downstream server to minimize memory usage
|
14
|
+
* Handles chunked encoding if used
|
15
|
+
* Proxies GET/PUT/POST/DELETE, XHR, and cookies
|
16
|
+
|
17
|
+
Use this when you need to have the response streamed back to the client,
|
18
|
+
for example when handling large file requests that could be proxied
|
19
|
+
directly but need to be authenticated against the rest of your middleware
|
20
|
+
stack.
|
21
|
+
|
22
|
+
Note that this will not work well with EventMachine. EM buffers the entire
|
23
|
+
rack response before sending it to the client. When testing, try
|
24
|
+
mongrel (via rackup) or passenger, rather than the EM-based thin. See
|
25
|
+
http://groups.google.com/group/thin-ruby/browse_thread/thread/4762f8f851b965f6
|
26
|
+
for more discussion.
|
27
|
+
|
28
|
+
I've included a simple streamer app for testing and development.
|
29
|
+
|
30
|
+
Thanks to:
|
31
|
+
|
32
|
+
* Tom Lea (cwninja) for Rack::Proxy (http://gist.github.com/207938)
|
33
|
+
* Tim Pease for bones, servolux, &c
|
34
|
+
|
35
|
+
== SYNOPSIS:
|
36
|
+
|
37
|
+
require "rack/streaming_proxy"
|
38
|
+
|
39
|
+
use Rack::StreamingProxy do |request|
|
40
|
+
# inside the request block, return the full URI to redirect the request to,
|
41
|
+
# or nil/false if the request should continue on down the middleware stack.
|
42
|
+
if request.path.start_with?("/proxy")
|
43
|
+
"http://another_server#{request.path}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
== REQUIREMENTS:
|
48
|
+
|
49
|
+
* servolux (gem install servolux)
|
50
|
+
|
51
|
+
== INSTALL:
|
52
|
+
|
53
|
+
* sudo gem install rack-streaming-proxy --source http://gemcutter.org
|
54
|
+
|
55
|
+
== LICENSE:
|
56
|
+
|
57
|
+
(The MIT License)
|
58
|
+
|
59
|
+
Copyright (c) 2009 Nathan Witmer
|
60
|
+
|
61
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
62
|
+
a copy of this software and associated documentation files (the
|
63
|
+
'Software'), to deal in the Software without restriction, including
|
64
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
65
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
66
|
+
permit persons to whom the Software is furnished to do so, subject to
|
67
|
+
the following conditions:
|
68
|
+
|
69
|
+
The above copyright notice and this permission notice shall be
|
70
|
+
included in all copies or substantial portions of the Software.
|
71
|
+
|
72
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
73
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
74
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
75
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
76
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
77
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
78
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/dev/client.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
# cf. https://gist.github.com/sonots/7751554
|
4
|
+
|
5
|
+
require 'net/http'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
# unicorn spec/app.ru -p 4321
|
9
|
+
# unicorn spec/proxy.ru -p 4322
|
10
|
+
PORT = ARGV[0] || 8080
|
11
|
+
|
12
|
+
http = Net::HTTP.new "localhost", PORT
|
13
|
+
request = Net::HTTP::Get.new "/slow_stream"
|
14
|
+
#request['Transfer-Encoding'] = 'chunked'
|
15
|
+
request['Connection'] = 'keep-alive'
|
16
|
+
http.request(request){|response|
|
17
|
+
puts "content-length: #{response.content_length}"
|
18
|
+
body = []
|
19
|
+
response.read_body{|x|
|
20
|
+
body << Time.now
|
21
|
+
puts "read_block: #{body.length}, #{x.size}byte(s)"
|
22
|
+
}
|
23
|
+
puts body
|
24
|
+
}
|
data/dev/proxy.ru
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path(
|
2
|
+
File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
|
3
|
+
|
4
|
+
use Rack::Reloader, 1
|
5
|
+
# use Rack::CommonLogger # rackup already has commonlogger loaded
|
6
|
+
use Rack::Lint
|
7
|
+
use Rack::StreamingProxy do |req|
|
8
|
+
url = "http://localhost:4000#{req.path}"
|
9
|
+
url << "?#{req.query_string}" unless req.query_string.empty?
|
10
|
+
url
|
11
|
+
end
|
12
|
+
|
13
|
+
run lambda { |env| [200, {"Content-Type" => "text/plain"}, ""] }
|
data/dev/streamer.ru
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
class Streamer
|
2
|
+
include Rack::Utils
|
3
|
+
|
4
|
+
def call(env)
|
5
|
+
req = Rack::Request.new(env)
|
6
|
+
headers = {"Content-Type" => "text/plain"}
|
7
|
+
|
8
|
+
@chunked = req.path.start_with?("/chunked")
|
9
|
+
|
10
|
+
if count = req.path.match(/(\d+)$/)
|
11
|
+
count = count[0].to_i
|
12
|
+
else
|
13
|
+
count = 100
|
14
|
+
end
|
15
|
+
@strings = count.times.collect {|n| "~~~~~ #{n} ~~~~~\n" }
|
16
|
+
|
17
|
+
if chunked?
|
18
|
+
headers["Transfer-Encoding"] = "chunked"
|
19
|
+
else
|
20
|
+
headers["Content-Length"] = @strings.inject(0) {|sum, s| sum += bytesize(s)}.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
[200, headers, self.dup]
|
24
|
+
end
|
25
|
+
|
26
|
+
def each
|
27
|
+
term = "\r\n"
|
28
|
+
@strings.each do |chunk|
|
29
|
+
if chunked?
|
30
|
+
size = bytesize(chunk)
|
31
|
+
yield [size.to_s(16), term, chunk, term].join
|
32
|
+
else
|
33
|
+
yield chunk
|
34
|
+
end
|
35
|
+
sleep 0.05
|
36
|
+
end
|
37
|
+
yield ["0", term, "", term].join if chunked?
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def chunked?
|
43
|
+
@chunked
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# use Rack::CommonLogger # rackup already has commonlogger loaded
|
48
|
+
use Rack::Lint
|
49
|
+
|
50
|
+
# GET /
|
51
|
+
# GET /10
|
52
|
+
# GET /chunked
|
53
|
+
# GET /chunked/10
|
54
|
+
run Streamer.new
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'logger'
|
3
|
+
require 'rack/streaming_proxy/session'
|
4
|
+
require 'rack/streaming_proxy/request'
|
5
|
+
require 'rack/streaming_proxy/response'
|
6
|
+
|
7
|
+
class Rack::StreamingProxy::Proxy
|
8
|
+
class << self
|
9
|
+
# Logs to stdout by default unless configured with another logger via Railtie.
|
10
|
+
def logger
|
11
|
+
Logger.new(STDOUT)
|
12
|
+
end
|
13
|
+
|
14
|
+
# At :low verbosity by default -- will not output :debug level messages.
|
15
|
+
# :high verbosity outputs :debug level messages.
|
16
|
+
# This is independent of the Logger's log_level, as set in Rails, for example,
|
17
|
+
# although the Logger's level can override this setting.
|
18
|
+
def log_verbosity
|
19
|
+
:low
|
20
|
+
end
|
21
|
+
|
22
|
+
# No retries are performed by default.
|
23
|
+
def num_retries_on_5xx
|
24
|
+
0
|
25
|
+
end
|
26
|
+
|
27
|
+
# If the proxy cannot recover from 5xx's through retries (see num_retries_on_5xx),
|
28
|
+
# then it by default passes through the content from the destination
|
29
|
+
# e.g. the Apache error page. If you want an exception to be raised instead so
|
30
|
+
# you can handle it yourself (i.e. display your own error page), set raise_on_5xx to true.
|
31
|
+
def raise_on_5xx
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def log(level, message)
|
36
|
+
unless log_verbosity == :low && level == :debug
|
37
|
+
logger.send level, "[Rack::StreamingProxy] #{message}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# The block provided to the initializer is given a Rack::Request
|
43
|
+
# and should return:
|
44
|
+
#
|
45
|
+
# * nil/false to skip the proxy and continue down the stack
|
46
|
+
# * a complete uri (with query string if applicable) to proxy to
|
47
|
+
#
|
48
|
+
# Example:
|
49
|
+
#
|
50
|
+
# use Rack::StreamingProxy::Proxy do |req|
|
51
|
+
# if req.path.start_with?('/search')
|
52
|
+
# "http://some_other_service/search?#{req.query}"
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# Most headers, request body, and HTTP method are preserved.
|
57
|
+
#
|
58
|
+
def initialize(app, &block)
|
59
|
+
@app = app
|
60
|
+
@block = block
|
61
|
+
end
|
62
|
+
|
63
|
+
def call(env)
|
64
|
+
current_request = Rack::Request.new(env)
|
65
|
+
destination_uri = destination_uri(current_request)
|
66
|
+
|
67
|
+
# Decide whether this request should be proxied.
|
68
|
+
if destination_uri
|
69
|
+
self.class.log :info, "Starting proxy request to: #{destination_uri}"
|
70
|
+
|
71
|
+
request = Rack::StreamingProxy::Request.new(destination_uri, current_request)
|
72
|
+
begin
|
73
|
+
response = Rack::StreamingProxy::Session.new(request).start
|
74
|
+
rescue Exception => e # Rescuing only for the purpose of logging to rack.errors
|
75
|
+
log_rack_error(env, e)
|
76
|
+
raise e
|
77
|
+
end
|
78
|
+
|
79
|
+
# Notify client http version to the instance of Response class.
|
80
|
+
response.client_http_version = env['HTTP_VERSION'].sub(/HTTP\//, '') if env.has_key?('HTTP_VERSION')
|
81
|
+
# Ideally, both a Content-Length header field and a Transfer-Encoding
|
82
|
+
# header field are not expected to be present from servers which
|
83
|
+
# are compliant with RFC2616. However, irresponsible servers may send
|
84
|
+
# both to rack-streaming-proxy.
|
85
|
+
# RFC2616 says if a message is received with both a Transfer-Encoding
|
86
|
+
# header field and a Content-Length header field, the latter MUST be
|
87
|
+
# ignored. So I deleted a Content-Length header here.
|
88
|
+
#
|
89
|
+
# Though there is a case that rack-streaming-proxy deletes both a
|
90
|
+
# Content-Length and a Transfer-Encoding, a client can acknowledge the
|
91
|
+
# end of body by closing the connection when the entire response has
|
92
|
+
# been sent without a Content-Length header. So a Content-Length header
|
93
|
+
# does not have to be required here in our understaing.
|
94
|
+
response.headers.delete('Content-Length') if response.headers.has_key?('Transfer-Encoding')
|
95
|
+
if env.has_key?('HTTP_VERSION') && env['HTTP_VERSION'] < 'HTTP/1.1'
|
96
|
+
# Be compliant with RFC2146
|
97
|
+
response.headers.delete('Transfer-Encoding')
|
98
|
+
end
|
99
|
+
|
100
|
+
self.class.log :info, "Finishing proxy request to: #{destination_uri}"
|
101
|
+
[response.status, response.headers, response]
|
102
|
+
|
103
|
+
# Continue down the middleware stack if the request is not to be proxied.
|
104
|
+
else
|
105
|
+
@app.call(env)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
def destination_uri(rack_request)
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
|
114
|
+
def log_rack_error(env, e)
|
115
|
+
env['rack.errors'].puts e.message
|
116
|
+
env['rack.errors'].puts e.backtrace #.collect { |line| "\t" + line }
|
117
|
+
env['rack.errors'].flush
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/https'
|
3
|
+
|
4
|
+
class Rack::StreamingProxy::Request
|
5
|
+
|
6
|
+
attr_reader :http_request, :rack_request
|
7
|
+
|
8
|
+
def initialize(destination_uri, current_request)
|
9
|
+
@destination_uri = URI.parse(destination_uri)
|
10
|
+
@rack_request = current_request
|
11
|
+
@http_request = translate_request(current_request, @destination_uri)
|
12
|
+
end
|
13
|
+
|
14
|
+
def host
|
15
|
+
@destination_uri.host
|
16
|
+
end
|
17
|
+
|
18
|
+
def port
|
19
|
+
@destination_uri.port
|
20
|
+
end
|
21
|
+
|
22
|
+
def use_ssl?
|
23
|
+
@destination_uri.is_a? URI::HTTPS
|
24
|
+
end
|
25
|
+
|
26
|
+
def use_basic_auth?
|
27
|
+
@destination_uri.user && @destination_uri.password
|
28
|
+
end
|
29
|
+
|
30
|
+
def uri
|
31
|
+
@destination_uri.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def translate_request(current_request, uri)
|
37
|
+
method = current_request.request_method.downcase
|
38
|
+
method[0..0] = method[0..0].upcase
|
39
|
+
|
40
|
+
request = Net::HTTP.const_get(method).new("#{uri.path}#{"?" if uri.query}#{uri.query}")
|
41
|
+
|
42
|
+
if request.request_body_permitted? and current_request.body
|
43
|
+
request.body_stream = current_request.body
|
44
|
+
request.content_length = current_request.content_length if current_request.content_length
|
45
|
+
request.content_type = current_request.content_type if current_request.content_type
|
46
|
+
end
|
47
|
+
|
48
|
+
log_headers :debug, 'Current Request Headers', current_request.env
|
49
|
+
|
50
|
+
current_headers = current_request.env.reject { |key, value| !(key.match /^HTTP_/) }
|
51
|
+
current_headers.each do |key, value|
|
52
|
+
fixed_name = key.sub(/^HTTP_/, '').gsub('_', '-')
|
53
|
+
request[fixed_name] = value unless fixed_name.downcase == 'host'
|
54
|
+
end
|
55
|
+
request['X-Forwarded-For'] = (current_request.env['X-Forwarded-For'].to_s.split(/, +/) + [current_request.env['REMOTE_ADDR']]).join(', ')
|
56
|
+
|
57
|
+
if rack_request.env[:'proxy_addon_headers']
|
58
|
+
rack_request.env[:'proxy_addon_headers'].each do |k,v|
|
59
|
+
request[k] = v
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
if use_basic_auth?
|
64
|
+
request.basic_auth @destination_uri.user, @destination_uri.password
|
65
|
+
end
|
66
|
+
|
67
|
+
log_headers :debug, 'Proxy Request Headers:', request
|
68
|
+
|
69
|
+
request
|
70
|
+
end
|
71
|
+
|
72
|
+
def log_headers(level, title, headers)
|
73
|
+
Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
|
74
|
+
Rack::StreamingProxy::Proxy.log level, "| #{title}"
|
75
|
+
Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
|
76
|
+
headers.each { |key, value| Rack::StreamingProxy::Proxy.log level, "| #{key} = #{value.to_s}" }
|
77
|
+
Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'rack/streaming_proxy/errors'
|
2
|
+
|
3
|
+
class Rack::StreamingProxy::Response
|
4
|
+
include Rack::Utils # For HeaderHash
|
5
|
+
|
6
|
+
attr_reader :status, :headers
|
7
|
+
attr_accessor :client_http_version
|
8
|
+
|
9
|
+
def initialize(piper)
|
10
|
+
@piper = piper
|
11
|
+
@client_http_version = '1.1'
|
12
|
+
receive
|
13
|
+
end
|
14
|
+
|
15
|
+
# This method is called by Rack itself, to iterate over the proxied contents.
|
16
|
+
def each
|
17
|
+
if @body_permitted
|
18
|
+
term = "\r\n"
|
19
|
+
|
20
|
+
while chunk = read_from_destination
|
21
|
+
break if chunk == :done
|
22
|
+
if @chunked
|
23
|
+
size = bytesize(chunk)
|
24
|
+
next if size == 0
|
25
|
+
if @client_http_version >= '1.1'
|
26
|
+
yield [size.to_s(16), term, chunk, term].join
|
27
|
+
else
|
28
|
+
yield chunk
|
29
|
+
end
|
30
|
+
else
|
31
|
+
yield chunk
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
finish
|
36
|
+
|
37
|
+
if @chunked && @client_http_version >= '1.1'
|
38
|
+
yield ['0', term, '', term].join
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def receive
|
46
|
+
# The first item received from the child will either be an HTTP status code or an Exception.
|
47
|
+
@status = read_from_destination
|
48
|
+
|
49
|
+
if @status.nil? # This should never happen
|
50
|
+
Rack::StreamingProxy::Proxy.log :error, "Parent received unexpected nil status!"
|
51
|
+
finish
|
52
|
+
raise Rack::StreamingProxy::UnknownError
|
53
|
+
elsif @status.kind_of? Exception
|
54
|
+
e = @status
|
55
|
+
Rack::StreamingProxy::Proxy.log :error, "Parent received an Exception from Child: #{e.class}: #{e.message}"
|
56
|
+
finish
|
57
|
+
raise e
|
58
|
+
end
|
59
|
+
|
60
|
+
Rack::StreamingProxy::Proxy.log :debug, "Parent received: Status = #{@status}."
|
61
|
+
@body_permitted = read_from_destination
|
62
|
+
Rack::StreamingProxy::Proxy.log :debug, "Parent received: Reponse has body? = #{@body_permitted}."
|
63
|
+
@headers = HeaderHash.new(read_from_destination)
|
64
|
+
@chunked = (@headers['Transfer-Encoding'] == 'chunked')
|
65
|
+
finish unless @body_permitted # If there is a body, finish will be called inside each.
|
66
|
+
end
|
67
|
+
|
68
|
+
# parent needs to wait for the child, or it results in the child process becoming defunct, resulting in zombie processes!
|
69
|
+
# This is very important. See: http://siliconisland.ca/2013/04/26/beware-of-the-zombie-process-apocalypse/
|
70
|
+
def finish
|
71
|
+
Rack::StreamingProxy::Proxy.log :info, "Parent process #{Process.pid} waiting for child process #{@piper.pid} to exit."
|
72
|
+
@piper.wait
|
73
|
+
end
|
74
|
+
|
75
|
+
def read_from_destination
|
76
|
+
@piper.gets
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/https'
|
3
|
+
require 'servolux'
|
4
|
+
require 'rack/streaming_proxy/errors'
|
5
|
+
|
6
|
+
class Rack::StreamingProxy::Session
|
7
|
+
|
8
|
+
def initialize(request)
|
9
|
+
@request = request
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns a Rack::StreamingProxy::Response
|
13
|
+
def start
|
14
|
+
@piper = Servolux::Piper.new 'r', timeout: 30
|
15
|
+
@piper.child { child }
|
16
|
+
@piper.parent { parent }
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def child
|
22
|
+
begin
|
23
|
+
Rack::StreamingProxy::Proxy.log :debug, "Child starting request to #{@request.uri}"
|
24
|
+
perform_request
|
25
|
+
|
26
|
+
rescue Exception => e
|
27
|
+
# Rescue all exceptions to help with development and debugging, as otherwise when exceptions
|
28
|
+
# occur the child process doesn't crash the parent process. Normally rescuing from Exception is a bad idea,
|
29
|
+
# but it's the only way to get a stacktrace here for all exceptions including SyntaxError etc,
|
30
|
+
# and we are simply passing it on so catastrophic exceptions will still be raised up the chain.
|
31
|
+
Rack::StreamingProxy::Proxy.log :debug, "Child process #{Process.pid} passing on #{e.class}: #{e.message}"
|
32
|
+
@piper.puts e # Pass on the exception to the parent.
|
33
|
+
|
34
|
+
ensure
|
35
|
+
Rack::StreamingProxy::Proxy.log :debug, "Child process #{Process.pid} closing connection."
|
36
|
+
@piper.close
|
37
|
+
|
38
|
+
Rack::StreamingProxy::Proxy.log :info, "Child process #{Process.pid} exiting."
|
39
|
+
exit!(0) # child needs to exit, always.
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def parent
|
44
|
+
Rack::StreamingProxy::Proxy.log :info, "Parent process #{Process.pid} forked a child process #{@piper.pid}."
|
45
|
+
|
46
|
+
response = Rack::StreamingProxy::Response.new(@piper)
|
47
|
+
return response
|
48
|
+
end
|
49
|
+
|
50
|
+
def perform_request
|
51
|
+
http_session = Net::HTTP.new(@request.host, @request.port)
|
52
|
+
http_session.use_ssl = @request.use_ssl?
|
53
|
+
|
54
|
+
http_session.start do |session|
|
55
|
+
# Retry the request up to self.class.num_retries_on_5xx times if a 5xx is experienced.
|
56
|
+
# This is because some 500/503 errors resolve themselves quickly, might as well give it a chance.
|
57
|
+
# do...while loop as suggested by Matz: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/6745
|
58
|
+
retries = 1
|
59
|
+
stop = false
|
60
|
+
loop do
|
61
|
+
session.request(@request.http_request) do |response|
|
62
|
+
# At this point the headers and status are available, but the body has not yet been read.
|
63
|
+
Rack::StreamingProxy::Proxy.log :debug, "Child got response: #{response.class.name}"
|
64
|
+
|
65
|
+
if response.class <= Net::HTTPServerError # Includes Net::HTTPServiceUnavailable, Net::HTTPInternalServerError
|
66
|
+
if retries <= Rack::StreamingProxy::Proxy.num_retries_on_5xx
|
67
|
+
Rack::StreamingProxy::Proxy.log :info, "Child got #{response.code}, retrying (Retry ##{retries})"
|
68
|
+
sleep 1
|
69
|
+
retries += 1
|
70
|
+
next
|
71
|
+
end
|
72
|
+
end
|
73
|
+
stop = true
|
74
|
+
|
75
|
+
Rack::StreamingProxy::Proxy.log :debug, "Child process #{Process.pid} returning Status = #{response.code}."
|
76
|
+
|
77
|
+
process_response(response)
|
78
|
+
end
|
79
|
+
|
80
|
+
break if stop
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def process_response(response)
|
86
|
+
|
87
|
+
# Raise an exception if the raise_on_5xx config is set, and the response is a 5xx.
|
88
|
+
# Otherwise continue and put the error body in the pipe. (e.g. Apache error page, for example)
|
89
|
+
if response.class <= Net::HTTPServerError && Rack::StreamingProxy::Proxy.raise_on_5xx
|
90
|
+
raise Rack::StreamingProxy::HttpServerError.new "Got a #{response.class.name} (#{response.code}) response while proxying to #{@request.uri}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# Put the response in the parent's pipe.
|
94
|
+
@piper.puts response.code
|
95
|
+
@piper.puts response.class.body_permitted?
|
96
|
+
|
97
|
+
# Could potentially use a one-liner here:
|
98
|
+
# @piper.puts Hash[response.to_hash.map { |key, value| [key, value.join(', ')] } ]
|
99
|
+
# But the following three lines seem to be more readable.
|
100
|
+
# Watch out: response.to_hash and response.each_header returns in different formats!
|
101
|
+
# to_hash requires the values to be joined with a comma.
|
102
|
+
headers = {}
|
103
|
+
response.each_header { |key, value| headers[key] = value }
|
104
|
+
log_headers :debug, 'Proxy Response Headers:', headers
|
105
|
+
@piper.puts headers
|
106
|
+
|
107
|
+
response.read_body { |chunk| @piper.puts chunk }
|
108
|
+
@piper.puts :done
|
109
|
+
end
|
110
|
+
|
111
|
+
def log_headers(level, title, headers)
|
112
|
+
Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
|
113
|
+
Rack::StreamingProxy::Proxy.log level, "| #{title}"
|
114
|
+
Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
|
115
|
+
headers.each { |key, value| Rack::StreamingProxy::Proxy.log level, "| #{key} = #{value.to_s}" }
|
116
|
+
Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rack/streaming_proxy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'bowtie-io-rack-streaming-proxy'
|
8
|
+
spec.version = Rack::StreamingProxy::VERSION
|
9
|
+
spec.authors = ['Fred Ngo', 'Nathan Witmer', 'Naotoshi Seo', 'James Kassemi']
|
10
|
+
spec.email = ['fredngo@gmail.com', 'nwitmer@gmail.com', 'sonots@gmail.com', 'jkassemi@gmail.com']
|
11
|
+
spec.description = %q{Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn.}
|
12
|
+
spec.summary = %q{Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn.}
|
13
|
+
spec.homepage = 'http://github.com/bowtie-io/rack-streaming-proxy'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'rack', '>= 1.4'
|
22
|
+
spec.add_runtime_dependency 'servolux', '~> 0.10'
|
23
|
+
|
24
|
+
# test
|
25
|
+
spec.add_development_dependency 'bundler', '>= 1.3'
|
26
|
+
spec.add_development_dependency 'rake', '>= 10.0'
|
27
|
+
spec.add_development_dependency 'rspec'
|
28
|
+
spec.add_development_dependency 'rack-test'
|
29
|
+
spec.add_development_dependency 'unicorn'
|
30
|
+
|
31
|
+
# debug
|
32
|
+
spec.add_development_dependency 'pry'
|
33
|
+
spec.add_development_dependency 'pry-nav'
|
34
|
+
end
|
data/spec/app.ru
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
class Streamer
|
4
|
+
include Rack::Utils
|
5
|
+
|
6
|
+
def initialize(sleep=0.05)
|
7
|
+
@sleep = sleep
|
8
|
+
@strings = 5.times.collect {|n| "~~~~~ #{n} ~~~~~\n" }
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
req = Rack::Request.new(env)
|
13
|
+
headers = {"Content-Type" => "text/plain"}
|
14
|
+
headers["Transfer-Encoding"] = "chunked"
|
15
|
+
[200, headers, self.dup]
|
16
|
+
end
|
17
|
+
|
18
|
+
def each
|
19
|
+
term = "\r\n"
|
20
|
+
@strings.each do |chunk|
|
21
|
+
size = bytesize(chunk)
|
22
|
+
yield [size.to_s(16), term, chunk, term].join
|
23
|
+
sleep @sleep
|
24
|
+
end
|
25
|
+
yield ["0", term, "", term].join
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# if no content-length is provided and the response isn't streamed,
|
30
|
+
# make sure the headers get a content length.
|
31
|
+
use Rack::ContentLength
|
32
|
+
|
33
|
+
map "/" do
|
34
|
+
run lambda { |env| [200, {"Content-Type" => "text/plain"}, ["ALL GOOD"]] }
|
35
|
+
end
|
36
|
+
|
37
|
+
map "/stream" do
|
38
|
+
run Streamer.new
|
39
|
+
end
|
40
|
+
|
41
|
+
map "/slow_stream" do
|
42
|
+
run Streamer.new(0.5)
|
43
|
+
end
|
44
|
+
|
45
|
+
map "/env" do
|
46
|
+
run lambda { |env|
|
47
|
+
req = Rack::Request.new(env)
|
48
|
+
req.POST # modifies env inplace to include "rack.request.form_vars" key
|
49
|
+
[200, {"Content-Type" => "application/x-yaml"}, [env.to_yaml]] }
|
50
|
+
end
|
51
|
+
|
52
|
+
map "/boom" do
|
53
|
+
run lambda { |env| [500, {"Content-Type" => "text/plain"}, ["kaboom!"]] }
|
54
|
+
end
|
55
|
+
|
data/spec/proxy.ru
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require File.expand_path(
|
2
|
+
File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
|
3
|
+
|
4
|
+
ENV['RACK_ENV'] = 'none' # 'development' automatically use Rack::Lint and results in errors with unicorn
|
5
|
+
# use Rack::CommonLogger
|
6
|
+
use Rack::StreamingProxy::Proxy do |req|
|
7
|
+
"http://localhost:4321#{req.path}"
|
8
|
+
end
|
9
|
+
run lambda { |env| [200, {}, ["should never get here..."]]}
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require File.expand_path( File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
|
2
|
+
|
3
|
+
require "rack/test"
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
7
|
+
config.run_all_when_everything_filtered = true
|
8
|
+
config.filter_run :focus
|
9
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require File.join(File.dirname(__FILE__), %w[spec_helper])
|
3
|
+
|
4
|
+
APP_PORT = 4321 # hardcoded in proxy.ru as well!
|
5
|
+
PROXY_PORT = 4322
|
6
|
+
|
7
|
+
shared_examples "rack-streaming-proxy" do
|
8
|
+
it "passes through to the rest of the stack if block returns false" do
|
9
|
+
get "/not_proxied"
|
10
|
+
last_response.should be_ok
|
11
|
+
last_response.body.should == "not proxied"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "proxies a request back to the app server" do
|
15
|
+
get "/", {}, rack_env
|
16
|
+
last_response.should be_ok
|
17
|
+
last_response.body.should == "ALL GOOD"
|
18
|
+
# Expect a Content-Length header field which the origin server sent is
|
19
|
+
# not deleted by streaming-proxy.
|
20
|
+
last_response.headers["Content-Length"].should eq '8'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "handles POST, PUT, and DELETE methods" do
|
24
|
+
post "/env", {}, rack_env
|
25
|
+
last_response.should be_ok
|
26
|
+
last_response.body.should =~ /REQUEST_METHOD: POST/
|
27
|
+
put "/env", {}, rack_env
|
28
|
+
last_response.should be_ok
|
29
|
+
last_response.body.should =~ /REQUEST_METHOD: PUT/
|
30
|
+
delete "/env", {}, rack_env
|
31
|
+
last_response.should be_ok
|
32
|
+
last_response.body.should =~ /REQUEST_METHOD: DELETE/
|
33
|
+
end
|
34
|
+
|
35
|
+
it "sets a X-Forwarded-For header" do
|
36
|
+
post "/env", {}, rack_env
|
37
|
+
last_response.should =~ /HTTP_X_FORWARDED_FOR: 127.0.0.1/
|
38
|
+
end
|
39
|
+
|
40
|
+
it "preserves the post body" do
|
41
|
+
post "/env", {"foo" => "bar"}, rack_env
|
42
|
+
last_response.body.should =~ /rack.request.form_vars: foo=bar/
|
43
|
+
end
|
44
|
+
|
45
|
+
it "raises a Rack::Proxy::StreamingProxy error when something goes wrong" do
|
46
|
+
Rack::StreamingProxy::Request.should_receive(:new).and_raise(RuntimeError.new("kaboom"))
|
47
|
+
lambda { get "/" }.should raise_error(RuntimeError, /kaboom/i)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "does not raise a Rack::Proxy error if the app itself raises something" do
|
51
|
+
lambda { get "/not_proxied/boom" }.should raise_error(RuntimeError, /app error/)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "preserves cookies" do
|
55
|
+
set_cookie "foo"
|
56
|
+
post "/env", {}, rack_env
|
57
|
+
YAML::load(last_response.body)["HTTP_COOKIE"].should == "foo"
|
58
|
+
end
|
59
|
+
|
60
|
+
it "preserves authentication info" do
|
61
|
+
basic_authorize "admin", "secret"
|
62
|
+
post "/env", {}, rack_env
|
63
|
+
YAML::load(last_response.body)["HTTP_AUTHORIZATION"].should == "Basic YWRtaW46c2VjcmV0"
|
64
|
+
end
|
65
|
+
|
66
|
+
it "preserves arbitrary headers" do
|
67
|
+
get "/env", {}, rack_env.merge("HTTP_X_FOOHEADER" => "Bar")
|
68
|
+
YAML::load(last_response.body)["HTTP_X_FOOHEADER"].should == "Bar"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe Rack::StreamingProxy::Proxy do
|
73
|
+
include Rack::Test::Methods
|
74
|
+
|
75
|
+
def app
|
76
|
+
@app ||= Rack::Builder.new do
|
77
|
+
use Rack::Lint
|
78
|
+
use Rack::StreamingProxy::Proxy do |req|
|
79
|
+
# STDERR.puts "== incoming request env =="
|
80
|
+
# STDERR.puts req.env
|
81
|
+
# STDERR.puts "=^ incoming request env ^="
|
82
|
+
# STDERR.puts
|
83
|
+
unless req.path.start_with?("/not_proxied")
|
84
|
+
url = "http://localhost:#{APP_PORT}#{req.path}"
|
85
|
+
url << "?#{req.query_string}" unless req.query_string.empty?
|
86
|
+
# STDERR.puts "PROXYING to #{url}"
|
87
|
+
url
|
88
|
+
end
|
89
|
+
end
|
90
|
+
run lambda { |env|
|
91
|
+
raise "app error" if env["PATH_INFO"] =~ /boom/
|
92
|
+
[200, {"Content-Type" => "text/plain"}, ["not proxied"]]
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
before(:all) do
|
98
|
+
app_path = File.join(File.dirname(__FILE__), %w[app.ru])
|
99
|
+
@app_server = Servolux::Child.new(
|
100
|
+
# :command => "thin -R #{app_path} -p #{APP_PORT} start", # buffers!
|
101
|
+
:command => "rackup #{app_path} -p #{APP_PORT}",
|
102
|
+
:timeout => 30, # all specs should take <30 sec to run
|
103
|
+
:suspend => 0.25
|
104
|
+
)
|
105
|
+
puts "----- starting app server -----"
|
106
|
+
@app_server.start
|
107
|
+
sleep 2 # give it a sec
|
108
|
+
puts "----- started app server -----"
|
109
|
+
end
|
110
|
+
|
111
|
+
after(:all) do
|
112
|
+
puts "----- shutting down app server -----"
|
113
|
+
@app_server.stop
|
114
|
+
@app_server.wait
|
115
|
+
puts "----- app server is stopped -----"
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'client requests with HTTP/1.0' do
|
119
|
+
let(:rack_env) { {'HTTP_VERSION' => 'HTTP/1.0'} }
|
120
|
+
it_behaves_like 'rack-streaming-proxy'
|
121
|
+
it "does not use chunked encoding when the app server send chunked body" do
|
122
|
+
get "/stream", {}, rack_env
|
123
|
+
last_response.should be_ok
|
124
|
+
# Expect a Transfer-Encoding header is deleted by rack-streaming-proxy
|
125
|
+
last_response.headers["Transfer-Encoding"].should be_nil
|
126
|
+
# I expected a Content-Length header which the origin server sent was deleted,
|
127
|
+
# But the following test failed against my expectation. The reason is
|
128
|
+
# that a Content-Length header was added in creating Rack::MockResponse
|
129
|
+
# instance. So I gave up writing this test right now.
|
130
|
+
#
|
131
|
+
# last_response.headers["Content-Length"].should be_nil
|
132
|
+
#
|
133
|
+
last_response.body.should == <<-EOS
|
134
|
+
~~~~~ 0 ~~~~~
|
135
|
+
~~~~~ 1 ~~~~~
|
136
|
+
~~~~~ 2 ~~~~~
|
137
|
+
~~~~~ 3 ~~~~~
|
138
|
+
~~~~~ 4 ~~~~~
|
139
|
+
EOS
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context 'client requests with HTTP/1.1' do
|
144
|
+
let(:rack_env) { {'HTTP_VERSION' => 'HTTP/1.1'} }
|
145
|
+
it_behaves_like 'rack-streaming-proxy'
|
146
|
+
it "uses chunked encoding when the app server send chunked body" do
|
147
|
+
get "/stream", {}, rack_env
|
148
|
+
last_response.should be_ok
|
149
|
+
last_response.headers["Transfer-Encoding"].should == 'chunked'
|
150
|
+
last_response.headers["Content-Length"].should be_nil
|
151
|
+
last_response.body.should =~ /^e\r\n~~~~~ 0 ~~~~~\n\r\n/
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
describe Rack::StreamingProxy::Proxy do
|
158
|
+
include Rack::Test::Methods
|
159
|
+
|
160
|
+
attr_reader :app
|
161
|
+
|
162
|
+
before(:all) do
|
163
|
+
app_path = File.join(File.dirname(__FILE__), %w[app.ru])
|
164
|
+
@app_server = Servolux::Child.new(
|
165
|
+
# :command => "thin -R #{app_path} -p #{APP_PORT} start", # buffers!
|
166
|
+
# :command => "rackup #{app_path} -p #{APP_PORT}", # webrick adds content-length, it should be wrong
|
167
|
+
:command => "unicorn #{app_path} -p #{APP_PORT} -E none",
|
168
|
+
:timeout => 30, # all specs should take <30 sec to run
|
169
|
+
:suspend => 0.25
|
170
|
+
)
|
171
|
+
puts "----- starting app server -----"
|
172
|
+
@app_server.start
|
173
|
+
sleep 2 # give it a sec
|
174
|
+
puts "----- started app server -----"
|
175
|
+
end
|
176
|
+
|
177
|
+
after(:all) do
|
178
|
+
puts "----- shutting down app server -----"
|
179
|
+
@app_server.stop
|
180
|
+
@app_server.wait
|
181
|
+
puts "----- app server is stopped -----"
|
182
|
+
end
|
183
|
+
|
184
|
+
def with_proxy_server
|
185
|
+
proxy_path = File.join(File.dirname(__FILE__), %w[proxy.ru])
|
186
|
+
@proxy_server = Servolux::Child.new(
|
187
|
+
:command => "unicorn #{proxy_path} -p #{PROXY_PORT} -E none",
|
188
|
+
:timeout => 10,
|
189
|
+
:suspend => 0.25
|
190
|
+
)
|
191
|
+
puts "----- starting proxy server -----"
|
192
|
+
@proxy_server.start
|
193
|
+
sleep 2
|
194
|
+
puts "----- started proxy server -----"
|
195
|
+
yield
|
196
|
+
ensure
|
197
|
+
puts "----- shutting down proxy server -----"
|
198
|
+
@proxy_server.stop
|
199
|
+
@proxy_server.wait
|
200
|
+
puts "----- proxy server is stopped -----"
|
201
|
+
end
|
202
|
+
|
203
|
+
# this is the most critical spec: it makes sure things are actually streamed, not buffered
|
204
|
+
# MEMO: only unicorn worked. webrick, thin, and puma did not progressively stream
|
205
|
+
it "streams data from the app server to the client" do
|
206
|
+
@app = Rack::Builder.new do
|
207
|
+
use Rack::Lint
|
208
|
+
run lambda { |env|
|
209
|
+
body = []
|
210
|
+
Net::HTTP.start("localhost", PROXY_PORT) do |http|
|
211
|
+
http.request_get("/slow_stream") do |response|
|
212
|
+
response.read_body do |chunk|
|
213
|
+
body << "#{Time.now.to_i}\n"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
[200, {"Content-Type" => "text/plain"}, body]
|
218
|
+
}
|
219
|
+
end
|
220
|
+
|
221
|
+
with_proxy_server do
|
222
|
+
get "/"
|
223
|
+
last_response.should be_ok
|
224
|
+
times = last_response.body.split("\n").map {|l| l.to_i}
|
225
|
+
unless (times.last - times.first) >= 2
|
226
|
+
fail "expected receive time of first chunk to be at least two seconds before the last chunk, but the times were: #{times.join(', ')}"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
metadata
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bowtie-io-rack-streaming-proxy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fred Ngo
|
8
|
+
- Nathan Witmer
|
9
|
+
- Naotoshi Seo
|
10
|
+
- James Kassemi
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2015-01-23 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rack
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '1.4'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.4'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: servolux
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - "~>"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0.10'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0.10'
|
44
|
+
- !ruby/object:Gem::Dependency
|
45
|
+
name: bundler
|
46
|
+
requirement: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '1.3'
|
51
|
+
type: :development
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '1.3'
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rake
|
60
|
+
requirement: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '10.0'
|
65
|
+
type: :development
|
66
|
+
prerelease: false
|
67
|
+
version_requirements: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '10.0'
|
72
|
+
- !ruby/object:Gem::Dependency
|
73
|
+
name: rspec
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
type: :development
|
80
|
+
prerelease: false
|
81
|
+
version_requirements: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
- !ruby/object:Gem::Dependency
|
87
|
+
name: rack-test
|
88
|
+
requirement: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
type: :development
|
94
|
+
prerelease: false
|
95
|
+
version_requirements: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
- !ruby/object:Gem::Dependency
|
101
|
+
name: unicorn
|
102
|
+
requirement: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
type: :development
|
108
|
+
prerelease: false
|
109
|
+
version_requirements: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
- !ruby/object:Gem::Dependency
|
115
|
+
name: pry
|
116
|
+
requirement: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
type: :development
|
122
|
+
prerelease: false
|
123
|
+
version_requirements: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
- !ruby/object:Gem::Dependency
|
129
|
+
name: pry-nav
|
130
|
+
requirement: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
type: :development
|
136
|
+
prerelease: false
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
description: Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn.
|
143
|
+
email:
|
144
|
+
- fredngo@gmail.com
|
145
|
+
- nwitmer@gmail.com
|
146
|
+
- sonots@gmail.com
|
147
|
+
- jkassemi@gmail.com
|
148
|
+
executables: []
|
149
|
+
extensions: []
|
150
|
+
extra_rdoc_files: []
|
151
|
+
files:
|
152
|
+
- ".gitignore"
|
153
|
+
- Gemfile
|
154
|
+
- LICENSE.txt
|
155
|
+
- README.md
|
156
|
+
- README.txt
|
157
|
+
- Rakefile
|
158
|
+
- dev/client.rb
|
159
|
+
- dev/proxy.ru
|
160
|
+
- dev/streamer.ru
|
161
|
+
- lib/rack/streaming_proxy.rb
|
162
|
+
- lib/rack/streaming_proxy/errors.rb
|
163
|
+
- lib/rack/streaming_proxy/proxy.rb
|
164
|
+
- lib/rack/streaming_proxy/request.rb
|
165
|
+
- lib/rack/streaming_proxy/response.rb
|
166
|
+
- lib/rack/streaming_proxy/session.rb
|
167
|
+
- lib/rack/streaming_proxy/version.rb
|
168
|
+
- rack-streaming-proxy.gemspec
|
169
|
+
- spec/app.ru
|
170
|
+
- spec/proxy.ru
|
171
|
+
- spec/spec_helper.rb
|
172
|
+
- spec/streaming_proxy_spec.rb
|
173
|
+
homepage: http://github.com/bowtie-io/rack-streaming-proxy
|
174
|
+
licenses:
|
175
|
+
- MIT
|
176
|
+
metadata: {}
|
177
|
+
post_install_message:
|
178
|
+
rdoc_options: []
|
179
|
+
require_paths:
|
180
|
+
- lib
|
181
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - ">="
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: '0'
|
186
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
187
|
+
requirements:
|
188
|
+
- - ">="
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '0'
|
191
|
+
requirements: []
|
192
|
+
rubyforge_project:
|
193
|
+
rubygems_version: 2.2.2
|
194
|
+
signing_key:
|
195
|
+
specification_version: 4
|
196
|
+
summary: Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn.
|
197
|
+
test_files:
|
198
|
+
- spec/app.ru
|
199
|
+
- spec/proxy.ru
|
200
|
+
- spec/spec_helper.rb
|
201
|
+
- spec/streaming_proxy_spec.rb
|