nonnative 3.35.0 → 3.36.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9a3ed94a590e8273136387f186364d01270536a472d476fc806b8e9d1d06c07
4
- data.tar.gz: 9b1b14eb944a13c83c93bf816d4b485d0f1818080167a6f5339c7bba73d59c31
3
+ metadata.gz: 6969c5c21e416e67752d3421af108506fe44730230537c9b426529e28dad7934
4
+ data.tar.gz: 148ab5d39ef4f367501cca5786de14bc50be175616048a19a792671eee27fc1a
5
5
  SHA512:
6
- metadata.gz: 370d1071e0665f5ea99a06a5dcf4fbe18224aee3d47b9f4279f3fd881c03340869fc11b1b8a58349065bae3d98ffa7be5243a34c26351366b53063676a7cb859
7
- data.tar.gz: 94ad2439c76e863ee3b6a34655454e0610f3a7f8c0b5db23df9af0c0bfc54924ca63410008eccf4b261b6693f601c0495d2bc668bce5a96f9a5a319db44088da
6
+ metadata.gz: 85218dfe59a108dad82130912aaaa1bb0a79e60017595fd089383aafc005e4b9c9e72c74341cd38efc97ac2f31dbf460967cb2dc4eb2fcb34c565ad663b12e41
7
+ data.tar.gz: 8125a4ef344c00e450f8b03df400cbe2df991f8e2905420673fa2fbd09032c1623f52b540c92cf7ab08c60967b7dfb1d9239c68fcf737341e97c21e1feffa10a
data/README.md CHANGED
@@ -602,11 +602,13 @@ The system allows you to define an in-process HTTP forward proxy server for exte
602
602
  The upstream scheme defaults to HTTPS on the scheme's default port; pass `scheme:`/`port:` to
603
603
  `Nonnative::HTTPProxyServer.new` to target an `http://` upstream or a non-default port. The proxy
604
604
  forwards the request path and query for `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, and
605
- `OPTIONS`, while removing proxy credentials plus `Host` and `Accept-Encoding` before the upstream
606
- request.
605
+ `OPTIONS`, while removing proxy credentials, `Host`, `Accept-Encoding`, hop-by-hop headers, and
606
+ headers nominated by `Connection` before the upstream request.
607
607
 
608
608
  The proxy preserves the upstream status, body, and safe end-to-end response headers such as `Content-Type`, `ETag`, and application-specific metadata. It removes hop-by-hop, connection-nominated, proxy-authentication, and framing headers; `Set-Cookie`, `Location`, and `Content-Encoding` are not forwarded.
609
609
 
610
+ When the upstream is unavailable, disconnects unexpectedly, or times out, the proxy returns a clean gateway response (`502` or `504`) instead of exposing an internal exception page.
611
+
610
612
  Define your server:
611
613
 
612
614
  ```ruby
@@ -44,13 +44,35 @@ module Nonnative
44
44
  ].freeze
45
45
 
46
46
  NON_FORWARDABLE_REQUEST_HEADERS = %w[
47
- Host
48
- Accept-Encoding
49
- Version
50
- Proxy-Authenticate
51
- Proxy-Authorization
47
+ host
48
+ accept-encoding
49
+ version
50
+ proxy-authenticate
51
+ proxy-authorization
52
+ proxy-connection
53
+ connection
54
+ keep-alive
55
+ te
56
+ trailer
57
+ transfer-encoding
58
+ upgrade
59
+ ].freeze
60
+
61
+ GATEWAY_TIMEOUT_ERRORS = [
62
+ RestClient::Exceptions::Timeout
52
63
  ].freeze
53
64
 
