rack-streaming-proxy 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +4 -0
- data/lib/rack/streaming_proxy/proxy_request.rb +20 -6
- data/lib/rack/streaming_proxy.rb +12 -10
- data/spec/app.ru +55 -0
- data/spec/proxy.ru +9 -0
- data/spec/spec_helper.rb +3 -9
- data/spec/streaming_proxy_spec.rb +142 -2
- metadata +14 -3
- data/coderack.rb +0 -133
data/Rakefile
CHANGED
@@ -19,5 +19,9 @@ Bones {
|
|
19
19
|
ignore_file '.gitignore'
|
20
20
|
depend_on "rack", :version => "~> 1.0.1"
|
21
21
|
depend_on "servolux", :version => "~> 0.8.1"
|
22
|
+
depend_on "rack-test", :version => "~> 0.5.1", :development => true
|
23
|
+
spec {
|
24
|
+
opts ["--colour", "--loadby mtime", "--reverse", "--diff unified"]
|
25
|
+
}
|
22
26
|
}
|
23
27
|
|
@@ -35,7 +35,8 @@ class Rack::StreamingProxy
|
|
35
35
|
# has not yet been read. start reading it and putting it in the parent's pipe.
|
36
36
|
response_headers = {}
|
37
37
|
response.each_header {|k,v| response_headers[k] = v}
|
38
|
-
@piper.puts
|
38
|
+
@piper.puts response.code.to_i
|
39
|
+
@piper.puts response_headers
|
39
40
|
|
40
41
|
response.read_body do |chunk|
|
41
42
|
@piper.puts chunk
|
@@ -43,22 +44,26 @@ class Rack::StreamingProxy
|
|
43
44
|
@piper.puts :done
|
44
45
|
end
|
45
46
|
end
|
46
|
-
|
47
|
-
exit!
|
48
47
|
end
|
49
48
|
|
50
49
|
@piper.parent do
|
51
50
|
# wait for the status and headers to come back from the child
|
52
|
-
@status
|
53
|
-
@headers = HeaderHash.new(
|
51
|
+
@status = read_from_child
|
52
|
+
@headers = HeaderHash.new(read_from_child)
|
54
53
|
end
|
54
|
+
rescue => e
|
55
|
+
@piper.parent { raise }
|
56
|
+
@piper.child { @piper.puts e }
|
57
|
+
ensure
|
58
|
+
# child needs to exit, always.
|
59
|
+
@piper.child { exit!(0) }
|
55
60
|
end
|
56
61
|
|
57
62
|
def each
|
58
63
|
chunked = @headers["Transfer-Encoding"] == "chunked"
|
59
64
|
term = "\r\n"
|
60
65
|
|
61
|
-
while chunk =
|
66
|
+
while chunk = read_from_child
|
62
67
|
break if chunk == :done
|
63
68
|
if chunked
|
64
69
|
size = bytesize(chunk)
|
@@ -72,5 +77,14 @@ class Rack::StreamingProxy
|
|
72
77
|
yield ["0", term, "", term].join if chunked
|
73
78
|
end
|
74
79
|
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
def read_from_child
|
84
|
+
val = @piper.gets
|
85
|
+
raise val if val.kind_of?(Exception)
|
86
|
+
val
|
87
|
+
end
|
88
|
+
|
75
89
|
end
|
76
90
|
end
|
data/lib/rack/streaming_proxy.rb
CHANGED
@@ -4,9 +4,9 @@ module Rack
|
|
4
4
|
class Error < StandardError; end
|
5
5
|
|
6
6
|
# :stopdoc:
|
7
|
-
VERSION = '1.0.
|
7
|
+
VERSION = '1.0.1'
|
8
8
|
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
9
|
-
PATH = ::File.dirname(
|
9
|
+
PATH = ::File.expand_path(::File.join(::File.dirname(__FILE__), "..", "..")) + ::File::SEPARATOR
|
10
10
|
# :startdoc:
|
11
11
|
|
12
12
|
# Returns the version string for the library.
|
@@ -68,14 +68,16 @@ module Rack
|
|
68
68
|
def call(env)
|
69
69
|
req = Rack::Request.new(env)
|
70
70
|
return app.call(env) unless uri = request_uri.call(req)
|
71
|
-
proxy
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
71
|
+
begin # only want to catch proxy errors, not app errors
|
72
|
+
proxy = ProxyRequest.new(req, uri)
|
73
|
+
[proxy.status, proxy.headers, proxy]
|
74
|
+
rescue => e
|
75
|
+
msg = "Proxy error when proxying to #{uri}: #{e.class}: #{e.message}"
|
76
|
+
env["rack.errors"].puts msg
|
77
|
+
env["rack.errors"].puts e.backtrace.map { |l| "\t" + l }
|
78
|
+
env["rack.errors"].flush
|
79
|
+
raise Error, msg
|
80
|
+
end
|
79
81
|
end
|
80
82
|
|
81
83
|
protected
|
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
|
+
use Rack::Lint
|
5
|
+
# use Rack::CommonLogger
|
6
|
+
use Rack::StreamingProxy do |req|
|
7
|
+
"http://localhost:4321#{req.path}"
|
8
|
+
end
|
9
|
+
run lambda { |env| [200, {}, "should never get here..."]}
|
data/spec/spec_helper.rb
CHANGED
@@ -1,14 +1,8 @@
|
|
1
1
|
require File.expand_path(
|
2
|
-
|
2
|
+
File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
|
3
|
+
|
4
|
+
require "rack/test"
|
3
5
|
|
4
6
|
Spec::Runner.configure do |config|
|
5
|
-
# == Mock Framework
|
6
|
-
#
|
7
|
-
# RSpec uses it's own mocking framework by default. If you prefer to
|
8
|
-
# use mocha, flexmock or RR, uncomment the appropriate line:
|
9
|
-
#
|
10
|
-
# config.mock_with :mocha
|
11
|
-
# config.mock_with :flexmock
|
12
|
-
# config.mock_with :rr
|
13
7
|
end
|
14
8
|
|
@@ -1,7 +1,147 @@
|
|
1
|
-
|
2
1
|
require File.join(File.dirname(__FILE__), %w[spec_helper])
|
3
2
|
|
4
3
|
describe Rack::StreamingProxy do
|
5
|
-
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
APP_PORT = 4321 # hardcoded in proxy.ru as well!
|
7
|
+
PROXY_PORT = 4322
|
8
|
+
|
9
|
+
def app
|
10
|
+
@app ||= Rack::Builder.new do
|
11
|
+
use Rack::Lint
|
12
|
+
use Rack::StreamingProxy do |req|
|
13
|
+
unless req.path.start_with?("/not_proxied")
|
14
|
+
url = "http://localhost:#{APP_PORT}#{req.path}"
|
15
|
+
url << "?#{req.query_string}" unless req.query_string.empty?
|
16
|
+
# STDERR.puts "PROXYING to #{url}"
|
17
|
+
url
|
18
|
+
end
|
19
|
+
end
|
20
|
+
run lambda { |env|
|
21
|
+
raise "app error" if env["PATH_INFO"] =~ /boom/
|
22
|
+
[200, {"Content-Type" => "text/plain"}, "not proxied"]
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
before(:all) do
|
28
|
+
app_path = Rack::StreamingProxy.path("spec", "app.ru")
|
29
|
+
@app_server = Servolux::Child.new(
|
30
|
+
# :command => "thin -R #{app_path} -p #{APP_PORT} start", # buffers!
|
31
|
+
:command => "rackup #{app_path} -p #{APP_PORT}",
|
32
|
+
:timeout => 30, # all specs should take <30 sec to run
|
33
|
+
:suspend => 0.25
|
34
|
+
)
|
35
|
+
puts "----- starting app server -----"
|
36
|
+
@app_server.start
|
37
|
+
sleep 2 # give it a sec
|
38
|
+
puts "----- started app server -----"
|
39
|
+
end
|
40
|
+
|
41
|
+
after(:all) do
|
42
|
+
puts "----- shutting down app server -----"
|
43
|
+
@app_server.stop
|
44
|
+
@app_server.wait
|
45
|
+
puts "----- app server is stopped -----"
|
46
|
+
end
|
47
|
+
|
48
|
+
def with_proxy_server
|
49
|
+
proxy_path = Rack::StreamingProxy.path("spec", "proxy.ru")
|
50
|
+
@proxy_server = Servolux::Child.new(
|
51
|
+
:command => "rackup #{proxy_path} -p #{PROXY_PORT}",
|
52
|
+
:timeout => 10,
|
53
|
+
:suspend => 0.25
|
54
|
+
)
|
55
|
+
puts "----- starting proxy server -----"
|
56
|
+
@proxy_server.start
|
57
|
+
sleep 2
|
58
|
+
puts "----- started proxy server -----"
|
59
|
+
yield
|
60
|
+
ensure
|
61
|
+
puts "----- shutting down proxy server -----"
|
62
|
+
@proxy_server.stop
|
63
|
+
@proxy_server.wait
|
64
|
+
puts "----- proxy server is stopped -----"
|
65
|
+
end
|
66
|
+
|
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
|
+
# this is the most critical spec: it makes sure things are actually streamed, not buffered
|
87
|
+
it "streams data from the app server to the client" do
|
88
|
+
@app = Rack::Builder.new do
|
89
|
+
use Rack::Lint
|
90
|
+
run lambda { |env|
|
91
|
+
body = []
|
92
|
+
Net::HTTP.start("localhost", PROXY_PORT) do |http|
|
93
|
+
http.request_get("/slow_stream") do |response|
|
94
|
+
response.read_body do |chunk|
|
95
|
+
body << "#{Time.now.to_i}\n"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
[200, {"Content-Type" => "text/plain"}, body]
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
with_proxy_server do
|
104
|
+
get "/"
|
105
|
+
last_response.should be_ok
|
106
|
+
times = last_response.body.split("\n").map {|l| l.to_i}
|
107
|
+
unless (times.last - times.first) >= 2
|
108
|
+
violated "expected receive time of first chunk to be at least " +
|
109
|
+
"two seconds before the last chunk"
|
110
|
+
end
|
111
|
+
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
|
+
|
6
146
|
end
|
7
147
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-streaming-proxy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Witmer
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-11-
|
12
|
+
date: 2009-11-16 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -32,6 +32,16 @@ dependencies:
|
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: 0.8.1
|
34
34
|
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rack-test
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.5.1
|
44
|
+
version:
|
35
45
|
- !ruby/object:Gem::Dependency
|
36
46
|
name: bones
|
37
47
|
type: :development
|
@@ -56,11 +66,12 @@ files:
|
|
56
66
|
- History.txt
|
57
67
|
- README.txt
|
58
68
|
- Rakefile
|
59
|
-
- coderack.rb
|
60
69
|
- dev/proxy.ru
|
61
70
|
- dev/streamer.ru
|
62
71
|
- lib/rack/streaming_proxy.rb
|
63
72
|
- lib/rack/streaming_proxy/proxy_request.rb
|
73
|
+
- spec/app.ru
|
74
|
+
- spec/proxy.ru
|
64
75
|
- spec/spec_helper.rb
|
65
76
|
- spec/streaming_proxy_spec.rb
|
66
77
|
has_rdoc: true
|
data/coderack.rb
DELETED
@@ -1,133 +0,0 @@
|
|
1
|
-
require "servolux"
|
2
|
-
require "net/http"
|
3
|
-
require "uri"
|
4
|
-
|
5
|
-
# see: http://github.com/aniero/rack-streaming-proxy for the latest code
|
6
|
-
# or: sudo gem install rack-streaming-proxy --source http://gemcutter.org
|
7
|
-
|
8
|
-
module Rack
|
9
|
-
class StreamingProxy
|
10
|
-
|
11
|
-
class Error < StandardError; end
|
12
|
-
|
13
|
-
# The block provided to the initializer is given a Rack::Request
|
14
|
-
# and should return:
|
15
|
-
#
|
16
|
-
# * nil/false to skip the proxy and continue down the stack
|
17
|
-
# * a complete uri (with query string if applicable) to proxy to
|
18
|
-
#
|
19
|
-
# E.g.
|
20
|
-
#
|
21
|
-
# use Rack::StreamingProxy do |req|
|
22
|
-
# if req.path.start_with?("/search")
|
23
|
-
# "http://some_other_service/search?#{req.query}"
|
24
|
-
# end
|
25
|
-
# end
|
26
|
-
#
|
27
|
-
# Most headers, request body, and HTTP method are preserved.
|
28
|
-
#
|
29
|
-
def initialize(app, &block)
|
30
|
-
@request_uri = block
|
31
|
-
@app = app
|
32
|
-
end
|
33
|
-
|
34
|
-
def call(env)
|
35
|
-
req = Rack::Request.new(env)
|
36
|
-
return app.call(env) unless uri = request_uri.call(req)
|
37
|
-
proxy = ProxyRequest.new(req, uri)
|
38
|
-
[proxy.status, proxy.headers, proxy]
|
39
|
-
rescue => e
|
40
|
-
msg = "Proxy error when proxying to #{uri}: #{e.class}: #{e.message}"
|
41
|
-
env["rack.errors"].puts msg
|
42
|
-
env["rack.errors"].puts e.backtrace.map { |l| "\t" + l }
|
43
|
-
env["rack.errors"].flush
|
44
|
-
raise Error, msg
|
45
|
-
end
|
46
|
-
|
47
|
-
protected
|
48
|
-
|
49
|
-
attr_reader :request_uri, :app
|
50
|
-
|
51
|
-
public
|
52
|
-
|
53
|
-
|
54
|
-
class ProxyRequest
|
55
|
-
include Rack::Utils
|
56
|
-
|
57
|
-
attr_reader :status, :headers
|
58
|
-
|
59
|
-
def initialize(request, uri)
|
60
|
-
uri = URI.parse(uri)
|
61
|
-
|
62
|
-
method = request.request_method.downcase
|
63
|
-
method[0..0] = method[0..0].upcase
|
64
|
-
|
65
|
-
proxy_request = Net::HTTP.const_get(method).new("#{uri.path}#{"?" if uri.query}#{uri.query}")
|
66
|
-
|
67
|
-
if proxy_request.request_body_permitted? and request.body
|
68
|
-
proxy_request.body_stream = request.body
|
69
|
-
proxy_request.content_length = request.content_length
|
70
|
-
proxy_request.content_type = request.content_type
|
71
|
-
end
|
72
|
-
|
73
|
-
%w(Accept Accept-Encoding Accept-Charset
|
74
|
-
X-Requested-With Referer User-Agent Cookie).each do |header|
|
75
|
-
key = "HTTP_#{header.upcase.gsub('-', '_')}"
|
76
|
-
proxy_request[header] = request.env[key] if request.env[key]
|
77
|
-
end
|
78
|
-
proxy_request["X-Forwarded-For"] =
|
79
|
-
(request.env["X-Forwarded-For"].to_s.split(/, +/) + [request.env["REMOTE_ADDR"]]).join(", ")
|
80
|
-
proxy_request.basic_auth(*uri.userinfo.split(':')) if (uri.userinfo && uri.userinfo.index(':'))
|
81
|
-
|
82
|
-
@piper = Servolux::Piper.new 'r', :timeout => 30
|
83
|
-
|
84
|
-
@piper.child do
|
85
|
-
Net::HTTP.start(uri.host, uri.port) do |http|
|
86
|
-
http.request(proxy_request) do |response|
|
87
|
-
# at this point the headers and status are available, but the body
|
88
|
-
# has not yet been read. start reading it and putting it in the parent's pipe.
|
89
|
-
response_headers = {}
|
90
|
-
response.each_header {|k,v| response_headers[k] = v}
|
91
|
-
@piper.puts [response.code.to_i, response_headers]
|
92
|
-
|
93
|
-
response.read_body do |chunk|
|
94
|
-
@piper.puts chunk
|
95
|
-
end
|
96
|
-
@piper.puts :done
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
exit!
|
101
|
-
end
|
102
|
-
|
103
|
-
@piper.parent do
|
104
|
-
# wait for the status and headers to come back from the child
|
105
|
-
@status, @headers = @piper.gets
|
106
|
-
@headers = HeaderHash.new(@headers)
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def each
|
111
|
-
chunked = @headers["Transfer-Encoding"] == "chunked"
|
112
|
-
term = "\r\n"
|
113
|
-
|
114
|
-
while chunk = @piper.gets
|
115
|
-
break if chunk == :done
|
116
|
-
if chunked
|
117
|
-
size = bytesize(chunk)
|
118
|
-
next if size == 0
|
119
|
-
yield [size.to_s(16), term, chunk, term].join
|
120
|
-
else
|
121
|
-
yield chunk
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
yield ["0", term, "", term].join if chunked
|
126
|
-
end
|
127
|
-
|
128
|
-
end # ProxyRequest
|
129
|
-
|
130
|
-
end
|
131
|
-
|
132
|
-
end
|
133
|
-
|