rack-proxy 0.7.0 → 0.7.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 228f3bf6e52dcd2bdcebf9c52bf1db47dfdc7179ed52cd3664e14cfd8df80db9
4
- data.tar.gz: 6613d7f13087046d9ad6ee22e5afa56db6910145df427e42101f5aa491d3fe26
3
+ metadata.gz: 888784aa8d1d28ae0dc2a1352aa44ba8e639d5cd604043facbb31da3fa1dc759
4
+ data.tar.gz: 9ba49effcffcacb930ab08fe2f6a9fd08040b60800b8aa8e5ccc274053f36c4e
5
5
  SHA512:
6
- metadata.gz: 151b04e286d1305d116cf743f00c6b811ed10fad0f24cf806b685a4e038a4dcf5c3b8f7e0dd0cf1e8aa95dd8c46763b0f8b114d36c668fdaabda541d48d5722a
7
- data.tar.gz: 234ec48678c973b203cf00f256039c6e88f63c67c5e279e6f0ec17fc51de07022f7a3099dfe0efdaacf46808a0512cd9e16444823b6d616fdee08ae5572efbb2
6
+ metadata.gz: 606ed720fb5b8c67cd1fc3058b9644e88fb2e7768d4fce4606ba0332fac24cadca11a36ab50d97cb7ff5767664864b1c1a2cf5108cd58a66fecfb3b93de37517
7
+ data.tar.gz: a91cc8541d7af6c390fe1c0faa3c923942a14cce746eebc3d170b95b45aafc5871a04ad1ec9fee6f0c07500534755c794f76d0c14bccdcf5fdaad06e239aeb07
@@ -0,0 +1,3 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: [ncr]
data/.travis.yml CHANGED
@@ -1,4 +1,3 @@
1
- sudo: false
2
1
  cache: bundler
3
2
  language: ruby
4
3
  before_install:
data/Gemfile.lock CHANGED
@@ -1,22 +1,22 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rack-proxy (0.7.0)
4
+ rack-proxy (0.7.7)
5
5
  rack
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- power_assert (0.2.6)
11
- rack (2.0.3)
12
- rack-test (0.5.6)
13
- rack (>= 1.0)
14
- rake (13.0.1)
15
- test-unit (3.1.5)
10
+ power_assert (2.0.3)
11
+ rack (3.0.8)
12
+ rack-test (2.1.0)
13
+ rack (>= 1.3)
14
+ rake (13.0.6)
15
+ test-unit (3.6.1)
16
16
  power_assert
17
17
 
18
18
  PLATFORMS
19
- ruby
19
+ arm64-darwin-22
20
20
 
21
21
  DEPENDENCIES
22
22
  rack-proxy!
@@ -25,4 +25,4 @@ DEPENDENCIES
25
25
  test-unit
26
26
 
27
27
  BUNDLED WITH
28
- 1.17.2
28
+ 2.4.17
data/README.md CHANGED
@@ -6,7 +6,7 @@ Installation
6
6
  Add the following to your `Gemfile`:
7
7
 
8
8
  ```
9
- gem 'rack-proxy', '~> 0.7.0'
9
+ gem 'rack-proxy', '~> 0.7.7'
10
10
  ```
11
11
 
12
12
  Or install:
@@ -136,7 +136,7 @@ Test with `require 'rack_proxy_examples/example_service_proxy'`
136
136
  # 1. rails new test_app
137
137
  # 2. cd test_app
138
138
  # 3. install Rack-Proxy in `Gemfile`
139
- # a. `gem 'rack-proxy', '~> 0.7.0'`
139
+ # a. `gem 'rack-proxy', '~> 0.7.7'`
140
140
  # 4. install gem: `bundle install`
141
141
  # 5. create `config/initializers/proxy.rb` adding this line `require 'rack_proxy_examples/example_service_proxy'`
142
142
  # 6. run: `SERVICE_URL=http://guides.rubyonrails.org rails server`
@@ -297,6 +297,35 @@ Add some domain name like `debug.your_app.com` into your local `/etc/hosts` file
297
297
 
298
298
  Next start the proxy and your app. And now you can access to your Spring application through SSL connection via `https://debug.your_app.com` URI in a browser.
299
299
 
