213 lines
5.0 KiB
Python
213 lines
5.0 KiB
Python
"""
|
|
Shared helpers for NPM ACME authority mutations.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
import os
|
|
|
|
|
|
AUTHORITY_FILE = "/acme_authority/state/acme-authority"
|
|
|
|
HELPER_MARKER = "const acmeAuthorityFile ="
|
|
|
|
AUTHORITY_HELPER = f"""const acmeAuthorityFile = "{AUTHORITY_FILE}";
|
|
|
|
const requireAcmeAuthority = () => {{
|
|
try {{
|
|
fs.accessSync(acmeAuthorityFile, fs.constants.R_OK);
|
|
}} catch {{
|
|
throw new error.ValidationError(
|
|
"ACME operations are disabled on this cluster node",
|
|
);
|
|
}}
|
|
}};
|
|
"""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GateConfig:
|
|
request: bool
|
|
renew: bool
|
|
revoke: bool
|
|
|
|
@property
|
|
def any_enabled(self) -> bool:
|
|
return self.request or self.renew or self.revoke
|
|
|
|
|
|
def log(message: str) -> None:
|
|
print(f"[acme-authority] {message}", flush=True)
|
|
|
|
|
|
def env_bool(name: str, default: bool = False) -> bool:
|
|
value = os.environ.get(name)
|
|
|
|
if value is None:
|
|
return default
|
|
|
|
value = value.strip().lower()
|
|
|
|
if value in {"1", "true", "yes", "on"}:
|
|
return True
|
|
|
|
if value in {"0", "false", "no", "off"}:
|
|
return False
|
|
|
|
raise RuntimeError(
|
|
f"{name} must be true/false, yes/no, on/off, or 1/0; got {value!r}"
|
|
)
|
|
|
|
|
|
def load_config() -> GateConfig:
|
|
return GateConfig(
|
|
request=env_bool("ACME_GATE_REQUEST"),
|
|
renew=env_bool("ACME_GATE_RENEW"),
|
|
revoke=env_bool("ACME_GATE_REVOKE"),
|
|
)
|
|
|
|
|
|
def load_source(path: Path) -> str:
|
|
if not path.is_file():
|
|
raise RuntimeError(f"NPM certificate source not found: {path}")
|
|
|
|
return path.read_text()
|
|
|
|
|
|
def write_source(path: Path, source: str) -> None:
|
|
path.write_text(source)
|
|
|
|
|
|
def replace_exactly_once(
|
|
source: str,
|
|
old: str,
|
|
new: str,
|
|
*,
|
|
description: str,
|
|
) -> str:
|
|
count = source.count(old)
|
|
|
|
if count != 1:
|
|
raise RuntimeError(
|
|
f"{description}: expected anchor exactly once, found {count}"
|
|
)
|
|
|
|
return source.replace(old, new, 1)
|
|
|
|
|
|
def require_fragment(
|
|
source: str,
|
|
fragment: str,
|
|
*,
|
|
description: str,
|
|
) -> None:
|
|
if fragment not in source:
|
|
raise RuntimeError(
|
|
f"{description}: required fragment not found"
|
|
)
|
|
|
|
|
|
def apply_authority_helper(source: str) -> str:
|
|
"""
|
|
Install the shared requireAcmeAuthority() helper.
|
|
|
|
The helper is inserted immediately after the top-level fs import so the
|
|
patch remains close to the dependency it requires.
|
|
"""
|
|
|
|
expected_declaration = (
|
|
f'const acmeAuthorityFile = "{AUTHORITY_FILE}";'
|
|
)
|
|
|
|
if expected_declaration in source:
|
|
require_fragment(
|
|
source,
|
|
"const requireAcmeAuthority = () => {",
|
|
description="existing authority helper",
|
|
)
|
|
require_fragment(
|
|
source,
|
|
"fs.accessSync(acmeAuthorityFile, fs.constants.R_OK);",
|
|
description="existing authority helper",
|
|
)
|
|
|
|
log("authority_helper=already-present")
|
|
return source
|
|
|
|
if HELPER_MARKER in source:
|
|
raise RuntimeError(
|
|
"authority helper already exists but does not match "
|
|
f"configured authority file {AUTHORITY_FILE}"
|
|
)
|
|
|
|
# Prefer the end of the known ESM import block used by current NPM.
|
|
# Older CommonJS releases fall back to their fs import.
|
|
supported_anchors = (
|
|
'import internalNginx from "./nginx.js";\n',
|
|
'const fs = require("fs");\n',
|
|
'const fs = require("node:fs");\n',
|
|
)
|
|
|
|
matches = [
|
|
anchor
|
|
for anchor in supported_anchors
|
|
if anchor in source
|
|
]
|
|
|
|
if len(matches) != 1:
|
|
raise RuntimeError(
|
|
"authority helper: expected exactly one supported insertion "
|
|
f"anchor, found {len(matches)}"
|
|
)
|
|
|
|
anchor = matches[0]
|
|
replacement = anchor + "\n" + AUTHORITY_HELPER
|
|
|
|
source = replace_exactly_once(
|
|
source,
|
|
anchor,
|
|
replacement,
|
|
description="authority helper",
|
|
)
|
|
|
|
log(f"authority_helper=added anchor={anchor.strip()!r}")
|
|
return source
|
|
|
|
|
|
def verify(source: str, config: GateConfig) -> None:
|
|
"""
|
|
Verify shared patch state.
|
|
|
|
Individual mutation modules will add their own verification markers as
|
|
they are implemented.
|
|
"""
|
|
|
|
if config.any_enabled:
|
|
require_fragment(
|
|
source,
|
|
f'const acmeAuthorityFile = "{AUTHORITY_FILE}";',
|
|
description="authority helper verification",
|
|
)
|
|
|
|
require_fragment(
|
|
source,
|
|
"const requireAcmeAuthority = () => {",
|
|
description="authority helper verification",
|
|
)
|
|
|
|
require_fragment(
|
|
source,
|
|
"fs.accessSync(acmeAuthorityFile, fs.constants.R_OK);",
|
|
description="authority helper verification",
|
|
)
|
|
|
|
require_fragment(
|
|
source,
|
|
"ACME operations are disabled on this cluster node",
|
|
description="authority helper verification",
|
|
)
|
|
|
|
log("verification=passed")
|