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