pretty_proxy 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.
- checksums.yaml +7 -0
- data/Rakefile +21 -0
- data/example/example.ru +35 -0
- data/example/example_conf.json +32 -0
- data/example/example_spec.rb +22 -0
- data/example/heresy.ru +12 -0
- data/lib/pretty_proxy.rb +452 -0
- data/spec/pretty_proxy_spec.rb +357 -0
- metadata +179 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 55e1a99924c4f5f41b6e78e0c26d8be032b59076
|
4
|
+
data.tar.gz: 2917669d77edead2513f8abfabbe232bd258a8e5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c47f72b5f7542ac0d2f6b7e0753721960e7552d7a6ffc518be2c48c3779e05f351e2d3c60f8faa1e0fbd2c185e4ee27fc18e66077ad5482fe0ae004edd6e793
|
7
|
+
data.tar.gz: 25c966ed96630729a914288da0df7db998130ba1485504607179a09643e9c164b342e3ceb77eb6bc1bcf685dd7e8efb902106d91766f2b3c732b1fe2dd7a99a1
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
|
3
|
+
RSpec::Core::RakeTask.new :spec
|
4
|
+
|
5
|
+
task :default => [:spec]
|
6
|
+
|
7
|
+
desc 'run a sample of the horrors this class is capable of (in localhost:9292/proxy)'
|
8
|
+
task :heresy_example do
|
9
|
+
sh 'rackup ./example/heresy.ru'
|
10
|
+
end
|
11
|
+
|
12
|
+
desc 'run a multithread example in http://localhost:9292/{p1,proxy/p1} with thin'
|
13
|
+
task :run_example do
|
14
|
+
sh 'thin start --threaded -p 9292 --rackup ./example/example.ru'
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "run the specs of the multithread example, run 'rake :run_example' before"
|
18
|
+
task :test_example do
|
19
|
+
sh 'rspec ./example/example_spec.rb'
|
20
|
+
end
|
21
|
+
|
data/example/example.ru
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'json'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'pretty_proxy'
|
5
|
+
|
6
|
+
# the json path below is relative to the Rakefile, call rake or change it
|
7
|
+
config = JSON.parse(open('example/example_conf.json').read)
|
8
|
+
|
9
|
+
pretty_proxy_new_args = config['pretty_proxy_new_args']
|
10
|
+
proxy_path = pretty_proxy_new_args['proxy_path']
|
11
|
+
original_domain = pretty_proxy_new_args['original_domain']
|
12
|
+
original_paths = pretty_proxy_new_args['original_paths']
|
13
|
+
|
14
|
+
original_html = config['xhtml_template'].join("\n")
|
15
|
+
.gsub('PROXY_PATH', proxy_path)
|
16
|
+
.gsub('ORIGINAL_DOMAIN', original_domain)
|
17
|
+
|
18
|
+
pp = PrettyProxy.new(proxy_path, original_domain, original_paths)
|
19
|
+
|
20
|
+
headers = { 'content-type' => 'application/xhtml+xml',
|
21
|
+
'content-encoding' => 'identity',
|
22
|
+
'content-length' => original_html.bytesize.to_s }
|
23
|
+
|
24
|
+
app = Rack::Builder.new do
|
25
|
+
map config['content_path'] do
|
26
|
+
run (->(env) { [200, headers, [original_html]] })
|
27
|
+
end
|
28
|
+
|
29
|
+
map Pathname.new(proxy_path).join('.' + config['content_path']).to_s do
|
30
|
+
run pp
|
31
|
+
end
|
32
|
+
end.to_app
|
33
|
+
|
34
|
+
run app
|
35
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
{
|
2
|
+
"pretty_proxy_new_args": {
|
3
|
+
"proxy_path": "/proxy/",
|
4
|
+
"original_domain": "http://localhost:9292",
|
5
|
+
"__comment": "if you change the 'Original paths' field you have to edit the 'Content path' and the 'XHTML Template' fields by hand",
|
6
|
+
"original_paths": ["/p1", "/p2/p2_2"]
|
7
|
+
},
|
8
|
+
"content_path": "/p1",
|
9
|
+
"xhtml_template": [
|
10
|
+
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"",
|
11
|
+
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">",
|
12
|
+
"<html xmlns=\"http://www.w3.org/1999/xhtml\">",
|
13
|
+
"<head>",
|
14
|
+
" <title>A title</title>",
|
15
|
+
" <meta http-equiv=\"content-type\" content=\"application/xhtml+xml; charset=UTF-8\" />",
|
16
|
+
"</head>",
|
17
|
+
"<body>",
|
18
|
+
" <a href=\"ORIGINAL_DOMAIN/p2/p2_2/\" >a link </a>",
|
19
|
+
" <p><a href=\"http://othersite.net\" >other link</a></p>",
|
20
|
+
" <div>",
|
21
|
+
" <a href=\"../p3\" >another link</a>",
|
22
|
+
" <p><a href=\"../p2/p2_2/\" >yet another link</a></p>",
|
23
|
+
" </div>",
|
24
|
+
" <div>",
|
25
|
+
" <a href=\"ORIGINAL_DOMAIN/PROXY_PATH/p1\" >and yet another link</a>",
|
26
|
+
" <p><a href=\"../PROXY_PATH/p1\" >the last link</a></p>",
|
27
|
+
" </div>",
|
28
|
+
"</body>",
|
29
|
+
"</html>"
|
30
|
+
]
|
31
|
+
}
|
32
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'equivalent-xml'
|
3
|
+
require 'json'
|
4
|
+
require 'pretty_proxy'
|
5
|
+
|
6
|
+
# the json path below is relative to the Rakefile, call rake or change it
|
7
|
+
config = JSON.parse(open('example/example_conf.json').read)
|
8
|
+
|
9
|
+
# this is ugly, but simple and clear, and this is a example
|
10
|
+
pretty_proxy_new_args = config['pretty_proxy_new_args']
|
11
|
+
proxy_path = pretty_proxy_new_args['proxy_path']
|
12
|
+
original_domain = pretty_proxy_new_args['original_domain']
|
13
|
+
original_paths = pretty_proxy_new_args['original_paths']
|
14
|
+
|
15
|
+
original_url = original_domain + config['content_path']
|
16
|
+
proxy_url = original_domain + Pathname.new(proxy_path).join('.' + config['content_path']).to_s
|
17
|
+
|
18
|
+
describe 'PrettyProxy example' do
|
19
|
+
let (:pp) { PrettyProxy.new(proxy_path, original_domain, original_paths) }
|
20
|
+
it { expect(open(proxy_url)).to be_equivalent_to(pp.proxify_html(open(original_url), proxy_url)) }
|
21
|
+
end
|
22
|
+
|
data/example/heresy.ru
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'pretty_proxy'
|
2
|
+
|
3
|
+
class Heresy < PrettyProxy
|
4
|
+
def sugared_rewrite_response(triplet, requested_to_proxy_env, rewritten_env)
|
5
|
+
status, headers, page = triplet
|
6
|
+
page = page.gsub(/(MTG )?Magic(: The Gathering)?/, 'Yu-Gi-Oh')
|
7
|
+
[status, headers, page]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
run Heresy.new('/proxy/', 'http://magiccards.info', '/')
|
12
|
+
|
data/lib/pretty_proxy.rb
ADDED
@@ -0,0 +1,452 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'uri'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'rack'
|
5
|
+
require 'rack-proxy'
|
6
|
+
|
7
|
+
# The PrettyProxy class aggregate and validate the configuration of a
|
8
|
+
# proxy based in simple pretty url oriented rewriting rules. It's too
|
9
|
+
# a rack app, and offers a abstract method for rewrite the responses
|
10
|
+
# returned by the proxy. The (X)HTML responses are rewritten to make
|
11
|
+
# the hyperlinks point to the proxy version of the page if it exist.
|
12
|
+
#
|
13
|
+
# @example A terrible example
|
14
|
+
# require 'pretty_proxy'
|
15
|
+
#
|
16
|
+
# class Heresy < PrettyProxy
|
17
|
+
# def sugared_rewrite_response(triplet, requested_to_proxy_env, rewritten_env)
|
18
|
+
# status, headers, page = triplet
|
19
|
+
# page = page.gsub(/(MTG )?Magic(: The Gathering)?/, 'Yu-Gi-Oh')
|
20
|
+
# [status, headers, page]
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# run Heresy.new('/proxy/', 'http://magiccards.info', '/')
|
25
|
+
#
|
26
|
+
# You can see the result in http://localhost:9292/proxy/ (if you use the
|
27
|
+
# command 'rake heresy_example' in the gem folder).
|
28
|
+
#
|
29
|
+
# @note: If you want to make a Rack app who use the proxy to point to
|
30
|
+
# another path of the same app you have to use a server in multithread
|
31
|
+
# mode, otherwise requests to the proxy will end in a deadlock.
|
32
|
+
# The proxy request the original page but the server don't respond because
|
33
|
+
# is waiting the proxy request to be resolved. The proxy request don't end
|
34
|
+
# because need the original page. A timeout error occur.
|
35
|
+
#
|
36
|
+
# What this class can't do but maybe will do in the future: smart
|
37
|
+
# handling of 3xx status response and chunked encoding (the chunks are
|
38
|
+
# concatened in the proxy and the transfer-encoding header removed);
|
39
|
+
# support more than deflate and gzip; exception classes with more
|
40
|
+
# than a message;
|
41
|
+
#
|
42
|
+
# Glossary:
|
43
|
+
# 'a valid proxy url/path': The path (or the path of the url) start with
|
44
|
+
# the proxy_path and is followed by a original_path.
|
45
|
+
# 'in(side)/out(side) the proxy control': The url have (or not) the path
|
46
|
+
# starting with a original_path and the scheme, port and host are the
|
47
|
+
# same of the original_domain.
|
48
|
+
#
|
49
|
+
# The exception classes (except Error) inherit Error, and Error inherit
|
50
|
+
# ArgumentError. They are empty yet, only have a message.
|
51
|
+
#
|
52
|
+
# @see PrettyProxy::Error
|
53
|
+
# @see PrettyProxy::ConfigError
|
54
|
+
# @see PrettyProxy::ProxyError
|
55
|
+
#
|
56
|
+
# @author: Henrique Becker
|
57
|
+
class PrettyProxy < Rack::Proxy
|
58
|
+
# The supertype of any exceptions explicitly raised by the methods
|
59
|
+
class Error < ArgumentError; end
|
60
|
+
# Class of exceptions thrown when trying to set the internal state
|
61
|
+
# of the class to a invalid value
|
62
|
+
class ConfigError < Error; end
|
63
|
+
# Class of exceptions thrown when the arguments of the method
|
64
|
+
# are invalid for the proxy configuration
|
65
|
+
class ProxyError < Error; end
|
66
|
+
|
67
|
+
@proxy_path = nil
|
68
|
+
@original_domain = nil
|
69
|
+
@original_paths = nil
|
70
|
+
|
71
|
+
# Create a new PrettyProxy instance or raise a ConfigError. Clone the arguments.
|
72
|
+
# @param proxy_path [String] Start and end with slashes, represent the
|
73
|
+
# path in the proxy site who map to the proxy app (and, in consequence,
|
74
|
+
# to another path in the same or another site).
|
75
|
+
# @param original_domain [String, URI] A URL without path (no trailing slash),
|
76
|
+
# query or fragment (can have scheme (http[s]), domain and port), the site
|
77
|
+
# to where the proxy map.
|
78
|
+
# @param original_paths [String, #each] The path (or the paths) to be mapped
|
79
|
+
# right inside the proxy_path (has to begin with slash).
|
80
|
+
# @note See the specs {file:../spec/pretty_proxy_spec.rb} for examples and
|
81
|
+
# complete definition of invalid args.
|
82
|
+
# @return [PrettyProxy] a new instance
|
83
|
+
# @raise PrettyProxy::ConfigError
|
84
|
+
def initialize(proxy_path, original_domain, original_paths)
|
85
|
+
Utils.validate_proxy_path(proxy_path)
|
86
|
+
Utils.validate_original_domain_and_paths(original_domain, original_paths)
|
87
|
+
|
88
|
+
@proxy_path = proxy_path.clone
|
89
|
+
@original_domain = URI(original_domain.clone)
|
90
|
+
if original_paths.respond_to? :each
|
91
|
+
@original_paths = original_paths.clone
|
92
|
+
else
|
93
|
+
@original_paths = [original_paths.clone]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# !@attribute proxy_path
|
98
|
+
# @param a input who will be validated as in the initialize
|
99
|
+
# @return the clone of the internal value
|
100
|
+
# !@attribute original_domain
|
101
|
+
# @param a input who will be validated as in the initialize
|
102
|
+
# @return the clone of the internal value
|
103
|
+
# !@attribute original_paths
|
104
|
+
# @param a input who will be validated as in the initialize
|
105
|
+
# @return the clone of the internal value
|
106
|
+
[:proxy_path, :original_domain, :original_paths].each do | reader |
|
107
|
+
define_method(reader) { instance_variable_get("@#{reader.to_s}").clone }
|
108
|
+
end
|
109
|
+
|
110
|
+
def proxy_path=(proxy_path)
|
111
|
+
Utils.validate_proxy_path(proxy_path)
|
112
|
+
@proxy_path = proxy_path
|
113
|
+
end
|
114
|
+
|
115
|
+
def original_domain=(original_domain)
|
116
|
+
Utils.validate_original_domain_and_paths(original_domain, @original_paths)
|
117
|
+
@original_domain = original_domain
|
118
|
+
end
|
119
|
+
|
120
|
+
def original_paths=(original_paths)
|
121
|
+
Utils.validate_original_domain_and_paths(@original_domain, original_paths)
|
122
|
+
@original_paths = original_paths
|
123
|
+
end
|
124
|
+
|
125
|
+
# Take a proxy url and return the original URL behind the proxy. Preserve the
|
126
|
+
# query and fragment, if any. For the rewrite of a request @see rewrite_env.
|
127
|
+
# @param [String, URI::HTTP, URI::HTTPS] A URL.
|
128
|
+
# @return [URI::HTTP, URI::HTTPS] A URI object.
|
129
|
+
# @raise PrettyProxy::ProxyError
|
130
|
+
def unproxify_url(url)
|
131
|
+
url = URI(url.clone)
|
132
|
+
unless url.path.start_with?(@proxy_path)
|
133
|
+
fail ProxyError, "url path has to be prefixed by proxy_path (#{@proxy_path})"
|
134
|
+
end
|
135
|
+
url.path = url.path.slice((proxy_path.size-1)..-1)
|
136
|
+
unless original_paths.any? { | path | url.path.start_with? path }
|
137
|
+
fail ProxyError, "the proxy only responds to paths in the original_paths (#{@original_paths})"
|
138
|
+
end
|
139
|
+
if url.host == original_domain.host && url.path.start_with?(@proxy_path)
|
140
|
+
fail ProxyError, 'this is a request for the proxy for a proxy page (recursive request)'
|
141
|
+
end
|
142
|
+
url.host = original_domain.host
|
143
|
+
url.scheme = original_domain.scheme
|
144
|
+
url.port = original_domain.port
|
145
|
+
|
146
|
+
url
|
147
|
+
rescue URI::InvalidURIError
|
148
|
+
raise ArgumentError, "the url argument isn't a valid uri"
|
149
|
+
rescue URI::Error => e
|
150
|
+
raise ProxyError, "an unexpected URI exception has been thrown, the message is '#{e.message}'"
|
151
|
+
end
|
152
|
+
|
153
|
+
# Take a hyperlink and the url of the proxy page (not the original page)
|
154
|
+
# where it come from and return the rewritten hyperlink. If the page
|
155
|
+
# pointed vy the hyperlink is in the proxy control the rewritten hyperlink
|
156
|
+
# gonna point to the proxyfied version, otherwise gonna point to the original
|
157
|
+
# version.
|
158
|
+
# @param hyperlink [String, URI::HTTP, URI::HTTPS] A string with a relative
|
159
|
+
# path or an url (string or URI).
|
160
|
+
# @param proxy_page_url [String, URI::HTTP, URI::HTTPS] The url from the
|
161
|
+
# proxy page where the hyperlink come from.
|
162
|
+
# @return [String] A relative path or an url.
|
163
|
+
# @raise PrettyProxy::ProxyError
|
164
|
+
def proxify_hyperlink(hyperlink, proxy_page_url)
|
165
|
+
hyperlink = URI(hyperlink.clone)
|
166
|
+
proxy_page_url = URI(proxy_page_url)
|
167
|
+
if Utils.relative_path? hyperlink
|
168
|
+
# recreate the original site url from the relative path
|
169
|
+
absolute_link = unproxify_url proxy_page_url
|
170
|
+
absolute_link.path = Pathname.new(absolute_link.path).join(hyperlink.path).to_s
|
171
|
+
if inside_proxy_control? absolute_link
|
172
|
+
if same_domain_as_original?(proxy_page_url) &&
|
173
|
+
valid_path_for_proxy?(absolute_link.path)
|
174
|
+
# in the case of a relative path in the original page who points
|
175
|
+
# to a proxy page, and the proxy page is inside the proxy control
|
176
|
+
# we have to use the absolute_link or the page will be double proxified
|
177
|
+
# example: ../proxy/content in http://example.com/proxy/content, with
|
178
|
+
# original_path as '/' is http://example.com/proxy/proxy/content
|
179
|
+
hyperlink = absolute_link
|
180
|
+
end
|
181
|
+
else
|
182
|
+
hyperlink = absolute_link
|
183
|
+
end
|
184
|
+
else
|
185
|
+
if inside_proxy_control? hyperlink
|
186
|
+
unless point_to_a_proxy_page?(hyperlink, proxy_page_url)
|
187
|
+
hyperlink.scheme = proxy_page_url.scheme
|
188
|
+
hyperlink.host = proxy_page_url.host
|
189
|
+
hyperlink.port = proxy_page_url.port
|
190
|
+
hyperlink.path = @proxy_path + hyperlink.path[1..-1]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
hyperlink.to_s
|
196
|
+
end
|
197
|
+
|
198
|
+
# Take a (X)HTML Document and apply proxify_hyperlink to the 'href'
|
199
|
+
# attribute of each 'a' element.
|
200
|
+
# @param html [String] A (X)HTML document.
|
201
|
+
# @param proxy_url [String, URI::HTTP, URI::HTTPS] The url where the
|
202
|
+
# the proxified version of the page will be displayed.
|
203
|
+
# @return [String] A copy of the document with the changes applied.
|
204
|
+
# @raise PrettyProxy::ProxyError
|
205
|
+
def proxify_html(html, proxy_url)
|
206
|
+
parsed_html = nil
|
207
|
+
|
208
|
+
# If you parse XHTML as HTML with Nokogiri and use to_s after the markup can be messed up
|
209
|
+
#
|
210
|
+
# Example: <meta name="description" content="not important" />
|
211
|
+
# becomes <meta name="description" content="not important" >
|
212
|
+
# To avoid this we parse a document who is XML valid as XML, and, otherwise as HTML
|
213
|
+
begin
|
214
|
+
# this also isn't a great way to do this
|
215
|
+
# the Nokogiri don't have exception classes, this way any StandardError will be silenced
|
216
|
+
options = Nokogiri::XML::ParseOptions::DEFAULT_XML &
|
217
|
+
Nokogiri::XML::ParseOptions::STRICT &
|
218
|
+
Nokogiri::XML::ParseOptions::DTDVALID
|
219
|
+
parsed_html = Nokogiri::XML::Document.parse(html, nil, nil, options)
|
220
|
+
rescue
|
221
|
+
parsed_html = Nokogiri::HTML(html)
|
222
|
+
end
|
223
|
+
|
224
|
+
parsed_html.css('a').each do | hyperlink |
|
225
|
+
hyperlink['href'] = proxify_hyperlink(hyperlink['href'], proxy_url)
|
226
|
+
end
|
227
|
+
|
228
|
+
parsed_html.to_s
|
229
|
+
end
|
230
|
+
|
231
|
+
# Modify a Rack environment hash of a request to the proxy version of
|
232
|
+
# a page to a request to the original page. As in Rack::proxy is used
|
233
|
+
# by #call for require the original page before call rewrite_response in
|
234
|
+
# the response. If you want to use your own rewrite rules maybe is more
|
235
|
+
# wise to subclass Rack::Proxy instead subclass this class. The purpose
|
236
|
+
# of this class is mainly implement and enforce these rules for you.
|
237
|
+
# @param html [Hash{String => String}] A Rack environment hash.
|
238
|
+
# (see: {http://rack.rubyforge.org/doc/SPEC.html})
|
239
|
+
# @return [Hash{String => String}] A unproxified copy of the argument.
|
240
|
+
# @raise PrettyProxy::ProxyError
|
241
|
+
def rewrite_env(env)
|
242
|
+
env = env.clone
|
243
|
+
url_requested_to_proxy = Rack::Request.new(env).url
|
244
|
+
unproxified_url = unproxify_url(url_requested_to_proxy)
|
245
|
+
|
246
|
+
if env['HTTP_HOST']
|
247
|
+
env['HTTP_HOST'] = unproxified_url.host
|
248
|
+
end
|
249
|
+
env['SERVER_NAME'] = unproxified_url.host
|
250
|
+
env['SERVER_PORT'] = unproxified_url.port.to_s
|
251
|
+
|
252
|
+
if env['SCRIPT_NAME'].empty? && !env['PATH_INFO'].empty?
|
253
|
+
env['PATH_INFO'] = unproxified_url.path
|
254
|
+
end
|
255
|
+
if !env['SCRIPT_NAME'].empty? && env['PATH_INFO'].empty?
|
256
|
+
env['SCRIPT_NAME'] = unproxified_url.path
|
257
|
+
end
|
258
|
+
# Seriously, i don't know how to split again the unproxified url, so PATH_INFO gonna have the full path
|
259
|
+
if (!env['SCRIPT_NAME'].empty? && !env['PATH_INFO'].empty?) ||
|
260
|
+
(env['SCRIPT_NAME'].empty? && env['PATH_INFO'].empty?)
|
261
|
+
env['PATH_INFO'] = unproxified_url.path
|
262
|
+
env['SCRIPT_NAME'] = ''
|
263
|
+
end
|
264
|
+
|
265
|
+
env['REQUEST_PATH'] = unproxified_url.path
|
266
|
+
env['REQUEST_URI'] = unproxified_url.path
|
267
|
+
|
268
|
+
env
|
269
|
+
end
|
270
|
+
|
271
|
+
# Mainly apply the proxify_html to the body of the response if it is a html.
|
272
|
+
# Raise an error if the 'content-encoding' is other than deflate, gzip or
|
273
|
+
# identity. Change the 'content-length' header for the new body bytesize.
|
274
|
+
# Remove the 'transfer-encoding' if it is chunked, and act as not chunked.
|
275
|
+
# This method is inherited of Rack::Proxy, but in the original it have only
|
276
|
+
# the first parameter (the triplet). This version have the request Rack env
|
277
|
+
# to the proxy and the rewritten Rack env as second and third parameters,
|
278
|
+
# respectively.
|
279
|
+
# @param triplet [Array<(Integer, Hash{String => String}, #each)>] A Rack
|
280
|
+
# response (see {http://rack.rubyforge.org/doc/SPEC.html}) for the request
|
281
|
+
# to the original site.
|
282
|
+
# @param [Hash{String => String}] A Rack environment hash. The requested to
|
283
|
+
# the proxy version.
|
284
|
+
# @param [Hash{String => String}] A Rack environment hash. The rewritten by
|
285
|
+
# the proxy to point to the original version.
|
286
|
+
# @return [Array<(Integer, Hash{String => String}, #each)>] A unproxified
|
287
|
+
# copy of the first argument.
|
288
|
+
# @raise PrettyProxy::ProxyError
|
289
|
+
def rewrite_response(triplet, requested_to_proxy_env, rewritten_env)
|
290
|
+
status, headers, body = triplet
|
291
|
+
content_type = headers['content-type']
|
292
|
+
return triplet unless %r{text/html} =~ content_type ||
|
293
|
+
%r{application/xhtml\+xml} =~ content_type
|
294
|
+
|
295
|
+
# the #each method of body can't be called twice, but we need to call it here and it is called
|
296
|
+
# after this method return, so we fake the body with a array of one string
|
297
|
+
# we can't return a string (even it responds to #each) see: http://rack.rubyforge.org/doc/SPEC.html (section 'The Body')
|
298
|
+
page = ''
|
299
|
+
body.each do | chunk |
|
300
|
+
page << chunk
|
301
|
+
end
|
302
|
+
|
303
|
+
case headers['content-encoding']
|
304
|
+
when 'gzip' then page = Zlib::GzipReader.new(StringIO.new(page)).read
|
305
|
+
when 'deflate' then page = Zlib::Inflate.inflate(page)
|
306
|
+
when 'identity' then page = page
|
307
|
+
else
|
308
|
+
fail ProxyError, 'unknown content-encoding, only encodings known are gzip, deflate and identity'
|
309
|
+
end
|
310
|
+
|
311
|
+
page = proxify_html(page, Rack::Request.new(requested_to_proxy_env).url)
|
312
|
+
status, headers, page = sugared_rewrite_response([status, headers, page],
|
313
|
+
requested_to_proxy_env,
|
314
|
+
rewritten_env)
|
315
|
+
|
316
|
+
case headers['content-encoding']
|
317
|
+
when 'gzip'
|
318
|
+
page_ = page.clone
|
319
|
+
gzip_stream = Zlib::GzipWriter.new(StringIO.new(page_))
|
320
|
+
gzip_stream.write page
|
321
|
+
gzip_stream.close
|
322
|
+
page = page_
|
323
|
+
when 'deflate' then page = Zlib::Deflate.deflate(page)
|
324
|
+
end
|
325
|
+
|
326
|
+
headers['content-length'] = page.bytesize.to_s if headers['content-length']
|
327
|
+
|
328
|
+
# TODO: find a way to make the code work with chunked encoding
|
329
|
+
if 'chunked' == headers['transfer-encoding']
|
330
|
+
headers.delete('transfer-encoding')
|
331
|
+
headers['content-length'] = page.bytesize.to_s
|
332
|
+
end
|
333
|
+
|
334
|
+
[status, headers, [page]]
|
335
|
+
end
|
336
|
+
|
337
|
+
# @abstract This method is called only over (X)HTML responses, after they are
|
338
|
+
# decompressed and the hyperlinks proxified, before they are compressed
|
339
|
+
# again and the new content-length calculated. The body of the triplet is
|
340
|
+
# a String and not a object who respond to #each, the same has to be true
|
341
|
+
# in the return. Return a modified clone of the response, don't change
|
342
|
+
# the argument.
|
343
|
+
# @param triplet [Array<(Integer, Hash{String => String}, String)>] Not a
|
344
|
+
# valid Rack response, the third element is a string with the response body.
|
345
|
+
# @param [Hash{String => String}] A Rack environment hash. The requested to
|
346
|
+
# the proxy version.
|
347
|
+
# @param [Hash{String => String}] A Rack environment hash. The rewritten by
|
348
|
+
# the proxy to point to the original version.
|
349
|
+
# @return [Array<(Integer, Hash{String => String}, String)>] A unproxified
|
350
|
+
# copy of the first argument.
|
351
|
+
def sugared_rewrite_response(triplet, requested_to_proxy_env, rewritten_env)
|
352
|
+
triplet
|
353
|
+
end
|
354
|
+
|
355
|
+
# Make this class a Rack app. Is overriden to repass to the rewrite_response
|
356
|
+
# the original Rack environment (request to the proxy) and the rewritten env
|
357
|
+
# (modified to point the original page request).
|
358
|
+
# If you don't know the parameters and return of this method, please read
|
359
|
+
# {http://rack.rubyforge.org/doc/SPEC.html}.
|
360
|
+
def call(env)
|
361
|
+
# in theory we only need to repass the rewritten_env, any original env info
|
362
|
+
# needed can be passed as a environment application variable
|
363
|
+
# example: (env['app_name.original_path'] = env['PATH_INFO'])
|
364
|
+
# but to avoid this to be a common idiom we repass the original env too
|
365
|
+
rewritten_env = rewrite_env(env)
|
366
|
+
rewrite_response(perform_request(rewritten_env), env, rewritten_env)
|
367
|
+
end
|
368
|
+
|
369
|
+
# Check if the #scheme, #host, and #port of the argument are equal to the
|
370
|
+
# original_domain ones.
|
371
|
+
def same_domain_as_original?(uri)
|
372
|
+
Utils.same_domain?(@original_domain, uri)
|
373
|
+
end
|
374
|
+
|
375
|
+
# Check if the URI::HTTP(S) is a page who can be accessed through the proxy
|
376
|
+
def inside_proxy_control?(uri)
|
377
|
+
same_domain_as_original?(uri) &&
|
378
|
+
valid_path_for_proxy?(@proxy_path + uri.path[1..-1])
|
379
|
+
end
|
380
|
+
|
381
|
+
# Check if the absolute path begin with a proxy_path and is followed by a
|
382
|
+
# original_paths element.
|
383
|
+
def valid_path_for_proxy?(absolute_path)
|
384
|
+
path_without_proxy_prefix = absolute_path[(@proxy_path.size-1)..-1]
|
385
|
+
# if we don't add the trailing slash '/about' and '/about_us' match
|
386
|
+
original_paths_with_trailing_slash = []
|
387
|
+
@original_paths.each do | path |
|
388
|
+
original_paths_with_trailing_slash << (path.end_with?('/') ? path : "#{path}/")
|
389
|
+
end
|
390
|
+
|
391
|
+
absolute_path.start_with?(@proxy_path) &&
|
392
|
+
original_paths_with_trailing_slash.any? do | original_path |
|
393
|
+
path_without_proxy_prefix.start_with? original_path
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# Take a url and the proxy domain (scheme, host and port) and return if
|
398
|
+
# the url point to a valid proxy page.
|
399
|
+
def point_to_a_proxy_page?(hyperlink, proxy_domain)
|
400
|
+
Utils.same_domain?(hyperlink, proxy_domain) &&
|
401
|
+
valid_path_for_proxy?(hyperlink.path)
|
402
|
+
end
|
403
|
+
|
404
|
+
# api private Don't use the methods of this class. They are for internal use only.
|
405
|
+
class Utils
|
406
|
+
def self.relative_path?(hyperlink)
|
407
|
+
! hyperlink.scheme
|
408
|
+
end
|
409
|
+
|
410
|
+
def self.same_domain?(u1, u2)
|
411
|
+
u1.scheme == u2.scheme &&
|
412
|
+
u1.host == u2.host &&
|
413
|
+
u1.port == u2.port
|
414
|
+
end
|
415
|
+
|
416
|
+
def self.validate_proxy_path(proxy_path)
|
417
|
+
fail ConfigError, "proxy_path argument don't end with a '/'" unless proxy_path.end_with? '/'
|
418
|
+
# NOTE: if the user want to proxify 'www.site.net', and not 'www.site.net/'?
|
419
|
+
# Well, majority of the internet answers for this are 'the right way is to use the trailing slash'
|
420
|
+
# See: http://tim-stanley.com/post/pretty-good-urls/
|
421
|
+
# http://www.w3.org/Provider/Style/URI.html
|
422
|
+
# http://stackoverflow.com/questions/7355305/preventing-trailing-slash-on-domain-name
|
423
|
+
# http://alistapart.com/article/slashforward
|
424
|
+
# http://www.searchenginejournal.com/linking-issues-why-a-trailing-slash-in-the-url-does-matter/13021/?ModPagespeed=noscript
|
425
|
+
end
|
426
|
+
|
427
|
+
def self.validate_original_domain_and_paths(original_domain, original_paths)
|
428
|
+
fail ConfigError, 'original_paths is empty' if original_paths.empty?
|
429
|
+
|
430
|
+
original_domain = URI(original_domain) # can raise URI:Error's
|
431
|
+
fail ConfigError, 'the original_domain has to have no query or fragment' if original_domain.query || original_domain.fragment
|
432
|
+
|
433
|
+
# can raise URI:Error's
|
434
|
+
test_uri = original_domain.clone
|
435
|
+
if original_paths.respond_to?(:each)
|
436
|
+
original_paths.each { | path | test_uri.path = path }
|
437
|
+
else
|
438
|
+
test_uri.path = original_paths
|
439
|
+
end
|
440
|
+
|
441
|
+
rescue URI::InvalidComponentError => e
|
442
|
+
raise ConfigError, "the original_paths contain a invalid path, message of the URI exception: '#{e.message}'"
|
443
|
+
rescue URI::InvalidURIError => e
|
444
|
+
raise ConfigError, "the original_domain isn't a valid URI, message of the URI exception: '#{e.message}'"
|
445
|
+
rescue URI::Error => e
|
446
|
+
raise ConfigError, "a unexpected URI::Error exception was raised, message of the exception: '#{e.message}'"
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
private_constant :Utils
|
451
|
+
end
|
452
|
+
|
@@ -0,0 +1,357 @@
|
|
1
|
+
require 'pretty_proxy'
|
2
|
+
require 'equivalent-xml' # needed for be_equivalent_to xml rspec matcher
|
3
|
+
require 'zlib'
|
4
|
+
|
5
|
+
shared_examples 'an reader method who encapsulate a mutable variable' do
|
6
|
+
context 'when the return is changed' do
|
7
|
+
it 'does not change the next return value' do
|
8
|
+
instance = described_class.new(*new_args)
|
9
|
+
first_return = instance.send reader_method_name
|
10
|
+
if change_return.respond_to? :call
|
11
|
+
change_return.call first_return
|
12
|
+
else
|
13
|
+
first_return.send change_return
|
14
|
+
end
|
15
|
+
second_return = instance.send reader_method_name
|
16
|
+
|
17
|
+
expect(second_return).to_not eq first_return
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe PrettyProxy do
|
23
|
+
|
24
|
+
def generate_html_for_test(hyperlinks)
|
25
|
+
doc = <<-END
|
26
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
27
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
28
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
29
|
+
<head>
|
30
|
+
<title>A title</title>
|
31
|
+
<meta http-equiv="content-type" content="application/xhtml+xml; charset=UTF-8" />
|
32
|
+
</head>
|
33
|
+
<body>
|
34
|
+
<a href="ARG_0" >a link </a>
|
35
|
+
<p><a href="ARG_1" >other link</a></p>
|
36
|
+
<div>
|
37
|
+
<a href="ARG_2" >another link</a>
|
38
|
+
<p><a href="ARG_3" >yet another link</a></p>
|
39
|
+
</div>
|
40
|
+
<div>
|
41
|
+
<a href="ARG_4" >and yet another link</a>
|
42
|
+
<p><a href="ARG_5" >the last link</a></p>
|
43
|
+
</div>
|
44
|
+
</body>
|
45
|
+
</html>
|
46
|
+
END
|
47
|
+
doc.gsub!(/ARG_\d+/) { | match | hyperlinks[match[4..-1].to_i] }
|
48
|
+
|
49
|
+
doc
|
50
|
+
end
|
51
|
+
|
52
|
+
let(:original_html) { generate_html_for_test(['http://site.net/p2/p2_2/',
|
53
|
+
'http://othersite.net',
|
54
|
+
'../p3', '../p2/p2_2/',
|
55
|
+
'http://site.net/proxy/p1',
|
56
|
+
'../proxy/p1']) }
|
57
|
+
|
58
|
+
let(:proxified_html) { generate_html_for_test(['http://site.net/proxy/p2/p2_2/',
|
59
|
+
'http://othersite.net',
|
60
|
+
'http://site.net/p3', '../p2/p2_2/',
|
61
|
+
'http://site.net/proxy/p1',
|
62
|
+
'http://site.net/proxy/p1']) }
|
63
|
+
|
64
|
+
let (:correct_new_args_example) { ['/proxy/', 'http://myoriginalsite.com', '/content'] }
|
65
|
+
let (:pp) { described_class.new(*correct_new_args_example) }
|
66
|
+
|
67
|
+
describe '.new' do
|
68
|
+
subject (:new) { described_class.method :new }
|
69
|
+
|
70
|
+
[ {desc: 'accept original_paths as a String',
|
71
|
+
args: ['/proxy/', 'http://myoriginalsite.com', '/content']},
|
72
|
+
{desc: 'accept original_paths as an object who yelds strings with #each',
|
73
|
+
args: ['/proxy/', 'http://myoriginalsite.com', ['/content', '/other_content']]},
|
74
|
+
{desc: 'accept https in the original_domain',
|
75
|
+
args: ['/proxy/', 'https://myoriginalsite.com', ['/content']]},
|
76
|
+
{desc: 'accept port in the original_domain',
|
77
|
+
args: ['/proxy/', 'https://myoriginalsite.com:8080', ['/content']]}
|
78
|
+
].each do | happy_case |
|
79
|
+
it happy_case[:desc] do
|
80
|
+
expect(new.call(*happy_case[:args])).to be_a_instance_of described_class
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# TODO: Add specs for '/' in the start of the proxy_path
|
85
|
+
let (:right_args) { correct_new_args_example }
|
86
|
+
context "when proxy_path doesn't end with a '/'" do
|
87
|
+
it { expect {new.call('/proxy', right_args[1], right_args[2])}.to raise_error(PrettyProxy::ConfigError) }
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'when the original_domain is invalid' do
|
91
|
+
it { expect {new.call(right_args[0], 'http://myoriginalsite.com/%%%/', right_args[2])}.to raise_error(PrettyProxy::ConfigError)}
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'when the original_domain has a query' do
|
95
|
+
it { expect {new.call(right_args[0], 'http://myoriginalsite.com/?q=error', right_args[2])}.to raise_error(PrettyProxy::ConfigError)}
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'when the original_domain has a fragment' do
|
99
|
+
it { expect {new.call(right_args[0], 'http://myoriginalsite.com/#id', right_args[2])}.to raise_error(PrettyProxy::ConfigError)}
|
100
|
+
end
|
101
|
+
|
102
|
+
context "when the original_paths don't begin with a '/'" do
|
103
|
+
it { expect {new.call(right_args[0], right_args[1], ['content'])}.to raise_error(PrettyProxy::ConfigError) }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
#NOTE: save ten lines of the not metaprogrammed way
|
108
|
+
[:proxy_path, :original_domain, :original_paths].each do | reader_method |
|
109
|
+
describe "##{reader_method.to_s}" do
|
110
|
+
return_changers = { proxy_path: :chop!,
|
111
|
+
original_domain: ->(uri){ uri.host = 'otherdomain.com'},
|
112
|
+
original_paths: :shift }
|
113
|
+
|
114
|
+
it_behaves_like 'an reader method who encapsulate a mutable variable' do
|
115
|
+
let(:reader_method_name) { reader_method }
|
116
|
+
let(:new_args) { ['/proxy/', 'http://myoriginalsite.com', '/content'] }
|
117
|
+
let(:change_return) { return_changers[reader_method] }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# NOTE: excessive metaprogramming? only save 3~6 lines
|
123
|
+
[ [ :proxy_path=, "when proxy_path doesn't end with a '/'", '/proxy'],
|
124
|
+
[ :original_domain=, 'when the original_domain is invalid', 'http://myoriginalsite.com/%%%/'],
|
125
|
+
[ :original_paths=, "when the original_paths don't begin with a '/'", 'content']
|
126
|
+
].each do | error_case |
|
127
|
+
writter, context_desc, invalid_input = *error_case
|
128
|
+
describe "##{writter.to_s}" do
|
129
|
+
context context_desc do
|
130
|
+
it { expect {pp.send(writter, invalid_input)}.to raise_error(PrettyProxy::ConfigError) }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe '#unproxify_url' do
|
136
|
+
new_args = ['/proxys/sitez/', 'http://site.net', ['/p1', '/p2/p2_2/']]
|
137
|
+
let (:pp) { described_class.new(*new_args) }
|
138
|
+
|
139
|
+
context 'when the original_path has no trailing slash' do
|
140
|
+
it 'allow no trailing slash in the url' do
|
141
|
+
expect(pp.unproxify_url('http://myproxy.net/proxys/sitez/p1')).to eq URI('http://site.net/p1')
|
142
|
+
end
|
143
|
+
it 'allow trailing slash in the url' do
|
144
|
+
expect(pp.unproxify_url('http://myproxy.net/proxys/sitez/p1/')).to eq URI('http://site.net/p1/')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
context 'when the original_path has a trailing slash' do
|
148
|
+
it 'allow trailing slash in the url' do
|
149
|
+
expect(pp.unproxify_url('http://myproxy.net/proxys/sitez/p2/p2_2/')).to eq URI('http://site.net/p2/p2_2/')
|
150
|
+
end
|
151
|
+
it "don't allow no trailing slash" do
|
152
|
+
expect { pp.unproxify_url('http://myproxy.net/proxys/sitez/p2/p2_2') }.to raise_error(PrettyProxy::ProxyError)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
it 'allow subdirectories inside that path' do
|
156
|
+
expect(pp.unproxify_url('http://myproxy.net/proxys/sitez/p1/a/b/c/')).to eq URI('http://site.net/p1/a/b/c/')
|
157
|
+
end
|
158
|
+
it 'preserve querys in the url' do
|
159
|
+
expect(pp.unproxify_url('http://myproxy.net/proxys/sitez/p1/?q=error&l=pt')).to eq URI('http://site.net/p1/?q=error&l=pt')
|
160
|
+
end
|
161
|
+
it 'preserve fragments in the url' do
|
162
|
+
expect(pp.unproxify_url('http://myproxy.net/proxys/sitez/p1/#id')).to eq URI('http://site.net/p1/#id')
|
163
|
+
end
|
164
|
+
it 'change the port to the original' do
|
165
|
+
expect(pp.unproxify_url('http://myproxy.net:9292/proxys/sitez/p1/#id').port).to eq 80
|
166
|
+
end
|
167
|
+
|
168
|
+
context 'when the url redirect to the own proxy' do
|
169
|
+
let (:pp) { described_class.new('/', 'http://myoriginalsite.com/', '/content') }
|
170
|
+
|
171
|
+
it { expect {pp.unproxify_url('http://myproxysite.com/proxy/proxy/')}.to raise_error(PrettyProxy::ProxyError) }
|
172
|
+
end
|
173
|
+
context "when the url don't begin with the proxy_path" do
|
174
|
+
it { expect {pp.unproxify_url('http://myproxysite.com/no_proxy/content')}.to raise_error(PrettyProxy::ProxyError) }
|
175
|
+
end
|
176
|
+
context "when the proxy_path in the url isn't followed by a original_paths" do
|
177
|
+
it { expect {pp.unproxify_url('http://myproxysite.com/proxy/other_content')}.to raise_error(PrettyProxy::ProxyError) }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
describe '#proxify_hyperlink' do
|
182
|
+
let (:pp) { described_class.new('/proxy/', 'http://site.net', ['/p1', '/p2/p2_2/']) }
|
183
|
+
|
184
|
+
it "proxify absolute hyperlinks to inside the proxy control" do
|
185
|
+
expect(pp.proxify_hyperlink('http://site.net/p2/p2_2/', 'http://theproxy.net/proxy/p1')).to eq 'http://theproxy.net/proxy/p2/p2_2/'
|
186
|
+
end
|
187
|
+
it "don't change absolute hyperlinks to ouside the proxy control" do
|
188
|
+
expect(pp.proxify_hyperlink('http://othersite.net', 'http://theproxy.net/proxy/p1')).to eq 'http://othersite.net'
|
189
|
+
end
|
190
|
+
it 'change to absolute hyperlinks the relative paths to outside the proxy control' do
|
191
|
+
expect(pp.proxify_hyperlink('../p3', 'http://theproxy.net/proxy/p1')).to eq 'http://site.net/p3'
|
192
|
+
expect(pp.proxify_hyperlink('../p2/p2_2', 'http://theproxy.net/proxy/p1')).to eq 'http://site.net/p2/p2_2' # without the trailing '/'
|
193
|
+
end
|
194
|
+
it "don't change relative paths to inside the proxy control" do
|
195
|
+
expect(pp.proxify_hyperlink('../p2/p2_2/', 'http://theproxy.net/proxy/p1')).to eq '../p2/p2_2/'
|
196
|
+
end
|
197
|
+
|
198
|
+
context 'when the proxy itself is inside the proxy control' do
|
199
|
+
let (:pp) { described_class.new('/proxy/', 'http://site.net', '/') }
|
200
|
+
|
201
|
+
it "dont't change absolute hyperlinks to the proxy itself" do
|
202
|
+
expect(pp.proxify_hyperlink('http://site.net/proxy/p1', 'http://site.net/proxy/p1')).to eq 'http://site.net/proxy/p1'
|
203
|
+
expect(pp.proxify_hyperlink('http://site.net/proxy/p1', 'http://site.net/proxy/p2/p2_2/')).to eq 'http://site.net/proxy/p1'
|
204
|
+
end
|
205
|
+
it 'change to absolute hyperlinks the relative paths to the proxy itself' do
|
206
|
+
expect(pp.proxify_hyperlink('../proxy/p1', 'http://site.net/proxy/p1')).to eq 'http://site.net/proxy/p1'
|
207
|
+
expect(pp.proxify_hyperlink('../../proxy/p1', 'http://site.net/proxy/p2/p2_2/')).to eq 'http://site.net/proxy/p1'
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
describe '#proxify_html' do
|
213
|
+
let (:pp) { described_class.new('/proxy/', 'http://site.net', ['/p1', '/p2/p2_2/']) }
|
214
|
+
|
215
|
+
it 'apply #proxify_hyperlink in all hyperlinks in the page' do
|
216
|
+
# We aren't really testing with HTML, but with XHTML, what is a XML
|
217
|
+
# This is because we dont have a matcher to test HTML equivalence, only XML equivalence
|
218
|
+
# This test is not guaranteed to pass if the input is a HTML non-XHTML
|
219
|
+
# The parse and unparse of the HTML can output a value who is not XML equivalent to the input
|
220
|
+
# Maybe the way is use regex instead of Nokogiri to this work
|
221
|
+
expect(pp.proxify_html(original_html, 'http://site.net/proxy/p1')).to be_equivalent_to(proxified_html)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
describe '#rewrite_env' do
|
226
|
+
# See http://rack.rubyforge.org/doc/SPEC.html for the rack env hash fields spec
|
227
|
+
example_request = {'HTTP_HOST' => 'myproxysite.com',
|
228
|
+
'SCRIPT_NAME' => '',
|
229
|
+
'PATH_INFO' => '/proxy/content',
|
230
|
+
'QUERY_STRING' => '',
|
231
|
+
'SERVER_NAME' => 'myproxysite.com',
|
232
|
+
'SERVER_PORT' => '9292',
|
233
|
+
'rack.url_scheme' => 'http'}
|
234
|
+
|
235
|
+
context "when the request is not prefixed by proxy_path" do
|
236
|
+
let (:request_to_outside_content) { example_request.clone.update({'PATH_INFO' => '/no_proxy/content'}) }
|
237
|
+
it { expect {pp.rewrite_env(request_to_outside_content)}.to raise_error(PrettyProxy::ProxyError) }
|
238
|
+
end
|
239
|
+
context "when the request don't point to a original_path" do
|
240
|
+
let (:request_to_not_a_proxy) { example_request.clone.update({'PATH_INFO' => '/no_proxy/content'}) }
|
241
|
+
it { expect {pp.rewrite_env(request_to_not_a_proxy)}.to raise_error(PrettyProxy::ProxyError) }
|
242
|
+
end
|
243
|
+
|
244
|
+
let (:by_proxy_request) { example_request.clone }
|
245
|
+
let (:rewritten_env) { pp.rewrite_env by_proxy_request }
|
246
|
+
|
247
|
+
context 'when the HTTP_HOST is not empty' do
|
248
|
+
it 'change the HTTP_HOST and SERVER_NAME to the unproxyfied version' do
|
249
|
+
expect(rewritten_env['HTTP_HOST']).to eq 'myoriginalsite.com'
|
250
|
+
expect(rewritten_env['SERVER_NAME']).to eq 'myoriginalsite.com'
|
251
|
+
end
|
252
|
+
end
|
253
|
+
context 'when the HTTP_HOST is empty' do
|
254
|
+
let (:by_proxy_request) { t = example_request.clone; t.delete('HTTP_HOST'); t }
|
255
|
+
it 'change the SERVER_NAME to the unproxyfied version' do
|
256
|
+
expect(rewritten_env.has_key? 'HTTP_HOST').to be_false
|
257
|
+
expect(rewritten_env['SERVER_NAME']).to eq 'myoriginalsite.com'
|
258
|
+
end
|
259
|
+
end
|
260
|
+
context 'when the SCRIPT_NAME is not empty and the PATH_INFO is empty' do
|
261
|
+
let (:by_proxy_request) { example_request.clone.update({'SCRIPT_NAME' => '/proxy/content',
|
262
|
+
'PATH_INFO' => ''}) }
|
263
|
+
it 'changes only the SCRIPT_NAME' do
|
264
|
+
expect(rewritten_env['SCRIPT_NAME']).to eq '/content'
|
265
|
+
expect(rewritten_env['PATH_INFO']).to eq ''
|
266
|
+
end
|
267
|
+
end
|
268
|
+
context 'when the PATH_INFO is not empty and the SCRIPT_NAME is empty' do
|
269
|
+
it 'changes only the PATH_INFO' do
|
270
|
+
expect(rewritten_env['PATH_INFO']).to eq '/content'
|
271
|
+
expect(rewritten_env['SCRIPT_NAME']).to eq ''
|
272
|
+
end
|
273
|
+
end
|
274
|
+
context 'when the SCRIPT_NAME and the PATH_INFO are not empty' do
|
275
|
+
# NOTE: in a real request the SCRIPT_NAME have a trailing slash?
|
276
|
+
# even if the PATH_INFO start with a slash?
|
277
|
+
let (:by_proxy_request) { example_request.update({'SCRIPT_NAME' => '/proxy',
|
278
|
+
'PATH_INFO' => '/content'}) }
|
279
|
+
it 'change the SCRIPT_NAME to empty and the PATH_INFO has the full path' do
|
280
|
+
expect(rewritten_env['PATH_INFO']).to eq '/content'
|
281
|
+
expect(rewritten_env['SCRIPT_NAME']).to eq ''
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
describe '#rewrite_response' do
|
287
|
+
let (:pp) { described_class.new('/proxy/', 'http://site.net', ['/p1', '/p2/p2_2/']) }
|
288
|
+
# See http://rack.rubyforge.org/doc/SPEC.html for the rack env hash fields spec
|
289
|
+
let (:original_env) {{'HTTP_HOST' => 'site.net',
|
290
|
+
'SCRIPT_NAME' => '',
|
291
|
+
'PATH_INFO' => '/proxy/p1',
|
292
|
+
'QUERY_STRING' => '',
|
293
|
+
'SERVER_NAME' => 'site.net',
|
294
|
+
'SERVER_PORT' => '80',
|
295
|
+
'rack.url_scheme' => 'http'}}
|
296
|
+
let (:rewritten_env) { pp.rewrite_env(original_env) }
|
297
|
+
let (:response_example) { original_content = [200,
|
298
|
+
{'content-type' => 'application/xhtml+xml',
|
299
|
+
'content-encoding' => 'identity',
|
300
|
+
'content-length' => original_html.bytesize.to_s },
|
301
|
+
[original_html]] }
|
302
|
+
|
303
|
+
context 'when the content-type is html or xhtml' do
|
304
|
+
let (:original_response) { response_example }
|
305
|
+
subject { pp.rewrite_response(original_response, original_env, rewritten_env) }
|
306
|
+
|
307
|
+
let (:rewritten_headers) { subject[1] }
|
308
|
+
let (:rewritten_body) { subject[2].join }
|
309
|
+
let (:original_url) { Rack::Request.new(original_env).url }
|
310
|
+
|
311
|
+
# NOTE: TESTING ONLY WITH XHTML, BY THE SAME MOTIVE EXPLAINED IN THE #proxify_html SPEC
|
312
|
+
it 'apply #proxify_html to the body' do
|
313
|
+
expect(rewritten_body).to be_equivalent_to pp.proxify_html(original_html, original_url)
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'change the content-length header to the new size of the body' do
|
317
|
+
expect(rewritten_headers['content-length']).to eq rewritten_body.bytesize.to_s
|
318
|
+
end
|
319
|
+
|
320
|
+
context 'compressed with deflate' do
|
321
|
+
it 'decompress, make the changes, and return it compressed again' do
|
322
|
+
original_response[1].update({'content-encoding' => 'deflate'})
|
323
|
+
deflate = Zlib::Deflate.method :deflate
|
324
|
+
original_response[2] = [deflate.call(original_html)]
|
325
|
+
inflate = Zlib::Inflate.method :inflate
|
326
|
+
|
327
|
+
expect(inflate.call(rewritten_body)).to be_equivalent_to(proxified_html)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
context 'compressed with gzip' do
|
332
|
+
it 'decompress, make the changes, and return it compressed again' do
|
333
|
+
original_response[1].update({'content-encoding' => 'gzip'})
|
334
|
+
gzip = ->(str) do
|
335
|
+
return_str = ''
|
336
|
+
gzip_stream = Zlib::GzipWriter.new(StringIO.new(return_str))
|
337
|
+
gzip_stream.write str
|
338
|
+
gzip_stream.close
|
339
|
+
return_str
|
340
|
+
end
|
341
|
+
ungzip = ->(str) do
|
342
|
+
Zlib::GzipReader.new(StringIO.new(str)).read
|
343
|
+
end
|
344
|
+
original_response[2] = [gzip.call(original_html)]
|
345
|
+
|
346
|
+
expect(ungzip.call(rewritten_body)).to be_equivalent_to proxified_html
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
context 'compressed with another method' do
|
351
|
+
let (:original_response) { response_example[1].update({'content-encoding' => 'unknown-encoding'}); response_example }
|
352
|
+
it { expect {subject}.to raise_error(PrettyProxy::ProxyError) }
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
metadata
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pretty_proxy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Henrique Becker
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-05-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.5'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rack-proxy
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.3'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: equivalent-xml
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: thin
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.5'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.5'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: json
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.7'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.7'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec-core
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.13'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ~>
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.13'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec-expectations
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '2.13'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ~>
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '2.13'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rake
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ~>
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '10.0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ~>
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '10.0'
|
139
|
+
description: If you want to replicate a site section with some change (like translation)
|
140
|
+
and mantain the url pretty maybe this is the right library.
|
141
|
+
email: henriquebecker91@gmail.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- lib/pretty_proxy.rb
|
147
|
+
- example/example_spec.rb
|
148
|
+
- example/example_conf.json
|
149
|
+
- example/example.ru
|
150
|
+
- example/heresy.ru
|
151
|
+
- spec/pretty_proxy_spec.rb
|
152
|
+
- Rakefile
|
153
|
+
homepage: http://rubygems.org/gems/pretty_proxy
|
154
|
+
licenses:
|
155
|
+
- Public domain
|
156
|
+
metadata: {}
|
157
|
+
post_install_message:
|
158
|
+
rdoc_options: []
|
159
|
+
require_paths:
|
160
|
+
- lib
|
161
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - '>='
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
167
|
+
requirements:
|
168
|
+
- - '>='
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '0'
|
171
|
+
requirements: []
|
172
|
+
rubyforge_project:
|
173
|
+
rubygems_version: 2.0.0
|
174
|
+
signing_key:
|
175
|
+
specification_version: 4
|
176
|
+
summary: A Rack::Proxy child pretty url oriented
|
177
|
+
test_files:
|
178
|
+
- spec/pretty_proxy_spec.rb
|
179
|
+
has_rdoc:
|