nonnative 1.107.0 → 1.109.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/.circleci/config.yml +36 -6
- data/.rubocop.yml +8 -2
- data/AGENTS.md +248 -0
- data/CHANGELOG.md +141 -0
- data/Gemfile.lock +69 -58
- data/README.md +51 -32
- data/lib/nonnative/close_all_socket_pair.rb +14 -0
- data/lib/nonnative/configuration.rb +67 -0
- data/lib/nonnative/configuration_process.rb +14 -0
- data/lib/nonnative/configuration_proxy.rb +28 -0
- data/lib/nonnative/configuration_runner.rb +44 -0
- data/lib/nonnative/configuration_server.rb +12 -0
- data/lib/nonnative/configuration_service.rb +9 -0
- data/lib/nonnative/cucumber.rb +4 -12
- data/lib/nonnative/delay_socket_pair.rb +15 -0
- data/lib/nonnative/error.rb +7 -0
- data/lib/nonnative/fault_injection_proxy.rb +63 -0
- data/lib/nonnative/go_command.rb +34 -0
- data/lib/nonnative/grpc_server.rb +30 -0
- data/lib/nonnative/header.rb +44 -0
- data/lib/nonnative/http_client.rb +45 -0
- data/lib/nonnative/http_proxy_server.rb +62 -1
- data/lib/nonnative/http_server.rb +40 -0
- data/lib/nonnative/invalid_data_socket_pair.rb +15 -0
- data/lib/nonnative/no_proxy.rb +35 -0
- data/lib/nonnative/not_found_error.rb +7 -0
- data/lib/nonnative/observability.rb +44 -0
- data/lib/nonnative/pool.rb +50 -0
- data/lib/nonnative/port.rb +26 -0
- data/lib/nonnative/process.rb +29 -0
- data/lib/nonnative/proxy.rb +24 -0
- data/lib/nonnative/proxy_factory.rb +16 -0
- data/lib/nonnative/runner.rb +30 -0
- data/lib/nonnative/server.rb +28 -0
- data/lib/nonnative/service.rb +16 -0
- data/lib/nonnative/socket_pair.rb +46 -0
- data/lib/nonnative/socket_pair_factory.rb +21 -0
- data/lib/nonnative/start_error.rb +5 -0
- data/lib/nonnative/stop_error.rb +5 -0
- data/lib/nonnative/timeout.rb +20 -0
- data/lib/nonnative/version.rb +4 -1
- data/lib/nonnative.rb +128 -0
- metadata +3 -2
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Service-specific configuration.
|
|
5
|
+
#
|
|
6
|
+
# A "service" is proxy-only: it does not start a Ruby thread or OS process. It exists so Nonnative can
|
|
7
|
+
# start and control a proxy in front of an external dependency.
|
|
8
|
+
#
|
|
9
|
+
# Instances are usually created through {Nonnative::Configuration#service}.
|
|
10
|
+
#
|
|
11
|
+
# @see Nonnative::Configuration
|
|
12
|
+
# @see Nonnative::Service
|
|
4
13
|
class ConfigurationService < ConfigurationRunner
|
|
5
14
|
end
|
|
6
15
|
end
|
data/lib/nonnative/cucumber.rb
CHANGED
|
@@ -49,12 +49,8 @@ Given('I should see {string} as unhealthy') do |service|
|
|
|
49
49
|
read_timeout: 10, open_timeout: 10
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
wait_for
|
|
53
|
-
|
|
54
|
-
@response.code
|
|
55
|
-
end.to eq(503)
|
|
56
|
-
|
|
57
|
-
expect(@response.body).to include(service)
|
|
52
|
+
wait_for { Nonnative.observability.health(opts).code }.to eq(503)
|
|
53
|
+
wait_for { Nonnative.observability.health(opts).body }.to include(service)
|
|
58
54
|
end
|
|
59
55
|
|
|
60
56
|
Then('I should reset the proxy for process {string}') do |name|
|
|
@@ -104,10 +100,6 @@ Then('I should see {string} as healthy') do |service|
|
|
|
104
100
|
read_timeout: 10, open_timeout: 10
|
|
105
101
|
}
|
|
106
102
|
|
|
107
|
-
wait_for
|
|
108
|
-
|
|
109
|
-
@response.code
|
|
110
|
-
end.to eq(200)
|
|
111
|
-
|
|
112
|
-
expect(@response.body).to_not include(service)
|
|
103
|
+
wait_for { Nonnative.observability.health(opts).code }.to eq(200)
|
|
104
|
+
wait_for { Nonnative.observability.health(opts).body }.to_not include(service)
|
|
113
105
|
end
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Socket-pair variant used by the fault-injection proxy to simulate slow or stalled connections.
|
|
5
|
+
#
|
|
6
|
+
# When active, reads from the socket are delayed by a configured duration before being forwarded.
|
|
7
|
+
#
|
|
8
|
+
# The delay duration is controlled by `proxy.options[:delay]` and defaults to 2 seconds.
|
|
9
|
+
#
|
|
10
|
+
# This behavior is enabled by calling {Nonnative::FaultInjectionProxy#delay}.
|
|
11
|
+
#
|
|
12
|
+
# @see Nonnative::FaultInjectionProxy
|
|
13
|
+
# @see Nonnative::SocketPairFactory
|
|
14
|
+
# @see Nonnative::SocketPair
|
|
4
15
|
class DelaySocketPair < SocketPair
|
|
16
|
+
# Reads from the socket after sleeping for the configured delay duration.
|
|
17
|
+
#
|
|
18
|
+
# @param socket [IO] the socket to read from
|
|
19
|
+
# @return [String] the bytes read from the socket
|
|
5
20
|
def read(socket)
|
|
6
21
|
Nonnative.logger.info "delaying socket '#{socket.inspect}' for 'delay' pair"
|
|
7
22
|
|
data/lib/nonnative/error.rb
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Base class for all Nonnative errors.
|
|
5
|
+
#
|
|
6
|
+
# Catch this error type if you want to handle any exception raised by this gem.
|
|
7
|
+
#
|
|
8
|
+
# @see Nonnative::StartError
|
|
9
|
+
# @see Nonnative::StopError
|
|
10
|
+
# @see Nonnative::NotFoundError
|
|
4
11
|
class Error < StandardError
|
|
5
12
|
end
|
|
6
13
|
end
|
|
@@ -1,7 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Fault-injection proxy for TCP services.
|
|
5
|
+
#
|
|
6
|
+
# This proxy accepts incoming TCP connections and forwards traffic to the configured upstream
|
|
7
|
+
# (`service.proxy.host` / `service.proxy.port`) via a socket-pair implementation. It can also inject
|
|
8
|
+
# failures to help validate client resilience.
|
|
9
|
+
#
|
|
10
|
+
# This class exposes a small public control surface for tests:
|
|
11
|
+
#
|
|
12
|
+
# - {#close_all}: close connections immediately on accept
|
|
13
|
+
# - {#delay}: delay reads by a configured duration (default: 2 seconds)
|
|
14
|
+
# - {#invalid_data}: corrupt outbound data by shuffling characters
|
|
15
|
+
# - {#reset}: return to healthy pass-through behavior
|
|
16
|
+
#
|
|
17
|
+
# State changes terminate any active connections so new connections observe the new behavior.
|
|
18
|
+
#
|
|
19
|
+
# ## Wiring
|
|
20
|
+
#
|
|
21
|
+
# When enabled, your test/client should typically connect to {#host}:{#port} (the proxy endpoint),
|
|
22
|
+
# and the proxy will connect onward to the underlying service.
|
|
23
|
+
#
|
|
24
|
+
# ## Configuration
|
|
25
|
+
#
|
|
26
|
+
# The proxy is configured via the runner’s `proxy` hash:
|
|
27
|
+
#
|
|
28
|
+
# - `kind`: `"fault_injection"`
|
|
29
|
+
# - `host` / `port`: where the proxy should be reached by clients (exposed via {#host}/{#port})
|
|
30
|
+
# - `log`: file path used by this proxy’s internal logger
|
|
31
|
+
# - `wait`: sleep interval (seconds) applied after state changes
|
|
32
|
+
# - `options`:
|
|
33
|
+
# - `delay`: delay duration in seconds used by {#delay}
|
|
34
|
+
#
|
|
35
|
+
# @see Nonnative::Proxy
|
|
36
|
+
# @see Nonnative::SocketPairFactory
|
|
4
37
|
class FaultInjectionProxy < Nonnative::Proxy
|
|
38
|
+
# @param service [Nonnative::ConfigurationRunner] runner configuration with proxy settings
|
|
5
39
|
def initialize(service)
|
|
6
40
|
@connections = Concurrent::Hash.new
|
|
7
41
|
@logger = Logger.new(service.proxy.log)
|
|
@@ -11,6 +45,12 @@ module Nonnative
|
|
|
11
45
|
super
|
|
12
46
|
end
|
|
13
47
|
|
|
48
|
+
# Starts the proxy accept loop in a background thread.
|
|
49
|
+
#
|
|
50
|
+
# This binds a TCP server on the underlying runner’s `service.host` / `service.port`.
|
|
51
|
+
# Clients should connect to {#host}:{#port}.
|
|
52
|
+
#
|
|
53
|
+
# @return [void]
|
|
14
54
|
def start
|
|
15
55
|
@tcp_server = ::TCPServer.new(service.host, service.port)
|
|
16
56
|
@thread = Thread.new { perform_start }
|
|
@@ -18,6 +58,9 @@ module Nonnative
|
|
|
18
58
|
Nonnative.logger.info "started with host '#{service.host}' and port '#{service.port}' for proxy 'fault_injection'"
|
|
19
59
|
end
|
|
20
60
|
|
|
61
|
+
# Stops the proxy and closes its listening socket.
|
|
62
|
+
#
|
|
63
|
+
# @return [void]
|
|
21
64
|
def stop
|
|
22
65
|
thread&.terminate
|
|
23
66
|
tcp_server&.close
|
|
@@ -25,26 +68,46 @@ module Nonnative
|
|
|
25
68
|
Nonnative.logger.info "stopped with host '#{service.host}' and port '#{service.port}' for proxy 'fault_injection'"
|
|
26
69
|
end
|
|
27
70
|
|
|
71
|
+
# Forces new connections to be closed immediately.
|
|
72
|
+
#
|
|
73
|
+
# @return [void]
|
|
28
74
|
def close_all
|
|
29
75
|
apply_state :close_all
|
|
30
76
|
end
|
|
31
77
|
|
|
78
|
+
# Delays reads before forwarding.
|
|
79
|
+
#
|
|
80
|
+
# The delay duration is controlled by `service.proxy.options[:delay]` and defaults to 2 seconds.
|
|
81
|
+
#
|
|
82
|
+
# @return [void]
|
|
32
83
|
def delay
|
|
33
84
|
apply_state :delay
|
|
34
85
|
end
|
|
35
86
|
|
|
87
|
+
# Corrupts forwarded data by shuffling characters.
|
|
88
|
+
#
|
|
89
|
+
# @return [void]
|
|
36
90
|
def invalid_data
|
|
37
91
|
apply_state :invalid_data
|
|
38
92
|
end
|
|
39
93
|
|
|
94
|
+
# Resets the proxy back to healthy pass-through behavior.
|
|
95
|
+
#
|
|
96
|
+
# @return [void]
|
|
40
97
|
def reset
|
|
41
98
|
apply_state :none
|
|
42
99
|
end
|
|
43
100
|
|
|
101
|
+
# Returns the host clients should connect to when using this proxy.
|
|
102
|
+
#
|
|
103
|
+
# @return [String]
|
|
44
104
|
def host
|
|
45
105
|
service.proxy.host
|
|
46
106
|
end
|
|
47
107
|
|
|
108
|
+
# Returns the port clients should connect to when using this proxy.
|
|
109
|
+
#
|
|
110
|
+
# @return [Integer]
|
|
48
111
|
def port
|
|
49
112
|
service.proxy.port
|
|
50
113
|
end
|
data/lib/nonnative/go_command.rb
CHANGED
|
@@ -1,13 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Builds command lines for running a Go test binary with optional profiling/trace/coverage flags.
|
|
5
|
+
#
|
|
6
|
+
# This helper is used by {Nonnative.go_executable} and by YAML configuration when a process has a
|
|
7
|
+
# `go:` section (see {Nonnative::Configuration}).
|
|
8
|
+
#
|
|
9
|
+
# The generated flags use Go's `testing` package flags (e.g. `-test.cpuprofile=...`), so this
|
|
10
|
+
# is intended to run a binary compiled from `go test -c`.
|
|
11
|
+
#
|
|
12
|
+
# ## Tools
|
|
13
|
+
#
|
|
14
|
+
# Tools can be enabled/disabled via the `tools` list. Supported values:
|
|
15
|
+
#
|
|
16
|
+
# - `"prof"`: cpu/mem/block/mutex profiles
|
|
17
|
+
# - `"trace"`: execution trace output
|
|
18
|
+
# - `"cover"`: coverage profile output
|
|
19
|
+
#
|
|
20
|
+
# If `tools` is `nil` or empty, all tools (`prof`, `trace`, `cover`) are enabled.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# cmd = Nonnative::GoCommand.new(%w[prof cover], './svc.test', 'reports')
|
|
24
|
+
# cmd.executable('serve', '--config', 'config.yaml')
|
|
25
|
+
# # => "./svc.test -test.cpuprofile=... -test.coverprofile=... serve --config config.yaml"
|
|
26
|
+
#
|
|
27
|
+
# @see Nonnative.go_executable
|
|
4
28
|
class GoCommand
|
|
29
|
+
# @param tools [Array<String>, nil] tool names to enable (see class docs)
|
|
30
|
+
# @param exec [String] path to the compiled Go test binary
|
|
31
|
+
# @param output [String] output directory for generated files
|
|
5
32
|
def initialize(tools, exec, output)
|
|
6
33
|
@tools = tools.nil? || tools.empty? ? %w[prof trace cover] : tools
|
|
7
34
|
@exec = exec
|
|
8
35
|
@output = output
|
|
9
36
|
end
|
|
10
37
|
|
|
38
|
+
# Returns an executable command string including enabled `-test.*` flags.
|
|
39
|
+
#
|
|
40
|
+
# A short random suffix is appended to output filenames to reduce collisions across runs.
|
|
41
|
+
#
|
|
42
|
+
# @param cmd [String] command/sub-command argument passed to the Go test binary
|
|
43
|
+
# @param params [Array<String>] additional parameters passed after `cmd`
|
|
44
|
+
# @return [String] the full command to execute
|
|
11
45
|
def executable(cmd, *params)
|
|
12
46
|
params = params.join(' ')
|
|
13
47
|
"#{exec} #{flags(cmd).join(' ')} #{cmd} #{params}".strip
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# gRPC server runner implemented using {GRPC::RpcServer}.
|
|
5
|
+
#
|
|
6
|
+
# This is a convenience server implementation for running a gRPC service in-process under
|
|
7
|
+
# Nonnative's server lifecycle. It binds to the configured proxy `host`/`port` and is started/stopped
|
|
8
|
+
# by {Nonnative::Server} via {#perform_start} / {#perform_stop}.
|
|
9
|
+
#
|
|
10
|
+
# Important note about logging: the `grpc` gem uses a global logger. This implementation sets
|
|
11
|
+
# `GRPC.logger` to write to the configured `service.log`, and whichever gRPC server is initialized
|
|
12
|
+
# first "wins" that global logger.
|
|
13
|
+
#
|
|
14
|
+
# @see Nonnative::Server
|
|
4
15
|
class GRPCServer < Nonnative::Server
|
|
16
|
+
# Creates a gRPC server and registers the provided service handler.
|
|
17
|
+
#
|
|
18
|
+
# @param svc [Object] a gRPC service implementation (typically a `...::Service` subclass instance)
|
|
19
|
+
# @param service [Nonnative::ConfigurationServer] server configuration
|
|
5
20
|
def initialize(svc, service)
|
|
6
21
|
@server = GRPC::RpcServer.new
|
|
7
22
|
server.handle(svc)
|
|
@@ -16,21 +31,36 @@ module Nonnative
|
|
|
16
31
|
|
|
17
32
|
protected
|
|
18
33
|
|
|
34
|
+
# Binds the gRPC server and begins serving requests.
|
|
35
|
+
#
|
|
36
|
+
# The server binds to the proxy host/port so that enabling a proxy results in traffic and readiness
|
|
37
|
+
# checks consistently targeting the proxy endpoint.
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
19
40
|
def perform_start
|
|
20
41
|
server.add_http2_port("#{proxy.host}:#{proxy.port}", :this_port_is_insecure)
|
|
21
42
|
server.run
|
|
22
43
|
end
|
|
23
44
|
|
|
45
|
+
# Stops the gRPC server.
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
24
48
|
def perform_stop
|
|
25
49
|
server.stop
|
|
26
50
|
end
|
|
27
51
|
|
|
52
|
+
# Waits until the gRPC server reports it is running, or the configured timeout elapses.
|
|
53
|
+
#
|
|
54
|
+
# @return [Object, false] the last evaluated expression from the timeout block, or `false` on timeout
|
|
28
55
|
def wait_start
|
|
29
56
|
timeout.perform do
|
|
30
57
|
super until server.running?
|
|
31
58
|
end
|
|
32
59
|
end
|
|
33
60
|
|
|
61
|
+
# Waits until the gRPC server reports it has stopped, or the configured timeout elapses.
|
|
62
|
+
#
|
|
63
|
+
# @return [Object, false] the last evaluated expression from the timeout block, or `false` on timeout
|
|
34
64
|
def wait_stop
|
|
35
65
|
timeout.perform do
|
|
36
66
|
super until server.stopped?
|
data/lib/nonnative/header.rb
CHANGED
|
@@ -1,20 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Helper utilities for building request headers for HTTP and gRPC clients.
|
|
5
|
+
#
|
|
6
|
+
# This class returns Ruby hashes suitable for passing into client libraries (for example
|
|
7
|
+
# RestClient for HTTP or gRPC call metadata).
|
|
8
|
+
#
|
|
9
|
+
# @example HTTP user-agent (RestClient style)
|
|
10
|
+
# headers = Nonnative::Header.http_user_agent('my-client/1.0')
|
|
11
|
+
# # => { user_agent: "my-client/1.0" }
|
|
12
|
+
#
|
|
13
|
+
# @example gRPC user-agent (metadata)
|
|
14
|
+
# metadata = Nonnative::Header.grpc_user_agent('my-client/1.0')
|
|
15
|
+
# # => { "grpc.primary_user_agent" => "my-client/1.0" }
|
|
16
|
+
#
|
|
17
|
+
# @example Basic auth header
|
|
18
|
+
# headers = Nonnative::Header.auth_basic('user:pass')
|
|
19
|
+
# # => { authorization: "Basic dXNlcjpwYXNz" }
|
|
20
|
+
#
|
|
21
|
+
# @example Bearer auth header
|
|
22
|
+
# headers = Nonnative::Header.auth_bearer('token')
|
|
23
|
+
# # => { authorization: "Bearer token" }
|
|
24
|
+
#
|
|
25
|
+
# @see https://github.com/rest-client/rest-client RestClient
|
|
26
|
+
# @see https://grpc.io/docs/guides/concepts/ gRPC concepts (metadata)
|
|
4
27
|
class Header
|
|
5
28
|
class << self
|
|
29
|
+
# Builds an HTTP `User-Agent` header hash.
|
|
30
|
+
#
|
|
31
|
+
# This uses the key style commonly used by RestClient (`user_agent:`).
|
|
32
|
+
#
|
|
33
|
+
# @param user_agent [String] user agent value
|
|
34
|
+
# @return [Hash{Symbol=>String}] header hash containing the user agent
|
|
6
35
|
def http_user_agent(user_agent)
|
|
7
36
|
{ user_agent: }
|
|
8
37
|
end
|
|
9
38
|
|
|
39
|
+
# Builds gRPC metadata for setting the primary user agent.
|
|
40
|
+
#
|
|
41
|
+
# @param user_agent [String] user agent value
|
|
42
|
+
# @return [Hash{String=>String}] gRPC metadata hash
|
|
10
43
|
def grpc_user_agent(user_agent)
|
|
11
44
|
{ 'grpc.primary_user_agent' => user_agent }
|
|
12
45
|
end
|
|
13
46
|
|
|
47
|
+
# Builds an HTTP Basic Authorization header.
|
|
48
|
+
#
|
|
49
|
+
# The credentials are base64-encoded using strict encoding. The `credentials` string should
|
|
50
|
+
# typically be in the form `"username:password"`.
|
|
51
|
+
#
|
|
52
|
+
# @param credentials [String] basic auth credentials in `"user:pass"` form
|
|
53
|
+
# @return [Hash{Symbol=>String}] header hash containing the Authorization header
|
|
14
54
|
def auth_basic(credentials)
|
|
15
55
|
{ authorization: "Basic #{Base64.strict_encode64(credentials)}" }
|
|
16
56
|
end
|
|
17
57
|
|
|
58
|
+
# Builds an HTTP Bearer Authorization header.
|
|
59
|
+
#
|
|
60
|
+
# @param token [String] bearer token
|
|
61
|
+
# @return [Hash{Symbol=>String}] header hash containing the Authorization header
|
|
18
62
|
def auth_bearer(token)
|
|
19
63
|
{ authorization: "Bearer #{token}" }
|
|
20
64
|
end
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Minimal RestClient-based HTTP client with consistent exception handling.
|
|
5
|
+
#
|
|
6
|
+
# This class is intended to be subclassed by higher-level clients (for example
|
|
7
|
+
# {Nonnative::Observability}). It provides protected helpers for common HTTP verbs and a retry
|
|
8
|
+
# wrapper.
|
|
9
|
+
#
|
|
10
|
+
# Error handling behavior:
|
|
11
|
+
# - Timeouts and broken connections (see {#initialize}) are re-raised so callers can handle them explicitly.
|
|
12
|
+
# - Other `RestClient::Exception` errors return the underlying `response` object.
|
|
13
|
+
#
|
|
14
|
+
# @see Nonnative::Observability
|
|
4
15
|
class HTTPClient
|
|
16
|
+
# @param host [String] base URL used to build request URLs (e.g. `"http://127.0.0.1:8080"`)
|
|
5
17
|
def initialize(host)
|
|
6
18
|
@host = host
|
|
7
19
|
@exceptions = [
|
|
@@ -12,34 +24,67 @@ module Nonnative
|
|
|
12
24
|
|
|
13
25
|
protected
|
|
14
26
|
|
|
27
|
+
# Executes the given block with retries for the configured network exceptions.
|
|
28
|
+
#
|
|
29
|
+
# @param tries [Integer] number of attempts
|
|
30
|
+
# @param wait [Numeric] base interval between retries (seconds)
|
|
31
|
+
# @yield the work to retry
|
|
32
|
+
# @return [Object] the block result
|
|
15
33
|
def with_retry(tries, wait, &)
|
|
16
34
|
Retriable.retriable(tries: tries, base_interval: wait, on: exceptions, &)
|
|
17
35
|
end
|
|
18
36
|
|
|
37
|
+
# Performs a GET request.
|
|
38
|
+
#
|
|
39
|
+
# @param pathname [String] path relative to `host`
|
|
40
|
+
# @param opts [Hash] RestClient request options (e.g. `headers`, `read_timeout`, `open_timeout`)
|
|
41
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
19
42
|
def get(pathname, opts = {})
|
|
20
43
|
with_exception do
|
|
21
44
|
resource(pathname, opts).get
|
|
22
45
|
end
|
|
23
46
|
end
|
|
24
47
|
|
|
48
|
+
# Performs a POST request.
|
|
49
|
+
#
|
|
50
|
+
# @param pathname [String] path relative to `host`
|
|
51
|
+
# @param payload [Object] request payload
|
|
52
|
+
# @param opts [Hash] RestClient request options
|
|
53
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
25
54
|
def post(pathname, payload, opts = {})
|
|
26
55
|
with_exception do
|
|
27
56
|
resource(pathname, opts).post(payload)
|
|
28
57
|
end
|
|
29
58
|
end
|
|
30
59
|
|
|
60
|
+
# Performs a DELETE request.
|
|
61
|
+
#
|
|
62
|
+
# @param pathname [String] path relative to `host`
|
|
63
|
+
# @param opts [Hash] RestClient request options
|
|
64
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
31
65
|
def delete(pathname, opts = {})
|
|
32
66
|
with_exception do
|
|
33
67
|
resource(pathname, opts).delete
|
|
34
68
|
end
|
|
35
69
|
end
|
|
36
70
|
|
|
71
|
+
# Performs a PUT request.
|
|
72
|
+
#
|
|
73
|
+
# @param pathname [String] path relative to `host`
|
|
74
|
+
# @param payload [Object] request payload
|
|
75
|
+
# @param opts [Hash] RestClient request options
|
|
76
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
37
77
|
def put(pathname, payload, opts = {})
|
|
38
78
|
with_exception do
|
|
39
79
|
resource(pathname, opts).put(payload)
|
|
40
80
|
end
|
|
41
81
|
end
|
|
42
82
|
|
|
83
|
+
# Creates a RestClient resource for a relative path.
|
|
84
|
+
#
|
|
85
|
+
# @param pathname [String] path relative to `host`
|
|
86
|
+
# @param opts [Hash] RestClient request options
|
|
87
|
+
# @return [RestClient::Resource]
|
|
43
88
|
def resource(pathname, opts)
|
|
44
89
|
RestClient::Resource.new(URI.join(host, pathname).to_s, opts)
|
|
45
90
|
end
|
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# Sinatra-based HTTP forward proxy server used as an in-process Nonnative server.
|
|
4
|
+
#
|
|
5
|
+
# The proxy receives inbound HTTP requests and forwards them to an upstream host over HTTPS, returning
|
|
6
|
+
# the upstream response status and body.
|
|
7
|
+
#
|
|
8
|
+
# This file defines two classes:
|
|
9
|
+
#
|
|
10
|
+
# - {Nonnative::HTTPProxy}: a Sinatra application that implements the proxying behavior.
|
|
11
|
+
# - {Nonnative::HTTPProxyServer}: a {Nonnative::HTTPServer} wrapper that runs the proxy app under Puma.
|
|
12
|
+
#
|
|
13
|
+
# Notes:
|
|
14
|
+
# - This code is adapted from https://gist.github.com/RaVbaker/d9ead3c92b915f997dab25c7f0c0ab65
|
|
15
|
+
# - Only a subset of request headers are forwarded; certain headers are removed to avoid conflicts.
|
|
16
|
+
#
|
|
17
|
+
# @see Nonnative::HTTPServer
|
|
18
|
+
# @see Nonnative::Server
|
|
4
19
|
module Nonnative
|
|
20
|
+
# Sinatra application implementing a simple forward proxy.
|
|
21
|
+
#
|
|
22
|
+
# The upstream host is configured via Sinatra settings (see {Nonnative::HTTPProxyServer}).
|
|
23
|
+
#
|
|
24
|
+
# Supported HTTP verbs: GET, POST, PUT, PATCH, DELETE.
|
|
5
25
|
class HTTPProxy < Sinatra::Application
|
|
26
|
+
# Extracts request headers from the Rack environment and normalizes them to standard HTTP names.
|
|
27
|
+
#
|
|
28
|
+
# Certain hop-by-hop or proxy-specific headers are removed.
|
|
29
|
+
#
|
|
30
|
+
# @param request [Sinatra::Request] the incoming request
|
|
31
|
+
# @return [Hash{String=>String}] headers to forward to the upstream
|
|
6
32
|
def retrieve_headers(request)
|
|
7
33
|
headers = request.env.map do |header, value|
|
|
8
34
|
[header[5..].split('_').map(&:capitalize).join('-'), value] if header.start_with?('HTTP_')
|
|
@@ -12,10 +38,21 @@ module Nonnative
|
|
|
12
38
|
headers.except('Host', 'Accept-Encoding', 'Version')
|
|
13
39
|
end
|
|
14
40
|
|
|
41
|
+
# Builds the upstream URL for the given request.
|
|
42
|
+
#
|
|
43
|
+
# @param request [Sinatra::Request] the incoming request
|
|
44
|
+
# @param settings [Sinatra::Base] Sinatra settings, expected to include `host`
|
|
45
|
+
# @return [String] HTTPS URL for the upstream request
|
|
15
46
|
def build_url(request, settings)
|
|
16
47
|
URI::HTTPS.build(host: settings.host, path: request.path_info, query: request.query_string).to_s
|
|
17
48
|
end
|
|
18
49
|
|
|
50
|
+
# Executes the upstream request and returns the response.
|
|
51
|
+
#
|
|
52
|
+
# @param verb [String] HTTP verb name (e.g. `"get"`)
|
|
53
|
+
# @param uri [String] upstream URI
|
|
54
|
+
# @param opts [Hash] RestClient options (e.g. headers)
|
|
55
|
+
# @return [RestClient::Response] response for error statuses, otherwise RestClient return value
|
|
19
56
|
def api_response(verb, uri, opts)
|
|
20
57
|
client = RestClient::Resource.new(uri, opts)
|
|
21
58
|
|
|
@@ -36,7 +73,31 @@ module Nonnative
|
|
|
36
73
|
end
|
|
37
74
|
end
|
|
38
75
|
|
|
76
|
+
# Runs {Nonnative::HTTPProxy} as a Puma-based in-process server under Nonnative.
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# Nonnative.configure do |config|
|
|
80
|
+
# config.server do |s|
|
|
81
|
+
# s.name = 'github-proxy'
|
|
82
|
+
# s.klass = Nonnative::Features::HTTPProxyServer
|
|
83
|
+
# s.timeout = 2
|
|
84
|
+
# s.host = '127.0.0.1'
|
|
85
|
+
# s.port = 4567
|
|
86
|
+
# s.log = 'proxy.log'
|
|
87
|
+
# end
|
|
88
|
+
# end
|
|
89
|
+
#
|
|
90
|
+
# # In your server subclass:
|
|
91
|
+
# # class HTTPProxyServer < Nonnative::HTTPProxyServer
|
|
92
|
+
# # def initialize(service)
|
|
93
|
+
# # super('api.github.com', service)
|
|
94
|
+
# # end
|
|
95
|
+
# # end
|
|
96
|
+
#
|
|
97
|
+
# @see Nonnative::HTTPServer
|
|
39
98
|
class HTTPProxyServer < Nonnative::HTTPServer
|
|
99
|
+
# @param host [String] upstream host to proxy to (HTTPS)
|
|
100
|
+
# @param service [Nonnative::ConfigurationServer] server configuration
|
|
40
101
|
def initialize(host, service)
|
|
41
102
|
app = Sinatra.new(Nonnative::HTTPProxy) do
|
|
42
103
|
configure do
|
|
@@ -1,7 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Puma-based HTTP server runner.
|
|
5
|
+
#
|
|
6
|
+
# This is a convenience server implementation for running a Rack/Sinatra application in-process
|
|
7
|
+
# under Nonnative's server lifecycle. It binds to the configured proxy `host`/`port` (so it works
|
|
8
|
+
# consistently with proxy configuration) and uses Puma for HTTP serving.
|
|
9
|
+
#
|
|
10
|
+
# The server is started and stopped by {Nonnative::Server} via {#perform_start} / {#perform_stop}.
|
|
11
|
+
#
|
|
12
|
+
# @example Running a Sinatra app
|
|
13
|
+
# app = Sinatra.new do
|
|
14
|
+
# get('/hello') { 'Hello World!' }
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Nonnative.configure do |config|
|
|
18
|
+
# config.server do |s|
|
|
19
|
+
# s.name = 'http'
|
|
20
|
+
# s.klass = ->(service) { Nonnative::HTTPServer.new(app, service) }
|
|
21
|
+
# s.timeout = 2
|
|
22
|
+
# s.host = '127.0.0.1'
|
|
23
|
+
# s.port = 4567
|
|
24
|
+
# s.log = 'http.log'
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# Note: In YAML configuration you typically set `class` to a concrete subclass that calls `super(app, service)`.
|
|
29
|
+
#
|
|
30
|
+
# @see Nonnative::Server
|
|
4
31
|
class HTTPServer < Nonnative::Server
|
|
32
|
+
# Creates a Puma server for the given Rack app and runner configuration.
|
|
33
|
+
#
|
|
34
|
+
# @param app [#call] a Rack-compatible application (e.g. Sinatra/Rack app)
|
|
35
|
+
# @param service [Nonnative::ConfigurationServer] server configuration
|
|
5
36
|
def initialize(app, service)
|
|
6
37
|
log = File.open(service.log, 'a')
|
|
7
38
|
options = {
|
|
@@ -15,11 +46,20 @@ module Nonnative
|
|
|
15
46
|
|
|
16
47
|
protected
|
|
17
48
|
|
|
49
|
+
# Binds the Puma server and begins serving.
|
|
50
|
+
#
|
|
51
|
+
# The listener binds to the proxy host/port so that enabling a proxy results in traffic and
|
|
52
|
+
# readiness checks consistently targeting the proxy endpoint.
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
18
55
|
def perform_start
|
|
19
56
|
server.add_tcp_listener proxy.host, proxy.port
|
|
20
57
|
server.run false
|
|
21
58
|
end
|
|
22
59
|
|
|
60
|
+
# Gracefully shuts down the Puma server.
|
|
61
|
+
#
|
|
62
|
+
# @return [void]
|
|
23
63
|
def perform_stop
|
|
24
64
|
server.graceful_shutdown
|
|
25
65
|
end
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Socket-pair variant used by the fault-injection proxy to simulate corrupted/incoherent traffic.
|
|
5
|
+
#
|
|
6
|
+
# When active, data written to the upstream socket is corrupted by shuffling the characters in the
|
|
7
|
+
# payload before forwarding.
|
|
8
|
+
#
|
|
9
|
+
# This behavior is enabled by calling {Nonnative::FaultInjectionProxy#invalid_data}.
|
|
10
|
+
#
|
|
11
|
+
# @see Nonnative::FaultInjectionProxy
|
|
12
|
+
# @see Nonnative::SocketPairFactory
|
|
13
|
+
# @see Nonnative::SocketPair
|
|
4
14
|
class InvalidDataSocketPair < SocketPair
|
|
15
|
+
# Writes corrupted data to the socket by shuffling characters.
|
|
16
|
+
#
|
|
17
|
+
# @param socket [IO] the socket to write to
|
|
18
|
+
# @param data [String] the original payload
|
|
19
|
+
# @return [Integer] number of bytes written
|
|
5
20
|
def write(socket, data)
|
|
6
21
|
Nonnative.logger.info "shuffling socket data '#{socket.inspect}' for 'invalid_data' pair"
|
|
7
22
|
|
data/lib/nonnative/no_proxy.rb
CHANGED
|
@@ -1,23 +1,58 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# No-op proxy implementation.
|
|
5
|
+
#
|
|
6
|
+
# This is the default proxy when `service.proxy.kind` is `"none"` (or an unknown kind is provided).
|
|
7
|
+
# It does not bind/listen or alter traffic; it simply exposes the underlying runner's configured
|
|
8
|
+
# `host` and `port`.
|
|
9
|
+
#
|
|
10
|
+
# Runners can always call `start`, `stop`, and `reset` safely on this proxy.
|
|
11
|
+
#
|
|
12
|
+
# @see Nonnative.proxy
|
|
13
|
+
# @see Nonnative::Proxy
|
|
4
14
|
class NoProxy < Proxy
|
|
15
|
+
# Starts the proxy.
|
|
16
|
+
#
|
|
17
|
+
# This implementation does nothing.
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
5
20
|
def start
|
|
6
21
|
# Do nothing.
|
|
7
22
|
end
|
|
8
23
|
|
|
24
|
+
# Stops the proxy.
|
|
25
|
+
#
|
|
26
|
+
# This implementation does nothing.
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
9
29
|
def stop
|
|
10
30
|
# Do nothing.
|
|
11
31
|
end
|
|
12
32
|
|
|
33
|
+
# Resets the proxy state.
|
|
34
|
+
#
|
|
35
|
+
# This implementation does nothing.
|
|
36
|
+
#
|
|
37
|
+
# @return [void]
|
|
13
38
|
def reset
|
|
14
39
|
# Do nothing.
|
|
15
40
|
end
|
|
16
41
|
|
|
42
|
+
# Returns the host clients should connect to.
|
|
43
|
+
#
|
|
44
|
+
# For {NoProxy}, this is the underlying runner configuration host.
|
|
45
|
+
#
|
|
46
|
+
# @return [String]
|
|
17
47
|
def host
|
|
18
48
|
service.host
|
|
19
49
|
end
|
|
20
50
|
|
|
51
|
+
# Returns the port clients should connect to.
|
|
52
|
+
#
|
|
53
|
+
# For {NoProxy}, this is the underlying runner configuration port.
|
|
54
|
+
#
|
|
55
|
+
# @return [Integer]
|
|
21
56
|
def port
|
|
22
57
|
service.port
|
|
23
58
|
end
|