rack-streaming-proxy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ # The list of files that should be ignored by Mr Bones.
2
+ # Lines that start with '#' are comments.
3
+ #
4
+ # A .gitignore file can be used instead by setting it as the ignore
5
+ # file in your Rakefile:
6
+ #
7
+ # Bones {
8
+ # ignore_file '.gitignore'
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
16
+ coverage
17
+ doc
18
+ pkg
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 1.0.0 / 2009-11-14
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
data/README.txt ADDED
@@ -0,0 +1,78 @@
1
+ rack-streaming-proxy
2
+ by Nathan Witmer <nwitmer@gmail.com>
3
+ http://github.com/aniero/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.
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ begin
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.1"
21
+ depend_on "servolux", :version => "~> 0.8.1"
22
+ }
23
+
data/coderack.rb ADDED
@@ -0,0 +1,133 @@
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
+
data/dev/proxy.ru ADDED
@@ -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"}, ""] }
data/dev/streamer.ru ADDED
@@ -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,76 @@
1
+ class Rack::StreamingProxy
2
+ class ProxyRequest
3
+ include Rack::Utils
4
+
5
+ attr_reader :status, :headers
6
+
7
+ def initialize(request, uri)
8
+ uri = URI.parse(uri)
9
+
10
+ method = request.request_method.downcase
11
+ method[0..0] = method[0..0].upcase
12
+
13
+ proxy_request = Net::HTTP.const_get(method).new("#{uri.path}#{"?" if uri.query}#{uri.query}")
14
+
15
+ if proxy_request.request_body_permitted? and request.body
16
+ proxy_request.body_stream = request.body
17
+ proxy_request.content_length = request.content_length
18
+ proxy_request.content_type = request.content_type
19
+ end
20
+
21
+ %w(Accept Accept-Encoding Accept-Charset
22
+ X-Requested-With Referer User-Agent Cookie).each do |header|
23
+ key = "HTTP_#{header.upcase.gsub('-', '_')}"
24
+ proxy_request[header] = request.env[key] if request.env[key]
25
+ end
26
+ proxy_request["X-Forwarded-For"] =
27
+ (request.env["X-Forwarded-For"].to_s.split(/, +/) + [request.env["REMOTE_ADDR"]]).join(", ")
28
+
29
+ @piper = Servolux::Piper.new 'r', :timeout => 30
30
+
31
+ @piper.child do
32
+ Net::HTTP.start(uri.host, uri.port) do |http|
33
+ http.request(proxy_request) do |response|
34
+ # at this point the headers and status are available, but the body
35
+ # has not yet been read. start reading it and putting it in the parent's pipe.
36
+ response_headers = {}
37
+ response.each_header {|k,v| response_headers[k] = v}
38
+ @piper.puts [response.code.to_i, response_headers]
39
+
40
+ response.read_body do |chunk|
41
+ @piper.puts chunk
42
+ end
43
+ @piper.puts :done
44
+ end
45
+ end
46
+
47
+ exit!
48
+ end
49
+
50
+ @piper.parent do
51
+ # wait for the status and headers to come back from the child
52
+ @status, @headers = @piper.gets
53
+ @headers = HeaderHash.new(@headers)
54
+ end
55
+ end
56
+
57
+ def each
58
+ chunked = @headers["Transfer-Encoding"] == "chunked"
59
+ term = "\r\n"
60
+
61
+ while chunk = @piper.gets
62
+ break if chunk == :done
63
+ if chunked
64
+ size = bytesize(chunk)
65
+ next if size == 0
66
+ yield [size.to_s(16), term, chunk, term].join
67
+ else
68
+ yield chunk
69
+ end
70
+ end
71
+
72
+ yield ["0", term, "", term].join if chunked
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,95 @@
1
+ module Rack
2
+ class StreamingProxy
3
+
4
+ class Error < StandardError; end
5
+
6
+ # :stopdoc:
7
+ VERSION = '1.0.0'
8
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
9
+ PATH = ::File.dirname(LIBPATH) + ::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
+ 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
79
+ end
80
+
81
+ protected
82
+
83
+ attr_reader :request_uri, :app
84
+
85
+ end
86
+
87
+ end
88
+
89
+ require "rack"
90
+ require "servolux"
91
+ require "net/http"
92
+ require "uri"
93
+
94
+ Rack::StreamingProxy.require_all_libs_relative_to(__FILE__)
95
+
@@ -0,0 +1,14 @@
1
+ require File.expand_path(
2
+ File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
3
+
4
+ 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
+ end
14
+
@@ -0,0 +1,7 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ describe Rack::StreamingProxy do
5
+ it "has specs"
6
+ end
7
+
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-streaming-proxy
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nathan Witmer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-15 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 1.0.1
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: servolux
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 0.8.1
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: bones
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 3.0.1
44
+ version:
45
+ description: Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn.
46
+ email: nwitmer@gmail.com
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ extra_rdoc_files:
52
+ - History.txt
53
+ - README.txt
54
+ files:
55
+ - .gitignore
56
+ - History.txt
57
+ - README.txt
58
+ - Rakefile
59
+ - coderack.rb
60
+ - dev/proxy.ru
61
+ - dev/streamer.ru
62
+ - lib/rack/streaming_proxy.rb
63
+ - lib/rack/streaming_proxy/proxy_request.rb
64
+ - spec/spec_helper.rb
65
+ - spec/streaming_proxy_spec.rb
66
+ has_rdoc: true
67
+ homepage: http://github.com/aniero/rack-streaming-proxy
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options:
72
+ - --main
73
+ - README.txt
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: "0"
81
+ version:
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ version:
88
+ requirements: []
89
+
90
+ rubyforge_project: rack-streaming-proxy
91
+ rubygems_version: 1.3.5
92
+ signing_key:
93
+ specification_version: 3
94
+ summary: Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn
95
+ test_files: []
96
+