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