mikehale-rat-hole 0.1.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/History.txt ADDED
@@ -0,0 +1,3 @@
1
+ == 0.1.0 / 2008-12-17
2
+
3
+ * First release.
data/Manifest.txt ADDED
@@ -0,0 +1,7 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.rdoc
4
+ lib/rat_hole.rb
5
+ lib/util.rb
6
+ test/test_rat_hole.rb
7
+ test/mock_request.rb
data/README.rdoc ADDED
@@ -0,0 +1,87 @@
1
+ Rat Hole is a handy class for creating a rack compliant http proxy that allows you to modify the request from the user and the response from the server.
2
+ The name is inspired by why's mousehole[http://code.whytheluckystiff.net/mouseHole/]
3
+
4
+ == Why
5
+ Use Rat Hole to proxy site A into the namespace of site B.
6
+
7
+ Along the way you can modify the request from the user (example: proxy to an ip and set the Host header to support virtual hosts without DNS).
8
+
9
+ You can also modify the response from the server to cleanup html tweak headers etc.
10
+
11
+ == Usage
12
+ require 'rat_hole'
13
+ require 'hpricot'
14
+
15
+ class PoliticalAgendaRatHole < RatHole
16
+ def process_user_request(rack_request)
17
+ # optionally munge the request before passing it to the old server
18
+
19
+ # required to return the rack request
20
+ rack_request
21
+ end
22
+
23
+ def process_server_response(rack_response)
24
+ # For any html pages proxied replace all links with http://ronpaul.com and
25
+ # add a Ron-Paul header.
26
+
27
+ if(rack_response.content_type == 'text/html')
28
+
29
+ # dump the body into hpricot so we can use hpricot's search/replace goodness
30
+ doc = Hpricot(rack_response.body.first)
31
+
32
+ # update all links to help spread our political views
33
+ (doc/"a").set('href', 'http://ronpaul.com')
34
+
35
+ # update the original string with our modified html
36
+ rack_response.body.first.replace(doc.to_html)
37
+
38
+ rack_response.headers['Ron-Paul'] = 'wish I could have voted for this guy'
39
+ end
40
+
41
+ # required to return the rack response
42
+ rack_response
43
+ end
44
+ end
45
+
46
+ app = PoliticalAgendaRatHole.new('www.google.com')
47
+ Rack::Handler::Mongrel.run(app, {:Host => 'localhost', :Port => 5001})
48
+
49
+ == How it Works
50
+ User Request --->
51
+ --- RatHoleProxy.process_user_request(rack_request) --->
52
+ <==========> OLD SERVER
53
+ <--- RatHoleProxy.process_server_response(rack_response) ---
54
+ User Response <---
55
+
56
+ == TODO
57
+ * add error handling
58
+ * handle gziped content (accept-encoding, transfer-encoding)
59
+ * maybe use a pool of Net::HTTP connections to speed things up
60
+ * provide an easy way for testing rat holes
61
+
62
+ == Credits
63
+ * Michael Hale (http://halethegeek.com)
64
+ * David Bogus
65
+
66
+ == LICENSE
67
+ The MIT License
68
+
69
+ Copyright (c) 2008 Michael Hale & David Bogus
70
+
71
+ Permission is hereby granted, free of charge, to any person obtaining a copy
72
+ of this software and associated documentation files (the "Software"), to deal
73
+ in the Software without restriction, including without limitation the rights
74
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
75
+ copies of the Software, and to permit persons to whom the Software is
76
+ furnished to do so, subject to the following conditions:
77
+
78
+ The above copyright notice and this permission notice shall be included in
79
+ all copies or substantial portions of the Software.
80
+
81
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
82
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
83
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
84
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
85
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
86
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
87
+ THE SOFTWARE.
data/lib/rat_hole.rb ADDED
@@ -0,0 +1,59 @@
1
+ require 'net/http'
2
+ require 'rubygems'
3
+ require 'rack'
4
+ require 'delegate'
5
+ require 'util'
6
+
7
+ class RatHole
8
+ def initialize(host)
9
+ @host = host
10
+ end
11
+
12
+ def process_user_request(rack_request)
13
+ rack_request
14
+ end
15
+
16
+ def process_server_response(rack_response)
17
+ rack_response
18
+ end
19
+
20
+ def call(env)
21
+ Net::HTTP.start(@host) do |http|
22
+ http.instance_eval{@socket = MethodSpy.new(@socket){|method| method =~ /write/}} if $DEBUG
23
+
24
+ env.delete('HTTP_ACCEPT_ENCODING')
25
+ source_request = process_user_request(Rack::Request.new(env))
26
+ source_headers = request_headers(source_request.env)
27
+
28
+ if source_request.get?
29
+ response = http.get(source_request.path_info, source_headers)
30
+ elsif source_request.post?
31
+ post = Net::HTTP::Post.new(source_request.path_info, source_headers)
32
+ class << post
33
+ include HTTPHeaderPatch
34
+ end
35
+ post.form_data = source_request.POST
36
+ response = http.request(post)
37
+ end
38
+
39
+ code = response.code.to_i
40
+ headers = response.to_hash
41
+ body = response.body || ''
42
+ headers.delete('transfer-encoding')
43
+
44
+ process_server_response(Rack::Response.new(body, code, headers)).finish
45
+ end
46
+ end
47
+
48
+ def request_headers(env)
49
+ env.select{|k,v| k =~ /^HTTP/}.inject({}) do |h, e|
50
+ k,v = e
51
+ h.merge(k.split('_')[1..-1].join('-').to_camel_case => v)
52
+ end
53
+ end
54
+ end
55
+
56
+ # This class simply extends RatHole and does nothing.
57
+ # It's only useful for making sure that you have everything hooked up correctly.
58
+ class EmptyRatHole < RatHole
59
+ end
data/lib/util.rb ADDED
@@ -0,0 +1,51 @@
1
+ class SocketSpy < SimpleDelegator
2
+ def write(content)
3
+ p :writing => content
4
+ __getobj__.write content
5
+ end
6
+
7
+ [:readline, :readuntil, :read_all, :read].each{|symbol|
8
+ define_method(symbol) do |*args|
9
+ content = __getobj__.send(symbol, *args)
10
+ p :reading => content
11
+ content
12
+ end
13
+ }
14
+ end
15
+
16
+ class MethodSpy
17
+ def initialize(delegate, &block)
18
+ @delegate = delegate
19
+ @filter = block
20
+ end
21
+
22
+ def method_missing(symbol, *args, &block)
23
+ result = @delegate.send(symbol, *args, &block)
24
+ @block.call if @block
25
+ p [symbol, args, result, block] if @filter && @filter.call(symbol.to_s)
26
+ result
27
+ end
28
+ end
29
+
30
+ class String
31
+ def to_camel_case(split_on='-')
32
+ self.split(split_on).collect{|e| e.capitalize}.join(split_on)
33
+ end
34
+ end
35
+
36
+ module HTTPHeaderPatch
37
+ # handle multiple parameters with the same name
38
+ def form_data=(params, sep = '&')
39
+ self.body = params.map {|key,value|
40
+ if value.is_a?(Array)
41
+ value.map{|v| param_line(key, v) }
42
+ else
43
+ param_line(key, value)
44
+ end
45
+ }.join(sep)
46
+ end
47
+
48
+ def param_line(k, v)
49
+ "#{urlencode(k.to_s)}=#{urlencode(v.to_s)}"
50
+ end
51
+ end
data/rat-hole.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "rat-hole"
3
+ s.version = "0.1.0"
4
+ s.date = "2008-12-17"
5
+ s.summary = "Rack compliant proxy"
6
+ s.email = "mikehale@gmail.com"
7
+ s.homepage = "http://github.com/mikehale/rat-hole"
8
+ s.description = "Rat Hole is a handy library for creating a rack compliant http proxy that allows you to modify the request from the user and the response from the server."
9
+ s.has_rdoc = true
10
+ s.authors = ["Michael Hale", "David Bogus"]
11
+ s.files = ["History.txt",
12
+ "README.rdoc",
13
+ "rat-hole.gemspec",
14
+ "lib/rat_hole.rb",
15
+ "lib/util.rb"]
16
+ s.test_files = ["test/test_rat_hole.rb",
17
+ "test/mock_request.rb"]
18
+ s.rdoc_options = ["--main", "README.rdoc"]
19
+ s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.rdoc"]
20
+ s.add_dependency("rack", ["> 0.4.0"])
21
+ s.add_dependency("rr", ["> 0.6.0"])
22
+ end
@@ -0,0 +1,35 @@
1
+ class MockRequest
2
+
3
+ attr_reader :headers, :body, :uri
4
+
5
+ def initialize(request_string)
6
+ lines = request_string.split("\r\n")
7
+
8
+ # find blank line which seperates the headers from the body
9
+ index_of_blank = nil
10
+ lines.each_with_index{|e,i|
11
+ index_of_blank = i if e == ""
12
+ }
13
+
14
+ @type, @uri = lines.first.split(/\s+/)
15
+ if index_of_blank
16
+ @headers = lines[1..index_of_blank]
17
+ @body = lines[(index_of_blank + 1)..-1].first
18
+ else
19
+ @headers = lines[1..-1]
20
+ end
21
+
22
+ @headers = @headers.inject({}){|h,e|
23
+ k,v = e.split(/:\s+/)
24
+ h.merge k => v
25
+ }
26
+ end
27
+
28
+ def get?
29
+ @type == 'GET'
30
+ end
31
+
32
+ def post?
33
+ @type == 'POST'
34
+ end
35
+ end
@@ -0,0 +1,221 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
+
3
+ require 'rubygems'
4
+ require 'rr'
5
+ require 'delegate'
6
+ require 'test/unit'
7
+ require 'ruby-debug'
8
+ require 'rat_hole'
9
+ require 'mock_request'
10
+ require 'hpricot'
11
+
12
+ class Test::Unit::TestCase
13
+ include RR::Adapters::TestUnit
14
+ end
15
+
16
+ class TestRatHole < Test::Unit::TestCase
17
+ def mock_server(opts={})
18
+ opts[:host] = opts[:host] || '127.0.0.1'
19
+ opts[:code] = opts[:code] || 200
20
+ opts[:headers] = opts[:headers] || {}
21
+
22
+ host = opts[:host]
23
+ code = opts[:code]
24
+ headers = opts[:headers]
25
+ body = opts[:body]
26
+
27
+ response = [%(HTTP/1.1 #{code} OK)]
28
+ headers.each{|k,vs|
29
+ if vs.is_a?(Array)
30
+ response << vs.map{|v| "#{k.to_s}: #{v.to_s}" }
31
+ else
32
+ response << "#{k.to_s}: #{vs.to_s}"
33
+ end
34
+ }
35
+ response << ''
36
+ response << %(#{body})
37
+ response = response.join("\r\n")
38
+
39
+ @io = StringIO.new(response)
40
+ class << @io
41
+ attr_reader :written
42
+
43
+ def write(content)
44
+ @written = '' unless @written
45
+ @written << content
46
+ 0
47
+ end
48
+ end
49
+
50
+ mock(TCPSocket).open(host, 80) { @io }
51
+ end
52
+
53
+ def proxied_request
54
+ MockRequest.new(@io.written)
55
+ end
56
+
57
+ def send_get_request(rack_env={})
58
+ opts = {:lint=>true}.merge(rack_env)
59
+ rh = RatHole.new('127.0.0.1')
60
+ Rack::MockRequest.new(rh).get('/someuri', opts)
61
+ end
62
+
63
+ def send_post_request(body='')
64
+ rh = RatHole.new('127.0.0.1')
65
+ Rack::MockRequest.new(rh).post('/someuri', {:lint=>true, :input=> body})
66
+ end
67
+
68
+ def test_response_unchanged
69
+ expected_body = 'the body'
70
+ mock_server(:body => expected_body)
71
+ response = send_get_request
72
+
73
+ assert_equal 200, response.status
74
+ assert_equal expected_body, response.body
75
+ end
76
+
77
+ def test_headers_normalized
78
+ mock_server(:headers => {'server' => 'freedom-2.0', 'set-cookie' => 'ronpaul=true'})
79
+ response = send_get_request
80
+ assert_equal('ronpaul=true', response.headers['Set-Cookie'])
81
+ assert_equal('freedom-2.0', response.headers['Server'])
82
+ end
83
+
84
+ def test_default_body
85
+ mock_server(:body => nil)
86
+ response = send_get_request
87
+ assert_equal '', response.body
88
+ end
89
+
90
+ def test_get_request
91
+ mock_server
92
+ send_get_request
93
+ assert proxied_request.get?
94
+ end
95
+
96
+ def test_post_request
97
+ mock_server
98
+ send_post_request("field1=value1")
99
+ assert proxied_request.post?
100
+ assert proxied_request.body.include?("field1=value1")
101
+ end
102
+
103
+ def test_post_duplicate_keys
104
+ mock_server
105
+ send_post_request("field1=value1&field1=value2")
106
+ assert_equal("field1=value1&field1=value2", proxied_request.body)
107
+ end
108
+
109
+ def test_post_data_urlencoded
110
+ mock_server
111
+ send_post_request("field%201=value%201")
112
+ assert("field%201=value%201", proxied_request.body)
113
+ end
114
+
115
+ def test_convert_rack_env_to_http_headers
116
+ headers_added_by_rack = {"Accept"=>"*/*", "Host"=>"127.0.0.1"}
117
+ expected_headers = {"X-Forwarded-Host"=>"www.example.com"}.merge(headers_added_by_rack)
118
+
119
+ mock_server
120
+ send_get_request({"HTTP_X_FORWARDED_HOST"=>"www.example.com", "NON_HTTP_HEADER" => '42'})
121
+ assert_equal(expected_headers, proxied_request.headers)
122
+ end
123
+
124
+ def test_convert_rack_env_to_http_headers_more_data
125
+ expected_headers = {
126
+ "X-Forwarded-Host"=>"www.example.com",
127
+ "User-Agent"=>"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.4) Gecko/2008102920 Firefox/3.0.4",
128
+ "Cache-Control"=>"max-age=0",
129
+ "If-None-Match"=>"\"58dc30c-216-3d878fe2\"-gzip",
130
+ "Accept-Language"=>"en-us,en;q=0.5",
131
+ "Host"=>"localhost:4001",
132
+ "Referer"=>"http://www.example.com/posts/",
133
+ "Cookie"=>"cookie1=YWJj; cookie2=ZGVm",
134
+ "Accept-Charset"=>"ISO-8859-1,utf-8;q=0.7,*;q=0.7",
135
+ "X-Forwarded-Server"=>"www.example.com",
136
+ "If-Modified-Since"=>"Tue, 17 Sep 2002 20:26:10 GMT",
137
+ "X-Forwarded-For"=>"127.0.0.1",
138
+ "Accept"=>"image/png,image/*;q=0.8,*/*;q=0.5",
139
+ "Connection"=>"Keep-Alive"}
140
+
141
+ rack_env = {"SERVER_NAME"=>"localhost",
142
+ "HTTP_X_FORWARDED_HOST"=>"www.example.com",
143
+ "HTTP_ACCEPT_ENCODING"=>"gzip,deflate",
144
+ "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.4) Gecko/2008102920 Firefox/3.0.4",
145
+ "HTTP_CACHE_CONTROL"=>"max-age=0",
146
+ "HTTP_IF_NONE_MATCH"=>"\"58dc30c-216-3d878fe2\"-gzip",
147
+ "HTTP_ACCEPT_LANGUAGE"=>"en-us,en;q=0.5",
148
+ "HTTP_HOST"=>"localhost:4001",
149
+ "HTTP_REFERER"=>"http://www.example.com/posts/",
150
+ "HTTP_COOKIE"=>"cookie1=YWJj; cookie2=ZGVm",
151
+ "HTTP_ACCEPT_CHARSET"=>"ISO-8859-1,utf-8;q=0.7,*;q=0.7",
152
+ "HTTP_X_FORWARDED_SERVER"=>"www.example.com",
153
+ "HTTP_IF_MODIFIED_SINCE"=>"Tue, 17 Sep 2002 20:26:10 GMT",
154
+ "HTTP_X_FORWARDED_FOR"=>"127.0.0.1",
155
+ "HTTP_ACCEPT"=>"image/png,image/*;q=0.8,*/*;q=0.5",
156
+ "HTTP_CONNECTION"=>"Keep-Alive",}
157
+
158
+ mock_server(:body => 'not testing this')
159
+ send_get_request(rack_env)
160
+ assert_equal(expected_headers, proxied_request.headers)
161
+ end
162
+
163
+ def test_systemic_empty_rathole
164
+ host = 'halethegeek.com'
165
+ app = EmptyRatHole.new(host)
166
+ app_response = Rack::MockRequest.new(app).get('/', {})
167
+ raw_response = Net::HTTP.start(host) do |http|
168
+ http.get('/', {})
169
+ end
170
+ # Wrap raw_response in Rack::Response to make things easier to work with.
171
+ raw_response = Rack::Response.new(raw_response.body, raw_response.code, raw_response.to_hash)
172
+
173
+ assert_equal raw_response.status.to_i, app_response.status.to_i
174
+ assert_equal normalize_headers(raw_response.headers), normalize_headers(app_response.headers)
175
+ assert_equal raw_response.body.to_s, app_response.body.to_s
176
+ end
177
+
178
+ def test_systemic_political_agenda
179
+ host = 'terralien.com'
180
+ app = PoliticalAgendaRatHole.new(host)
181
+ app_response = Rack::MockRequest.new(app).get('/', {})
182
+ raw_response = Net::HTTP.start(host) do |http|
183
+ http.get('/', {})
184
+ end
185
+ # Wrap raw_response in Rack::Response to make things easier to work with.
186
+ raw_response = Rack::Response.new(raw_response.body, raw_response.code, raw_response.to_hash)
187
+ raw_headers = normalize_headers(raw_response.headers)
188
+ app_headers = normalize_headers(app_response.headers)
189
+
190
+ assert_equal raw_response.status.to_i, app_response.status.to_i
191
+ assert !raw_headers.has_key?('Ron-Paul')
192
+ assert app_headers.has_key?('Ron-Paul')
193
+
194
+ assert !raw_response.body.to_s.include?('http://ronpaul.com')
195
+ assert app_response.body.to_s.include?('http://ronpaul.com')
196
+ end
197
+
198
+ def normalize_headers(headers)
199
+ headers.inject({}){|h,e|
200
+ k,v = e
201
+ # don't compare headers that change or that we remove
202
+ unless k =~ /cookie|transfer|date|runtime/i
203
+ v = [v] unless v.is_a? Array #normalize things
204
+ h.merge!(k => v)
205
+ end
206
+ h
207
+ }
208
+ end
209
+ end
210
+
211
+ class PoliticalAgendaRatHole < RatHole
212
+ def process_server_response(rack_response)
213
+ if(rack_response.content_type == 'text/html')
214
+ doc = Hpricot(rack_response.body.first)
215
+ (doc/"a").set('href', 'http://ronpaul.com')
216
+ rack_response.body.first.replace(doc.to_html)
217
+ rack_response.headers['Ron-Paul'] = 'wish I could have voted for this guy'
218
+ end
219
+ rack_response
220
+ end
221
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mikehale-rat-hole
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Hale
8
+ - David Bogus
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2008-12-17 00:00:00 -08:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rack
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.4.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rr
27
+ version_requirement:
28
+ version_requirements: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">"
31
+ - !ruby/object:Gem::Version
32
+ version: 0.6.0
33
+ version:
34
+ description: Rat Hole is a handy library for creating a rack compliant http proxy that allows you to modify the request from the user and the response from the server.
35
+ email: mikehale@gmail.com
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files:
41
+ - History.txt
42
+ - Manifest.txt
43
+ - README.rdoc
44
+ files:
45
+ - History.txt
46
+ - README.rdoc
47
+ - rat-hole.gemspec
48
+ - lib/rat_hole.rb
49
+ - lib/util.rb
50
+ - Manifest.txt
51
+ has_rdoc: true
52
+ homepage: http://github.com/mikehale/rat-hole
53
+ post_install_message:
54
+ rdoc_options:
55
+ - --main
56
+ - README.rdoc
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ requirements: []
72
+
73
+ rubyforge_project:
74
+ rubygems_version: 1.2.0
75
+ signing_key:
76
+ specification_version: 2
77
+ summary: Rack compliant proxy
78
+ test_files:
79
+ - test/test_rat_hole.rb
80
+ - test/mock_request.rb