300
+ ### Using SSL/TLS certificates with HTTP connection
301
+ This may be helpful, when third-party API has authentication by client TLS certificates and you need to proxy your requests and sign them with certificate.
302
+
303
+ Just specify Rack::Proxy SSL options and your request will use TLS HTTP connection:
304
+ ```ruby
305
+ # config.ru
306
+ . . .
307
+
308
+ cert_raw = File.read('./certs/rootCA.crt')
309
+ key_raw = File.read('./certs/key.pem')
310
+
311
+ cert = OpenSSL::X509::Certificate.new(cert_raw)
312
+ key = OpenSSL::PKey.read(key_raw)
313
+
314
+ use TLSProxy, cert: cert, key: key, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_PEER, ssl_version: 'TLSv1_2'
315
+ ```
316
+
317
+ And rewrite host for example:
318
+ ```ruby
319
+ # tls_proxy.rb
320
+ class TLSProxy < Rack::Proxy
321
+ attr_accessor :original_request, :query_params
322
+
323
+ def rewrite_env(env)
324
+ env["HTTP_HOST"] = "client-tls-auth-api.com:443"
325
+ env
326
+ end
327
+ end
328
+ ```
300
329
 
301
330
  WARNING
302
331
  ----
@@ -1,13 +1,16 @@
1
1
  require "net_http_hacked"
2
+ require "stringio"
2
3
 
3
4
  module Rack
4
-
5
5
  # Wraps the hacked net/http in a Rack way.
6
6
  class HttpStreamingResponse
7
- attr_accessor :use_ssl
8
- attr_accessor :verify_mode
9
- attr_accessor :read_timeout
10
- attr_accessor :ssl_version
7
+ STATUSES_WITH_NO_ENTITY_BODY = {
8
+ 204 => true,
9
+ 205 => true,
10
+ 304 => true
11
+ }.freeze
12
+
13
+ attr_accessor :use_ssl, :verify_mode, :read_timeout, :ssl_version, :cert, :key
11
14
 
12
15
  def initialize(request, host, port = nil)
13
16
  @request, @host, @port = request, host, port
@@ -18,60 +21,62 @@ module Rack
18
21
  end
19
22
 
20
23
  def code
21
- response.code.to_i
24
+ response.code.to_i.tap do |response_code|
25
+ STATUSES_WITH_NO_ENTITY_BODY[response_code] && close_connection
26
+ end
22
27
  end
23
28
  # #status is deprecated
24
29
  alias_method :status, :code
25
30
 
26
31
  def headers
27
- h = Utils::HeaderHash.new
28
-
29
- response.to_hash.each do |k, v|
30
- h[k] = v
31
- end
32
-
33
- h
32
+ Rack::Proxy.build_header_hash(response.to_hash)
34
33
  end
35
34
 
36
35
  # Can be called only once!
37
36
  def each(&block)
37
+ return if connection_closed
38
+
38
39
  response.read_body(&block)
39
40
  ensure
40
- session.end_request_hacked
41
- session.finish
41
+ close_connection
42
42
  end
43
43
 
44
44
  def to_s
45
- @body ||= begin
46
- lines = []
47
-
48
- each do |line|
49
- lines << line
50
- end
51
-
52
- lines.join
53
- end
45
+ @to_s ||= StringIO.new.tap { |io| each { |line| io << line } }.string
54
46
  end
55
47
 
56
48
  protected
57
49
 
58
50
  # Net::HTTPResponse
59
51
  def response
60
- @response ||= session.begin_request_hacked(@request)
52
+ @response ||= session.begin_request_hacked(request)
61
53
  end
62
54
 
63
55
  # Net::HTTP
64
56
  def session
65
- @session ||= begin
66
- http = Net::HTTP.new @host, @port
67
- http.use_ssl = self.use_ssl
68
- http.verify_mode = self.verify_mode
69
- http.read_timeout = self.read_timeout
70
- http.ssl_version = self.ssl_version if self.use_ssl
57
+ @session ||= Net::HTTP.new(host, port).tap do |http|
58
+ http.use_ssl = use_ssl
59
+ http.verify_mode = verify_mode
60
+ http.read_timeout = read_timeout
61
+ http.ssl_version = ssl_version if ssl_version
62
+ http.cert = cert if cert
63
+ http.key = key if key
71
64
  http.start
72
65
  end
73
66
  end
74
67
 
