rack-proxy 0.7.7 → 2.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.
data/lib/rack/proxy.rb CHANGED
@@ -1,45 +1,112 @@
1
- require "net_http_hacked"
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require "net/https"
2
5
  require "rack/http_streaming_response"
6
+ require "rack/proxy/version"
3
7
 
4
8
  module Rack
5
-
6
9
  # Subclass and bring your own #rewrite_request and #rewrite_response
7
10
  class Proxy
8
- VERSION = "0.7.7".freeze
9
-
10
11
  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
12
+ "connection" => true,
13
+ "keep-alive" => true,
14
+ "proxy-authenticate" => true,
15
+ "proxy-authorization" => true,
16
+ "te" => true,
17
+ "trailer" => true,
18
+ "transfer-encoding" => true,
19
+ "upgrade" => true
19
20
  }.freeze
20
21
 
22
+ # Backend/network failures that must surface as 502 Bad Gateway rather than
23
+ # crashing the proxy with a raw 500. Construction- and policy-time failures
24
+ # are mapped separately (400/501/502) in #perform_request.
25
+ BACKEND_ERRORS = [
26
+ Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED,
27
+ Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ETIMEDOUT, Errno::EPIPE,
28
+ SocketError,
29
+ Timeout::Error, # includes Net::OpenTimeout
30
+ Net::ReadTimeout, Net::WriteTimeout,
31
+ IOError, # includes EOFError
32
+ OpenSSL::SSL::SSLError,
33
+ Net::ProtocolError,
34
+ # A malformed status line / header block raises these; they subclass
35
+ # StandardError directly, NOT Net::ProtocolError, so list them explicitly
36
+ # or a hostile backend crashes the proxy with a raw 500 instead of a 502.
37
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError
38
+ ].freeze
39
+
40
+ class InvalidRequest < StandardError; end
41
+
42
+ # Net::HTTP copies a body stream until EOF, even when Content-Length is
43
+ # smaller. Bound the input so extra bytes cannot become a second request.
44
+ class RequestBodyStream
45
+ def initialize(input, length)
46
+ @input, @remaining = input, length
47
+ end
48
+
49
+ def read(length, buffer = nil)
50
+ if @remaining.zero?
51
+ buffer&.clear
52
+ return nil
53
+ end
54
+
55
+ data = @input.read([length, @remaining].min, buffer)
56
+ raise InvalidRequest, "request body is shorter than Content-Length" if data.nil? || data.empty?
57
+
58
+ @remaining -= data.bytesize
59
+ data
60
+ end
61
+ end
62
+ private_constant :InvalidRequest, :RequestBodyStream
63
+
21
64
  class << self
22
65
  def extract_http_request_headers(env)
23
66
  headers = env.reject do |k, v|
24
- !(/^HTTP_[A-Z0-9_\.]+$/ === k) || v.nil?
67
+ !(/^HTTP_[A-Z0-9_.]+$/ === k) || v.nil?
25
68
  end.map do |k, v|
26
69
  [reconstruct_header_name(k), v]
27
70
  end.then { |pairs| build_header_hash(pairs) }
28
71
 
29
- x_forwarded_for = (headers['X-Forwarded-For'].to_s.split(/, +/) << env['REMOTE_ADDR']).join(', ')
72
+ # Strip hop-by-hop headers before forwarding. Relaying the client's
73
+ # Connection / TE / Transfer-Encoding (etc.) enables request smuggling
74
+ # and confuses the backend — these are connection-scoped, not end-to-end.
75
+ # Per RFC 7230 §6.1 any field named in the inbound Connection header is
76
+ # itself hop-by-hop for this hop. Use #delete (not #reject!) so the
77
+ # returned HeaderHash's case-insensitive index stays consistent on Rack 2.
78
+ connection_named = headers["Connection"].to_s.downcase.split(/,\s*/).map(&:strip)
79
+ headers.keys.each do |key|
80
+ headers.delete(key) if HOP_BY_HOP_HEADERS[key.downcase] || connection_named.include?(key.downcase)
81
+ end
82
+
83
+ x_forwarded_for = (headers["X-Forwarded-For"].to_s.split(/, +/) << env["REMOTE_ADDR"]).join(", ")
30
84
 
