diff --git a/.env.example b/.env.example index 779bd6b..da70187 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,8 @@ FTP_USER= FTP_PASSWORD= # Relativer Pfad zum Document Root (in Plesk prüfen, typisch httpdocs) FTP_REMOTE_PATH=httpdocs +# Ausserhalb Document Root — nicht öffentlich (config, .env, data) +FTP_PRIVATE_PATH=private # --- Domain & Server --- DOMAIN=0012.de diff --git a/HOSTING.md b/HOSTING.md index 53e92a1..d65dae3 100644 --- a/HOSTING.md +++ b/HOSTING.md @@ -42,6 +42,26 @@ DNS-Einträge nur für Domains mit Bezug zum Paket oder über wint.global bestel Domain-Delegation kann nach Registration bis zu **24 Stunden** dauern. Bis dahin ggf. temporäre URL aus dem Kundenbereich nutzen. +## TLS / HTTPS + +Konfiguration in Plesk (Stand 2026-06-11): + +| | | +|---|---| +| Zertifikat | Let's Encrypt (`CN=0012.de`) | +| Gültig | 2026-06-11 — 2026-09-09 (Auto-Renewal in Plesk) | +| HTTP → HTTPS | 301 Permanent Redirect | +| HSTS | `Strict-Transport-Security: max-age=15768000; includeSubDomains` (~182 Tage) | + +Prüfung: + +```bash +curl -sI http://0012.de/ # → 301 Location: https://0012.de/ +curl -sI https://0012.de/ # → 200, HSTS-Header +``` + +**PHP (Web):** 8.3.31 (laut Response-Header). Cron-Aufgaben können abweichende Version wählen (z. B. 8.5.x). + ## E-Mail (Plesk) | | | diff --git a/README.md b/README.md index d48ffc7..840bb23 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,10 @@ Der Webspace sieht boka **von außen** — interne Metriken brauchen das Push-Sk │ ├── index.php # Status-Beacon Landing │ ├── monitor.php # Cron: Pull-Checks │ └── ingest.php # Empfang Push von boka -├── config/ # PHP-Konfiguration (lädt .env) +├── config/ # PHP-Konfiguration → deploy nach private/config/ +├── scripts/ +│ ├── deploy.sh # FTP-Deploy (ruft deploy.py auf) +│ └── deploy.py └── todo/ # Aufgabenliste ``` @@ -67,6 +70,6 @@ Offene Arbeiten: [`todo/README.md`](todo/README.md) ## Schnellstart (wenn eingerichtet) 1. `.env.example` → `.env` kopieren und ausfüllen -2. Dateien nach `httpdocs/` deployen (FTP/rsync) +2. `./scripts/deploy.sh` — spielt `public/` nach `httpdocs/`, Config und `.env` nach `private/` 3. In Plesk: geplante Aufgabe für `monitor.php` (Cron-Stil, z. B. `*/10 * * * *`) 4. Auf boka: Push-Skript + Crontab (siehe `todo/04-monitoring-push-boka.md`) diff --git a/config/bootstrap.php b/config/bootstrap.php new file mode 100644 index 0000000..fb707f0 --- /dev/null +++ b/config/bootstrap.php @@ -0,0 +1,60 @@ + + + + + + + <?= htmlspecialchars($domain, ENT_QUOTES, 'UTF-8') ?> · external observer + + + +
+

0012

+

external observer node · watching from outside

+ +
+ +

Monitoring active — last check

+ +

Monitoring not initialized — awaiting first cron run

+ +

+ Node + · independent of boka () +