75
- end
68
+ private
69
+
70
+ attr_reader :request, :host, :port
71
+
72
+ attr_accessor :connection_closed
76
73
 
74
+ def close_connection
75
+ return if connection_closed
76
+
77
+ session.end_request_hacked
78
+ session.finish
79
+ self.connection_closed = true
80
+ end
81
+ end
77
82
  end
data/lib/rack/proxy.rb CHANGED
@@ -5,7 +5,18 @@ module Rack
5
5
 
6
6
  # Subclass and bring your own #rewrite_request and #rewrite_response
7
7
  class Proxy
8
- VERSION = "0.7.0"
8
+ VERSION = "0.7.7".freeze
9
+
10
+ HOP_BY_HOP_HEADERS = {
11
+ 'connection' => true,
12
+ 'keep-alive' => true,
13
+ 'proxy-authenticate' => true,
14
+ 'proxy-authorization' => true,
15
+ 'te' => true,
16
+ 'trailer' => true,
17
+ 'transfer-encoding' => true,
18
+ 'upgrade' => true
19
+ }.freeze
9
20
 
10
21
  class << self
11
22
  def extract_http_request_headers(env)
@@ -13,28 +24,38 @@ module Rack
13
24
  !(/^HTTP_[A-Z0-9_\.]+$/ === k) || v.nil?
14
25
  end.map do |k, v|
15
26
  [reconstruct_header_name(k), v]
16
- end.inject(Utils::HeaderHash.new) do |hash, k_v|
17
- k, v = k_v
18
- hash[k] = v
19
- hash
20
- end
27
+ end.then { |pairs| build_header_hash(pairs) }
21
28
 
22
- x_forwarded_for = (headers["X-Forwarded-For"].to_s.split(/, +/) << env["REMOTE_ADDR"]).join(", ")
29
+ x_forwarded_for = (headers['X-Forwarded-For'].to_s.split(/, +/) << env['REMOTE_ADDR']).join(', ')
23
30
 
24
- headers.merge!("X-Forwarded-For" => x_forwarded_for)
31
+ headers.merge!('X-Forwarded-For' => x_forwarded_for)
25
32
  end
26
33
 
27
34
  def normalize_headers(headers)
28
35
  mapped = headers.map do |k, v|
29
- [k, if v.is_a? Array then v.join("\n") else v end]
36
+ [titleize(k), if v.is_a? Array then v.join("\n") else v end]
37
+ end
38
+ build_header_hash Hash[mapped]
39
+ end
40
+
41
+ def build_header_hash(pairs)
42
+ if Rack.const_defined?(:Headers)
43
+ # Rack::Headers is only available from Rack 3 onward
44
+ Headers.new.tap { |headers| pairs.each { |k, v| headers[k] = v } }
45
+ else
46
+ # Rack::Utils::HeaderHash is deprecated from Rack 3 onward and is to be removed in 3.1
47
+ Utils::HeaderHash.new(pairs)
30
48
  end
31
- Utils::HeaderHash.new Hash[mapped]
32
49
  end
33
50
 
34
51
  protected
35
52
 
36
53
  def reconstruct_header_name(name)
37
- name.sub(/^HTTP_/, "").gsub("_", "-")
54
+ titleize(name.sub(/^HTTP_/, "").gsub("_", "-"))
55
+ end
56
+
57
+ def titleize(str)
58
+ str.split("-").map(&:capitalize).join("-")
38
59
  end
39
60
  end
40
61
 
@@ -49,12 +70,15 @@ module Rack
49
70
 
50
71
  @streaming = opts.fetch(:streaming, true)
51
72
  @ssl_verify_none = opts.fetch(:ssl_verify_none, false)
52
- @backend = URI(opts[:backend]) if opts[:backend]
73
+ @backend = opts[:backend] ? URI(opts[:backend]) : nil
53
74
  @read_timeout = opts.fetch(:read_timeout, 60)
54
- @ssl_version = opts[:ssl_version] if opts[:ssl_version]
75
+ @ssl_version = opts[:ssl_version]
76
+ @cert = opts[:cert]
77
+ @key = opts[:key]
78
+ @verify_mode = opts[:verify_mode]
55
79
 
56
- @username = opts[:username] if opts[:username]
57
- @password = opts[:password] if opts[:password]
80
+ @username = opts[:username]
81
+ @password = opts[:password]
58
82
 
59
83
  @opts = opts
