nginx-configurator/ncc/certs.py

132 lines
3.7 KiB
Python
Raw Permalink Normal View History

2024-11-04 01:05:25 +01:00
import glob
import os
import re
import sys
from collections import deque
from collections.abc import Generator
from pathlib import Path
from string import Template
from typing import Tuple
from .error import NccException
SSL_CONFIG_TEMPLATE = """
ssl_certificate_key $keypath;
ssl_certificate $certpath;
""".lstrip()
SERVER_BLOCK_RE = re.compile(r"(?:^|[{};])\s*server\s*{", re.MULTILINE)
INCLUDE_RE = re.compile(r"(?:^|[{};])\s*include\s+([^;]+);", re.MULTILINE)
SERVER_NAME_RE = re.compile(r"(?:^|[{};])\s*server_name\s+([^;]+);", re.MULTILINE)
class ConfigError(Exception):
pass
def _config_files(main_cfg: Path) -> Generator[Tuple[Path, str]]:
cfg = main_cfg
text = cfg.read_text()
yield (cfg, text)
for include in re.finditer(INCLUDE_RE, text):
for file in glob.glob(include.group(1)):
for cfg in _config_files(Path(file)):
yield cfg
def _remove_comments(cfg: str) -> str:
"""I haven't actually read the parser, so we might be discarding more than we should, oh well"""
output = []
for line in cfg.splitlines():
output.append(line.split("#")[0])
return "\n".join(output)
def _get_server_blocks(cfg: str) -> Generator[str]:
for server_block_start in re.finditer(SERVER_BLOCK_RE, cfg):
start_idx = idx = server_block_start.end()
brackets = 1
while brackets > 0 and idx < len(cfg):
if cfg[idx] == "{":
brackets += 1
elif cfg[idx] == "}":
brackets -= 1
idx += 1
yield cfg[start_idx : idx - 1]
def get_sites(cfg: Path) -> Generator[Tuple[Path, str]]:
"""Expects to be in the conf dir"""
for c in _config_files(cfg):
config_part = _remove_comments(c[1])
for server_block in _get_server_blocks(config_part):
sn = next(re.finditer(SERVER_NAME_RE, server_block))
if not sn:
continue
domains = sn.group(1).split()
for domain in domains:
yield (c[0], domain)
def generate_ssl(cfg: Path, domainstxt_file: Path) -> int:
"""Expects to be in the conf dir"""
to_generate = {}
os.makedirs("genssl/", exist_ok=True)
os.makedirs(domainstxt_file.parent, exist_ok=True)
for c in _config_files(cfg):
config_part = _remove_comments(c[1])
for server_block in _get_server_blocks(config_part):
sn = next(re.finditer(SERVER_NAME_RE, server_block))
if not sn:
continue
wants_ssl = next(
(
i.group(1)
for i in INCLUDE_RE.finditer(server_block)
if i.group(1).startswith("genssl/")
),
None,
)
domains = sn.group(1).split()
domains.sort()
if wants_ssl:
if (entry := to_generate.get(wants_ssl)) and entry != domains:
raise NccException(
f'config requests "{wants_ssl}" with different domain sets'
)
else:
to_generate[wants_ssl] = domains
domainstxt = []
for file, domains in to_generate.items():
file_no_prefix = file.removeprefix("genssl/")
Path(file).write_text(
Template(SSL_CONFIG_TEMPLATE).substitute(
{
"keypath": domainstxt_file.parent
/ f"certs/{file_no_prefix}/privkey.pem",
"certpath": domainstxt_file.parent
/ f"certs/{file_no_prefix}/fullchain.pem",
}
)
)
domainstxt.append(" ".join(domains) + " > " + file_no_prefix + "\n")
domainstxt_file.write_text("".join(domainstxt))
return 0