rack-streaming-proxy 1.0.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -16
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +95 -0
- data/README.txt +1 -1
- data/Rakefile +1 -28
- data/dev/client.rb +24 -0
- data/lib/rack/streaming_proxy.rb +3 -97
- data/lib/rack/streaming_proxy/errors.rb +5 -0
- data/lib/rack/streaming_proxy/proxy.rb +115 -0
- data/lib/rack/streaming_proxy/railtie.rb +14 -0
- data/lib/rack/streaming_proxy/request.rb +65 -0
- data/lib/rack/streaming_proxy/response.rb +79 -0
- data/lib/rack/streaming_proxy/session.rb +119 -0
- data/lib/rack/streaming_proxy/version.rb +5 -0
- data/rack-streaming-proxy.gemspec +34 -0
- data/spec/app.ru +2 -2
- data/spec/proxy.ru +3 -3
- data/spec/spec_helper.rb +5 -4
- data/spec/streaming_proxy_spec.rb +147 -76
- metadata +167 -76
- data/History.txt +0 -4
- data/lib/rack/streaming_proxy/proxy_request.rb +0 -96
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 053ec0676586f5914af3a502686ef50783f25034
|
4
|
+
data.tar.gz: 6ee9e037cab39fec94571ff1dee67aa014f8d1cf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 589c2c12792a03f1329b2b7fb49b7095cd3faebe0c53c8cf1e5ebf4c21c76fe055720c3ee8d727e93e4209fa295aa2f9fe72bbe6d9710ff6507d9ddefea5ba62
|
7
|
+
data.tar.gz: 25e30cb68919cbfc459b67095d35cebac93c510b9bb493ce7736eef73bc4ac70490ffc43794356cf61b9d5a08429f37e8be3d974e896c784523b3ee43c3717ad
|
data/.gitignore
CHANGED
@@ -1,18 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
# }
|
10
|
-
#
|
11
|
-
# For a project with a C extension, the following would be a good set of
|
12
|
-
# exclude patterns (uncomment them if you want to use them):
|
13
|
-
# *.[oa]
|
14
|
-
# *~
|
15
|
-
announcement.txt
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
16
9
|
coverage
|
17
|
-
doc
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
18
12
|
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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)
|
data/README.txt
CHANGED
data/Rakefile
CHANGED
@@ -1,28 +1 @@
|
|
1
|
-
|
2
|
-
require 'bones'
|
3
|
-
rescue LoadError
|
4
|
-
abort '### Please install the "bones" gem ###'
|
5
|
-
end
|
6
|
-
|
7
|
-
ensure_in_path 'lib'
|
8
|
-
require 'rack/streaming_proxy'
|
9
|
-
|
10
|
-
task :default => 'spec:specdoc'
|
11
|
-
task 'gem:release' => 'spec:specdoc'
|
12
|
-
|
13
|
-
Bones {
|
14
|
-
name 'rack-streaming-proxy'
|
15
|
-
authors 'Nathan Witmer'
|
16
|
-
email 'nwitmer@gmail.com'
|
17
|
-
url 'http://github.com/aniero/rack-streaming-proxy'
|
18
|
-
version Rack::StreamingProxy::VERSION
|
19
|
-
ignore_file '.gitignore'
|
20
|
-
depend_on "rack", :version => ">= 1.0"
|
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
|
-
}
|
26
|
-
enable_sudo
|
27
|
-
}
|
28
|
-
|
1
|
+
require "bundler/gem_tasks"
|
data/dev/client.rb
ADDED
@@ -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
|
+
}
|
data/lib/rack/streaming_proxy.rb
CHANGED
@@ -1,97 +1,3 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
class Error < StandardError; end
|
5
|
-
|
6
|
-
# :stopdoc:
|
7
|
-
VERSION = '1.0.3'
|
8
|
-
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
9
|
-
PATH = ::File.expand_path(::File.join(::File.dirname(__FILE__), "..", "..")) + ::File::SEPARATOR
|
10
|
-
# :startdoc:
|
11
|
-
|
12
|
-
# Returns the version string for the library.
|
13
|
-
#
|
14
|
-
def self.version
|
15
|
-
VERSION
|
16
|
-
end
|
17
|
-
|
18
|
-
# Returns the library path for the module. If any arguments are given,
|
19
|
-
# they will be joined to the end of the libray path using
|
20
|
-
# <tt>File.join</tt>.
|
21
|
-
#
|
22
|
-
def self.libpath( *args )
|
23
|
-
args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
24
|
-
end
|
25
|
-
|
26
|
-
# Returns the lpath for the module. If any arguments are given,
|
27
|
-
# they will be joined to the end of the path using
|
28
|
-
# <tt>File.join</tt>.
|
29
|
-
#
|
30
|
-
def self.path( *args )
|
31
|
-
args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
32
|
-
end
|
33
|
-
|
34
|
-
# Utility method used to require all files ending in .rb that lie in the
|
35
|
-
# directory below this file that has the same name as the filename passed
|
36
|
-
# in. Optionally, a specific _directory_ name can be passed in such that
|
37
|
-
# the _filename_ does not have to be equivalent to the directory.
|
38
|
-
#
|
39
|
-
def self.require_all_libs_relative_to( fname, dir = nil )
|
40
|
-
dir ||= ::File.basename(fname, '.*')
|
41
|
-
search_me = ::File.expand_path(
|
42
|
-
::File.join(::File.dirname(fname), dir, '**', '*.rb'))
|
43
|
-
|
44
|
-
Dir.glob(search_me).sort.each {|rb| require rb}
|
45
|
-
end
|
46
|
-
|
47
|
-
# The block provided to the initializer is given a Rack::Request
|
48
|
-
# and should return:
|
49
|
-
#
|
50
|
-
# * nil/false to skip the proxy and continue down the stack
|
51
|
-
# * a complete uri (with query string if applicable) to proxy to
|
52
|
-
#
|
53
|
-
# E.g.
|
54
|
-
#
|
55
|
-
# use Rack::StreamingProxy do |req|
|
56
|
-
# if req.path.start_with?("/search")
|
57
|
-
# "http://some_other_service/search?#{req.query}"
|
58
|
-
# end
|
59
|
-
# end
|
60
|
-
#
|
61
|
-
# Most headers, request body, and HTTP method are preserved.
|
62
|
-
#
|
63
|
-
def initialize(app, &block)
|
64
|
-
@request_uri = block
|
65
|
-
@app = app
|
66
|
-
end
|
67
|
-
|
68
|
-
def call(env)
|
69
|
-
req = Rack::Request.new(env)
|
70
|
-
return app.call(env) unless uri = request_uri.call(req)
|
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
|
81
|
-
end
|
82
|
-
|
83
|
-
protected
|
84
|
-
|
85
|
-
attr_reader :request_uri, :app
|
86
|
-
|
87
|
-
end
|
88
|
-
|
89
|
-
end
|
90
|
-
|
91
|
-
require "rack"
|
92
|
-
require "servolux"
|
93
|
-
require "net/http"
|
94
|
-
require "uri"
|
95
|
-
|
96
|
-
Rack::StreamingProxy.require_all_libs_relative_to(__FILE__)
|
97
|
-
|
1
|
+
require 'rack/streaming_proxy/version'
|
2
|
+
require 'rack/streaming_proxy/proxy'
|
3
|
+
require 'rack/streaming_proxy/railtie' if defined? ::Rails::Railtie
|
@@ -0,0 +1,115 @@
|
|
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
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :logger, :log_verbosity, :num_retries_on_5xx, :raise_on_5xx
|
11
|
+
|
12
|
+
def set_default_configuration
|
13
|
+
# Logs to stdout by default unless configured with another logger via Railtie.
|
14
|
+
@logger ||= Logger.new(STDOUT)
|
15
|
+
|
16
|
+
# At :low verbosity by default -- will not output :debug level messages.
|
17
|
+
# :high verbosity outputs :debug level messages.
|
18
|
+
# This is independent of the Logger's log_level, as set in Rails, for example,
|
19
|
+
# although the Logger's level can override this setting.
|
20
|
+
@log_verbosity ||= :low
|
21
|
+
|
22
|
+
# No retries are performed by default.
|
23
|
+
@num_retries_on_5xx ||= 0
|
24
|
+
|
25
|
+
# If the proxy cannot recover from 5xx's through retries (see num_retries_on_5xx),
|
26
|
+
# then it by default passes through the content from the destination
|
27
|
+
# e.g. the Apache error page. If you want an exception to be raised instead so
|
28
|
+
# you can handle it yourself (i.e. display your own error page), set raise_on_5xx to true.
|
29
|
+
@raise_on_5xx ||= false
|
30
|
+
end
|
31
|
+
|
32
|
+
def log(level, message)
|
33
|
+
unless log_verbosity == :low && level == :debug
|
34
|
+
@logger.send level, "[Rack::StreamingProxy] #{message}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
# The block provided to the initializer is given a Rack::Request
|
41
|
+
# and should return:
|
42
|
+
#
|
43
|
+
# * nil/false to skip the proxy and continue down the stack
|
44
|
+
# * a complete uri (with query string if applicable) to proxy to
|
45
|
+
#
|
46
|
+
# Example:
|
47
|
+
#
|
48
|
+
# use Rack::StreamingProxy::Proxy do |req|
|
49
|
+
# if req.path.start_with?('/search')
|
50
|
+
# "http://some_other_service/search?#{req.query}"
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# Most headers, request body, and HTTP method are preserved.
|
55
|
+
#
|
56
|
+
def initialize(app, &block)
|
57
|
+
self.class.set_default_configuration
|
58
|
+
@app = app
|
59
|
+
@block = block
|
60
|
+
end
|
61
|
+
|
62
|
+
def call(env)
|
63
|
+
current_request = Rack::Request.new(env)
|
64
|
+
|
65
|
+
# Decide whether this request should be proxied.
|
66
|
+
if destination_uri = @block.call(current_request)
|
67
|
+
self.class.log :info, "Starting proxy request to: #{destination_uri}"
|
68
|
+
|
69
|
+
request = Rack::StreamingProxy::Request.new(destination_uri, current_request)
|
70
|
+
begin
|
71
|
+
response = Rack::StreamingProxy::Session.new(request).start
|
72
|
+
rescue Exception => e # Rescuing only for the purpose of logging to rack.errors
|
73
|
+
log_rack_error(env, e)
|
74
|
+
raise e
|
75
|
+
end
|
76
|
+
|
77
|
+
# Notify client http version to the instance of Response class.
|
78
|
+
response.client_http_version = env['HTTP_VERSION'].sub(/HTTP\//, '') if env.has_key?('HTTP_VERSION')
|
79
|
+
# Ideally, both a Content-Length header field and a Transfer-Encoding
|
80
|
+
# header field are not expected to be present from servers which
|
81
|
+
# are compliant with RFC2616. However, irresponsible servers may send
|
82
|
+
# both to rack-streaming-proxy.
|
83
|
+
# RFC2616 says if a message is received with both a Transfer-Encoding
|
84
|
+
# header field and a Content-Length header field, the latter MUST be
|
85
|
+
# ignored. So I deleted a Content-Length header here.
|
86
|
+
#
|
87
|
+
# Though there is a case that rack-streaming-proxy deletes both a
|
88
|
+
# Content-Length and a Transfer-Encoding, a client can acknowledge the
|
89
|
+
# end of body by closing the connection when the entire response has
|
90
|
+
# been sent without a Content-Length header. So a Content-Length header
|
91
|
+
# does not have to be required here in our understaing.
|
92
|
+
response.headers.delete('Content-Length') if response.headers.has_key?('Transfer-Encoding')
|
93
|
+
if env.has_key?('HTTP_VERSION') && env['HTTP_VERSION'] < 'HTTP/1.1'
|
94
|
+
# Be compliant with RFC2146
|
95
|
+
response.headers.delete('Transfer-Encoding')
|
96
|
+
end
|
97
|
+
|
98
|
+
self.class.log :info, "Finishing proxy request to: #{destination_uri}"
|
99
|
+
[response.status, response.headers, response]
|
100
|
+
|
101
|
+
# Continue down the middleware stack if the request is not to be proxied.
|
102
|
+
else
|
103
|
+
@app.call(env)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def log_rack_error(env, e)
|
110
|
+
env['rack.errors'].puts e.message
|
111
|
+
env['rack.errors'].puts e.backtrace #.collect { |line| "\t" + line }
|
112
|
+
env['rack.errors'].flush
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|