nonnative 3.34.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: '028726d6f2465ec50e55504560c08adf2a1d4cb1cc487687f4308e03e50ebcaa'
4
- data.tar.gz: 3309d497da450d41ac5044d446375ed8a937588045dc4a0305240880b59ac03d
3
+ metadata.gz: 6969c5c21e416e67752d3421af108506fe44730230537c9b426529e28dad7934
4
+ data.tar.gz: 148ab5d39ef4f367501cca5786de14bc50be175616048a19a792671eee27fc1a
5
5
  SHA512:
6
- metadata.gz: 6213f4f042a0dab8767bb29021f80cd4d483b66a805165b746ba28c658532747778c51cf7c6720471d56ab5c2c8412f9694116331171d02b98e722d96eeef815
7
- data.tar.gz: 1f36fe9bac571d834dfe837fb19173b1d9c062a7389e044df8620a940d38aa3bca40864a7e429dacfb20f15c57a36706d2ed352a1de53f5e86d9b2c8736e1407
6
+ metadata.gz: 85218dfe59a108dad82130912aaaa1bb0a79e60017595fd089383aafc005e4b9c9e72c74341cd38efc97ac2f31dbf460967cb2dc4eb2fcb34c565ad663b12e41
7
+ data.tar.gz: 8125a4ef344c00e450f8b03df400cbe2df991f8e2905420673fa2fbd09032c1623f52b540c92cf7ab08c60967b7dfb1d9239c68fcf737341e97c21e1feffa10a
data/README.md CHANGED
@@ -79,6 +79,7 @@ Process/server fields:
79
79
  - `ports`: client-facing ports. These are also used for readiness/shutdown port checks.
80
80
  - `timeout`: max time (seconds) for each readiness/shutdown check. For processes, the same value also
81
81
  bounds optional HTTP/gRPC probes and graceful child exit after the stop signal. Defaults to `1.0`.
82
+ A value of `0` fails immediately; setting it to `nil` programmatically does the same.
82
83
  - `wait`: small sleep (seconds) between lifecycle steps.
83
84
  - `log`: per-runner log file used by process output redirection or server implementations.
84
85
 
@@ -89,7 +90,8 @@ Process-only fields:
89
90
 
90
91
  Service fields:
91
92
  - `port`: client-facing service port.
92
- - `timeout`: max time (seconds) for opt-in service readiness checks. Defaults to `1.0`.
93
+ - `timeout`: max time (seconds) for opt-in service readiness checks. Defaults to `1.0`. A value of
94
+ `0` fails immediately; setting it to `nil` programmatically does the same.
93
95
  - `readiness`: optional list of startup readiness checks. Supported kind is `tcp`, which requires
94
96
  explicit `host` and `port`.
95
97
 
@@ -114,8 +116,8 @@ Nonnative.stop
114
116
 
115
117
  `Nonnative.start` runs ordered tiers: service lifecycle calls and optional readiness checks complete,
116
118
  then server lifecycle and readiness checks complete, then process lifecycle and readiness checks run.
117
- A failed service readiness check prevents later tiers; other collected startup errors trigger rollback
118
- after the attempted tiers finish. `Nonnative.stop` reverses the tiers: processes, servers, then
119
+ A failed service lifecycle call or readiness check prevents later tiers; other collected startup errors
120
+ trigger rollback after the attempted tiers finish. `Nonnative.stop` reverses the tiers: processes, servers, then
119
121
  services. Model dependencies in that direction; a managed server can satisfy a process dependency,
120
122
  but a server cannot wait on a managed process. Startup failures raise `Nonnative::StartError`, and
121
123
  shutdown failures raise `Nonnative::StopError`.
@@ -600,11 +602,13 @@ The system allows you to define an in-process HTTP forward proxy server for exte
600
602
  The upstream scheme defaults to HTTPS on the scheme's default port; pass `scheme:`/`port:` to
601
603
  `Nonnative::HTTPProxyServer.new` to target an `http://` upstream or a non-default port. The proxy
602
604
  forwards the request path and query for `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, and
603
- `OPTIONS`, while removing proxy credentials plus `Host` and `Accept-Encoding` before the upstream
604
- request.
605
+ `OPTIONS`, while removing proxy credentials, `Host`, `Accept-Encoding`, hop-by-hop headers, and
606
+ headers nominated by `Connection` before the upstream request.
605
607
 
606
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.
607
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
+
608
612
  Define your server:
609
613
 
610
614
  ```ruby
@@ -52,6 +52,8 @@ module Nonnative
52
52
  # @see Nonnative::Proxy
53
53
  # @see Nonnative::SocketPairFactory
54
54
  class FaultInjectionProxy < Nonnative::Proxy
55
+ STOP_DRAIN_TIMEOUT = 1
56
+
55
57
  class Connection
56
58
  attr_accessor :pair, :thread
57
59
  attr_reader :socket
@@ -73,6 +75,7 @@ module Nonnative
73
75
  @logger = Logger.new(service.proxy.log)
