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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +36 -6
  3. data/.rubocop.yml +8 -2
  4. data/AGENTS.md +248 -0
  5. data/CHANGELOG.md +141 -0
  6. data/Gemfile.lock +69 -58
  7. data/README.md +51 -32
  8. data/lib/nonnative/close_all_socket_pair.rb +14 -0
  9. data/lib/nonnative/configuration.rb +67 -0
  10. data/lib/nonnative/configuration_process.rb +14 -0
  11. data/lib/nonnative/configuration_proxy.rb +28 -0
  12. data/lib/nonnative/configuration_runner.rb +44 -0
  13. data/lib/nonnative/configuration_server.rb +12 -0
  14. data/lib/nonnative/configuration_service.rb +9 -0
  15. data/lib/nonnative/cucumber.rb +4 -12
  16. data/lib/nonnative/delay_socket_pair.rb +15 -0
  17. data/lib/nonnative/error.rb +7 -0
  18. data/lib/nonnative/fault_injection_proxy.rb +63 -0
  19. data/lib/nonnative/go_command.rb +34 -0
  20. data/lib/nonnative/grpc_server.rb +30 -0
  21. data/lib/nonnative/header.rb +44 -0
  22. data/lib/nonnative/http_client.rb +45 -0
  23. data/lib/nonnative/http_proxy_server.rb +62 -1
  24. data/lib/nonnative/http_server.rb +40 -0
  25. data/lib/nonnative/invalid_data_socket_pair.rb +15 -0
  26. data/lib/nonnative/no_proxy.rb +35 -0
  27. data/lib/nonnative/not_found_error.rb +7 -0
  28. data/lib/nonnative/observability.rb +44 -0
  29. data/lib/nonnative/pool.rb +50 -0
  30. data/lib/nonnative/port.rb +26 -0
  31. data/lib/nonnative/process.rb +29 -0
  32. data/lib/nonnative/proxy.rb +24 -0
  33. data/lib/nonnative/proxy_factory.rb +16 -0
  34. data/lib/nonnative/runner.rb +30 -0
  35. data/lib/nonnative/server.rb +28 -0
  36. data/lib/nonnative/service.rb +16 -0
  37. data/lib/nonnative/socket_pair.rb +46 -0
  38. data/lib/nonnative/socket_pair_factory.rb +21 -0
  39. data/lib/nonnative/start_error.rb +5 -0
  40. data/lib/nonnative/stop_error.rb +5 -0
  41. data/lib/nonnative/timeout.rb +20 -0
  42. data/lib/nonnative/version.rb +4 -1
  43. data/lib/nonnative.rb +128 -0
  44. 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
@@ -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 do
53
- @response = Nonnative.observability.health(opts)
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 do
108
- @response = Nonnative.observability.health(opts)
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
 
@@ -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
@@ -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?
@@ -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
- # Adapted from https://gist.github.com/RaVbaker/d9ead3c92b915f997dab25c7f0c0ab65
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
 
@@ -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