31
- headers.merge!('X-Forwarded-For' => x_forwarded_for)
85
+ headers.merge!("X-Forwarded-For" => x_forwarded_for)
32
86
  end
33
87
 
34
88
  def normalize_headers(headers)
35
89
  mapped = headers.map do |k, v|
36
- [titleize(k), if v.is_a? Array then v.join("\n") else v end]
90
+ value = if v.is_a?(Array)
91
+ if v.length == 1
92
+ v.first
93
+ elsif Rack.const_defined?(:Headers, false)
94
+ v
95
+ else
96
+ v.join("\n")
97
+ end
98
+ else
99
+ v
100
+ end
101
+ [titleize(k), value]
37
102
  end
38
- build_header_hash Hash[mapped]
103
+ build_header_hash mapped.to_h
39
104
  end
40
105
 
41
106
  def build_header_hash(pairs)
42
- if Rack.const_defined?(:Headers)
107
+ # Pass inherit: false so we only check Rack's own constants — otherwise
108
+ # a top-level ::Headers defined by the host app would falsely match.
109
+ if Rack.const_defined?(:Headers, false)
43
110
  # Rack::Headers is only available from Rack 3 onward
44
111
  Headers.new.tap { |headers| pairs.each { |k, v| headers[k] = v } }
45
112
  else
@@ -51,7 +118,7 @@ module Rack
51
118
  protected
52
119
 
53
120
  def reconstruct_header_name(name)
54
- titleize(name.sub(/^HTTP_/, "").gsub("_", "-"))
121
+ titleize(name.sub(/^HTTP_/, "").tr("_", "-"))
55
122
  end
56
123
 
57
124
  def titleize(str)
@@ -60,7 +127,7 @@ module Rack
60
127
  end
61
128
 
62
129
  # @option opts [String, URI::HTTP] :backend Backend host to proxy requests to
63
- def initialize(app = nil, opts= {})
130
+ def initialize(app = nil, opts = {})
64
131
  if app.is_a?(Hash)
65
132
  opts = app
66
133
  @app = nil
@@ -69,17 +136,55 @@ module Rack
69
136
  end
70
137
 
71
138
  @streaming = opts.fetch(:streaming, true)
72
- @ssl_verify_none = opts.fetch(:ssl_verify_none, false)
73
139
  @backend = opts[:backend] ? URI(opts[:backend]) : nil
140
+ # With no :backend (and no env["rack.backend"]), the destination is
141
+ # derived from the client-controlled Host header. Since 1.0 that dynamic
142
+ # mode is refused (502) unless explicitly opted into, because a bare
143
+ # proxy would otherwise be an open proxy / SSRF pivot (cloud metadata
144
+ # endpoints, loopback, RFC1918). Combine the opt-in with a
145
+ # #backend_allowed? allowlist — see the README "Security considerations".
146
+ @allow_dynamic_backend = opts.fetch(:allow_dynamic_backend, false)
74
147
  @read_timeout = opts.fetch(:read_timeout, 60)
148
+ # Connect and per-write deadlines. Without these a slow/hostile backend can
149
+ # stall a thread for Net::HTTP's 60s defaults even with a small read_timeout.
150
+ @open_timeout = opts[:open_timeout]
151
+ @write_timeout = opts[:write_timeout]
152
+ # Optional cap (in bytes) on the backend response size, to bound memory
153
+ # against a hostile/huge backend. Checked before buffering each chunk in
154
+ # either mode, as well as against declared lengths. Default: no cap.
155
+ @max_response_length = opts[:max_response_length]
156
+ # :ssl_version pins an exact protocol and is deprecated (it forbids TLS 1.3);
157
+ # prefer :min_version / :max_version, which map to Net::HTTP#min_version=/#max_version=.
75
158
  @ssl_version = opts[:ssl_version]
159
+ @min_version = opts[:min_version]
160
+ @max_version = opts[:max_version]
76
161
  @cert = opts[:cert]
77
162
  @key = opts[:key]
