ae_reverse_proxy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +15 -0
- data/README.md +41 -0
- data/Rakefile +12 -0
- data/ae_reverse_proxy.gemspec +21 -0
- data/lib/ae_reverse_proxy.rb +7 -0
- data/lib/ae_reverse_proxy/client.rb +163 -0
- data/lib/ae_reverse_proxy/controller_callback_method.rb +63 -0
- metadata +79 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3c66d26c7edb28c270618fa3e210d7d96cc46f8d81d85c0247daf31839d4f9eb
|
4
|
+
data.tar.gz: e7d859b126d02a8e2c726774b8c634a9f11fbd161ead8eb82ecc589006e20f66
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: de053974f52bf99341f8d5457661aa42ff8f107fa2e31bc2b0b2155a185c6a8531d16132d71de45f17cbfab36461e6d6c85c112e3ebc3cf60d779ebea459e54d
|
7
|
+
data.tar.gz: 11b8b5b987d1441dc4cf91fbca5a1746e29f66fd3e20ff5adda84d48fa4bd7d1110b757bc3ee61346fd7f55deca3cae5af5b3c671de08f527059bcff783e4aee
|
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org' do
|
4
|
+
group :test, :development do
|
5
|
+
gem 'bundler', '>= 1.10.4', '< 3'
|
6
|
+
gem 'minitest', '>= 5.8', '< 6'
|
7
|
+
gem 'mocha', '>= 1.11', '< 2'
|
8
|
+
gem 'pry'
|
9
|
+
gem 'rake', '>= 13', '< 14'
|
10
|
+
gem 'rubocop', '~> 1.8', require: false
|
11
|
+
gem 'webmock', '~> 3.11.1'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
gemspec
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# AeReverseProxy
|
2
|
+
|
3
|
+
A reverse proxy accepts a request from a client, forwards it to a server that can fulfill it, and returns the server's response to the client
|
4
|
+
|
5
|
+
This is forked from https://github.com/axsuul/rails-reverse-proxy. Thanks to https://github.com/axsuul and contributors.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'ae_reverse_proxy'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle install
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install ae_reverse_proxy
|
22
|
+
|
23
|
+
Use it in a console with:
|
24
|
+
|
25
|
+
$ ./console
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class ImageController < ActionController::Base
|
31
|
+
include AeReverseProxy::ControllerCallbackMethod
|
32
|
+
|
33
|
+
before_action do
|
34
|
+
reverse_proxy('https://www.another_server.com')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
## License
|
40
|
+
|
41
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/ae_reverse_proxy'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'ae_reverse_proxy'
|
7
|
+
spec.version = AeReverseProxy::VERSION
|
8
|
+
spec.platform = Gem::Platform::RUBY
|
9
|
+
spec.authors = ['Appfolio']
|
10
|
+
spec.email = ['opensource@appfolio.com']
|
11
|
+
spec.summary = 'Gem for reverse proxying requests.'
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.license = 'MIT'
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0')
|
15
|
+
spec.files = Dir['**/*'].select { |f| f[%r{^(lib/|Gemfile$|Rakefile|README.md|.*gemspec)}] }
|
16
|
+
spec.require_paths = ['lib']
|
17
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
18
|
+
|
19
|
+
spec.add_runtime_dependency('addressable', '>= 2.3.6')
|
20
|
+
spec.add_runtime_dependency('rack', '~> 2.2.3')
|
21
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require 'addressable/uri'
|
5
|
+
|
6
|
+
module AeReverseProxy
|
7
|
+
class Client
|
8
|
+
attr_accessor :uri, :callbacks
|
9
|
+
|
10
|
+
CALLBACK_METHODS = %i[
|
11
|
+
on_response
|
12
|
+
on_set_cookies
|
13
|
+
on_connect
|
14
|
+
on_success
|
15
|
+
on_redirect
|
16
|
+
on_missing
|
17
|
+
on_error
|
18
|
+
on_complete
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
# Define callback setters
|
22
|
+
CALLBACK_METHODS.each do |method|
|
23
|
+
define_method(method) do |&block|
|
24
|
+
callbacks[method] = block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(uri)
|
29
|
+
self.uri = uri
|
30
|
+
self.callbacks = CALLBACK_METHODS.to_h { |method| [method, proc {}] }
|
31
|
+
|
32
|
+
yield(self) if block_given?
|
33
|
+
end
|
34
|
+
|
35
|
+
def forward_request(env, options = {})
|
36
|
+
# Initialize requests
|
37
|
+
source_request = Rack::Request.new(env)
|
38
|
+
target_request = Net::HTTP.const_get(source_request.request_method.capitalize).new(source_request.path)
|
39
|
+
|
40
|
+
# Setup headers for forwarding.
|
41
|
+
target_request_headers = extract_http_request_headers(source_request.env).merge({
|
42
|
+
'ORIGIN' => uri.origin,
|
43
|
+
'HOST' => uri.authority,
|
44
|
+
})
|
45
|
+
target_request.initialize_http_header(target_request_headers)
|
46
|
+
|
47
|
+
# Setup basic auth.
|
48
|
+
target_request.basic_auth(options[:username], options[:password]) if options[:username] && options[:password]
|
49
|
+
|
50
|
+
# Setup body.
|
51
|
+
if target_request.request_body_permitted? && source_request.body
|
52
|
+
source_request.body.rewind
|
53
|
+
target_request.body_stream = source_request.body
|
54
|
+
end
|
55
|
+
|
56
|
+
# Setup content encoding and type.
|
57
|
+
target_request.content_length = source_request.content_length || 0
|
58
|
+
target_request.content_type = source_request.content_type if source_request.content_type
|
59
|
+
|
60
|
+
# Don't encode response/support compression which was
|
61
|
+
# causing content length not match the actual content
|
62
|
+
# length of the response which ended up causing issues
|
63
|
+
# within Varnish (503)
|
64
|
+
target_request['Accept-Encoding'] = nil
|
65
|
+
|
66
|
+
# Setup HTTP SSL options.
|
67
|
+
http_options = {}
|
68
|
+
http_options[:use_ssl] = (uri.scheme == 'https')
|
69
|
+
|
70
|
+
# Make the request.
|
71
|
+
target_response = nil
|
72
|
+
Net::HTTP.start(uri.hostname, uri.port, http_options) do |http|
|
73
|
+
callbacks[:on_connect].call(http)
|
74
|
+
target_response = http.request(target_request)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Initiate callbacks.
|
78
|
+
status_code = target_response.code.to_i
|
79
|
+
payload = [status_code, target_response]
|
80
|
+
|
81
|
+
callbacks[:on_response].call(payload)
|
82
|
+
|
83
|
+
if target_response.to_hash['set-cookie']
|
84
|
+
set_cookies_hash = {}
|
85
|
+
set_cookie_headers = target_response.to_hash['set-cookie']
|
86
|
+
|
87
|
+
set_cookie_headers.each do |set_cookie_header|
|
88
|
+
set_cookie_hash = parse_cookie(set_cookie_header)
|
89
|
+
name = set_cookie_hash[:name]
|
90
|
+
set_cookies_hash[name] = set_cookie_hash
|
91
|
+
end
|
92
|
+
|
93
|
+
callbacks[:on_set_cookies].call(payload | [set_cookies_hash])
|
94
|
+
end
|
95
|
+
|
96
|
+
case status_code
|
97
|
+
when 200..299
|
98
|
+
callbacks[:on_success].call(payload)
|
99
|
+
when 300..399
|
100
|
+
callbacks[:on_redirect].call(payload | [target_response['Location']]) if target_response['Location']
|
101
|
+
when 400..499
|
102
|
+
callbacks[:on_missing].call(payload)
|
103
|
+
when 500..599
|
104
|
+
callbacks[:on_error].call(payload)
|
105
|
+
end
|
106
|
+
|
107
|
+
callbacks[:on_complete].call(payload)
|
108
|
+
|
109
|
+
payload
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
COOKIE_PARAM_PATTERN = %r{\A([^(),/<>@;:\\"\[\]?={}\s]+)(?:=([^;]*))?\Z}.freeze
|
115
|
+
COOKIE_SPLIT_PATTERN = /;\s*/.freeze
|
116
|
+
|
117
|
+
def extract_http_request_headers(env)
|
118
|
+
env
|
119
|
+
.reject { |k, v| !(/^HTTP_[A-Z_]+$/ === k) || k == 'HTTP_VERSION' || v.nil? }
|
120
|
+
.map { |k, v| [reconstruct_header_name(k), v] }
|
121
|
+
.each_with_object(Rack::Utils::HeaderHash.new) do |k_v, hash|
|
122
|
+
k, v = k_v
|
123
|
+
hash[k] = v
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def reconstruct_header_name(name)
|
128
|
+
name.sub(/^HTTP_/, '').gsub('_', '-')
|
129
|
+
end
|
130
|
+
|
131
|
+
def parse_cookie(cookie_str)
|
132
|
+
params = cookie_str.split(COOKIE_SPLIT_PATTERN)
|
133
|
+
info = params.shift.match(COOKIE_PARAM_PATTERN)
|
134
|
+
return {} unless info
|
135
|
+
|
136
|
+
cookie = {
|
137
|
+
name: info[1],
|
138
|
+
value: CGI.unescape(info[2]),
|
139
|
+
}
|
140
|
+
|
141
|
+
params.each do |param|
|
142
|
+
result = param.match(COOKIE_PARAM_PATTERN)
|
143
|
+
next unless result
|
144
|
+
|
145
|
+
key = result[1].downcase.to_sym
|
146
|
+
value = result[2]
|
147
|
+
case key
|
148
|
+
when :expires
|
149
|
+
begin
|
150
|
+
cookie[:expires] = Time.parse(value)
|
151
|
+
rescue ArgumentError
|
152
|
+
end
|
153
|
+
when :httponly, :secure
|
154
|
+
cookie[key] = true
|
155
|
+
else
|
156
|
+
cookie[key] = value
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
cookie
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'addressable/uri'
|
4
|
+
|
5
|
+
module AeReverseProxy
|
6
|
+
module ControllerCallbackMethod
|
7
|
+
def reverse_proxy(uri)
|
8
|
+
proxy_uri = Addressable::URI.parse(uri)
|
9
|
+
|
10
|
+
client = AeReverseProxy::Client.new(proxy_uri) do |config|
|
11
|
+
config.on_response do |_code, response|
|
12
|
+
blacklist = [
|
13
|
+
'Connection', # Always close connection
|
14
|
+
'Transfer-Encoding', # Let Rails calculate this
|
15
|
+
'Content-Length', # Let Rails calculate this
|
16
|
+
]
|
17
|
+
|
18
|
+
response.each_capitalized do |key, value|
|
19
|
+
next if blacklist.include?(key)
|
20
|
+
|
21
|
+
headers[key] = value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
config.on_set_cookies do |_code, _response, set_cookies|
|
26
|
+
set_cookies.each do |key, attributes|
|
27
|
+
cookies[key] = attributes
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
config.on_redirect do |code, _response, redirect_url|
|
32
|
+
request_uri = Addressable::URI.parse(request.url)
|
33
|
+
redirect_uri = Addressable::URI.parse(redirect_url)
|
34
|
+
|
35
|
+
# Make redirect uri absolute if it's relative by
|
36
|
+
# joining it with the request url
|
37
|
+
redirect_uri = request_uri.join(redirect_url) if redirect_uri.host.nil?
|
38
|
+
|
39
|
+
if !redirect_uri.port.nil? && (redirect_uri.port == proxy_uri.port)
|
40
|
+
# Make sure it's consistent with our request port
|
41
|
+
redirect_uri.port = request.port
|
42
|
+
end
|
43
|
+
|
44
|
+
redirect_to redirect_uri.to_s, status: code
|
45
|
+
return
|
46
|
+
end
|
47
|
+
|
48
|
+
config.on_complete do |code, response|
|
49
|
+
content_type = response['Content-Type']
|
50
|
+
body = response.body.to_s
|
51
|
+
|
52
|
+
if content_type&.match(/image/)
|
53
|
+
send_data body, content_type: content_type, disposition: 'inline', status: code
|
54
|
+
else
|
55
|
+
render body: body, content_type: content_type, status: code
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
client.forward_request(request.env)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ae_reverse_proxy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Appfolio
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-01-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: addressable
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.3.6
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.3.6
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.2.3
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.2.3
|
41
|
+
description: Gem for reverse proxying requests.
|
42
|
+
email:
|
43
|
+
- opensource@appfolio.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- Gemfile
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- ae_reverse_proxy.gemspec
|
52
|
+
- lib/ae_reverse_proxy.rb
|
53
|
+
- lib/ae_reverse_proxy/client.rb
|
54
|
+
- lib/ae_reverse_proxy/controller_callback_method.rb
|
55
|
+
homepage:
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
metadata:
|
59
|
+
allowed_push_host: https://rubygems.org
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.4.0
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubygems_version: 3.0.3
|
76
|
+
signing_key:
|
77
|
+
specification_version: 4
|
78
|
+
summary: Gem for reverse proxying requests.
|
79
|
+
test_files: []
|