60
84
  end
@@ -85,7 +109,7 @@ module Rack
85
109
  full_path = source_request.fullpath
86
110
  end
87
111
 
88
- target_request = Net::HTTP.const_get(source_request.request_method.capitalize).new(full_path)
112
+ target_request = Net::HTTP.const_get(source_request.request_method.capitalize, false).new(full_path)
89
113
 
90
114
  # Setup headers
91
115
  target_request.initialize_http_header(self.class.extract_http_request_headers(source_request.env))
@@ -102,8 +126,7 @@ module Rack
102
126
  target_request.basic_auth(@username, @password) if @username && @password
103
127
 
104
128
  backend = env.delete('rack.backend') || @backend || source_request
105
- use_ssl = backend.scheme == "https"
106
- ssl_verify_none = (env.delete('rack.ssl_verify_none') || @ssl_verify_none) == true
129
+ use_ssl = backend.scheme == "https" || @cert
107
130
  read_timeout = env.delete('http.read_timeout') || @read_timeout
108
131
 
109
132
  # Create the response
@@ -112,30 +135,34 @@ module Rack
112
135
  target_response = HttpStreamingResponse.new(target_request, backend.host, backend.port)
113
136
  target_response.use_ssl = use_ssl
114
137
  target_response.read_timeout = read_timeout
115
- target_response.verify_mode = OpenSSL::SSL::VERIFY_NONE if use_ssl && ssl_verify_none
116
138
  target_response.ssl_version = @ssl_version if @ssl_version
139
+ target_response.verify_mode = (@verify_mode || OpenSSL::SSL::VERIFY_NONE) if use_ssl
140
+ target_response.cert = @cert if @cert
141
+ target_response.key = @key if @key
117
142
  else
118
143
  http = Net::HTTP.new(backend.host, backend.port)
119
144
  http.use_ssl = use_ssl if use_ssl
120
145
  http.read_timeout = read_timeout
121
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE if use_ssl && ssl_verify_none
122
146
  http.ssl_version = @ssl_version if @ssl_version
147
+ http.verify_mode = (@verify_mode || OpenSSL::SSL::VERIFY_NONE if use_ssl) if use_ssl
148
+ http.cert = @cert if @cert
149
+ http.key = @key if @key
123
150
 
124
151
  target_response = http.start do
125
152
  http.request(target_request)
126
153
  end
127
154
  end
128
155
 
156
+ code = target_response.code
129
157
  headers = self.class.normalize_headers(target_response.respond_to?(:headers) ? target_response.headers : target_response.to_hash)
130
158
  body = target_response.body || [""]
131
159
  body = [body] unless body.respond_to?(:each)
132
160
 
133
161
  # According to https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-7.1.3.1Acc
134
162
  # should remove hop-by-hop header fields
