Class: Hyperion::AdminMiddleware

Inherits:
Object
  • Object
show all
Defined in:
lib/hyperion/admin_middleware.rb

Overview

Rack middleware that exposes administrative endpoints on the same listener as the application. Disabled by default — only mounted when ‘admin_token` is configured. Currently provides:

POST /-/quit  →  triggers graceful master drain (SIGTERM to ppid)

Auth: the request must include ‘X-Hyperion-Admin-Token: <token>`. Mismatch → 401. Path/method mismatch → falls through to the app (so the app can still own /-/anything if Hyperion’s admin is off). When the token is unset, the constructor refuses to wrap — callers must skip mounting this middleware at all.

SECURITY: the bearer token is defense-in-depth, not a substitute for network isolation. Operators MUST keep the listener on a private network or behind TLS + an authenticating reverse proxy. Anyone who can reach the listener AND knows the token can drain the server.

Constant Summary collapse

PATH =
'/-/quit'

Instance Method Summary collapse

Constructor Details

#initialize(app, token:, signal_target: nil) ⇒ AdminMiddleware

Returns a new instance of AdminMiddleware.

Raises:

  • (ArgumentError)


25
26
27
28
29
30
31
32
33
# File 'lib/hyperion/admin_middleware.rb', line 25

def initialize(app, token:, signal_target: nil)
  raise ArgumentError, 'admin_token must be a non-empty String' if token.nil? || token.to_s.empty?

  @app           = app
  @token         = token.to_s
  # Override hook for tests. Defaults to ppid in worker context, pid
  # for single-worker context (caller decides).
  @signal_target = signal_target
end

Instance Method Details

#call(env) ⇒ Object



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/hyperion/admin_middleware.rb', line 35

def call(env)
  return @app.call(env) unless admin_request?(env)

  provided = env['HTTP_X_HYPERION_ADMIN_TOKEN'].to_s
  # Constant-time comparison. Rack::Utils.secure_compare requires same
  # length, so prefix-pad first to avoid a length-leak side channel.
  unless secure_match?(provided)
    return [401, { 'content-type' => 'application/json' },
            [%({"error":"unauthorized"}\n)]]
  end

  target = resolve_signal_target
  Hyperion.logger.info { { message: 'admin drain requested', remote_addr: env['REMOTE_ADDR'], target_pid: target } }
  begin
    Process.kill('TERM', target)
  rescue StandardError => e
    Hyperion.logger.warn { { message: 'admin drain signal failed', error: e.message } }
    return [500, { 'content-type' => 'application/json' }, [%({"error":"signal_failed"}\n)]]
  end

  [202, { 'content-type' => 'application/json' }, [%({"status":"draining"}\n)]]
end