rack-streaming-proxy 1.0.0

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