bowtie-io-rack-streaming-proxy 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cea9bd8a7ee62c0e0606871db98bba8f5de3e3e8
4
+ data.tar.gz: 7c5cfcf36d77ed6e53a6f478a2fce63f9b7037bd
5
+ SHA512:
6
+ metadata.gz: 5e1f9c1c0f8471331bdd2baead76a7754d1a88aad4691b15757bdf892814760faa2118135c5a9cb1caf065f931490f3e2a54f4eed2a8bf66dcec6a5fd20ab1e0
7
+ data.tar.gz: 1e78c5b5686d47e41d933658c8af6b813ea10dc7a2ceeaec74c74b5273569cee33f7719fab178f677a4fe68baaf5bcb9f05c8c030455fbb8fa01630ba002d7b2
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rack-streaming_proxy.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009-2013 Fred Ngo, Nathan Witmer
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,95 @@
1
+ # Rack::StreamingProxy
2
+
3
+ A transparent streaming proxy to be used as rack middleware.
4
+
5
+ * Streams the response from the downstream server to minimize memory usage
6
+ * Handles chunked encoding if used
7
+ * Proxies GET/PUT/POST/DELETE, XHR, and cookies
8
+
9
+ Now updated to be compatible with Rails 3 and 4, and fixes major concurrency issues that were present in 1.0.
10
+
11
+ Use Rack::StreamingProxy when you need to have the response streamed back to the client, for example when handling large file requests that could be proxied directly but need to be authenticated against the rest of your middleware stack.
12
+
13
+ Note that this will not work well with EventMachine. EM buffers the entire rack response before sending it to the client. When testing, try Unicorn or Passenger rather than the EM-based Thin (See [discussion](http://groups.google.com/group/thin-ruby/browse_thread/thread/4762f8f851b965f6)).
14
+
15
+ A simple streamer app has been included for testing and development.
16
+
17
+ ## Usage
18
+
19
+ To use inside a Rails app, add a `config/initializers/streaming_proxy.rb` initialization file, and place in it:
20
+
21
+ ```ruby
22
+ require 'rack/streaming_proxy'
23
+
24
+ YourRailsApp::Application.configure do
25
+ config.streaming_proxy.logger = Rails.logger # stdout by default
26
+ config.streaming_proxy.log_verbosity = Rails.env.production? ? :low : :high # :low or :high, :low by default
27
+ config.streaming_proxy.num_retries_on_5xx = 5 # 0 by default
28
+ config.streaming_proxy.raise_on_5xx = true # false by default
29
+
30
+ # Will be inserted at the end of the middleware stack by default.
31
+ config.middleware.use Rack::StreamingProxy::Proxy do |request|
32
+
33
+ # Inside the request block, return the full URI to redirect the request to,
34
+ # or nil/false if the request should continue on down the middleware stack.
35
+ if request.path.start_with?('/search')
36
+ "http://www.some-other-service.com/search?#{request.query}"
37
+ end
38
+ end
39
+ end
40
+ ```
41
+
42
+ To use as a Rack app:
43
+
44
+ ```ruby
45
+ require 'rack/streaming_proxy'
46
+
47
+ use Rack::StreamingProxy::Proxy do |request|
48
+ if request.path.start_with?('/proxy')
49
+ # You probably want to get rid of the '/proxy' in the path, when requesting from the destination.
50
+ proxy_path = request.path.sub %r{^/proxy}, ''
51
+ "http://www.another-server.com#{proxy_path}"
52
+ end
53
+ end
54
+ ```
55
+
56
+ ## Installation
57
+
58
+ Add this line to your application's Gemfile:
59
+
60
+ gem 'rack-streaming-proxy'
61
+
62
+ And then execute:
63
+
64
+ $ bundle
65
+
66
+ Or install it yourself as:
67
+
68
+ $ gem install rack-streaming-proxy
69
+
70
+ ## Requirements
71
+
72
+ * Ruby = 1.9.3
73
+ * rack >= 1.4
74
+ * servolux ~> 0.10
75
+
76
+ These requirements (other than Ruby) will be automatically installed via Bundler.
77
+
78
+ This gem has not been tested with versions lower than those indicated.
79
+
80
+ This gem works with Ubuntu 10.04. It has not been tested with later versions of Ubuntu or other Linuxes, but it should work just fine. It has not been tested with OS X but should work as well. However, I doubt it will work on any version of Windows, as it does process-based stuff. You are most welcome to try it and report back.
81
+
82
+ ## Contributing
83
+
84
+ 1. Fork it
85
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
86
+ 3. Implement your changes, and make sure to add tests!
87
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
88
+ 5. Push to the branch (`git push origin my-new-feature`)
89
+ 6. Create new Pull Request
90
+
91
+ ## Thanks To
92
+
93
+ * [Nathan Witmer](http://github.com/zerowidth) for the 1.0 implementation of [Rack::StreamingProxy](http://github.com/zerowidth/rack-streaming-proxy)
94
+ * [Tom Lea](http://github.com/cwninja) for [Rack::Proxy](http://gist.github.com/207938), which inspired Rack::StreamingProxy.
95
+ * [Tim Pease](http://github.com/TwP) for [Servolux](https://github.com/Twp/servolux)
@@ -0,0 +1,78 @@
1
+ rack-streaming-proxy
2
+ by Nathan Witmer <nwitmer@gmail.com>
3
+ http://github.com/zerowidth/rack-streaming-proxy
4
+
5
+ == DESCRIPTION:
6
+
7
+ Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ Provides a transparent streaming proxy to be used as rack middleware.
12
+
13
+ * Streams the response from the downstream server to minimize memory usage
14
+ * Handles chunked encoding if used
15
+ * Proxies GET/PUT/POST/DELETE, XHR, and cookies
16
+
17
+ Use this when you need to have the response streamed back to the client,
18
+ for example when handling large file requests that could be proxied
19
+ directly but need to be authenticated against the rest of your middleware
20
+ stack.
21
+
22
+ Note that this will not work well with EventMachine. EM buffers the entire
23
+ rack response before sending it to the client. When testing, try
24
+ mongrel (via rackup) or passenger, rather than the EM-based thin. See
25
+ http://groups.google.com/group/thin-ruby/browse_thread/thread/4762f8f851b965f6
26
+ for more discussion.
27
+
28
+ I've included a simple streamer app for testing and development.
29
+
30
+ Thanks to:
31
+
32
+ * Tom Lea (cwninja) for Rack::Proxy (http://gist.github.com/207938)
33
+ * Tim Pease for bones, servolux, &c
34
+
35
+ == SYNOPSIS:
36
+
37
+ require "rack/streaming_proxy"
38
+
39
+ use Rack::StreamingProxy do |request|
40
+ # inside the request block, return the full URI to redirect the request to,
41
+ # or nil/false if the request should continue on down the middleware stack.
42
+ if request.path.start_with?("/proxy")
43
+ "http://another_server#{request.path}"
44
+ end
45
+ end
46
+
47
+ == REQUIREMENTS:
48
+
49
+ * servolux (gem install servolux)
50
+
51
+ == INSTALL:
52
+
53
+ * sudo gem install rack-streaming-proxy --source http://gemcutter.org
54
+
55
+ == LICENSE:
56
+
57
+ (The MIT License)
58
+
59
+ Copyright (c) 2009 Nathan Witmer
60
+
61
+ Permission is hereby granted, free of charge, to any person obtaining
62
+ a copy of this software and associated documentation files (the
63
+ 'Software'), to deal in the Software without restriction, including
64
+ without limitation the rights to use, copy, modify, merge, publish,
65
+ distribute, sublicense, and/or sell copies of the Software, and to
66
+ permit persons to whom the Software is furnished to do so, subject to
67
+ the following conditions:
68
+
69
+ The above copyright notice and this permission notice shall be
70
+ included in all copies or substantial portions of the Software.
71
+
72
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
73
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
74
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
75
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
76
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
77
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
78
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+ # cf. https://gist.github.com/sonots/7751554
4
+
5
+ require 'net/http'
6
+ require 'uri'
7
+
8
+ # unicorn spec/app.ru -p 4321
9
+ # unicorn spec/proxy.ru -p 4322
10
+ PORT = ARGV[0] || 8080
11
+
12
+ http = Net::HTTP.new "localhost", PORT
13
+ request = Net::HTTP::Get.new "/slow_stream"
14
+ #request['Transfer-Encoding'] = 'chunked'
15
+ request['Connection'] = 'keep-alive'
16
+ http.request(request){|response|
17
+ puts "content-length: #{response.content_length}"
18
+ body = []
19
+ response.read_body{|x|
20
+ body << Time.now
21
+ puts "read_block: #{body.length}, #{x.size}byte(s)"
22
+ }
23
+ puts body
24
+ }
@@ -0,0 +1,13 @@
1
+ require File.expand_path(
2
+ File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
3
+
4
+ use Rack::Reloader, 1
5
+ # use Rack::CommonLogger # rackup already has commonlogger loaded
6
+ use Rack::Lint
7
+ use Rack::StreamingProxy do |req|
8
+ url = "http://localhost:4000#{req.path}"
9
+ url << "?#{req.query_string}" unless req.query_string.empty?
10
+ url
11
+ end
12
+
13
+ run lambda { |env| [200, {"Content-Type" => "text/plain"}, ""] }
@@ -0,0 +1,54 @@
1
+ class Streamer
2
+ include Rack::Utils
3
+
4
+ def call(env)
5
+ req = Rack::Request.new(env)
6
+ headers = {"Content-Type" => "text/plain"}
7
+
8
+ @chunked = req.path.start_with?("/chunked")
9
+
10
+ if count = req.path.match(/(\d+)$/)
11
+ count = count[0].to_i
12
+ else
13
+ count = 100
14
+ end
15
+ @strings = count.times.collect {|n| "~~~~~ #{n} ~~~~~\n" }
16
+
17
+ if chunked?
18
+ headers["Transfer-Encoding"] = "chunked"
19
+ else
20
+ headers["Content-Length"] = @strings.inject(0) {|sum, s| sum += bytesize(s)}.to_s
21
+ end
22
+
23
+ [200, headers, self.dup]
24
+ end
25
+
26
+ def each
27
+ term = "\r\n"
28
+ @strings.each do |chunk|
29
+ if chunked?
30
+ size = bytesize(chunk)
31
+ yield [size.to_s(16), term, chunk, term].join
32
+ else
33
+ yield chunk
34
+ end
35
+ sleep 0.05
36
+ end
37
+ yield ["0", term, "", term].join if chunked?
38
+ end
39
+
40
+ protected
41
+
42
+ def chunked?
43
+ @chunked
44
+ end
45
+ end
46
+
47
+ # use Rack::CommonLogger # rackup already has commonlogger loaded
48
+ use Rack::Lint
49
+
50
+ # GET /
51
+ # GET /10
52
+ # GET /chunked
53
+ # GET /chunked/10
54
+ run Streamer.new
@@ -0,0 +1,2 @@
1
+ require 'rack/streaming_proxy/version'
2
+ require 'rack/streaming_proxy/proxy'
@@ -0,0 +1,5 @@
1
+ module Rack::StreamingProxy
2
+ class Error < RuntimeError; end
3
+ class UnknownError < Error; end
4
+ class HttpServerError < Error; end
5
+ end
@@ -0,0 +1,120 @@
1
+ require 'rack'
2
+ require 'logger'
3
+ require 'rack/streaming_proxy/session'
4
+ require 'rack/streaming_proxy/request'
5
+ require 'rack/streaming_proxy/response'
6
+
7
+ class Rack::StreamingProxy::Proxy
8
+ class << self
9
+ # Logs to stdout by default unless configured with another logger via Railtie.
10
+ def logger
11
+ Logger.new(STDOUT)
12
+ end
13
+
14
+ # At :low verbosity by default -- will not output :debug level messages.
15
+ # :high verbosity outputs :debug level messages.
16
+ # This is independent of the Logger's log_level, as set in Rails, for example,
17
+ # although the Logger's level can override this setting.
18
+ def log_verbosity
19
+ :low
20
+ end
21
+
22
+ # No retries are performed by default.
23
+ def num_retries_on_5xx
24
+ 0
25
+ end
26
+
27
+ # If the proxy cannot recover from 5xx's through retries (see num_retries_on_5xx),
28
+ # then it by default passes through the content from the destination
29
+ # e.g. the Apache error page. If you want an exception to be raised instead so
30
+ # you can handle it yourself (i.e. display your own error page), set raise_on_5xx to true.
31
+ def raise_on_5xx
32
+ false
33
+ end
34
+
35
+ def log(level, message)
36
+ unless log_verbosity == :low && level == :debug
37
+ logger.send level, "[Rack::StreamingProxy] #{message}"
38
+ end
39
+ end
40
+ end
41
+
42
+ # The block provided to the initializer is given a Rack::Request
43
+ # and should return:
44
+ #
45
+ # * nil/false to skip the proxy and continue down the stack
46
+ # * a complete uri (with query string if applicable) to proxy to
47
+ #
48
+ # Example:
49
+ #
50
+ # use Rack::StreamingProxy::Proxy do |req|
51
+ # if req.path.start_with?('/search')
52
+ # "http://some_other_service/search?#{req.query}"
53
+ # end
54
+ # end
55
+ #
56
+ # Most headers, request body, and HTTP method are preserved.
57
+ #
58
+ def initialize(app, &block)
59
+ @app = app
60
+ @block = block
61
+ end
62
+
63
+ def call(env)
64
+ current_request = Rack::Request.new(env)
65
+ destination_uri = destination_uri(current_request)
66
+
67
+ # Decide whether this request should be proxied.
68
+ if destination_uri
69
+ self.class.log :info, "Starting proxy request to: #{destination_uri}"
70
+
71
+ request = Rack::StreamingProxy::Request.new(destination_uri, current_request)
72
+ begin
73
+ response = Rack::StreamingProxy::Session.new(request).start
74
+ rescue Exception => e # Rescuing only for the purpose of logging to rack.errors
75
+ log_rack_error(env, e)
76
+ raise e
77
+ end
78
+
79
+ # Notify client http version to the instance of Response class.
80
+ response.client_http_version = env['HTTP_VERSION'].sub(/HTTP\//, '') if env.has_key?('HTTP_VERSION')
81
+ # Ideally, both a Content-Length header field and a Transfer-Encoding
82
+ # header field are not expected to be present from servers which
83
+ # are compliant with RFC2616. However, irresponsible servers may send
84
+ # both to rack-streaming-proxy.
85
+ # RFC2616 says if a message is received with both a Transfer-Encoding
86
+ # header field and a Content-Length header field, the latter MUST be
87
+ # ignored. So I deleted a Content-Length header here.
88
+ #
89
+ # Though there is a case that rack-streaming-proxy deletes both a
90
+ # Content-Length and a Transfer-Encoding, a client can acknowledge the
91
+ # end of body by closing the connection when the entire response has
92
+ # been sent without a Content-Length header. So a Content-Length header
93
+ # does not have to be required here in our understaing.
94
+ response.headers.delete('Content-Length') if response.headers.has_key?('Transfer-Encoding')
95
+ if env.has_key?('HTTP_VERSION') && env['HTTP_VERSION'] < 'HTTP/1.1'
96
+ # Be compliant with RFC2146
97
+ response.headers.delete('Transfer-Encoding')
98
+ end
99
+
100
+ self.class.log :info, "Finishing proxy request to: #{destination_uri}"
101
+ [response.status, response.headers, response]
102
+
103
+ # Continue down the middleware stack if the request is not to be proxied.
104
+ else
105
+ @app.call(env)
106
+ end
107
+ end
108
+
109
+ private
110
+ def destination_uri(rack_request)
111
+ nil
112
+ end
113
+
114
+ def log_rack_error(env, e)
115
+ env['rack.errors'].puts e.message
116
+ env['rack.errors'].puts e.backtrace #.collect { |line| "\t" + line }
117
+ env['rack.errors'].flush
118
+ end
119
+
120
+ end
@@ -0,0 +1,80 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+
4
+ class Rack::StreamingProxy::Request
5
+
6
+ attr_reader :http_request, :rack_request
7
+
8
+ def initialize(destination_uri, current_request)
9
+ @destination_uri = URI.parse(destination_uri)
10
+ @rack_request = current_request
11
+ @http_request = translate_request(current_request, @destination_uri)
12
+ end
13
+
14
+ def host
15
+ @destination_uri.host
16
+ end
17
+
18
+ def port
19
+ @destination_uri.port
20
+ end
21
+
22
+ def use_ssl?
23
+ @destination_uri.is_a? URI::HTTPS
24
+ end
25
+
26
+ def use_basic_auth?
27
+ @destination_uri.user && @destination_uri.password
28
+ end
29
+
30
+ def uri
31
+ @destination_uri.to_s
32
+ end
33
+
34
+ private
35
+
36
+ def translate_request(current_request, uri)
37
+ method = current_request.request_method.downcase
38
+ method[0..0] = method[0..0].upcase
39
+
40
+ request = Net::HTTP.const_get(method).new("#{uri.path}#{"?" if uri.query}#{uri.query}")
41
+
42
+ if request.request_body_permitted? and current_request.body
43
+ request.body_stream = current_request.body
44
+ request.content_length = current_request.content_length if current_request.content_length
45
+ request.content_type = current_request.content_type if current_request.content_type
46
+ end
47
+
48
+ log_headers :debug, 'Current Request Headers', current_request.env
49
+
50
+ current_headers = current_request.env.reject { |key, value| !(key.match /^HTTP_/) }
51
+ current_headers.each do |key, value|
52
+ fixed_name = key.sub(/^HTTP_/, '').gsub('_', '-')
53
+ request[fixed_name] = value unless fixed_name.downcase == 'host'
54
+ end
55
+ request['X-Forwarded-For'] = (current_request.env['X-Forwarded-For'].to_s.split(/, +/) + [current_request.env['REMOTE_ADDR']]).join(', ')
56
+
57
+ if rack_request.env[:'proxy_addon_headers']
58
+ rack_request.env[:'proxy_addon_headers'].each do |k,v|
59
+ request[k] = v
60
+ end
61
+ end
62
+
63
+ if use_basic_auth?
64
+ request.basic_auth @destination_uri.user, @destination_uri.password
65
+ end
66
+
67
+ log_headers :debug, 'Proxy Request Headers:', request
68
+
69
+ request
70
+ end
71
+
72
+ def log_headers(level, title, headers)
73
+ Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
74
+ Rack::StreamingProxy::Proxy.log level, "| #{title}"
75
+ Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
76
+ headers.each { |key, value| Rack::StreamingProxy::Proxy.log level, "| #{key} = #{value.to_s}" }
77
+ Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
78
+ end
79
+
80
+ 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,5 @@
1
+ module Rack
2
+ module StreamingProxy
3
+ VERSION = "2.0.1"
4
+ end
5
+ 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 = 'bowtie-io-rack-streaming-proxy'
8
+ spec.version = Rack::StreamingProxy::VERSION
9
+ spec.authors = ['Fred Ngo', 'Nathan Witmer', 'Naotoshi Seo', 'James Kassemi']
10
+ spec.email = ['fredngo@gmail.com', 'nwitmer@gmail.com', 'sonots@gmail.com', 'jkassemi@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/bowtie-io/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
@@ -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
+
@@ -0,0 +1,9 @@
1
+ require File.expand_path(
2
+ File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
3
+
4
+ ENV['RACK_ENV'] = 'none' # 'development' automatically use Rack::Lint and results in errors with unicorn
5
+ # use Rack::CommonLogger
6
+ use Rack::StreamingProxy::Proxy do |req|
7
+ "http://localhost:4321#{req.path}"
8
+ end
9
+ run lambda { |env| [200, {}, ["should never get here..."]]}
@@ -0,0 +1,9 @@
1
+ require File.expand_path( File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
2
+
3
+ require "rack/test"
4
+
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
9
+ end
@@ -0,0 +1,230 @@
1
+ require 'yaml'
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ APP_PORT = 4321 # hardcoded in proxy.ru as well!
5
+ PROXY_PORT = 4322
6
+
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
74
+
75
+ def app
76
+ @app ||= Rack::Builder.new do
77
+ use Rack::Lint
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
83
+ unless req.path.start_with?("/not_proxied")
84
+ url = "http://localhost:#{APP_PORT}#{req.path}"
85
+ url << "?#{req.query_string}" unless req.query_string.empty?
86
+ # STDERR.puts "PROXYING to #{url}"
87
+ url
88
+ end
89
+ end
90
+ run lambda { |env|
91
+ raise "app error" if env["PATH_INFO"] =~ /boom/
92
+ [200, {"Content-Type" => "text/plain"}, ["not proxied"]]
93
+ }
94
+ end
95
+ end
96
+
97
+ before(:all) do
98
+ app_path = File.join(File.dirname(__FILE__), %w[app.ru])
99
+ @app_server = Servolux::Child.new(
100
+ # :command => "thin -R #{app_path} -p #{APP_PORT} start", # buffers!
101
+ :command => "rackup #{app_path} -p #{APP_PORT}",
102
+ :timeout => 30, # all specs should take <30 sec to run
103
+ :suspend => 0.25
104
+ )
105
+ puts "----- starting app server -----"
106
+ @app_server.start
107
+ sleep 2 # give it a sec
108
+ puts "----- started app server -----"
109
+ end
110
+
111
+ after(:all) do
112
+ puts "----- shutting down app server -----"
113
+ @app_server.stop
114
+ @app_server.wait
115
+ puts "----- app server is stopped -----"
116
+ end
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
+
184
+ def with_proxy_server
185
+ proxy_path = File.join(File.dirname(__FILE__), %w[proxy.ru])
186
+ @proxy_server = Servolux::Child.new(
187
+ :command => "unicorn #{proxy_path} -p #{PROXY_PORT} -E none",
188
+ :timeout => 10,
189
+ :suspend => 0.25
190
+ )
191
+ puts "----- starting proxy server -----"
192
+ @proxy_server.start
193
+ sleep 2
194
+ puts "----- started proxy server -----"
195
+ yield
196
+ ensure
197
+ puts "----- shutting down proxy server -----"
198
+ @proxy_server.stop
199
+ @proxy_server.wait
200
+ puts "----- proxy server is stopped -----"
201
+ end
202
+
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
205
+ it "streams data from the app server to the client" do
206
+ @app = Rack::Builder.new do
207
+ use Rack::Lint
208
+ run lambda { |env|
209
+ body = []
210
+ Net::HTTP.start("localhost", PROXY_PORT) do |http|
211
+ http.request_get("/slow_stream") do |response|
212
+ response.read_body do |chunk|
213
+ body << "#{Time.now.to_i}\n"
214
+ end
215
+ end
216
+ end
217
+ [200, {"Content-Type" => "text/plain"}, body]
218
+ }
219
+ end
220
+
221
+ with_proxy_server do
222
+ get "/"
223
+ last_response.should be_ok
224
+ times = last_response.body.split("\n").map {|l| l.to_i}
225
+ unless (times.last - times.first) >= 2
226
+ fail "expected receive time of first chunk to be at least two seconds before the last chunk, but the times were: #{times.join(', ')}"
227
+ end
228
+ end
229
+ end
230
+ end
metadata ADDED
@@ -0,0 +1,201 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bowtie-io-rack-streaming-proxy
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Fred Ngo
8
+ - Nathan Witmer
9
+ - Naotoshi Seo
10
+ - James Kassemi
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2015-01-23 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rack
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: '1.4'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.4'
30
+ - !ruby/object:Gem::Dependency
31
+ name: servolux
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - "~>"
35
+ - !ruby/object:Gem::Version
36
+ version: '0.10'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '0.10'
44
+ - !ruby/object:Gem::Dependency
45
+ name: bundler
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '1.3'
51
+ type: :development
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '1.3'
58
+ - !ruby/object:Gem::Dependency
59
+ name: rake
60
+ requirement: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '10.0'
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '10.0'
72
+ - !ruby/object:Gem::Dependency
73
+ name: rspec
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ - !ruby/object:Gem::Dependency
87
+ name: rack-test
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: unicorn
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ - !ruby/object:Gem::Dependency
115
+ name: pry
116
+ requirement: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ - !ruby/object:Gem::Dependency
129
+ name: pry-nav
130
+ requirement: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ type: :development
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ description: Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn.
143
+ email:
144
+ - fredngo@gmail.com
145
+ - nwitmer@gmail.com
146
+ - sonots@gmail.com
147
+ - jkassemi@gmail.com
148
+ executables: []
149
+ extensions: []
150
+ extra_rdoc_files: []
151
+ files:
152
+ - ".gitignore"
153
+ - Gemfile
154
+ - LICENSE.txt
155
+ - README.md
156
+ - README.txt
157
+ - Rakefile
158
+ - dev/client.rb
159
+ - dev/proxy.ru
160
+ - dev/streamer.ru
161
+ - lib/rack/streaming_proxy.rb
162
+ - lib/rack/streaming_proxy/errors.rb
163
+ - lib/rack/streaming_proxy/proxy.rb
164
+ - lib/rack/streaming_proxy/request.rb
165
+ - lib/rack/streaming_proxy/response.rb
166
+ - lib/rack/streaming_proxy/session.rb
167
+ - lib/rack/streaming_proxy/version.rb
168
+ - rack-streaming-proxy.gemspec
169
+ - spec/app.ru
170
+ - spec/proxy.ru
171
+ - spec/spec_helper.rb
172
+ - spec/streaming_proxy_spec.rb
173
+ homepage: http://github.com/bowtie-io/rack-streaming-proxy
174
+ licenses:
175
+ - MIT
176
+ metadata: {}
177
+ post_install_message:
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ requirements: []
192
+ rubyforge_project:
193
+ rubygems_version: 2.2.2
194
+ signing_key:
195
+ specification_version: 4
196
+ summary: Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn.
197
+ test_files:
198
+ - spec/app.ru
199
+ - spec/proxy.ru
200
+ - spec/spec_helper.rb
201
+ - spec/streaming_proxy_spec.rb