135
- headers.reject! { |k| ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade'].include? k.downcase }
136
- [target_response.code, headers, body]
137
- end
163
+ headers.reject! { |k| HOP_BY_HOP_HEADERS[k.downcase] }
138
164
 
165
+ [code, headers, body]
166
+ end
139
167
  end
140
-
141
168
  end
@@ -5,7 +5,7 @@
5
5
  # 1. rails new test_app
6
6
  # 2. cd test_app
7
7
  # 3. install Rack-Proxy in `Gemfile`
8
- # a. `gem 'rack-proxy', '~> 0.7.0'`
8
+ # a. `gem 'rack-proxy', '~> 0.7.7'`
9
9
  # 4. install gem: `bundle install`
10
10
  # 5. create `config/initializers/proxy.rb` adding this line `require 'rack_proxy_examples/example_service_proxy'`
11
11
  # 6. run: `SERVICE_URL=http://guides.rubyonrails.org rails server`
data/rack-proxy.gemspec CHANGED
@@ -12,6 +12,7 @@ Gem::Specification.new do |s|
12
12
  s.homepage = "https://github.com/ncr/rack-proxy"
13
13
  s.summary = %q{A request/response rewriting HTTP proxy. A Rack app.}
14
14
  s.description = %q{A Rack app that provides request/response rewriting proxy capabilities with streaming.}
15
+ s.required_ruby_version = '>= 2.6'
15
16
 
16
17
  s.files = `git ls-files`.split("\n")
17
18
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -4,23 +4,24 @@ require "rack/http_streaming_response"
4
4
  class HttpStreamingResponseTest < Test::Unit::TestCase
5
5
 
6
6
  def setup
7
- host, req = "www.trix.pl", Net::HTTP::Get.new("/")
8
- @response = Rack::HttpStreamingResponse.new(req, host)
7
+ host, req = "example.com", Net::HTTP::Get.new("/")
8
+ @response = Rack::HttpStreamingResponse.new(req, host, 443)
9
+ @response.use_ssl = true
9
10
  end
10
11
 
11
12
  def test_streaming
12
13
  # Response status
13
- assert @response.code == 200
14
- assert @response.status == 200
14
+ assert_equal 200, @response.status
15
+ assert_equal 200, @response.status
15
16
 
16
17
  # Headers
17
18
  headers = @response.headers
18
19
 
19
- assert headers.size > 0
20
+ assert headers.size.positive?
20
21
 
21
- assert headers["content-type"] == ["text/html;charset=utf-8"]
22
- assert headers["CoNtEnT-TyPe"] == headers["content-type"]
23
- assert headers["content-length"].first.to_i > 0
22
+ assert_match %r{text/html; ?charset=utf-8}, headers["content-type"].first.downcase
23
+ assert_equal headers["content-type"], headers["CoNtEnT-TyPe"]
24
+ assert headers["content-length"].first.to_i.positive?
24
25
 
25
26
  # Body
26
27
  chunks = []
@@ -28,7 +29,7 @@ class HttpStreamingResponseTest < Test::Unit::TestCase
28
29
  chunks << chunk
29
30
  end
30
31
 
31
- assert chunks.size > 0
32
+ assert chunks.size.positive?
32
33
  chunks.each do |chunk|
33
34
  assert chunk.is_a?(String)
34
35
  end
@@ -36,7 +37,7 @@ class HttpStreamingResponseTest < Test::Unit::TestCase
36
37
  end
37
38
 
38
39
  def test_to_s
39
- assert_equal @response.headers["Content-Length"].first.to_i, @response.body.to_s.size
40
+ assert_equal @response.headers["Content-Length"].first.to_i, @response.body.to_s.bytesize
40
41
  end
41
42
 
42
43
  def test_to_s_called_twice
@@ -78,10 +78,10 @@ class RackProxyTest < Test::Unit::TestCase
78
78
  proxy_class = Rack::Proxy
79
79
 
80
80
  header = proxy_class.send(:reconstruct_header_name, "HTTP_ABC")
81
- assert header == "ABC"
81
+ assert header == "Abc"
82
82
 
83
83
  header = proxy_class.send(:reconstruct_header_name, "HTTP_ABC_D")
84
- assert header == "ABC-D"
84
+ assert header == "Abc-D"
85
85
  end
86
86
 
87
87
  def test_extract_http_request_headers
@@ -120,7 +120,7 @@ class RackProxyTest < Test::Unit::TestCase
120
120
  end
121
121
 
122
122
  def test_response_header_included_Hop_by_hop
123
- app({:streaming => true}).host = 'auth.goeasyship.com'
123
+ app({:streaming => true}).host = 'mockapi.io'
124
124
  get 'https://example.com/oauth2/token/info?access_token=123'
125
125
  assert !last_response.headers.key?('transfer-encoding')
126
126
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-proxy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jacek Becela
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-28 00:00:00.000000000 Z
11
+ date: 2023-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -60,6 +60,7 @@ executables: []
60
60
  extensions: []
61
61
  extra_rdoc_files: []
62
62
  files:
63
+ - ".github/FUNDING.yml"
63
64
  - ".gitignore"
64
65
  - ".travis.yml"
65
66
  - Gemfile
@@ -92,14 +93,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
92
93
  requirements:
93
94
  - - ">="
94
95
  - !ruby/object:Gem::Version
95
- version: '0'
96
+ version: '2.6'
96
97
  required_rubygems_version: !ruby/object:Gem::Requirement
97
98
  requirements:
98
99
  - - ">="
99
100
  - !ruby/object:Gem::Version
100
101
  version: '0'
101
102
  requirements: []
102
- rubygems_version: 3.0.3
103
+ rubygems_version: 3.2.3
103
104
  signing_key:
104
105
  specification_version: 4
105
106
  summary: A request/response rewriting HTTP proxy. A Rack app.