163
+ # Trust anchors for VERIFY_PEER: :ca_file is a PEM bundle path, :cert_store
164
+ # an OpenSSL::X509::Store. Use these for private-CA backends instead of
165
+ # disabling verification with ssl_verify_none.
166
+ @ca_file = opts[:ca_file]
167
+ @cert_store = opts[:cert_store]
168
+ # SSL verification: defaults to VERIFY_PEER (Ruby's Net::HTTP default).
169
+ # Pass ssl_verify_none: true to explicitly disable cert verification, or
170
+ # pass verify_mode: <OpenSSL::SSL::VERIFY_*> for full control.
78
171
  @verify_mode = opts[:verify_mode]
172
+ @verify_mode ||= OpenSSL::SSL::VERIFY_NONE if opts[:ssl_verify_none]
79
173
 
80
174
  @username = opts[:username]
81
175
  @password = opts[:password]
82
176
 
177
+ # Opt-in request hardening (see README "Security considerations"):
178
+ # :strip_credentials drops Cookie/Authorization from the forwarded
179
+ # request; :replace_x_forwarded_for discards the client-supplied
180
+ # X-Forwarded-For chain and forwards only this hop's REMOTE_ADDR.
181
+ @strip_credentials = opts[:strip_credentials]
182
+ @replace_x_forwarded_for = opts[:replace_x_forwarded_for]
183
+
184
+ # Optional logger for Net::HTTP debug output. Accepts anything with a #<< method
185
+ # (e.g. $stdout, a StringIO, or a Ruby Logger instance).
186
+ @logger = opts[:logger]
187
+
83
188
  @opts = opts
84
189
  end
85
190
 
@@ -97,72 +202,238 @@ module Rack
97
202
  triplet
98
203
  end
99
204
 
205
+ # SSRF guardrail, consulted for EVERY request with the resolved backend
206
+ # (`backend` responds to #host, #port and #scheme). Return false to refuse,
207
+ # which makes the proxy respond 502. The default allows the backend, because
208
+ # by the time this hook runs the destination is either app-configured
209
+ # (:backend / env["rack.backend"]) or the deployment has explicitly passed
210
+ # allow_dynamic_backend: true — override it to pin an allowlist on top:
211
+ #
212
+ # def backend_allowed?(backend)
213
+ # %w[api.internal.example.com].include?(backend.host)
214
+ # end
215
+ def backend_allowed?(backend)
216
+ true
217
+ end
218
+
100
219
  protected
101
220
 
102
221
  def perform_request(env)
103
222
  source_request = Rack::Request.new(env)
104
223
 
