rack-streaming-proxy 1.0.3 → 2.0.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.
- checksums.yaml +7 -0
- data/.gitignore +15 -16
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +95 -0
- data/README.txt +1 -1
- data/Rakefile +1 -28
- data/dev/client.rb +24 -0
- data/lib/rack/streaming_proxy.rb +3 -97
- data/lib/rack/streaming_proxy/errors.rb +5 -0
- data/lib/rack/streaming_proxy/proxy.rb +115 -0
- data/lib/rack/streaming_proxy/railtie.rb +14 -0
- data/lib/rack/streaming_proxy/request.rb +65 -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 +2 -2
- data/spec/proxy.ru +3 -3
- data/spec/spec_helper.rb +5 -4
- data/spec/streaming_proxy_spec.rb +147 -76
- metadata +167 -76
- data/History.txt +0 -4
- data/lib/rack/streaming_proxy/proxy_request.rb +0 -96
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rails/railtie'
|
2
|
+
|
3
|
+
class Rack::StreamingProxy::Railtie < Rails::Railtie
|
4
|
+
|
5
|
+
config.streaming_proxy = ActiveSupport::OrderedOptions.new
|
6
|
+
|
7
|
+
config.after_initialize do
|
8
|
+
options = config.streaming_proxy
|
9
|
+
Rack::StreamingProxy::Proxy.logger = options.logger if options.logger
|
10
|
+
Rack::StreamingProxy::Proxy.log_verbosity = options.log_verbosity if options.log_verbosity
|
11
|
+
Rack::StreamingProxy::Proxy.num_retries_on_5xx = options.num_retries_on_5xx if options.num_retries_on_5xx
|
12
|
+
Rack::StreamingProxy::Proxy.raise_on_5xx = options.raise_on_5xx if options.raise_on_5xx
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/https'
|
3
|
+
|
4
|
+
class Rack::StreamingProxy::Request
|
5
|
+
|
6
|
+
attr_reader :http_request
|
7
|
+
|
8
|
+
def initialize(destination_uri, current_request)
|
9
|
+
@destination_uri = URI.parse(destination_uri)
|
10
|
+
@http_request = translate_request(current_request, @destination_uri)
|
11
|
+
end
|
12
|
+
|
13
|
+
def host
|
14
|
+
@destination_uri.host
|
15
|
+
end
|
16
|
+
|
17
|
+
def port
|
18
|
+
@destination_uri.port
|
19
|
+
end
|
20
|
+
|
21
|
+
def use_ssl?
|
22
|
+
@destination_uri.is_a? URI::HTTPS
|
23
|
+
end
|
24
|
+
|
25
|
+
def uri
|
26
|
+
@destination_uri.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def translate_request(current_request, uri)
|
32
|
+
method = current_request.request_method.downcase
|
33
|
+
method[0..0] = method[0..0].upcase
|
34
|
+
|
35
|
+
request = Net::HTTP.const_get(method).new("#{uri.path}#{"?" if uri.query}#{uri.query}")
|
36
|
+
|
37
|
+
if request.request_body_permitted? and current_request.body
|
38
|
+
request.body_stream = current_request.body
|
39
|
+
request.content_length = current_request.content_length if current_request.content_length
|
40
|
+
request.content_type = current_request.content_type if current_request.content_type
|
41
|
+
end
|
42
|
+
|
43
|
+
log_headers :debug, 'Current Request Headers', current_request.env
|
44
|
+
|
45
|
+
current_headers = current_request.env.reject { |key, value| !(key.match /^HTTP_/) }
|
46
|
+
current_headers.each do |key, value|
|
47
|
+
fixed_name = key.sub(/^HTTP_/, '').gsub('_', '-')
|
48
|
+
request[fixed_name] = value unless fixed_name.downcase == 'host'
|
49
|
+
end
|
50
|
+
request['X-Forwarded-For'] = (current_request.env['X-Forwarded-For'].to_s.split(/, +/) + [current_request.env['REMOTE_ADDR']]).join(', ')
|
51
|
+
|
52
|
+
log_headers :debug, 'Proxy Request Headers:', request
|
53
|
+
|
54
|
+
request
|
55
|
+
end
|
56
|
+
|
57
|
+
def log_headers(level, title, headers)
|
58
|
+
Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
|
59
|
+
Rack::StreamingProxy::Proxy.log level, "| #{title}"
|
60
|
+
Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
|
61
|
+
headers.each { |key, value| Rack::StreamingProxy::Proxy.log level, "| #{key} = #{value.to_s}" }
|
62
|
+
Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
|
63
|
+
end
|
64
|
+
|
65
|
+
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 = 'rack-streaming-proxy'
|
8
|
+
spec.version = Rack::StreamingProxy::VERSION
|
9
|
+
spec.authors = ['Fred Ngo', 'Nathan Witmer']
|
10
|
+
spec.email = ['fredngo@gmail.com', 'nwitmer@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/fredngo/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
CHANGED
@@ -31,7 +31,7 @@ end
|
|
31
31
|
use Rack::ContentLength
|
32
32
|
|
33
33
|
map "/" do
|
34
|
-
run lambda { |env| [200, {"Content-Type" => "text/plain"}, "ALL GOOD"] }
|
34
|
+
run lambda { |env| [200, {"Content-Type" => "text/plain"}, ["ALL GOOD"]] }
|
35
35
|
end
|
36
36
|
|
37
37
|
map "/stream" do
|
@@ -50,6 +50,6 @@ map "/env" do
|
|
50
50
|
end
|
51
51
|
|
52
52
|
map "/boom" do
|
53
|
-
run lambda { |env| [500, {"Content-Type" => "text/plain"}, "kaboom!"] }
|
53
|
+
run lambda { |env| [500, {"Content-Type" => "text/plain"}, ["kaboom!"]] }
|
54
54
|
end
|
55
55
|
|
data/spec/proxy.ru
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require File.expand_path(
|
2
2
|
File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
|
3
3
|
|
4
|
-
use Rack::Lint
|
4
|
+
ENV['RACK_ENV'] = 'none' # 'development' automatically use Rack::Lint and results in errors with unicorn
|
5
5
|
# use Rack::CommonLogger
|
6
|
-
use Rack::StreamingProxy do |req|
|
6
|
+
use Rack::StreamingProxy::Proxy do |req|
|
7
7
|
"http://localhost:4321#{req.path}"
|
8
8
|
end
|
9
|
-
run lambda { |env| [200, {}, "should never get here..."]}
|
9
|
+
run lambda { |env| [200, {}, ["should never get here..."]]}
|
data/spec/spec_helper.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
require File.expand_path(
|
2
|
-
File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
|
1
|
+
require File.expand_path( File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
|
3
2
|
|
4
3
|
require "rack/test"
|
5
4
|
|
6
|
-
|
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
|
7
9
|
end
|
8
|
-
|
@@ -1,15 +1,85 @@
|
|
1
|
+
require 'yaml'
|
1
2
|
require File.join(File.dirname(__FILE__), %w[spec_helper])
|
2
3
|
|
3
|
-
|
4
|
-
|
4
|
+
APP_PORT = 4321 # hardcoded in proxy.ru as well!
|
5
|
+
PROXY_PORT = 4322
|
5
6
|
|
6
|
-
|
7
|
-
|
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
|
8
74
|
|
9
75
|
def app
|
10
76
|
@app ||= Rack::Builder.new do
|
11
77
|
use Rack::Lint
|
12
|
-
use Rack::StreamingProxy do |req|
|
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
|
13
83
|
unless req.path.start_with?("/not_proxied")
|
14
84
|
url = "http://localhost:#{APP_PORT}#{req.path}"
|
15
85
|
url << "?#{req.query_string}" unless req.query_string.empty?
|
@@ -19,13 +89,13 @@ describe Rack::StreamingProxy do
|
|
19
89
|
end
|
20
90
|
run lambda { |env|
|
21
91
|
raise "app error" if env["PATH_INFO"] =~ /boom/
|
22
|
-
[200, {"Content-Type" => "text/plain"}, "not proxied"]
|
92
|
+
[200, {"Content-Type" => "text/plain"}, ["not proxied"]]
|
23
93
|
}
|
24
94
|
end
|
25
95
|
end
|
26
96
|
|
27
97
|
before(:all) do
|
28
|
-
app_path =
|
98
|
+
app_path = File.join(File.dirname(__FILE__), %w[app.ru])
|
29
99
|
@app_server = Servolux::Child.new(
|
30
100
|
# :command => "thin -R #{app_path} -p #{APP_PORT} start", # buffers!
|
31
101
|
:command => "rackup #{app_path} -p #{APP_PORT}",
|
@@ -45,10 +115,76 @@ describe Rack::StreamingProxy do
|
|
45
115
|
puts "----- app server is stopped -----"
|
46
116
|
end
|
47
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
|
+
|
48
184
|
def with_proxy_server
|
49
|
-
proxy_path =
|
185
|
+
proxy_path = File.join(File.dirname(__FILE__), %w[proxy.ru])
|
50
186
|
@proxy_server = Servolux::Child.new(
|
51
|
-
:command => "
|
187
|
+
:command => "unicorn #{proxy_path} -p #{PROXY_PORT} -E none",
|
52
188
|
:timeout => 10,
|
53
189
|
:suspend => 0.25
|
54
190
|
)
|
@@ -64,26 +200,8 @@ describe Rack::StreamingProxy do
|
|
64
200
|
puts "----- proxy server is stopped -----"
|
65
201
|
end
|
66
202
|
|
67
|
-
it "passes through to the rest of the stack if block returns false" do
|
68
|
-
get "/not_proxied"
|
69
|
-
last_response.should be_ok
|
70
|
-
last_response.body.should == "not proxied"
|
71
|
-
end
|
72
|
-
|
73
|
-
it "proxies a request back to the app server" do
|
74
|
-
get "/"
|
75
|
-
last_response.should be_ok
|
76
|
-
last_response.body.should == "ALL GOOD"
|
77
|
-
end
|
78
|
-
|
79
|
-
it "uses chunked encoding when the app server send data that way" do
|
80
|
-
get "/stream"
|
81
|
-
last_response.should be_ok
|
82
|
-
last_response.headers["Transfer-Encoding"].should == "chunked"
|
83
|
-
last_response.body.should =~ /^e\r\n~~~~~ 0 ~~~~~\n\r\n/
|
84
|
-
end
|
85
|
-
|
86
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
|
87
205
|
it "streams data from the app server to the client" do
|
88
206
|
@app = Rack::Builder.new do
|
89
207
|
use Rack::Lint
|
@@ -105,55 +223,8 @@ describe Rack::StreamingProxy do
|
|
105
223
|
last_response.should be_ok
|
106
224
|
times = last_response.body.split("\n").map {|l| l.to_i}
|
107
225
|
unless (times.last - times.first) >= 2
|
108
|
-
|
109
|
-
"two seconds before the last chunk"
|
226
|
+
fail "expected receive time of first chunk to be at least two seconds before the last chunk, but the times were: #{times.join(', ')}"
|
110
227
|
end
|
111
228
|
end
|
112
|
-
|
113
|
-
end
|
114
|
-
|
115
|
-
it "handles POST, PUT, and DELETE methods" do
|
116
|
-
post "/env"
|
117
|
-
last_response.should be_ok
|
118
|
-
last_response.body.should =~ /REQUEST_METHOD: POST/
|
119
|
-
put "/env"
|
120
|
-
last_response.should be_ok
|
121
|
-
last_response.body.should =~ /REQUEST_METHOD: PUT/
|
122
|
-
delete "/env"
|
123
|
-
last_response.should be_ok
|
124
|
-
last_response.body.should =~ /REQUEST_METHOD: DELETE/
|
125
|
-
end
|
126
|
-
|
127
|
-
it "sets a X-Forwarded-For header" do
|
128
|
-
post "/env"
|
129
|
-
last_response.should =~ /HTTP_X_FORWARDED_FOR: 127.0.0.1/
|
130
|
-
end
|
131
|
-
|
132
|
-
it "preserves the post body" do
|
133
|
-
post "/env", "foo" => "bar"
|
134
|
-
last_response.body.should =~ /rack.request.form_vars: foo=bar/
|
135
|
-
end
|
136
|
-
|
137
|
-
it "raises a Rack::Proxy::StreamingProxy error when something goes wrong" do
|
138
|
-
Rack::StreamingProxy::ProxyRequest.should_receive(:new).and_raise(RuntimeError.new("kaboom"))
|
139
|
-
lambda { get "/" }.should raise_error(Rack::StreamingProxy::Error, /proxy error.*kaboom/i)
|
140
|
-
end
|
141
|
-
|
142
|
-
it "does not raise a Rack::Proxy error if the app itself raises something" do
|
143
|
-
lambda { get "/not_proxied/boom" }.should raise_error(RuntimeError, /app error/)
|
144
|
-
end
|
145
|
-
|
146
|
-
it "preserves cookies" do
|
147
|
-
set_cookie "foo"
|
148
|
-
post "/env"
|
149
|
-
YAML.load(last_response.body)["HTTP_COOKIE"].should == "foo"
|
150
|
-
end
|
151
|
-
|
152
|
-
it "preserves authentication info" do
|
153
|
-
basic_authorize "admin", "secret"
|
154
|
-
post "/env"
|
155
|
-
YAML.load(last_response.body)["HTTP_AUTHORIZATION"].should == "Basic YWRtaW46c2VjcmV0\n"
|
156
229
|
end
|
157
|
-
|
158
230
|
end
|
159
|
-
|