+
+
+ + diff --git a/scripts/__pycache__/deploy.cpython-314.pyc b/scripts/__pycache__/deploy.cpython-314.pyc new file mode 100644 index 0000000..4313351 Binary files /dev/null and b/scripts/__pycache__/deploy.cpython-314.pyc differ diff --git a/scripts/deploy.py b/scripts/deploy.py new file mode 100755 index 0000000..920c3a4 --- /dev/null +++ b/scripts/deploy.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Deploy public/, config/, and .env to wint.global / Plesk via FTP/FTPS.""" + +from __future__ import annotations + +import argparse +import ftplib +import os +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent + + +def load_env(path: Path) -> dict[str, str]: + if not path.is_file(): + sys.exit(f"Missing {path} — copy .env.example to .env and fill in credentials.") + + env: dict[str, str] = {} + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + key, _, value = line.partition("=") + env[key.strip()] = value.strip() + return env + + +def require_env(env: dict[str, str], *keys: str) -> None: + missing = [k for k in keys if not env.get(k)] + if missing: + sys.exit(f"Missing in .env: {', '.join(missing)}") + + +def connect(env: dict[str, str]) -> ftplib.FTP: + host = env["FTP_HOST"] + user = env["FTP_USER"] + password = env["FTP_PASSWORD"] + + try: + ftp = ftplib.FTP_TLS(host, timeout=60) + ftp.login(user, password) + ftp.prot_p() + print("Connected (FTPS)") + return ftp + except ftplib.all_errors as exc: + print(f"FTPS failed ({exc}), trying plain FTP …") + ftp = ftplib.FTP(host, timeout=60) + ftp.login(user, password) + print("Connected (FTP)") + return ftp + + +def cwd_to_root(ftp: ftplib.FTP) -> None: + try: + ftp.cwd("/") + except ftplib.error_perm: + pass + + +def ensure_remote_dir(ftp: ftplib.FTP, remote_dir: str) -> None: + cwd_to_root(ftp) + for part in [p for p in remote_dir.split("/") if p]: + try: + ftp.cwd(part) + except ftplib.error_perm: + ftp.mkd(part) + ftp.cwd(part) + + +def upload_file(ftp: ftplib.FTP, local: Path, remote_path: str) -> None: + remote_path = remote_path.replace("\\", "/").lstrip("/") + remote_dir = os.path.dirname(remote_path) + remote_name = os.path.basename(remote_path) + + if remote_dir: + ensure_remote_dir(ftp, remote_dir) + else: + cwd_to_root(ftp) + + with local.open("rb") as handle: + ftp.storbinary(f"STOR {remote_name}", handle) + + print(f" ↑ {local.relative_to(ROOT)} → {remote_path}") + + +def upload_tree(ftp: ftplib.FTP, local_dir: Path, remote_base: str) -> None: + remote_base = remote_base.strip("/") + for path in sorted(local_dir.rglob("*")): + if path.is_file(): + rel = path.relative_to(local_dir).as_posix() + upload_file(ftp, path, f"{remote_base}/{rel}") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Deploy 0012.de webspace") + parser.add_argument("--dry-run", action="store_true", help="Show plan only") + args = parser.parse_args() + + env = load_env(ROOT / ".env") + require_env(env, "FTP_HOST", "FTP_USER", "FTP_PASSWORD") + + remote_web = env.get("FTP_REMOTE_PATH", "httpdocs").strip("/") + remote_private = env.get("FTP_PRIVATE_PATH", "private").strip("/") + + print("Deploy plan:") + public_files = sum(1 for p in (ROOT / "public").rglob("*") if p.is_file()) + config_files = sum(1 for p in (ROOT / "config").rglob("*") if p.is_file()) + print(f" public/ ({public_files} files) → {remote_web}/") + print(f" config/ ({config_files} files) → {remote_private}/config/") + print(f" .env → {remote_private}/.env") + print(f" (mkdir) {remote_private}/data/") + + if args.dry_run: + return + + print("\nConnecting …") + ftp = connect(env) + try: + print("Uploading …") + upload_tree(ftp, ROOT / "public", remote_web) + upload_tree(ftp, ROOT / "config", f"{remote_private}/config") + ensure_remote_dir(ftp, f"{remote_private}/data") + upload_file(ftp, ROOT / ".env", f"{remote_private}/.env") + print("\nDone.") + finally: + ftp.quit() + + +if __name__ == "__main__": + main() diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..a1453cb --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +exec python3 "$(dirname "$0")/deploy.py" "$@" diff --git a/todo/01-bootstrap-env-deploy.md b/todo/01-bootstrap-env-deploy.md index 1fc73ae..383b97d 100644 --- a/todo/01-bootstrap-env-deploy.md +++ b/todo/01-bootstrap-env-deploy.md @@ -1,6 +1,7 @@ # 01 — Bootstrap, `.env`, Deployment **Priorität:** high +**Status:** erledigt (2026-06-11) **Abhängigkeiten:** keine **Blockiert:** 02, 03, 04, 05, 06 @@ -10,22 +11,38 @@ Lokales Repo mit dem Webspace verbinden und eine deploybare Grundstruktur anlege ## Checkliste -- [ ] `.env.example` nach `.env` kopieren -- [ ] Plesk-User/Passwort, FTP, später SMTP in `.env` eintragen (nicht committen) -- [ ] In Plesk Document Root prüfen (typisch `httpdocs/`) -- [ ] PHP-Version notieren (Cron-Screenshot: 8.5.x) -- [ ] Verzeichnis `public/` als Quelle für `httpdocs/` festlegen -- [ ] Erstes Test-Deployment: `public/index.php` mit `phpinfo()` oder „hello“ — danach wieder entfernen -- [ ] HTTPS für `0012.de` prüfen (Let's Encrypt in Plesk) -- [ ] Optional: Deploy-Skript (`scripts/deploy.sh` rsync/lftp) — nur wenn gewünscht +- [x] `.env.example` nach `.env` kopieren +- [x] Plesk-User/Passwort, FTP in `.env` eintragen (nicht committen) +- [x] Document Root: `httpdocs/` (Plesk-Standard) +- [x] PHP-Version: 8.5.x (Plesk Cron) +- [x] Verzeichnis `public/` als Quelle für `httpdocs/` +- [x] Erstes Deployment: `public/index.php` (Status-Beacon Platzhalter) +- [x] HTTPS Let's Encrypt in Plesk (gültig bis 2026-09-09) +- [x] HTTP → HTTPS Redirect (301) +- [x] HSTS (`Strict-Transport-Security`, max-age 15768000, includeSubDomains) +- [x] Deploy-Skript: `scripts/deploy.sh` (FTPS/FTP via Python) + +## Server-Layout (nach Deploy) + +``` +0012.de/ +├── httpdocs/ ← public/ +│ └── index.php +└── private/ ← nicht öffentlich + ├── .env + ├── config/bootstrap.php + └── data/ ← später last-checks.json +``` + +## Deploy + +```bash +./scripts/deploy.sh # upload +./scripts/deploy.sh --dry-run +``` ## Akzeptanzkriterien -- FTP-Login funktioniert -- `https://0012.de/` liefert eine Testseite -- `.env` liegt lokal, ist in `.gitignore` - -## Notizen - -- Temporäre URL aus Kundenbereich nutzen, falls DNS noch propagiert -- Firewall: Port 8443 (Plesk), 21/22 (FTP/SFTP je nach Anbieter) +- [x] FTP-Login funktioniert (deploy.sh) +- [x] `https://0012.de/` — Landing Page über TLS +- [x] `.env` liegt lokal, ist in `.gitignore`