105
- # Initialize request
106
- if source_request.fullpath == ""
107
- full_path = URI.parse(env['REQUEST_URI']).request_uri
108
- else
109
- full_path = source_request.fullpath
224
+ # Everything that can fail on a hostile/unreachable request or backend is
225
+ # mapped to a status code here so the proxy never surfaces a raw 500:
226
+ # 400 malformed request URI, 501 unknown method, 502 backend failure.
227
+ begin
228
+ # Initialize request
229
+ full_path = if source_request.fullpath == ""
230
+ URI.parse(env["REQUEST_URI"]).request_uri
231
+ else
232
+ source_request.fullpath
233
+ end
234
+
235
+ request_class = net_http_request_class(source_request.request_method)
236
+ return [501, {}, []] if request_class.nil?
237
+
238
+ target_request = request_class.new(full_path)
239
+
240
+ # Setup headers
241
+ request_headers = self.class.extract_http_request_headers(source_request.env)
242
+ if @strip_credentials
243
+ request_headers.delete("Cookie")
244
+ request_headers.delete("Authorization")
245
+ end
246
+ if @replace_x_forwarded_for
247
+ if (remote_addr = env["REMOTE_ADDR"])
248
+ request_headers["X-Forwarded-For"] = remote_addr
249
+ else
250
+ request_headers.delete("X-Forwarded-For")
251
+ end
252
+ end
253
+ target_request.initialize_http_header(request_headers)
254
+
255
+ # Forward the backend response verbatim: don't let Net::HTTP transparently
256
+ # gzip-decode it, which would leave the forwarded Content-Length describing
257
+ # the compressed size (a body/Content-Length desync). The client decodes
258
+ # its own content-encoding.
259
+ target_request.instance_variable_set(:@decode_content, false) if target_request.instance_variable_defined?(:@decode_content)
260
+
261
+ # Rack supplies decoded input. Generate framing for this hop instead
262
+ # of forwarding the client's Transfer-Encoding or guessing a zero size.
263
+ if target_request.request_body_permitted? && source_request.body
264
+ input = source_request.body
265
+ input.rewind if input.respond_to?(:rewind)
266
+ if (length = source_request.content_length)
267
+ raise InvalidRequest, "invalid Content-Length" unless /\A[0-9]+\z/.match?(length)
268
+
269
+ target_request.content_length = length.to_i
270
+ target_request.body_stream = RequestBodyStream.new(input, length.to_i)
271
+ else
272
+ target_request.delete("Content-Length")
273
+ target_request["Transfer-Encoding"] = "chunked"
274
+ target_request.body_stream = input
275
+ end
276
+ target_request.content_type = source_request.content_type if source_request.content_type
277
+ end
278
+
279
+ # Use basic auth if we have to
280
+ target_request.basic_auth(@username, @password) if @username && @password
281
+
282
+ backend = env.delete("rack.backend") || @backend
283
+ # env["rack.backend"] is documented as a URI, but accept a URI-parseable
284
+ # string too (symmetric with the :backend option) rather than crash on
285
+ # #scheme later; a malformed string surfaces as 400 via the rescue.
286
+ backend = URI(backend) if backend.is_a?(String)
287
+ if backend.nil?
288
+ # Dynamic mode: the destination would come from the client-controlled
289
+ # Host header. Refused unless the deployment opted in (SSRF guard).
290
+ unless @allow_dynamic_backend
291
+ if @logger.respond_to?(:<<)
292
+ @logger << "rack-proxy: refusing Host-derived backend #{source_request.host.inspect} " \
293
+ "(no :backend configured; pass allow_dynamic_backend: true to opt in)\n"
294
+ end
295
+ return [502, {}, []]
296
+ end
297
+ backend = source_request
298
+ end
299
+ unless backend_allowed?(backend)
300
+ if @logger.respond_to?(:<<)
301
+ @logger << "rack-proxy: backend #{backend.host.inspect} refused by backend_allowed?\n"
302
+ end
303
+ return [502, {}, []]
304
+ end
305
+
306
+ use_ssl = backend.scheme == "https" || @cert
307
+ read_timeout = env.delete("http.read_timeout") || @read_timeout
308
+
309
+ if @streaming
310
+ # streaming response (the actual network communication is deferred, a.k.a. streamed)
311
+ target_response = HttpStreamingResponse.new(target_request, backend.host, backend.port) do |http|
312
+ configure_backend_connection(http, use_ssl: use_ssl, read_timeout: read_timeout)
313
+ end
314
+ target_response.logger = @logger if @logger
315
+ code = target_response.code
316
+ headers = prepare_response_headers(target_response.headers, code, target_request)
317
+ if response_body_permitted?(target_request, code)
318
+ target_response.max_response_length = @max_response_length
319
+ body = target_response
320
+ else
321
+ target_response.close
322
+ body = []
323
+ end
324
+ else
325
+ http = Net::HTTP.new(backend.host, backend.port)
326
+ configure_backend_connection(http, use_ssl: use_ssl, read_timeout: read_timeout)
327
+
328
+ http.start do
329
+ http.request(target_request) do |response|
330
+ code = response.code.to_i
331
+ headers = prepare_response_headers(response.to_hash, code, target_request)
332
+ body = []
333
+ if response_body_permitted?(target_request, code)
334
+ buffered = +"".b
335
+ response.read_body do |chunk|
336
+ check_response_length!(buffered.bytesize + chunk.bytesize)
337
+ buffered << chunk
338
+ end
339
+ if response.content_length && buffered.bytesize != response.content_length
340
+ raise EOFError, "backend response is shorter than Content-Length"
341
+ end
342
+ body << buffered unless buffered.empty?
343
+ end
344
+ end
345
+ end
346
+ end
347
+ rescue URI::InvalidURIError, InvalidRequest
348
+ target_response&.close
349
+ return [400, {}, []]
350
+ rescue *BACKEND_ERRORS, HttpStreamingResponse::ResponseTooLarge => e
351
+ target_response&.close
352
+ @logger << "rack-proxy: backend request failed: #{e.class}: #{e.message}\n" if @logger.respond_to?(:<<)
353
+ return [502, {}, []]
110
354
  end
