Class: Rubino::API::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/api/server.rb

Overview

Rack app entry point. Wires the middleware stack + router and runs it under Puma.

Reads RUBINO_API_KEY from the environment when no key is passed explicitly; start! refuses to boot without one so the bearer-auth middleware is never bypassed. The pure Rack app (no Puma) is exposed via .build_app for tests and embedding.

server = Rubino::API::Server.new(port: 4820)
server.start!

Constant Summary collapse

DEFAULT_PORT =
4820
DEFAULT_HOST =

Loopback by default (#69): the server speaks to a shell tool, so a routable bind is opt-in (–host 0.0.0.0 / RUBINO_API_HOST).

"127.0.0.1"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, api_key: nil, router: nil, logger: nil, tls_cert: nil, tls_key: nil) ⇒ Server

Returns a new instance of Server.

Parameters:

  • port (Integer) (defaults to: DEFAULT_PORT)

    TCP port (default 4820, or pass via constructor)

  • host (String) (defaults to: DEFAULT_HOST)

    bind address (default 127.0.0.1)

  • api_key (String, nil) (defaults to: nil)

    bearer token; falls back to ENV

  • tls_cert (String, nil) (defaults to: nil)

    path to a TLS cert PEM; when set (with tls_key) the listener serves HTTPS via ssl_bind instead of plain TCP

  • tls_key (String, nil) (defaults to: nil)

    path to the matching private-key PEM



33
34
35
36
37
38
39
40
41
42
# File 'lib/rubino/api/server.rb', line 33

def initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, api_key: nil, router: nil, logger: nil,
               tls_cert: nil, tls_key: nil)
  @port = port
  @host = host
  @api_key = api_key || ENV.fetch("RUBINO_API_KEY", nil)
  @router = router || Router.new
  @logger = logger || Rubino.logger
  @tls_cert = tls_cert
  @tls_key = tls_key
end

Class Method Details

.bind_url(host:, port:, tls_cert: nil, tls_key: nil) ⇒ #call, String

Composes the Rack middleware stack around the router. Order matters: Observability is outermost (sees every status, including 500s from ErrorHandler), then ErrorHandler, then RateLimit (so /v1/health and /v1/metrics also get a per-IP ceiling before Auth waves them through), then JsonParser, then Auth closest to the router so unauthorized requests never reach operations.

Builds the Puma bind URL. When a TLS cert+key are configured it returns an ssl:// bind so Puma terminates TLS with the self-signed cert; the web client pins that cert (see Rubino::API::TLS). Otherwise it returns a plain tcp:// bind (local dev / fake stay HTTP).

Returns:

  • (#call)

    a Rack-compatible app

  • (String)

    a Puma bind URL (“tcp://…” or “ssl://…”)



102
103
104
105
106
107
# File 'lib/rubino/api/server.rb', line 102

def self.bind_url(host:, port:, tls_cert: nil, tls_key: nil)
  return "tcp://#{host}:#{port}" if tls_cert.nil? || tls_key.nil?

  query = URI.encode_www_form(cert: tls_cert, key: tls_key)
  "ssl://#{host}:#{port}?#{query}"
end

.build_app(router:, api_key:, logger: Rubino.logger) ⇒ Object



109
110
111
112
113
114
115
116
117
118
# File 'lib/rubino/api/server.rb', line 109

def self.build_app(router:, api_key:, logger: Rubino.logger)
  Rack::Builder.new do
    use Middleware::Observability, logger: logger
    use Middleware::ErrorHandler, logger: logger
    use Middleware::RateLimit
    use Middleware::JsonParser
    use Middleware::Auth, api_key: api_key
    run router
  end.to_app
end

.lowlevel_error_handlerProc

A Puma lowlevel_error_handler that mirrors ErrorHandler’s error:{code,message} JSON envelope and never exposes the exception class, message, backtrace, Puma version, or file paths.

Returns:

  • (Proc)

    callable Puma invokes as (error, env=nil, status=nil)



81
82
83
84
85
86
# File 'lib/rubino/api/server.rb', line 81

def self.lowlevel_error_handler
  lambda do |_error, _env = nil, _status = nil|
    body = JSON.generate(error: { code: "bad_request", message: "bad request" })
    [400, { "content-type" => "application/json" }, [body]]
  end
end

Instance Method Details

#start!Object

Boots Puma and blocks. Fails fast if no API key is configured.

Raises:



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/rubino/api/server.rb', line 52

def start!
  if @api_key.nil? || @api_key.empty?
    raise ConfigurationError,
          "RUBINO_API_KEY must be set to start the API server"
  end

  app = self.class.build_app(router: @router, api_key: @api_key, logger: @logger)
  @logger.info(event: "api.server.starting", host: @host, port: @port, tls: tls?)

  bind_url = self.class.bind_url(host: @host, port: @port, tls_cert: @tls_cert, tls_key: @tls_key)
  config = Puma::Configuration.new do |c|
    c.bind(bind_url)
    c.app(app)
    c.quiet
    # Errors raised below the Rack stack (e.g. Puma's HTTP parser rejecting
    # an oversized QUERY_STRING) bypass ErrorHandler and would otherwise
    # render Puma's verbose default page — leaking the Puma version and
    # gem file paths/line numbers (S5-1). Render the same clean envelope
    # with no internals instead.
    c.lowlevel_error_handler(Server.lowlevel_error_handler)
  end
  Puma::Launcher.new(config).run
end

#tls?Boolean

Returns whether this server will serve over TLS.

Returns:

  • (Boolean)

    whether this server will serve over TLS



45
46
47
# File 'lib/rubino/api/server.rb', line 45

def tls?
  !@tls_cert.nil? && !@tls_key.nil?
end