import os import re from pathlib import Path from typing import List from .templating import jinja import glob DOMAINS_RE = re.compile( r"^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}|[a-z0-9-]{1,30}\.[a-z]{2,})$" ) def _walk_nginx_conf(nginx_conf: Path): """Recursively finds all configuration files of a nginx config""" # this path is not necessarily correct... someone could have a weird prefix and # conf file combination conf_dir = nginx_conf.parent stack = [nginx_conf] while len(stack): file = stack.pop() conf = file.read_text() yield file, conf for include in re.finditer(r"(?:^|\n\s*|[{;]\s*)include (.+);", conf): pattern = include.group(1) for file in glob.glob( pattern if pattern.startswith("/") else f"{conf_dir}/pattern" ): stack.append(Path(file)) def gather_autossl_directives(nginx_conf: Path): """ Finds #AUTOSSL directives inside an nginx configuration. The server_name must be on a separate line. (which it usually is) """ directives = [] for _, conf in _walk_nginx_conf(nginx_conf): for directive in re.finditer( r"(?:^|\n\s*|[{;]\s*)server_name (.*); *# *AUTOSSL *> *(\S+)", conf ): domains, alias = directive.groups() domains = domains.split() if any(not re.match(DOMAINS_RE, domain) for domain in domains): raise ValueError( f"Cannot get SSL cert for \"{''.join(domains)}\". Invalid domains." ) if not re.match(r"^[a-zA-Z0-9-_]+$", alias): raise ValueError(f'Invalid cert alias "{alias}"') directives.append((domains, alias)) return directives def get_site_files(nginx_dir: Path) -> List[Path]: names = [] for file in (nginx_dir / "sites").iterdir(): if file.is_file() and re.match(r"\d+-.+\.conf", file.name): names.append(file) return names def build_domains_txt(directives): return ( "\n".join(" ".join(domains) + " > " + alias for domains, alias in directives) + "\n" ) def generate_ssl_configs(dir: Path, cert_aliases: List[str]): for alias in cert_aliases: file = dir / (alias + ".conf") template = jinja.get_template("ssl.conf") file.write_text(template.render(alias=alias))