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.
@@ -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
@@ -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).
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ end
11
+
12
+ task default: %i[test]
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AeReverseProxy
4
+ VERSION = '1.0.0'
5
+ autoload :Client, 'ae_reverse_proxy/client'
6
+ autoload :ControllerCallbackMethod, 'ae_reverse_proxy/controller_callback_method'
7
+ 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: []