commit f6197a83bc740609229ad95f8a1c0341033ca1f2
parent 546718851148f54da9c0791061ed369c543efaf1
Author: Pablo Murad <pablo@pablomurad.com>
Date: Sat, 21 Mar 2026 15:36:23 -0300
chat, gemini and gopher
Diffstat:
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"