diff --git a/pikit-api.py b/pikit-api.py index 0a3aa47..b4a9cba 100644 --- a/pikit-api.py +++ b/pikit-api.py @@ -174,6 +174,14 @@ def normalize_path(path: str | None) -> str: return p +def default_host(): + """Return preferred hostname (append .local if bare).""" + host = socket.gethostname() + if "." not in host: + host = f"{host}.local" + return host + + def dbg(msg): # Legacy debug file logging (when /boot/pikit-debug exists) if DEBUG_FLAG: @@ -226,7 +234,7 @@ def load_services(): try: data = json.loads(SERVICE_JSON.read_text()) # Normalize entries: ensure url built from port if missing - host = socket.gethostname() + host = default_host() for svc in data: svc_path = normalize_path(svc.get("path")) if svc_path: @@ -506,16 +514,8 @@ def set_updates_config(opts: dict): def detect_https(host, port): - try: - import ssl - ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - with socket.create_connection((host, int(port)), timeout=1.5) as sock: - with ctx.wrap_socket(sock, server_hostname=host): - return True - except Exception: - return False + """Heuristic: known HTTPS ports or .local certs.""" + return int(port) in HTTPS_PORTS or str(host).lower().endswith(".local") or str(host).lower() == "pikit" def factory_reset(): @@ -1176,6 +1176,12 @@ class Handler(BaseHTTPRequestHandler): if port: svc["online"] = port_online("127.0.0.1", port) svc["firewall_open"] = ufw_status_allows(port) + # Rebuild URL with preferred host (adds .local) + host = default_host() + path = normalize_path(svc.get("path")) + scheme = svc.get("scheme") or ("https" if detect_https(host, port) else "http") + svc["scheme"] = scheme + svc["url"] = f"{scheme}://{host}:{port}{path}" services.append(svc) self._send(200, {"services": services}) elif self.path.startswith("/api/updates/auto"): @@ -1294,7 +1300,7 @@ class Handler(BaseHTTPRequestHandler): services = load_services() if any(s.get("port") == port for s in services): return self._send(400, {"error": "port already exists"}) - host = socket.gethostname() + host = default_host() scheme = payload.get("scheme") if scheme not in ("http", "https"): scheme = "https" if detect_https(host, port) else "http" @@ -1368,7 +1374,7 @@ class Handler(BaseHTTPRequestHandler): svc["port"] = new_port_int target_port = new_port_int port_changed = True - host = socket.gethostname() + host = default_host() if new_path is not None: path = normalize_path(new_path) if path: diff --git a/pikit-web/onboarding/index.html b/pikit-web/onboarding/index.html new file mode 100644 index 0000000..79a709f --- /dev/null +++ b/pikit-web/onboarding/index.html @@ -0,0 +1,113 @@ + + + + + + Welcome to your Pi-Kit + + + +
+
+
+

Secure connection to your Pi-Kit

+
+ +

+ You’re on your Pi-Kit. Everything stays on your local network. Let’s switch to the + secure (HTTPS) dashboard. +

+ +
+ + Download Pi-Kit CA +
+ +
+

If you see a warning

+ +

This is safe for your own Pi on your own network.

+
+ +
+

Install the Pi-Kit CA (recommended, one-time)

+

This removes future warnings for both Pi-Kit and DietPi dashboards.

+
+ Linux (Arch/Endeavour) + curl -s https://pikit.local/assets/pikit-ca.crt -o /tmp/pikit-ca.crt && sudo install -m644 /tmp/pikit-ca.crt /etc/ca-certificates/trust-source/anchors/ && sudo trust extract-compat + +
+
+ Linux (Debian/Ubuntu) + curl -s https://pikit.local/assets/pikit-ca.crt -o /tmp/pikit-ca.crt && sudo cp /tmp/pikit-ca.crt /usr/local/share/ca-certificates/pikit-ca.crt && sudo update-ca-certificates + +
+
+ macOS +

Double-click pikit-ca.crt → Always Trust.

+
+
+ Windows +

Run mmc → Add/Remove Snap-in → Certificates (Computer) → Trusted Root CAs → Import pikit-ca.crt.

+
+
+ +

+ Once trusted, this page will auto-forward you to the secure dashboard. +

+
+ + + + diff --git a/pikit-web/onboarding/style.css b/pikit-web/onboarding/style.css new file mode 100644 index 0000000..a53ed7b --- /dev/null +++ b/pikit-web/onboarding/style.css @@ -0,0 +1,131 @@ +:root { + color-scheme: dark; + --bg: #0c111a; + --panel: #131a24; + --text: #dce5f7; + --muted: #95a3c1; + --accent: #3dd598; + --border: #1f2734; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + display: grid; + place-items: center; + background: radial-gradient(120% 120% at 20% 20%, #162133, #0c111a 60%); + color: var(--text); + font-family: "DM Sans", "Inter", system-ui, -apple-system, sans-serif; + padding: 24px; +} + +.card { + max-width: 720px; + width: 100%; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 16px; + padding: 20px 22px 24px; + box-shadow: 0 12px 50px rgba(0, 0, 0, 0.35); +} + +header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +h1 { + margin: 0; + font-size: 1.35rem; +} + +p { + margin: 8px 0; + color: var(--muted); +} + +.dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 12px var(--accent); +} + +.actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin: 12px 0 4px; +} + +button, +a.ghost { + border: 1px solid var(--border); + background: var(--accent); + color: #0c111a; + padding: 10px 16px; + border-radius: 10px; + font-weight: 600; + cursor: pointer; + text-decoration: none; +} + +button.ghost, +a.ghost { + background: transparent; + color: var(--text); +} + +button.copy { + margin-left: 8px; + background: transparent; + color: var(--text); + border: 1px solid var(--border); + padding: 6px 10px; +} + +.steps { + margin: 12px 0; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 12px; + background: rgba(255, 255, 255, 0.02); +} + +.steps h3 { + margin: 0 0 6px; +} + +ul { + margin: 6px 0 4px 18px; + color: var(--text); +} + +code { + display: block; + background: #0b1018; + border: 1px solid var(--border); + padding: 10px; + border-radius: 10px; + margin-top: 6px; + color: var(--text); + word-break: break-all; +} + +summary { + cursor: pointer; + font-weight: 600; + color: var(--text); +} + +.footnote { + font-size: 0.9rem; + color: var(--muted); +}