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 +0 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +42 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +36 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/lib/net_http_hacked.rb +91 -0
- data/lib/rack/http_streaming_response.rb +63 -0
- data/lib/rack/reverse_proxy.rb +142 -0
- data/lib/rack/streaming_proxy.rb +70 -0
- data/lib/whack-a-node.rb +105 -0
- data/spec/rack/reverse_proxy_spec.rb +180 -0
- data/spec/rack/streaming_proxy_spec.rb +16 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/whack-a-node_spec.rb +24 -0
- metadata +204 -0
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
|
+
|
data/lib/whack-a-node.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|