Plain is headed towards 1.0! Subscribe for development updates →

  1import platform
  2import shutil
  3import subprocess
  4import sys
  5import time
  6import urllib.request
  7from pathlib import Path
  8
  9import click
 10
 11
 12class MkcertManager:
 13    def __init__(self) -> None:
 14        self.mkcert_bin = None
 15
 16    def setup_mkcert(self, install_path: Path) -> None:
 17        """Set up mkcert by checking if it's installed or downloading the binary and installing the local CA."""
 18        if mkcert_path := shutil.which("mkcert"):
 19            # mkcert is already installed somewhere
 20            self.mkcert_bin = mkcert_path
 21        else:
 22            self.mkcert_bin = install_path / "mkcert"
 23            install_path.mkdir(parents=True, exist_ok=True)
 24            if not self.mkcert_bin.exists():
 25                system = platform.system()
 26                arch = platform.machine()
 27
 28                # Map platform.machine() to mkcert's expected architecture strings
 29                arch_map = {
 30                    "x86_64": "amd64",
 31                    "amd64": "amd64",
 32                    "AMD64": "amd64",
 33                    "arm64": "arm64",
 34                    "aarch64": "arm64",
 35                }
 36                arch = arch_map.get(
 37                    arch.lower(), "amd64"
 38                )  # Default to amd64 if unknown
 39
 40                if system == "Darwin":
 41                    os_name = "darwin"
 42                elif system == "Linux":
 43                    os_name = "linux"
 44                elif system == "Windows":
 45                    os_name = "windows"
 46                else:
 47                    click.secho("Unsupported OS", fg="red")
 48                    sys.exit(1)
 49
 50                mkcert_url = f"https://dl.filippo.io/mkcert/latest?for={os_name}/{arch}"
 51                click.secho(f"Downloading mkcert from {mkcert_url}...", bold=True)
 52                urllib.request.urlretrieve(mkcert_url, self.mkcert_bin)
 53                self.mkcert_bin.chmod(0o755)
 54            self.mkcert_bin = str(self.mkcert_bin)  # Convert Path object to string
 55
 56        if not self.is_mkcert_ca_installed():
 57            click.secho(
 58                "Installing mkcert local CA. You may be prompted for your password.",
 59                bold=True,
 60            )
 61            subprocess.run([self.mkcert_bin, "-install"], check=True)
 62
 63    def is_mkcert_ca_installed(self) -> bool:
 64        """Check if mkcert local CA is already installed using mkcert -check."""
 65        try:
 66            result = subprocess.run([self.mkcert_bin, "-check"], capture_output=True)
 67            output = result.stdout.decode() + result.stderr.decode()
 68            if "The local CA is not installed" in output:
 69                return False
 70            return True
 71        except Exception as e:
 72            click.secho(f"Error checking mkcert CA installation: {e}", fg="red")
 73            return False
 74
 75    def generate_certs(self, domain: str, storage_path: Path) -> tuple[Path, Path]:
 76        cert_path = storage_path / f"{domain}-cert.pem"
 77        key_path = storage_path / f"{domain}-key.pem"
 78        timestamp_path = storage_path / f"{domain}.timestamp"
 79        update_interval = 60 * 24 * 3600  # 60 days in seconds
 80
 81        # Check if the certs exist and if the timestamp is recent enough
 82        if cert_path.exists() and key_path.exists() and timestamp_path.exists():
 83            last_updated = timestamp_path.stat().st_mtime
 84            if time.time() - last_updated < update_interval:
 85                return cert_path, key_path
 86
 87        storage_path.mkdir(parents=True, exist_ok=True)
 88
 89        click.secho(f"Generating SSL certificates for {domain}...", bold=True)
 90        subprocess.run(
 91            [
 92                self.mkcert_bin,
 93                "-cert-file",
 94                str(cert_path),
 95                "-key-file",
 96                str(key_path),
 97                domain,
 98            ],
 99            check=True,
100        )
101
102        # Update the timestamp file to the current time
103        with open(timestamp_path, "w") as f:
104            f.write(str(time.time()))
105
106        return cert_path, key_path