111
355
 
112
- target_request = Net::HTTP.const_get(source_request.request_method.capitalize, false).new(full_path)
356
+ [code, headers, body]
357
+ end
358
+
359
+ private
113
360
 
114
- # Setup headers
115
- target_request.initialize_http_header(self.class.extract_http_request_headers(source_request.env))
361
+ def response_body_permitted?(request, code)
362
+ request.response_body_permitted? && !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY[code] && code != 205
363
+ end
116
364
 
117
- # Setup body
118
- if target_request.request_body_permitted? && source_request.body
119
- target_request.body_stream = source_request.body
120
- target_request.content_length = source_request.content_length.to_i
121
- target_request.content_type = source_request.content_type if source_request.content_type
122
- target_request.body_stream.rewind
365
+ def check_response_length!(length)
366
+ if @max_response_length && length && length > @max_response_length
367
+ raise HttpStreamingResponse::ResponseTooLarge, "backend response exceeded max_response_length=#{@max_response_length}"
123
368
  end
369
+ end
124
370
 
125
- # Use basic auth if we have to
126
- target_request.basic_auth(@username, @password) if @username && @password
127
-
128
- backend = env.delete('rack.backend') || @backend || source_request
129
- use_ssl = backend.scheme == "https" || @cert
130
- read_timeout = env.delete('http.read_timeout') || @read_timeout
131
-
132
- # Create the response
133
- if @streaming
134
- # streaming response (the actual network communication is deferred, a.k.a. streamed)
135
- target_response = HttpStreamingResponse.new(target_request, backend.host, backend.port)
136
- target_response.use_ssl = use_ssl
137
- target_response.read_timeout = read_timeout
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
142
- else
143
- http = Net::HTTP.new(backend.host, backend.port)
144
- http.use_ssl = use_ssl if use_ssl
145
- http.read_timeout = read_timeout
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
150
-
151
- target_response = http.start do
152
- http.request(target_request)
371
+ # Validate framing before dropping hop-by-hop fields or reading a body.
372
+ # Net::HTTP dechunks responses but otherwise preserves their headers.
373
+ def prepare_response_headers(raw_headers, code, request)
374
+ # Rack 2 HeaderHash#each joins arrays with newlines. Read values directly
375
+ # so repeated framing/Connection fields stay distinct during validation.
376
+ headers = self.class.build_header_hash(raw_headers.keys.map { |key| [key, raw_headers[key]] })
377
+ transfer_encoding = headers["Transfer-Encoding"]
378
+ content_length = headers["Content-Length"]
379
+ if transfer_encoding
380
+ if content_length || Array(transfer_encoding).join(",").strip.downcase != "chunked"
381
+ raise Net::HTTPBadResponse, "ambiguous or unsupported backend transfer encoding"
153
382
  end
154
383
  end
384
+ if content_length
385
+ lengths = Array(content_length).flat_map { |value| value.split(",", -1).map(&:strip) }
386
+ unless lengths.all? { |value| /\A[0-9]+\z/.match?(value) } && lengths.map(&:to_i).uniq.length == 1
387
+ raise Net::HTTPBadResponse, "invalid backend Content-Length"
388
+ end
389
+ headers["Content-Length"] = lengths.first.to_i.to_s
390
+ check_response_length!(lengths.first.to_i) if response_body_permitted?(request, code)
391
+ end
392
+
393
+ connection_named = Array(headers["Connection"]).join(",").downcase.split(",").map(&:strip)
394
+ headers.keys.each do |key|
395
+ headers.delete(key) if HOP_BY_HOP_HEADERS[key.downcase] || connection_named.include?(key.downcase)
396
+ end
397
+ if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY[code]
398
+ headers.delete("Content-Length")
399
+ headers.delete("Content-Type")
400
+ end
401
+ self.class.normalize_headers(headers)
402
+ end
155
403
 
