Class: Otto::CaddyTLS::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/otto/caddy_tls/server.rb

Overview

Registers the permission route and (by default) the localhost guard, and wraps the app-supplied decision block with fail-closed semantics.

Mirrors +Otto::MCP::Server+: a small per-integration object that owns its route and middleware registration and is referenced by its handler.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(otto) ⇒ Server

Returns a new instance of Server.

Parameters:

  • otto (Otto)

    the owning Otto instance



24
25
26
27
# File 'lib/otto/caddy_tls/server.rb', line 24

def initialize(otto)
  @otto    = otto
  @enabled = false
end

Instance Attribute Details

#endpointString? (readonly)

Returns the registered endpoint path.

Returns:

  • (String, nil)

    the registered endpoint path



21
22
23
# File 'lib/otto/caddy_tls/server.rb', line 21

def endpoint
  @endpoint
end

#ottoOtto (readonly)

Returns the owning Otto instance.

Returns:

  • (Otto)

    the owning Otto instance



18
19
20
# File 'lib/otto/caddy_tls/server.rb', line 18

def otto
  @otto
end

Instance Method Details

#enable!(endpoint:, localhost_only:, permission:) ⇒ void

This method returns an undefined value.

Enable the integration. Idempotent: a second call is ignored so the route and guard are never duplicated.

Parameters:

  • endpoint (String)

    path to serve (registered programmatically)

  • localhost_only (Boolean)

    install the loopback guard (default true)

  • permission (#call)

    block receiving the domain, returning truthy to allow



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/otto/caddy_tls/server.rb', line 41

def enable!(endpoint:, localhost_only:, permission:)
  return if @enabled

  # Normalize once so the guard's endpoint, the router's literal-route
  # key, and @endpoint all agree. Without this, a configured trailing
  # slash (or percent-encoding) registers a literal route the router can
  # never match — requests are normalized before lookup — while the guard
  # still targets it, so Caddy would get a denial instead of a decision.
  endpoint = Otto::Utils.normalize_path(endpoint)

  @endpoint       = endpoint
  @permission     = permission
  @localhost_only = localhost_only
  @enabled        = true

  register_route(endpoint)

  if localhost_only
    # SECURITY: appended (via #use) so it is OUTERMOST in the stack and
    # runs BEFORE IPPrivacyMiddleware — the guard must see the raw socket
    # peer, not the forwarded-header-resolved client IP. See LocalhostGuard.
    @otto.use(Otto::CaddyTLS::LocalhostGuard, endpoint)
  else
    # Explicit opt-out: the endpoint then has no built-in access control,
    # so it must be isolated at the network layer. Surface that at setup.
    Otto.structured_log(:warn,
      '[CaddyTLS] localhost guard disabled (localhost_only: false); endpoint is ' \
      'reachable by any client that can reach this app — ensure network-level isolation',
      endpoint: endpoint)
  end

  # structured_log self-skips :debug unless Otto.debug is set.
  Otto.structured_log(:debug, '[CaddyTLS] enabled',
    endpoint: endpoint, localhost_only: localhost_only)
end

#enabled?Boolean

Returns:

  • (Boolean)


30
31
32
# File 'lib/otto/caddy_tls/server.rb', line 30

def enabled?
  @enabled
end

#permit?(domain) ⇒ Boolean

Fail-closed decision wrapper. Any exception, +nil+, or +false+ from the app block denies — a broken decision must never authorize a cert.

Parameters:

  • domain (String)

    the domain Caddy is asking about

Returns:

  • (Boolean)

    true to allow (200), false to deny (403)



82
83
84
85
86
87
88
# File 'lib/otto/caddy_tls/server.rb', line 82

def permit?(domain)
  !!@permission.call(domain)
rescue StandardError => e
  Otto.structured_log(:error, '[CaddyTLS] permission callback raised; denying',
    domain: domain, error: e.message, error_class: e.class.name)
  false
end