ae_reverse_proxy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []