rack-streaming-proxy 1.0.0 → 1.0.1

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.
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
-