65
+ GATEWAY_CONNECTION_ERRORS = [
66
+ RestClient::ServerBrokeConnection,
67
+ Errno::ECONNREFUSED,
68
+ Errno::EHOSTUNREACH,
69
+ SocketError
70
+ ].freeze
71
+
72
+ # Match malformed percent escapes and bytes outside RFC 3986's path-safe set. Valid `%HH`
73
+ # escapes remain intact; each match is percent-encoded before building the upstream URI.
74
+ PATH_ESCAPE_PATTERN = %r{%(?![\da-fA-F]{2})|[^A-Za-z0-9\-._~!$&'()*+,;=:@/%]}
75
+
54
76
  # Extracts request headers from the Rack environment and normalizes them to standard HTTP names.
55
77
  #
56
78
  # Certain hop-by-hop or proxy-specific headers are removed.
@@ -64,7 +86,8 @@ module Nonnative
64
86
  result[normalized_request_header_name(header)] = value
65
87
  end
66
88
 
67
- headers.except(*NON_FORWARDABLE_REQUEST_HEADERS)
89
+ connection_headers = request_connection_headers(headers)
90
+ headers.reject { |header, _| non_forwardable_request_header?(header, connection_headers) }
68
91
  end
69
92
 
70
93
  # Builds the upstream URL for the given request.
@@ -75,8 +98,9 @@ module Nonnative
75
98
  def build_url(request, settings)
76
99
  uri_class = settings.scheme == 'http' ? URI::HTTP : URI::HTTPS
77
100
  query = request.query_string
101
+ path = escape_path(request.path_info)
78
102
 
79
- uri_class.build(host: settings.host, port: settings.port, path: request.path_info, query: query.empty? ? nil : query).to_s
103
+ uri_class.build(host: settings.host, port: settings.port, path:, query: query.empty? ? nil : query).to_s
80
104
  end
81
105
 
82
106
  # Executes the upstream request and returns the response.
@@ -86,13 +110,20 @@ module Nonnative
86
110
  # @param headers [Hash] request headers
87
111
  # @param payload [String, nil] request payload
88
112
  # @return [RestClient::Response] response for error statuses, otherwise RestClient return value
113
+ # @raise [Sinatra::Halt] with a gateway status when the upstream is unavailable or times out
89
114
  def api_response(method:, url:, headers:, payload: nil)
90
115
  options = { method:, url:, headers: }
91
116
  options[:payload] = payload unless payload.nil?
92
117
 
93
118
  RestClient::Request.execute(options)
119
+ rescue *GATEWAY_TIMEOUT_ERRORS
120
+ halt 504
121
+ rescue *GATEWAY_CONNECTION_ERRORS
122
+ halt 502
94
123
  rescue RestClient::Exception => e
95
- e.response
124
+ return e.response if e.response
125
+
126
+ halt 502
96
127
  end
97
128
 
98
129
  # Extracts the request payload for verbs that can carry a body.
@@ -149,6 +180,20 @@ module Nonnative
149
180
  header.delete_prefix('HTTP_').split('_').map(&:capitalize).join('-')
150
181
  end
151
182
 
183
+ def escape_path(path)
184
+ URI::DEFAULT_PARSER.escape(path, PATH_ESCAPE_PATTERN)
185
+ end
186
+
187
+ def request_connection_headers(headers)
188
+ headers.fetch('Connection', '').split(',').map { |header| header.strip.downcase }
189
+ end
190
+
191
+ def non_forwardable_request_header?(header, connection_headers)
192
+ normalized_header = header.downcase
193
+
194
+ NON_FORWARDABLE_REQUEST_HEADERS.include?(normalized_header) || connection_headers.include?(normalized_header)
195
+ end
196
+
152
197
  # Registered before `get` so it takes precedence over Sinatra's GET-generated HEAD route,
153
198
  # which would otherwise forward HEAD requests upstream as GET.
154
199
  head(/.*/) do
@@ -4,5 +4,5 @@ module Nonnative
4
4
  # The current gem version.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '3.35.0'
7
+ VERSION = '3.36.0'
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nonnative
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.35.0
4
+ version: 3.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alejandro Falkowski