whack-a-node 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.DS_Store ADDED
Binary file
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+ gem 'rack-proxy'
6
+ gem 'rack'
7
+
8
+ # Add dependencies to develop your gem here.
9
+ # Include everything needed to run rake, tests, features, etc.
10
+ group :development do
11
+ gem "rspec", "~> 2.3.0"
12
+ gem "bundler", "~> 1.0.0"
13
+ gem "jeweler", "~> 1.5.2"
14
+ gem "rack-test"
15
+ gem "webmock"
16
+ gem "rcov", ">= 0"
17
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,42 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ addressable (2.2.5)
5
+ crack (0.1.8)
6
+ diff-lcs (1.1.2)
7
+ git (1.2.5)
8
+ jeweler (1.5.2)
9
+ bundler (~> 1.0.0)
10
+ git (>= 1.2.5)
11
+ rake
12
+ rack (1.2.2)
13
+ rack-proxy (0.3.4)
14
+ rack
15
+ rack-test (0.5.7)
16
+ rack (>= 1.0)
17
+ rake (0.8.7)
18
+ rcov (0.9.9)
19
+ rspec (2.3.0)
20
+ rspec-core (~> 2.3.0)
21
+ rspec-expectations (~> 2.3.0)
22
+ rspec-mocks (~> 2.3.0)
23
+ rspec-core (2.3.1)
24
+ rspec-expectations (2.3.0)
25
+ diff-lcs (~> 1.1.2)
26
+ rspec-mocks (2.3.0)
27
+ webmock (1.6.2)
28
+ addressable (>= 2.2.2)
29
+ crack (>= 0.1.7)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ bundler (~> 1.0.0)
36
+ jeweler (~> 1.5.2)
37
+ rack
38
+ rack-proxy
39
+ rack-test
40
+ rcov
41
+ rspec (~> 2.3.0)
42
+ webmock
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Niels Meersschaert
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,36 @@
1
+ = whack-a-node
2
+
3
+ Node apps running in any rack server as a endpoint, and potentially a middleware layer.
4
+
5
+ Leverage authentication, authorization and more in front of your node apps, letting node be used for high concurrency portions of your app.
6
+
7
+ == Usage
8
+
9
+ match "/proxy" => WhackANode.new("/")
10
+
11
+ This will attempt to reach a node server on localhost at port 8810. The response will be proxied by Rack.
12
+
13
+ match "/redirect" => WhackANode.new("/","www.example.com", 2306, true)
14
+
15
+ This will redirect to a node server at www.example.com on port 2306.
16
+
17
+ In this case, the last param causes the WhackANode to actually redirect instead of proxy the request. In some cases, this can result in faster performance, as you don't have to have a Rack instance sitting around for the whole request.
18
+
19
+ Generally speaking, using the proxy over a Rails controller ends up being about 8-10 times faster. While still much slower than Node directly, this can still yield benefit.
20
+
21
+
22
+ == Contributing to whack-a-node
23
+
24
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
25
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
26
+ * Fork the project
27
+ * Start a feature/bugfix branch
28
+ * Commit and push until you are happy with your contribution
29
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
30
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
31
+
32
+ == Copyright
33
+
34
+ Copyright (c) 2011 Niels Meersschaert & Matthew Jording. See LICENSE.txt for
35
+ further details.
36
+
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "whack-a-node"
16
+ gem.homepage = "http://github.com/nielsm/whack-a-node"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{Run node apps as Rack apps}
19
+ gem.description = %Q{Extend your middleware to include Node.js}
20
+ gem.email = "nmeersschaert@mac.com"
21
+ gem.authors = ["Niels Meersschaert", "Matthew Jording"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
25
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "whack-a-node #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,91 @@
1
+ # We are hacking net/http to change semantics of streaming handling
2
+ # from "block" semantics to regular "return" semnatics.
3
+ # We need it to construct a streamable rack triplet:
4
+ #
5
+ # [status, headers, streamable_body]
6
+ #
7
+ # See http://github.com/aniero/rack-streaming-proxy
8
+ # for alternative that uses additional process.
9
+ #
10
+ # BTW I don't like monkey patching either
11
+ # but this is not real monkey patching.
12
+ # I just added some methods and named them very uniquely
13
+ # to avoid eventual conflicts. You're safe. Trust me.
14
+ #
15
+ # Also, in Ruby 1.9.2 you could use Fibers to avoid hacking net/http.
16
+
17
+ require 'net/http'
18
+
19
+ class Net::HTTP
20
+ # Original #request with block semantics.
21
+ #
22
+ # def request(req, body = nil, &block)
23
+ # unless started?
24
+ # start {
25
+ # req['connection'] ||= 'close'
26
+ # return request(req, body, &block)
27
+ # }
28
+ # end
29
+ # if proxy_user()
30
+ # unless use_ssl?
31
+ # req.proxy_basic_auth proxy_user(), proxy_pass()
32
+ # end
33
+ # end
34
+ #
35
+ # req.set_body_internal body
36
+ # begin_transport req
37
+ # req.exec @socket, @curr_http_version, edit_path(req.path)
38
+ # begin
39
+ # res = HTTPResponse.read_new(@socket)
40
+ # end while res.kind_of?(HTTPContinue)
41
+ # res.reading_body(@socket, req.response_body_permitted?) {
42
+ # yield res if block_given?
43
+ # }
44
+ # end_transport req, res
45
+ #
46
+ # res
47
+ # end
48
+
49
+ def begin_request_hacked(req)
50
+ begin_transport req
51
+ req.exec @socket, @curr_http_version, edit_path(req.path)
52
+ begin
53
+ res = Net::HTTPResponse.read_new(@socket)
54
+ end while res.kind_of?(Net::HTTPContinue)
55
+ res.begin_reading_body_hacked(@socket, req.response_body_permitted?)
56
+ @req_hacked, @res_hacked = req, res
57
+ @res_hacked
58
+ end
59
+
60
+ def end_request_hacked
61
+ @res_hacked.end_reading_body_hacked
62
+ end_transport @req_hacked, @res_hacked
63
+ @res_hacked
64
+ end
65
+ end
66
+
67
+ class Net::HTTPResponse
68
+ # Original #reading_body with block semantics
69
+ #
70
+ # def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only
71
+ # @socket = sock
72
+ # @body_exist = reqmethodallowbody && self.class.body_permitted?
73
+ # begin
74
+ # yield
75
+ # self.body # ensure to read body
76
+ # ensure
77
+ # @socket = nil
78
+ # end
79
+ # end
80
+
81
+ def begin_reading_body_hacked(sock, reqmethodallowbody)
82
+ @socket = sock
83
+ @body_exist = reqmethodallowbody && self.class.body_permitted?
84
+ end
85
+
86
+ def end_reading_body_hacked
87
+ self.body
88
+ @socket = nil
89
+ end
90
+ end
91
+
@@ -0,0 +1,63 @@
1
+ require "net_http_hacked"
2
+
3
+ module Rack
4
+
5
+ # Wraps the hacked net/http in a Rack way.
6
+ class HttpStreamingResponse
7
+ def initialize(request, host, port = nil)
8
+ @request, @host, @port = request, host, port
9
+ end
10
+
11
+ def status
12
+ response.code.to_i
13
+ end
14
+
15
+ def headers
16
+ h = Utils::HeaderHash.new
17
+
18
+ response.each_header do |k, v|
19
+ h[k] = v
20
+ end
21
+
22
+ h
23
+ end
24
+
25
+ def body
26
+ self
27
+ end
28
+
29
+ # Can be called only once!
30
+ def each(&block)
31
+ response.read_body(&block)
32
+ ensure
33
+ session.end_request_hacked
34
+ end
35
+
36
+ def to_s
37
+ @body ||= begin
38
+ lines = []
39
+
40
+ each do |line|
41
+ lines << line
42
+ end
43
+
44
+ lines.join
45
+ end
46
+ end
47
+
48
+ protected
49
+
50
+ # Net::HTTPResponse
51
+ def response
52
+ @response ||= session.begin_request_hacked(@request)
53
+ end
54
+
55
+ # Net::HTTP
56
+ def session
57
+ @session ||= Net::HTTP.start(@host, @port)
58
+ end
59
+
60
+ end
61
+
62
+ end
63
+
@@ -0,0 +1,142 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+
4
+ module Rack
5
+ class ReverseProxy
6
+
7
+
8
+ def initialize(app = nil, &b)
9
+ @app = app || lambda { [404, [], []] }
10
+ @paths = {}
11
+ @opts = {:preserve_host => false}
12
+ instance_eval &b if block_given?
13
+ end
14
+
15
+ def call(env)
16
+ rackreq = Rack::Request.new(env)
17
+ matcher, url = get_matcher_and_url rackreq.fullpath
18
+ return @app.call(env) if matcher.nil?
19
+
20
+
21
+
22
+ uri = get_uri(url, matcher, rackreq.fullpath)
23
+ headers = Rack::Utils::HeaderHash.new
24
+ env.each { |key, value|
25
+ if key =~ /HTTP_(.*)/
26
+ headers[$1] = value
27
+ end
28
+ }
29
+ headers['HOST'] = uri.host if @opts[:preserve_host]
30
+
31
+ session = Net::HTTP.new(uri.host, uri.port)
32
+ session.use_ssl = (uri.scheme == 'https')
33
+ session.verify_mode = OpenSSL::SSL::VERIFY_NONE
34
+ session.start { |http|
35
+ m = rackreq.request_method
36
+ case m
37
+ when "GET", "HEAD", "DELETE", "OPTIONS", "TRACE"
38
+ req = Net::HTTP.const_get(m.capitalize).new(uri.request_uri, headers)
39
+ req.basic_auth @opts[:username], @opts[:password] if @opts[:username] and @opts[:password]
40
+ when "PUT", "POST"
41
+ req = Net::HTTP.const_get(m.capitalize).new(uri.request_uri, headers)
42
+ req.basic_auth @opts[:username], @opts[:password] if @opts[:username] and @opts[:password]
43
+ req.content_length = rackreq.body.length
44
+ req.body_stream = rackreq.body
45
+ else
46
+ raise "method not supported: #{m}"
47
+ end
48
+
49
+ body = ''
50
+ res = http.request(req) do |res|
51
+ res.read_body do |segment|
52
+ body << segment
53
+ end
54
+ end
55
+
56
+ [res.code, create_response_headers(res), [body]]
57
+ }
58
+ end
59
+
60
+ private
61
+
62
+
63
+ def get_matcher_and_url path
64
+ matches = @paths.select do |matcher, url|
65
+ match_path(path, matcher)
66
+ end
67
+
68
+ if matches.length < 1
69
+ nil
70
+ elsif matches.length > 1
71
+ raise AmbiguousProxyMatch.new(path, matches)
72
+ else
73
+ matches.first.map{|a| a.dup}
74
+ end
75
+ end
76
+
77
+ def create_response_headers http_response
78
+ response_headers = Rack::Utils::HeaderHash.new(http_response.to_hash)
79
+ # handled by Rack
80
+ response_headers.delete('status')
81
+ # TODO: figure out how to handle chunked responses
82
+ response_headers.delete('transfer-encoding')
83
+ # TODO: Verify Content Length, and required Rack headers
84
+ response_headers
85
+ end
86
+
87
+ def match_path(path, matcher)
88
+ if matcher.is_a?(Regexp)
89
+ path.match(matcher)
90
+ else
91
+ path.match(/^#{matcher.to_s}/)
92
+ end
93
+ end
94
+
95
+ def get_uri(url, matcher, path)
96
+ if url =~/\$\d/
97
+ match_path(path, matcher).to_a.each_with_index { |m, i| url.gsub!("$#{i.to_s}", m) }
98
+ URI(url)
99
+ else
100
+ URI.join(url, path)
101
+ end
102
+ end
103
+
104
+ def reverse_proxy matcher, url, opts={}
105
+ raise GenericProxyURI.new(url) if matcher.is_a?(String) && URI(url).class == URI::Generic
106
+ @paths.merge!(matcher => url)
107
+ @opts.merge!(opts)
108
+ end
109
+ end
110
+
111
+ class GenericProxyURI < Exception
112
+ attr_reader :url
113
+
114
+ def intialize(url)
115
+ @url = url
116
+ end
117
+
118
+ def to_s
119
+ %Q(Your URL "#{@url}" is too generic. Did you mean "http://#{@url}"?)
120
+ end
121
+ end
122
+
123
+ class AmbiguousProxyMatch < Exception
124
+ attr_reader :path, :matches
125
+ def initialize(path, matches)
126
+ @path = path
127
+ @matches = matches
128
+ end
129
+
130
+ def to_s
131
+ %Q(Path "#{path}" matched multiple endpoints: #{formatted_matches})
132
+ end
133
+
134
+ private
135
+
136
+ def formatted_matches
137
+ matches.map {|m| %Q("#{m[0].to_s}" => "#{m[1]}")}.join(', ')
138
+ end
139
+ end
140
+
141
+ end
142
+
@@ -0,0 +1,70 @@
1
+ require "rack/http_streaming_response"
2
+
3
+ module Rack
4
+
5
+ # Subclass and bring your own #rewrite_request and #rewrite_response
6
+ class StreamingProxy
7
+ VERSION = "0.3.4"
8
+
9
+ def call(env)
10
+ rewrite_response(perform_request(rewrite_env(env)))
11
+ end
12
+
13
+ # Return modified env
14
+ def rewrite_env(env)
15
+ env
16
+ end
17
+
18
+ # Return a rack triplet [status, headers, body]
19
+ def rewrite_response(triplet)
20
+ triplet
21
+ end
22
+
23
+ protected
24
+
25
+ def perform_request(env)
26
+ source_request = Rack::Request.new(env)
27
+
28
+ # Initialize request
29
+ target_request = Net::HTTP.const_get(source_request.request_method.capitalize).new(source_request.fullpath)
30
+
31
+ # Setup headers
32
+ target_request.initialize_http_header(extract_http_request_headers(source_request.env))
33
+
34
+ # Setup body
35
+ if target_request.request_body_permitted? && source_request.body
36
+ target_request.body_stream = source_request.body
37
+ target_request.content_length = source_request.content_length
38
+ target_request.content_type = source_request.content_type if source_request.content_type
39
+ end
40
+
41
+ # Create a streaming response (the actual network communication is deferred, a.k.a. streamed)
42
+ target_response = HttpStreamingResponse.new(target_request, source_request.host, source_request.port)
43
+
44
+ [target_response.status, target_response.headers, target_response.body]
45
+ end
46
+
47
+ def extract_http_request_headers(env)
48
+ headers = env.reject do |k, v|
49
+ !(/^HTTP_[A-Z_]+$/ === k)
50
+ end.map do |k, v|
51
+ [reconstruct_header_name(k), v]
52
+ end.inject(Utils::HeaderHash.new) do |hash, k_v|
53
+ k, v = k_v
54
+ hash[k] = v
55
+ hash
56
+ end
57
+
58
+ x_forwarded_for = (headers["X-Forwarded-For"].to_s.split(/, +/) << env["REMOTE_ADDR"]).join(", ")
59
+
60
+ headers.merge!("X-Forwarded-For" => x_forwarded_for)
61
+ end
62
+
63
+ def reconstruct_header_name(name)
64
+ name.sub(/^HTTP_/, "").gsub("_", "-")
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+
@@ -0,0 +1,105 @@
1
+ require 'rack/streaming_proxy'
2
+ class WhackANode
3
+
4
+ def initialize(path,host="localhost", port="8810",redirect=false)
5
+ @path = path
6
+ @host = host
7
+ @port = port
8
+ @redirect = redirect
9
+ @paths = {}
10
+ end
11
+
12
+ def uri
13
+ URI("http://#{@host}:#{@port}#{@path}")
14
+ end
15
+
16
+ def proxy_request
17
+ uri = self.uri
18
+ session = Net::HTTP.new(uri.host, uri.port)
19
+ session.start {|http|
20
+ req = Net::HTTP::Get.new(uri.request_uri)
21
+ body = ''
22
+ res = http.request(req) do |res|
23
+ res.read_body do |segment|
24
+ body << segment
25
+ end
26
+ end
27
+
28
+ [res.code, create_response_headers(res), [body]]
29
+ }
30
+ end
31
+
32
+ def forward_request
33
+ [ 302, {'Location'=> uri.to_s }, [] ]
34
+ end
35
+
36
+ def call(env)
37
+ return @redirect ? forward_request : proxy_request
38
+ end
39
+
40
+ private
41
+
42
+
43
+ def get_matcher_and_url path
44
+ matches = @paths.select do |matcher, url|
45
+ match_path(path, matcher)
46
+ end
47
+
48
+ if matches.length < 1
49
+ nil
50
+ elsif matches.length > 1
51
+ raise AmbiguousProxyMatch.new(path, matches)
52
+ else
53
+ matches.first.map{|a| a.dup}
54
+ end
55
+ end
56
+
57
+ def create_response_headers http_response
58
+ response_headers = Rack::Utils::HeaderHash.new(http_response.to_hash)
59
+ # handled by Rack
60
+ response_headers.delete('status')
61
+ # TODO: figure out how to handle chunked responses
62
+ response_headers.delete('transfer-encoding')
63
+ # TODO: Verify Content Length, and required Rack headers
64
+ response_headers
65
+ end
66
+
67
+ def match_path(path, matcher)
68
+ if matcher.is_a?(Regexp)
69
+ path.match(matcher)
70
+ else
71
+ path.match(/^#{matcher.to_s}/)
72
+ end
73
+ end
74
+
75
+ def get_uri(url, matcher, path)
76
+ if url =~/\$\d/
77
+ match_path(path, matcher).to_a.each_with_index { |m, i| url.gsub!("$#{i.to_s}", m) }
78
+ URI(url)
79
+ else
80
+ URI.join(url, path)
81
+ end
82
+ end
83
+
84
+ def reverse_proxy matcher, url, opts={}
85
+ raise GenericProxyURI.new(url) if matcher.is_a?(String) && URI(url).class == URI::Generic
86
+ @paths.merge!(matcher => url)
87
+ @opts.merge!(opts)
88
+ end
89
+
90
+
91
+ def rewrite_env(env)
92
+ env["PORT"] = "8000"
93
+
94
+ env
95
+ end
96
+
97
+ def rewrite_response(triplet)
98
+ status, headers, body = triplet
99
+
100
+ headers["X-Foo"] = "Bar"
101
+
102
+ triplet
103
+ end
104
+
105
+ end
@@ -0,0 +1,180 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Rack::ReverseProxy do
4
+ include Rack::Test::Methods
5
+ include WebMock::API
6
+
7
+ def app
8
+ Rack::ReverseProxy.new
9
+ end
10
+
11
+ def dummy_app
12
+ lambda { [200, {}, ['Dummy App']] }
13
+ end
14
+
15
+ describe "as middleware" do
16
+ def app
17
+ Rack::ReverseProxy.new(dummy_app) do
18
+ reverse_proxy '/test', 'http://example.com/', {:preserve_host => true}
19
+ end
20
+ end
21
+
22
+ it "should forward requests to the calling app when the path is not matched" do
23
+ stub_request(:get, 'http://example.com/').to_return(:headers => {'Status' => '200 OK'})
24
+ get '/'
25
+ last_response.body.should == "Dummy App"
26
+ last_response.should be_ok
27
+ end
28
+
29
+ it "should proxy requests when a pattern is matched" do
30
+ stub_request(:get, 'http://example.com/test').to_return({:body => "Proxied App"})
31
+ get '/test'
32
+ last_response.body.should == "Proxied App"
33
+ end
34
+
35
+ it "the response header should never contain Status" do
36
+ stub_request(:any, 'example.com/test/stuff').to_return(:headers => {'Status' => '200 OK'})
37
+ get '/test/stuff'
38
+ last_response.headers['Status'].should == nil
39
+ end
40
+
41
+ it "the response header should never transfer-encoding" do
42
+ stub_request(:any, 'example.com/test/stuff').to_return(:headers => {'transfer-encoding' => 'Chunked'})
43
+ get '/test/stuff'
44
+ last_response.headers['transfer-encoding'].should == nil
45
+ end
46
+
47
+ it "should set the Host header" do
48
+ stub_request(:any, 'example.com/test/stuff')
49
+ get '/test/stuff'
50
+ a_request(:get, 'http://example.com/test/stuff').with(:headers => {"Host" => "example.com"}).should have_been_made
51
+ end
52
+
53
+ describe "with preserve host turned off" do
54
+ def app
55
+ Rack::ReverseProxy.new(dummy_app) do
56
+ reverse_proxy '/test', 'http://example.com/'
57
+ end
58
+ end
59
+
60
+ it "should not set the Host header" do
61
+ stub_request(:any, 'example.com/test/stuff')
62
+ get '/test/stuff'
63
+ a_request(:get, 'http://example.com/test/stuff').should have_been_made
64
+ end
65
+ end
66
+
67
+ describe "with basic auth turned on" do
68
+ def app
69
+ Rack::ReverseProxy.new(dummy_app) do
70
+ reverse_proxy '/test', 'http://example.com/', {:username => "joe", :password => "shmoe"}
71
+ end
72
+ end
73
+
74
+ it "should make request with basic auth" do
75
+ stub_request(:get, "http://joe:shmoe@example.com/test/stuff").to_return(:body => "secured content")
76
+ get '/test/stuff'
77
+ last_response.body.should == "secured content"
78
+ end
79
+ end
80
+
81
+ describe "with ambiguous routes" do
82
+ def app
83
+ Rack::ReverseProxy.new(dummy_app) do
84
+ reverse_proxy '/test', 'http://example.com/'
85
+ reverse_proxy /^\/test/, 'http://example.com/'
86
+ end
87
+ end
88
+
89
+ it "should throw an exception" do
90
+ lambda { get '/test' }.should raise_error(Rack::AmbiguousProxyMatch)
91
+ end
92
+ end
93
+
94
+ describe "with a route as a regular expression" do
95
+ def app
96
+ Rack::ReverseProxy.new(dummy_app) do
97
+ reverse_proxy %r|^/test(/.*)$|, 'http://example.com$1'
98
+ end
99
+ end
100
+
101
+ it "should support subcaptures" do
102
+ stub_request(:get, 'http://example.com/path').to_return({:body => "Proxied App"})
103
+ get '/test/path'
104
+ last_response.body.should == "Proxied App"
105
+ end
106
+ end
107
+
108
+ describe "with a https route" do
109
+ def app
110
+ Rack::ReverseProxy.new(dummy_app) do
111
+ reverse_proxy '/test', 'https://example.com'
112
+ end
113
+ end
114
+
115
+ it "should make a secure request" do
116
+ stub_request(:get, 'https://example.com/test/stuff').to_return({:body => "Proxied Secure App"})
117
+ get '/test/stuff'
118
+ last_response.body.should == "Proxied Secure App"
119
+ end
120
+
121
+ end
122
+
123
+ describe "with a route as a string" do
124
+ def app
125
+ Rack::ReverseProxy.new(dummy_app) do
126
+ reverse_proxy '/test', 'http://example.com'
127
+ reverse_proxy '/path', 'http://example.com/foo$0'
128
+ end
129
+ end
130
+
131
+ it "should append the full path to the uri" do
132
+ stub_request(:get, 'http://example.com/test/stuff').to_return({:body => "Proxied App"})
133
+ get '/test/stuff'
134
+ last_response.body.should == "Proxied App"
135
+ end
136
+
137
+ end
138
+
139
+ describe "with a generic url" do
140
+ def app
141
+ Rack::ReverseProxy.new(dummy_app) do
142
+ reverse_proxy '/test', 'example.com'
143
+ end
144
+ end
145
+
146
+ it "should throw an exception" do
147
+ lambda{ app }.should raise_error(Rack::GenericProxyURI)
148
+ end
149
+ end
150
+
151
+ describe "with a matching route" do
152
+ def app
153
+ Rack::ReverseProxy.new(dummy_app) do
154
+ reverse_proxy '/test', 'http://example.com/'
155
+ end
156
+ end
157
+
158
+ %w|get head delete put post|.each do |method|
159
+ describe "and using method #{method}" do
160
+ it "should forward the correct request" do
161
+ stub_request(method.to_sym, 'http://example.com/test').to_return({:body => "Proxied App for #{method}"})
162
+ eval "#{method} '/test'"
163
+ last_response.body.should == "Proxied App for #{method}"
164
+ end
165
+
166
+ if %w|put post|.include?(method)
167
+ it "should forward the request payload" do
168
+ stub_request(method.to_sym, 'http://example.com/test').to_return { |req| {:body => req.body} }
169
+ eval "#{method} '/test', {:test => 'test'}"
170
+ last_response.body.should == "test=test"
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+
179
+ end
180
+
@@ -0,0 +1,16 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Rack::StreamingProxy do
4
+ include Rack::Test::Methods
5
+ include WebMock::API
6
+
7
+ def app
8
+ Rack::StreamingProxy.new
9
+ end
10
+
11
+ it "should have a port of 90210" do
12
+ get "/"
13
+ last_response.should_not be_nil
14
+ #@app.should_not be_nil
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'rack/test'
5
+ require 'whack-a-node'
6
+ require 'rack/reverse_proxy'
7
+ require 'rack/streaming_proxy'
8
+ require 'webmock/rspec'
9
+
10
+ # Requires supporting files with custom matchers and macros, etc,
11
+ # in ./support/ and its subdirectories.
12
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
13
+
14
+ RSpec.configure do |config|
15
+
16
+ end
@@ -0,0 +1,24 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'rack/test'
3
+ include Rack::Test::Methods
4
+
5
+ describe "WhackANode" do
6
+ class WhackyTest < WhackANode
7
+ def rewrite_env(env)
8
+ env['PORT'] = 90210
9
+ end
10
+ end
11
+
12
+ def app
13
+ WhackANode.new
14
+ end
15
+
16
+ #before(:each) do
17
+ #@app = WhackyTest.new
18
+ #end
19
+ it "should have a port of 90210" do
20
+ get "/"
21
+ last_response.should_not be_nil
22
+ #@app.should_not be_nil
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,204 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: whack-a-node
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Niels Meersschaert
14
+ - Matthew Jording
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2011-05-17 00:00:00 -04:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ prerelease: false
24
+ type: :runtime
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 3
31
+ segments:
32
+ - 0
33
+ version: "0"
34
+ name: rack-proxy
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ prerelease: false
38
+ type: :runtime
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 3
45
+ segments:
46
+ - 0
47
+ version: "0"
48
+ name: rack
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ prerelease: false
52
+ type: :development
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ~>
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 2
61
+ - 3
62
+ - 0
63
+ version: 2.3.0
64
+ name: rspec
65
+ version_requirements: *id003
66
+ - !ruby/object:Gem::Dependency
67
+ prerelease: false
68
+ type: :development
69
+ requirement: &id004 !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ~>
73
+ - !ruby/object:Gem::Version
74
+ hash: 23
75
+ segments:
76
+ - 1
77
+ - 0
78
+ - 0
79
+ version: 1.0.0
80
+ name: bundler
81
+ version_requirements: *id004
82
+ - !ruby/object:Gem::Dependency
83
+ prerelease: false
84
+ type: :development
85
+ requirement: &id005 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ~>
89
+ - !ruby/object:Gem::Version
90
+ hash: 7
91
+ segments:
92
+ - 1
93
+ - 5
94
+ - 2
95
+ version: 1.5.2
96
+ name: jeweler
97
+ version_requirements: *id005
98
+ - !ruby/object:Gem::Dependency
99
+ prerelease: false
100
+ type: :development
101
+ requirement: &id006 !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ hash: 3
107
+ segments:
108
+ - 0
109
+ version: "0"
110
+ name: rack-test
111
+ version_requirements: *id006
112
+ - !ruby/object:Gem::Dependency
113
+ prerelease: false
114
+ type: :development
115
+ requirement: &id007 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ hash: 3
121
+ segments:
122
+ - 0
123
+ version: "0"
124
+ name: webmock
125
+ version_requirements: *id007
126
+ - !ruby/object:Gem::Dependency
127
+ prerelease: false
128
+ type: :development
129
+ requirement: &id008 !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ hash: 3
135
+ segments:
136
+ - 0
137
+ version: "0"
138
+ name: rcov
139
+ version_requirements: *id008
140
+ description: Extend your middleware to include Node.js
141
+ email: nmeersschaert@mac.com
142
+ executables: []
143
+
144
+ extensions: []
145
+
146
+ extra_rdoc_files:
147
+ - LICENSE.txt
148
+ - README.rdoc
149
+ files:
150
+ - .DS_Store
151
+ - Gemfile
152
+ - Gemfile.lock
153
+ - LICENSE.txt
154
+ - README.rdoc
155
+ - Rakefile
156
+ - VERSION
157
+ - lib/net_http_hacked.rb
158
+ - lib/rack/http_streaming_response.rb
159
+ - lib/rack/reverse_proxy.rb
160
+ - lib/rack/streaming_proxy.rb
161
+ - lib/whack-a-node.rb
162
+ - spec/rack/reverse_proxy_spec.rb
163
+ - spec/rack/streaming_proxy_spec.rb
164
+ - spec/spec_helper.rb
165
+ - spec/whack-a-node_spec.rb
166
+ has_rdoc: true
167
+ homepage: http://github.com/nielsm/whack-a-node
168
+ licenses:
169
+ - MIT
170
+ post_install_message:
171
+ rdoc_options: []
172
+
173
+ require_paths:
174
+ - lib
175
+ required_ruby_version: !ruby/object:Gem::Requirement
176
+ none: false
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ hash: 3
181
+ segments:
182
+ - 0
183
+ version: "0"
184
+ required_rubygems_version: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ hash: 3
190
+ segments:
191
+ - 0
192
+ version: "0"
193
+ requirements: []
194
+
195
+ rubyforge_project:
196
+ rubygems_version: 1.5.0
197
+ signing_key:
198
+ specification_version: 3
199
+ summary: Run node apps as Rack apps
200
+ test_files:
201
+ - spec/rack/reverse_proxy_spec.rb
202
+ - spec/rack/streaming_proxy_spec.rb
203
+ - spec/spec_helper.rb
204
+ - spec/whack-a-node_spec.rb