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 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 [response.code.to_i, response_headers]
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, @headers = @piper.gets
53
- @headers = HeaderHash.new(@headers)
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 = @piper.gets
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
@@ -4,9 +4,9 @@ module Rack
4
4
  class Error < StandardError; end
5
5
 
6
6
  # :stopdoc:
7
- VERSION = '1.0.0'
7
+ VERSION = '1.0.1'
8
8
  LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
9
- PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
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 = ProxyRequest.new(req, uri)
72
- [proxy.status, proxy.headers, proxy]
73
- rescue => e
74
- msg = "Proxy error when proxying to #{uri}: #{e.class}: #{e.message}"
75
- env["rack.errors"].puts msg
76
- env["rack.errors"].puts e.backtrace.map { |l| "\t" + l }
77
- env["rack.errors"].flush
78
- raise Error, msg
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
- File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
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
- it "has specs"
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.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-15 00:00:00 -07:00
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
-