74
76
  @mutex = Mutex.new
75
77
  @state = :none
78
+ @stopping = false
76
79
 
77
80
  super
78
81
  end
@@ -84,6 +87,7 @@ module Nonnative
84
87
  #
85
88
  # @return [void]
86
89
  def start
90
+ mutex.synchronize { @stopping = false }
87
91
  @tcp_server = ::TCPServer.new(service.host, service.port)
88
92
  @thread = Thread.new { perform_start }
89
93
 
@@ -95,6 +99,9 @@ module Nonnative
95
99
  # @return [void]
96
100
  def stop
97
101
  server = tcp_server
102
+ mark_stopping
103
+ close_connections
104
+ close_queued_connections(server)
98
105
  server&.close
99
106
 
100
107
  listener_thread = thread
@@ -107,8 +114,6 @@ module Nonnative
107
114
  @tcp_server = nil
108
115
  @thread = nil
109
116
 
110
- close_connections
111
-
112
117
  Nonnative.logger.info "stopped with host '#{service.host}' and port '#{service.port}' for proxy 'fault_injection'"
113
118
  end
114
119
 
@@ -225,13 +230,16 @@ module Nonnative
225
230
 
226
231
  private
227
232
 
228
- attr_reader :tcp_server, :thread, :connections, :mutex, :state, :logger
233
+ attr_reader :tcp_server, :thread, :connections, :mutex, :state, :logger, :stopping
229
234
 
230
235
  def perform_start
231
236
  loop do
232
237
  local_socket = tcp_server.accept
233
238
  id = local_socket.object_id
234
- register_connection(id, local_socket)
239
+ unless register_connection(id, local_socket)
240
+ local_socket.close
241
+ next
242
+ end
235
243
  connection_thread = Thread.start(local_socket) do |accepted_socket|
236
244
  accept_connection id, accepted_socket
237
245
  end
@@ -275,6 +283,19 @@ module Nonnative
275
283
  end
276
284
  end
277
285
 
286
+ def close_queued_connections(server)
287
+ return unless server
288
+
289
+ # This connection is queued after every client that connected before shutdown began.
290
+ # When the listener accepts and closes it, those earlier clients have been closed as well.
291
+ barrier = TCPSocket.new(service.host, service.port)
292
+ barrier.wait_readable(STOP_DRAIN_TIMEOUT)
293
+ rescue IOError, SystemCallError
294
+ nil
295
+ ensure
296
+ barrier&.close
297
+ end
298
+
278
299
  def apply_state(state)
279
300
  Nonnative.logger.info "applying state '#{state}' for proxy 'fault_injection'"
280
301
 
@@ -293,7 +314,16 @@ module Nonnative
293
314
  end
294
315
 
295
316
  def register_connection(id, socket)
296
- mutex.synchronize { connections[id] = Connection.new(socket) }
317
+ mutex.synchronize do
318
+ return false if stopping
319
+
320
+ connections[id] = Connection.new(socket)
321
+ true
322
+ end
323
+ end
324
+
325
+ def mark_stopping
326
+ mutex.synchronize { @stopping = true }
297
327
  end
298
328
 
299
329
  def attach_connection_thread(id, thread)
@@ -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
@@ -37,7 +37,7 @@ module Nonnative
37
37
  errors.concat(service_lifecycle(services, :start, :start))
38
38
  service_readiness_errors = check_service_readiness(services)
39
39
  errors.concat(service_readiness_errors)
40
- return errors if service_readiness_errors.any?
40
+ return errors if errors.any?
41
41
 
42
42
  [servers, processes].each { |runners| errors.concat(run_lifecycle_checks(runners, :start, :open?, :start)) }
43
43
 
@@ -15,7 +15,7 @@ module Nonnative
15
15
  # # ok is either the block result or false if the timeout elapsed
16
16
  #
17
17
  class Timeout
18
- # @param time [Numeric] timeout duration in seconds
18
+ # @param time [Numeric, nil] timeout duration in seconds; zero and `nil` values time out immediately
19
19
  def initialize(time)
20
20
  @time = time
21
21
  end
@@ -23,10 +23,13 @@ module Nonnative
23
23
  # Executes the given block with the configured timeout.
24
24
  #
25
25
  # If the timeout elapses, returns `false` instead of raising `Timeout::Error`.
26
+ # Zero and `nil` durations also return `false` without running the block.
26
27
  #
27
28
  # @yield the work to execute under a timeout
28
29
  # @return [Object, false] the block's return value, or `false` if the timeout elapsed
29
30
  def perform(&)
31
+ return false if time.nil? || time.zero?
32
+
30
33
  ::Timeout.timeout(time, &)
31
34
  rescue ::Timeout::Error
32
35
  false
@@ -4,5 +4,5 @@ module Nonnative
4
4
  # The current gem version.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '3.34.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.34.0
4
+ version: 3.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alejandro Falkowski