156
- code = target_response.code
157
- headers = self.class.normalize_headers(target_response.respond_to?(:headers) ? target_response.headers : target_response.to_hash)
158
- body = target_response.body || [""]
159
- body = [body] unless body.respond_to?(:each)
404
+ # Resolve the Net::HTTP request class for an HTTP method, or nil if there is
405
+ # no matching Net::HTTP::<Verb> (unknown/unsupported method -> 501).
406
+ def net_http_request_class(method)
407
+ Net::HTTP.const_get(method.capitalize, false)
408
+ rescue NameError
409
+ nil
410
+ end
160
411
 
161
- # According to https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-7.1.3.1Acc
162
- # should remove hop-by-hop header fields
163
- headers.reject! { |k| HOP_BY_HOP_HEADERS[k.downcase] }
412
+ # Single source of truth for TLS/timeout setup, applied to the (real
413
+ # Net::HTTP) connection on both the streaming and non-streaming paths so a
414
+ # TLS option — notably the VERIFY_PEER default — can never land on only one.
415
+ def configure_backend_connection(conn, use_ssl:, read_timeout:)
416
+ conn.use_ssl = use_ssl
417
+ # Request input need not be rewindable. A transport retry could replay an
418
+ # operation with an empty or partial body, so neither path retries it.
419
+ conn.max_retries = 0
420
+ conn.read_timeout = read_timeout
421
+ conn.open_timeout = @open_timeout if @open_timeout
422
+ conn.write_timeout = @write_timeout if @write_timeout
164
423
 
165
- [code, headers, body]
424
+ if use_ssl
425
+ conn.verify_mode = @verify_mode || OpenSSL::SSL::VERIFY_PEER
426
+ conn.ca_file = @ca_file if @ca_file
427
+ conn.cert_store = @cert_store if @cert_store
428
+ conn.min_version = @min_version if @min_version
429
+ conn.max_version = @max_version if @max_version
430
+ conn.ssl_version = @ssl_version if @ssl_version # deprecated; prefer min/max_version
431
+ conn.cert = @cert if @cert
432
+ conn.key = @key if @key
433
+ end
434
+
435
+ conn.set_debug_output(@logger) if @logger
436
+ conn
166
437
  end
167
438
  end
168
439
  end
data/lib/rack-proxy.rb CHANGED
@@ -1 +1,3 @@
1
- require "rack/proxy"
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/proxy"
data/rack-proxy.gemspec CHANGED
@@ -1,25 +1,30 @@
1
- # -*- encoding: utf-8 -*-
2
1
  $:.push File.expand_path("../lib", __FILE__)
3
- require "rack-proxy"
2
+ require "rack/proxy/version"
4
3
 
5
4
  Gem::Specification.new do |s|
6
- s.name = "rack-proxy"
7
- s.version = Rack::Proxy::VERSION
8
- s.platform = Gem::Platform::RUBY
9
- s.license = 'MIT'
10
- s.authors = ["Jacek Becela"]
11
- s.email = ["jacek.becela@gmail.com"]
12
- s.homepage = "https://github.com/ncr/rack-proxy"
13
- s.summary = %q{A request/response rewriting HTTP proxy. A Rack app.}
14
- s.description = %q{A Rack app that provides request/response rewriting proxy capabilities with streaming.}
15
- s.required_ruby_version = '>= 2.6'
5
+ s.name = "rack-proxy"
6
+ s.version = Rack::Proxy::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.license = "MIT"
9
+ s.authors = ["Jacek Becela"]
10
+ s.email = ["jacek.becela@gmail.com"]
11
+ s.homepage = "https://github.com/ncr/rack-proxy"
12
+ s.summary = "A request/response rewriting HTTP proxy. A Rack app."
13
+ s.description = "A Rack app that provides request/response rewriting proxy capabilities with streaming."
14
+ s.required_ruby_version = ">= 3.0"
16
15
 
