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 +4 -4
- data/README.md +9 -5
- data/lib/nonnative/fault_injection_proxy.rb +35 -5
- data/lib/nonnative/http_proxy_server.rb +53 -8
- data/lib/nonnative/pool.rb +1 -1
- data/lib/nonnative/timeout.rb +4 -1
- data/lib/nonnative/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6969c5c21e416e67752d3421af108506fe44730230537c9b426529e28dad7934
|
|
4
|
+
data.tar.gz: 148ab5d39ef4f367501cca5786de14bc50be175616048a19a792671eee27fc1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
|
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
|
data/lib/nonnative/pool.rb
CHANGED
|
@@ -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
|
|
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
|
|
data/lib/nonnative/timeout.rb
CHANGED
|
@@ -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
|
data/lib/nonnative/version.rb
CHANGED