runv-server

server tooling for runv.club
Log | Files | Refs | README

commit f6197a83bc740609229ad95f8a1c0341033ca1f2
parent 546718851148f54da9c0791061ed369c543efaf1
Author: Pablo Murad <pablo@pablomurad.com>
Date:   Sat, 21 Mar 2026 15:36:23 -0300

chat, gemini and gopher

Diffstat:
Mpatches/patch_permissions.py | 2+-
Mpatches/yetgg.py | 2+-
Mscripts/admin/create_runv_user.py | 4++--
Mscripts/admin/del-user.py | 4++--
Mscripts/admin/setup_alt_protocols.py | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mscripts/admin/skel.py | 4++--
Mscripts/admin/starthere.py | 2+-
Mscripts/admin/update_user.py | 4++--
Mscripts/create_runv_user.md | 2+-
Mscripts/del-user.md | 2+-
Mscripts/docs/alt_protocols.md | 11++++++++++-
Mscripts/skel.md | 2+-
Mscripts/starthere.md | 2+-
Msite/genlanding.py | 4++--
Mterminal/entre_app.py | 2+-
Mterminal/entre_core.py | 4++--
16 files changed, 125 insertions(+), 26 deletions(-)

diff --git a/patches/patch_permissions.py b/patches/patch_permissions.py @@ -30,7 +30,7 @@ import time from pathlib import Path from typing import Final -VERSION: Final[str] = "0.01" +VERSION: Final[str] = "0.02" GROUP_NAME: Final[str] = "runv-jailed" SSHD_DROPIN: Final[str] = "/etc/ssh/sshd_config.d/runv-jailed.conf" diff --git a/patches/yetgg.py b/patches/yetgg.py @@ -21,7 +21,7 @@ import sys from pathlib import Path from typing import Any, Final -VERSION: Final[str] = "0.01" +VERSION: Final[str] = "0.02" GEMINI_ROOT: Final[Path] = Path("/var/gemini") GEMINI_USERS: Final[Path] = GEMINI_ROOT / "users" diff --git a/scripts/admin/create_runv_user.py b/scripts/admin/create_runv_user.py @@ -25,7 +25,7 @@ Quota ext4, metadados JSON e logging seguem após estes passos. Não é signup público: executar manualmente como root/sudo no servidor. Requer Linux (Debian). Quota: ext4 com ``usrquota``/``usrjquota`` via ``setquota`` (não altera fstab). -Versão 0.01 — desenvolvido por pmurad, 2026. +Versão 0.02 — desenvolvido por pmurad, 2026. """ from __future__ import annotations @@ -115,7 +115,7 @@ DEFAULT_QUOTA_HARD_MIB: Final[int] = 500 DEFAULT_QUOTA_INODE_SOFT: Final[int] = 10_000 DEFAULT_QUOTA_INODE_HARD: Final[int] = 12_000 -VERSION: Final[str] = "0.01" +VERSION: Final[str] = "0.02" AUTHOR: Final[str] = "pmurad" COPYRIGHT_YEAR: Final[str] = "2026" diff --git a/scripts/admin/del-user.py b/scripts/admin/del-user.py @@ -7,7 +7,7 @@ Usa ``deluser`` com remoção da home. Opcionalmente remove o registro em Executar como root. Não altera Apache nem SSH diretamente. -Versão 0.01 — runv.club +Versão 0.02 — runv.club """ from __future__ import annotations @@ -63,7 +63,7 @@ RESERVED_USERNAMES: Final[frozenset[str]] = frozenset( DEFAULT_METADATA_PATH: Final[Path] = Path("/var/lib/runv/users.json") DEFAULT_LOCK_PATH: Final[Path] = Path("/var/lib/runv/users.lock") -VERSION: Final[str] = "0.01" +VERSION: Final[str] = "0.02" EXIT_OK: Final[int] = 0 EXIT_VALIDATION: Final[int] = 1 diff --git a/scripts/admin/setup_alt_protocols.py b/scripts/admin/setup_alt_protocols.py @@ -7,7 +7,7 @@ Infraestrutura Gopher (gophernicus) e Gemini (molly-brown) para runv.club. Idempotente, dry-run, subprocess sem shell. Executar como root no Debian. -Versão 0.02 — runv.club +Versão 0.03 — runv.club """ from __future__ import annotations @@ -30,7 +30,7 @@ from typing import Any, Final # Constantes # --------------------------------------------------------------------------- -VERSION: Final[str] = "0.02" +VERSION: Final[str] = "0.03" DEFAULT_USERS_JSON: Final[Path] = Path("/var/lib/runv/users.json") DEFAULT_HOMES_ROOT: Final[Path] = Path("/home") @@ -46,6 +46,8 @@ GOPHER_DEFAULT_PATH: Final[Path] = Path("/etc/default/gophernicus") GOPHER_SYSTEMD_SERVICE: Final[Path] = Path("/lib/systemd/system/gophernicus@.service") MOLLY_CONF_DIR: Final[Path] = Path("/etc/molly-brown") MOLLY_INSTANCE: Final[str] = "runv.club" # molly-brown@runv.club.service +MOLLY_LOG_DIR: Final[Path] = Path("/var/log/molly-brown") +MOLLY_SERVICE_USER_FALLBACK: Final[str] = "molly-brown" PACKAGES_GOPHER: Final[tuple[str, ...]] = ("gophernicus",) PACKAGES_GEMINI: Final[tuple[str, ...]] = ("molly-brown",) @@ -168,11 +170,88 @@ def write_gophernicus_default( log.info("atualizado: %s", path) +def molly_log_paths(instance: str) -> tuple[Path, Path]: + """Caminhos de access / error log para a instância (ex. runv.club).""" + return ( + MOLLY_LOG_DIR / f"{instance}-access.log", + MOLLY_LOG_DIR / f"{instance}-error.log", + ) + + +def molly_service_user(instance: str, log: logging.Logger) -> str: + """User= do unit systemd molly-brown@instance (fallback Debian: molly-brown).""" + unit = f"molly-brown@{instance}.service" + try: + r = subprocess.run( + ["systemctl", "show", "-p", "User", "--value", unit], + capture_output=True, + text=True, + timeout=30, + ) + if r.returncode == 0: + name = (r.stdout or "").strip() + if name and name != "(null)": + return name + except (OSError, subprocess.TimeoutExpired) as e: + log.debug("systemctl User para %s: %s", unit, e) + return MOLLY_SERVICE_USER_FALLBACK + + +def ensure_molly_log_files( + instance: str, + *, + dry_run: bool, + log: logging.Logger, +) -> tuple[Path, Path]: + """ + Cria /var/log/molly-brown e ficheiros de log com dono = User do serviço. + Molly-brown não aceita AccessLog/ErrorLog = \"-\" (interpreta como path /- e falha). + """ + access_p, error_p = molly_log_paths(instance) + user_name = molly_service_user(instance, log) + if dry_run: + log.info( + "[dry-run] criaria %s e %s, %s (dono: %s)", + MOLLY_LOG_DIR, + access_p, + error_p, + user_name, + ) + return access_p, error_p + + MOLLY_LOG_DIR.mkdir(parents=True, exist_ok=True) + os.chmod(MOLLY_LOG_DIR, 0o755) + + try: + pw = pwd.getpwnam(user_name) + uid, gid = pw.pw_uid, pw.pw_gid + except KeyError: + log.warning( + "Utilizador «%s» inexistente — logs com dono root; o serviço pode falhar ao escrever", + user_name, + ) + uid, gid = 0, 0 + + for p in (access_p, error_p): + if not p.exists(): + p.touch(exist_ok=True) + try: + os.chown(p, uid, gid) + os.chmod(p, 0o644) + except OSError as e: + log.warning("chown/chmod %s: %s", p, e) + + log.info("logs Molly: %s, %s (dono %s)", access_p, error_p, user_name) + return access_p, error_p + + def molly_brown_conf_text( *, hostname: str, cert: Path, key: Path, + access_log: Path, + error_log: Path, ) -> str: return f"""# runv.club — gerido por setup_alt_protocols.py Hostname = "{hostname}" @@ -181,8 +260,8 @@ DocBase = "{GEMINI_ROOT.as_posix()}" HomeDocBase = "users" CertPath = "{cert.as_posix()}" KeyPath = "{key.as_posix()}" -AccessLog = "-" -ErrorLog = "-" +AccessLog = "{access_log.as_posix()}" +ErrorLog = "{error_log.as_posix()}" GeminiExt = "gmi" ReadMollyFiles = true """ @@ -551,8 +630,19 @@ def main(argv: list[str] | None = None) -> int: key, ) else: + access_p, error_p = ensure_molly_log_files( + MOLLY_INSTANCE, + dry_run=args.dry_run, + log=log, + ) conf_path = MOLLY_CONF_DIR / f"{MOLLY_INSTANCE}.conf" - body = molly_brown_conf_text(hostname=args.gemini_hostname, cert=cert, key=key) + body = molly_brown_conf_text( + hostname=args.gemini_hostname, + cert=cert, + key=key, + access_log=access_p, + error_log=error_p, + ) if args.dry_run: log.info("[dry-run] gravaria %s", conf_path) else: diff --git a/scripts/admin/skel.py b/scripts/admin/skel.py @@ -3,7 +3,7 @@ Prepara /etc/skel para novos usuários do runv.club (Debian). Executar como root. Não cria usuários, não altera Apache nem SSH. -Versão 0.01 — runv.club +Versão 0.02 — runv.club """ from __future__ import annotations @@ -23,7 +23,7 @@ PUBLIC_HTML_DIR: Final[Path] = SKEL_ROOT / "public_html" INDEX_HTML: Final[Path] = PUBLIC_HTML_DIR / "index.html" README_MD: Final[Path] = SKEL_ROOT / "README.md" -VERSION: Final[str] = "0.01" +VERSION: Final[str] = "0.02" EXIT_OK: Final[int] = 0 EXIT_ERROR: Final[int] = 1 diff --git a/scripts/admin/starthere.py b/scripts/admin/starthere.py @@ -54,7 +54,7 @@ except ModuleNotFoundError: DEFAULT_QUOTA_PROBE: Final[Path] = Path("/home") -VERSION: Final[str] = "0.01" +VERSION: Final[str] = "0.02" FSTAB = Path("/etc/fstab") BACKUP_DIR = Path("/root/runv-fstab-backups") diff --git a/scripts/admin/update_user.py b/scripts/admin/update_user.py @@ -7,7 +7,7 @@ Executar como root. Alinha-se a create_runv_user / del-user / runv_mount. Modo interativo no terminal (sem argumentos ou -i) ou flags CLI. -Versão 0.01 — runv.club +Versão 0.02 — runv.club """ from __future__ import annotations @@ -55,7 +55,7 @@ DEFAULT_QUOTA_HARD_MIB: Final[int] = 500 DEFAULT_QUOTA_INODE_SOFT: Final[int] = 10_000 DEFAULT_QUOTA_INODE_HARD: Final[int] = 12_000 -VERSION: Final[str] = "0.01" +VERSION: Final[str] = "0.02" EXIT_OK: Final[int] = 0 EXIT_VALIDATION: Final[int] = 1 EXIT_SYSTEM: Final[int] = 2 diff --git a/scripts/create_runv_user.md b/scripts/create_runv_user.md @@ -1,6 +1,6 @@ # create_runv_user — provisionamento interno (runv.club) -**Versão 0.01** · **Desenvolvido por pmurad — 2026** +**Versão 0.02** · **Desenvolvido por pmurad — 2026** Ferramenta de linha de comando para **administradores** criarem contas Unix no servidor **Debian/Linux** (runv.club). Não é cadastro público. diff --git a/scripts/del-user.md b/scripts/del-user.md @@ -1,6 +1,6 @@ # del-user.py — banimento / remoção de conta (runv.club) -**Versão 0.01** · runv.club +**Versão 0.02** · runv.club Ferramenta para **administradores** removerem **permanentemente** um utilizador Unix no Debian (banimento no runv.club): apaga a conta e, por defeito, a home com `deluser --remove-home`. diff --git a/scripts/docs/alt_protocols.md b/scripts/docs/alt_protocols.md @@ -22,6 +22,15 @@ Script em **`scripts/admin/setup_alt_protocols.py`**: instala e configura **goph - **TLS obrigatório** (certificado + chave PEM). Por defeito o script tenta Let's Encrypt em `/etc/letsencrypt/live/runv.club/`; use **`--gemini-cert`** e **`--gemini-key`** se forem noutro sítio. - Sem certificados válidos, o script **não** ativa o serviço `molly-brown@`, mas pode criar `/var/gemini` e symlinks. +## Erro `Error opening error log file: open /-` (read-only file system) + +O **molly-brown** trata `AccessLog` e `ErrorLog` como **caminhos de ficheiro**. Valores como `"-"` (estilo «stdout» noutros programas) são interpretados de forma errada e o processo tenta abrir `/-`, falhando de imediato. + +- **Comportamento actual do script (v0.03+):** cria `/var/log/molly-brown/`, ficheiros `runv.club-access.log` e `runv.club-error.log`, ajusta o dono ao `User=` do unit (`systemctl show`, fallback `molly-brown`), e grava esses caminhos em `/etc/molly-brown/runv.club.conf`. +- **Servidor já provisionado com conf antiga:** o script só reescreve o `.conf` se o ficheiro não existir ou se correr com **`--force`** (faz backup com timestamp). Exemplo: + `sudo python3 scripts/admin/setup_alt_protocols.py --verbose --force` +- **Correcção manual rápida:** `sudo mkdir -p /var/log/molly-brown`; criar os dois `.log`; `sudo chown` para o utilizador do serviço (`systemctl show -p User --value molly-brown@runv.club.service`); editar o `.conf` e substituir `AccessLog` / `ErrorLog` pelos caminhos absolutos; depois `sudo systemctl reset-failed molly-brown@runv.club.service` e `sudo systemctl start molly-brown@runv.club.service`. + ## Molly não sobe ou fica em «activating» - **`journalctl` sem mensagens:** os logs do serviço do sistema exigem **root** — use `sudo journalctl -u molly-brown@runv.club.service -b --no-pager -n 80`. @@ -46,7 +55,7 @@ sudo python3 scripts/admin/setup_alt_protocols.py --verbose |------|--------| | `--dry-run` | Simula; não grava (validação de root ignorada em alguns passos só se documentado). | | `--verbose` | Log detalhado. | -| `--force` | Sobrescreve configs de sistema (com backup com timestamp) e ficheiros modelo no backfill. | +| `--force` | Sobrescreve configs de sistema (com backup com timestamp) e ficheiros modelo no backfill. Necessário para **regravar** `/etc/molly-brown/runv.club.conf` após correcções (ex. logs Molly). | | `--skip-install` | Não corre `apt-get`. | | `--skip-gopher` / `--skip-gemini` | Ignora pacote, config e serviço desse protocolo. | | `--skip-firewall` | Não altera UFW. | diff --git a/scripts/skel.md b/scripts/skel.md @@ -1,6 +1,6 @@ # skel.py — preparar `/etc/skel` (runv.club) -**Versão 0.01** · runv.club +**Versão 0.02** · runv.club Script para **administradores** prepararem o diretório `/etc/skel` no **Debian** (ex.: Debian 13), de modo que `adduser` copie automaticamente uma home inicial com `public_html`, página de boas-vindas e README para novos usuários. diff --git a/scripts/starthere.md b/scripts/starthere.md @@ -2,7 +2,7 @@ Bootstrap **conservador** para preparar um servidor **Debian**: pacotes úteis ao runv.club e **quotas de utilizador** (`usrquota`) no **mesmo filesystem ext4 onde vivem as homes** (detetado automaticamente — tipicamente `/` se `/home` está na raiz, ou `/home` se é um volume dedicado). A lógica de descoberta é a de [`runv_mount.py`](admin/runv_mount.py), alinhada a [`create_runv_user.py`](create_runv_user.md). -Versão do script: **0.01** (use `python3 admin/starthere.py --version` a partir do diretório `scripts/` do repositório). +Versão do script: **0.02** (use `python3 admin/starthere.py --version` a partir do diretório `scripts/` do repositório). ### Comportamento de `--dry-run` diff --git a/site/genlanding.py b/site/genlanding.py @@ -6,7 +6,7 @@ www → apex em HTTP. Produção ou modo --dev para testes locais. Executar como root (excepto --dry-run). Apenas biblioteca padrão Python 3. -Versão 0.01 — runv.club +Versão 0.02 — runv.club """ from __future__ import annotations @@ -22,7 +22,7 @@ import sys from pathlib import Path from typing import Final -VERSION: Final[str] = "0.01" +VERSION: Final[str] = "0.02" EXIT_OK: Final[int] = 0 EXIT_USAGE: Final[int] = 1 EXIT_ERROR: Final[int] = 2 diff --git a/terminal/entre_app.py b/terminal/entre_app.py @@ -5,7 +5,7 @@ Experiência SSH guiada para pedidos de entrada na runv.club (utilizador «entre Executado via ForceCommand no OpenSSH. Não cria contas Linux; apenas fila + log + notificação opcional. -Versão 0.01 — runv.club +Versão 0.02 — runv.club """ from __future__ import annotations diff --git a/terminal/entre_core.py b/terminal/entre_core.py @@ -6,7 +6,7 @@ Mantido alinhado com as regras de ``scripts/admin/create_runv_user.py`` (usernam email, tipos de chave). Campo ``online_presence`` é texto livre na fila (não duplicado em ``create_runv_user``). Sem dependências PyPI. -Versão 0.01 — runv.club +Versão 0.02 — runv.club """ from __future__ import annotations @@ -91,7 +91,7 @@ MAX_PUBKEY_LEN: Final[int] = 16_384 MIN_ONLINE_PRESENCE_LEN: Final[int] = 16 MAX_ONLINE_PRESENCE_LEN: Final[int] = 4000 -APP_VERSION: Final[str] = "0.01" +APP_VERSION: Final[str] = "0.02" SOURCE_TAG: Final[str] = "entre-ssh" # Remetente por omissão das notificações sendmail do fluxo «entre» (cabeçalho From). DEFAULT_MAIL_FROM: Final[str] = "entre@runv.club"