17
- s.files = `git ls-files`.split("\n")
18
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.metadata = {
17
+ "source_code_uri" => "https://github.com/ncr/rack-proxy",
18
+ "changelog_uri" => "https://github.com/ncr/rack-proxy/blob/master/CHANGELOG.md",
19
+ "bug_tracker_uri" => "https://github.com/ncr/rack-proxy/issues",
20
+ "rubygems_mfa_required" => "true"
21
+ }
22
+
23
+ s.files = Dir["lib/**/*.rb"] + %w[README.md LICENSE CHANGELOG.md SECURITY.md rack-proxy.gemspec]
20
24
  s.require_paths = ["lib"]
21
25
 
22
- s.add_dependency("rack")
26
+ s.add_dependency("rack", ">= 2.0", "< 4")
23
27
  s.add_development_dependency("rack-test")
24
28
  s.add_development_dependency("test-unit")
29
+ s.add_development_dependency("webrick")
25
30
  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.7
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jacek Becela
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-01 00:00:00.000000000 Z
11
+ date: 2026-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -16,14 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '2.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '0'
29
+ version: '2.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: rack-test
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +58,20 @@ dependencies:
52
58
  - - ">="
53
59
  - !ruby/object:Gem::Version
54
60
  version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: webrick
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
55
75
  description: A Rack app that provides request/response rewriting proxy capabilities
56
76
  with streaming.
57
77
  email:
@@ -60,31 +80,23 @@ executables: []
60
80
  extensions: []
61
81
  extra_rdoc_files: []
62
82
  files:
63
- - ".github/FUNDING.yml"
64
- - ".gitignore"
65
- - ".travis.yml"
66
- - Gemfile
67
- - Gemfile.lock
83
+ - CHANGELOG.md
68
84
  - LICENSE
69
85
  - README.md
70
- - Rakefile
71
- - lib/net_http_hacked.rb
86
+ - SECURITY.md
72
87
  - lib/rack-proxy.rb
73
88
  - lib/rack/http_streaming_response.rb
74
89
  - lib/rack/proxy.rb
75
- - lib/rack_proxy_examples/example_service_proxy.rb
76
- - lib/rack_proxy_examples/forward_host.rb
77
- - lib/rack_proxy_examples/rack_php_proxy.rb
78
- - lib/rack_proxy_examples/trusting_proxy.rb
90
+ - lib/rack/proxy/version.rb
79
91
  - rack-proxy.gemspec
80
- - test/http_streaming_response_test.rb
81
- - test/net_http_hacked_test.rb
82
- - test/rack_proxy_test.rb
83
- - test/test_helper.rb
84
92
  homepage: https://github.com/ncr/rack-proxy
85
93
  licenses:
86
94
  - MIT
87
- metadata: {}
95
+ metadata:
96
+ source_code_uri: https://github.com/ncr/rack-proxy
97
+ changelog_uri: https://github.com/ncr/rack-proxy/blob/master/CHANGELOG.md
98
+ bug_tracker_uri: https://github.com/ncr/rack-proxy/issues
99
+ rubygems_mfa_required: 'true'
88
100
  post_install_message:
89
101
  rdoc_options: []
90
102
  require_paths:
@@ -93,19 +105,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
93
105
  requirements:
94
106
  - - ">="
95
107
  - !ruby/object:Gem::Version
96
- version: '2.6'
108
+ version: '3.0'
97
109
  required_rubygems_version: !ruby/object:Gem::Requirement
98
110
  requirements:
99
111
  - - ">="
100
112
  - !ruby/object:Gem::Version
101
113
  version: '0'
102
114
  requirements: []
103
- rubygems_version: 3.2.3
115
+ rubygems_version: 3.5.22
104
116
  signing_key:
105
117
  specification_version: 4
106
118
  summary: A request/response rewriting HTTP proxy. A Rack app.
107
- test_files:
108
- - test/http_streaming_response_test.rb
109
- - test/net_http_hacked_test.rb
110
- - test/rack_proxy_test.rb
111
- - test/test_helper.rb
119
+ test_files: []
data/.github/FUNDING.yml DELETED
@@ -1,3 +0,0 @@
1
- # These are supported funding model platforms
2
-
3
- github: [ncr]