Plain is headed towards 1.0! Subscribe for development updates →

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