131 lines
3.8 KiB
Python
131 lines
3.8 KiB
Python
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), None)
|
|
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), None)
|
|
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
|