rack-streaming-proxy 1.0.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
|