runv-server

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

commit 0db766cd18c714fc020048f6a15c22298de47b77
parent 5f03b36f0a64d53c38be79f5dc6fe31d3fea0423
Author: Pablo Murad <pablo@pablomurad.com>
Date:   Sun, 22 Mar 2026 14:33:26 -0300

documentação: still

Diffstat:
ADOCS_REBUILD_CHANGELOG.md | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DINSTALL.md | 184-------------------------------------------------------------------------------
MREADME.md | 6+++++-
DRUNV_CURRENT_STATE_AUDIT.md | 224-------------------------------------------------------------------------------
Adocs/00-overview.md | 48++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/01-server-baseline-debian.md | 28++++++++++++++++++++++++++++
Adocs/02-admin-access-and-ssh.md | 31+++++++++++++++++++++++++++++++
Adocs/03-paths-files-and-state.md | 32++++++++++++++++++++++++++++++++
Adocs/04-bootstrap-and-base-system.md | 27+++++++++++++++++++++++++++
Adocs/05-tools-and-system-experience.md | 32++++++++++++++++++++++++++++++++
Adocs/06-site-and-apache.md | 28++++++++++++++++++++++++++++
Adocs/07-public-members-directory.md | 44++++++++++++++++++++++++++++++++++++++++++++
Adocs/08-email.md | 32++++++++++++++++++++++++++++++++
Adocs/09-terminal-entre.md | 41+++++++++++++++++++++++++++++++++++++++++
Adocs/10-user-provisioning-and-admin-ops.md | 31+++++++++++++++++++++++++++++++
Adocs/11-daily-operations.md | 36++++++++++++++++++++++++++++++++++++
Adocs/12-security-and-privacy.md | 25+++++++++++++++++++++++++
Adocs/13-troubleshooting.md | 33+++++++++++++++++++++++++++++++++
Adocs/14-smoke-tests-and-validation.md | 46++++++++++++++++++++++++++++++++++++++++++++++
Adocs/15-glossary-and-reference.md | 44++++++++++++++++++++++++++++++++++++++++++++
Adocs/README.md | 43+++++++++++++++++++++++++++++++++++++++++++
Adocs/diagrams/architecture.mmd | 21+++++++++++++++++++++
Adocs/diagrams/member-flow.mmd | 20++++++++++++++++++++
Demail/README.md | 68--------------------------------------------------------------------
Memail/configure_mailgun.py | 4++--
Memail/configure_msmtp_legacy.py | 4++--
Demail/docs/ADMIN.md | 52----------------------------------------------------
Demail/docs/INSTALL.md | 134-------------------------------------------------------------------------------
Demail/docs/INTEGRATION.md | 92-------------------------------------------------------------------------------
Demail/docs/TROUBLESHOOTING.md | 62--------------------------------------------------------------
Mscripts/admin/create_runv_user.py | 67+++++++++++++++++++++++++++++++++++++++++++++++++------------------
Dscripts/admin/perm1.md | 34----------------------------------
Dscripts/create_runv_user.md | 281-------------------------------------------------------------------------------
Dscripts/del-user.md | 123-------------------------------------------------------------------------------
Dscripts/docs/1 - begining.md | 184-------------------------------------------------------------------------------
Dscripts/docs/2 - server setup.md | 625-------------------------------------------------------------------------------
Dscripts/docs/alt_protocols.md | 190-------------------------------------------------------------------------------
Dscripts/docs/irc_patch.md | 69---------------------------------------------------------------------
Dscripts/doom/doom.md | 88-------------------------------------------------------------------------------
Dscripts/skel.md | 145-------------------------------------------------------------------------------
Dscripts/starthere.md | 94-------------------------------------------------------------------------------
Dsite/README.md | 104-------------------------------------------------------------------------------
Dsite/build_directory.md | 126-------------------------------------------------------------------------------
Msite/build_directory.py | 2+-
Dsite/genlanding.md | 115-------------------------------------------------------------------------------
Msite/genlanding.py | 21+++++++++++++++++++--
Dsite/news/README.md | 25-------------------------
Dterminal/README.md | 65-----------------------------------------------------------------
Dterminal/docs/ADMIN.md | 103-------------------------------------------------------------------------------
Dterminal/docs/ARCHITECTURE.md | 65-----------------------------------------------------------------
Dterminal/docs/INSTALL.md | 241-------------------------------------------------------------------------------
Mterminal/templates/admin_mail.txt | 2+-
Dtools/README.md | 63---------------------------------------------------------------
Dtools/docs/ADMIN.md | 107-------------------------------------------------------------------------------
Dtools/docs/INSTALL.md | 133-------------------------------------------------------------------------------
Dtools/docs/USER_EXPERIENCE.md | 57---------------------------------------------------------
Dtools/skel/README.md | 58----------------------------------------------------------
Mtools/tools.py | 2+-
58 files changed, 824 insertions(+), 3939 deletions(-)

diff --git a/DOCS_REBUILD_CHANGELOG.md b/DOCS_REBUILD_CHANGELOG.md @@ -0,0 +1,102 @@ +# Changelog da reconstrução da documentação (runv-server) + +Documento em **pt-BR**. Data da passagem: conforme o commit em que este ficheiro foi adicionado. + +## Ficheiros criados (canónico `docs/`) + +| Ficheiro | Função | +|----------|--------| +| [docs/README.md](docs/README.md) | Porta de entrada, ordem de leitura, mapa rápido | +| [docs/00-overview.md](docs/00-overview.md) | Visão geral, limites público/privado, fontes de verdade | +| [docs/01-server-baseline-debian.md](docs/01-server-baseline-debian.md) | Debian, tempo, locale, pré-requisitos | +| [docs/02-admin-access-and-ssh.md](docs/02-admin-access-and-ssh.md) | Modelo root/admin, SSH | +| [docs/03-paths-files-and-state.md](docs/03-paths-files-and-state.md) | Caminhos `/var/lib/runv`, logs, email, web | +| [docs/04-bootstrap-and-base-system.md](docs/04-bootstrap-and-base-system.md) | `starthere.py`, quotas ext4, Apache, UFW | +| [docs/05-tools-and-system-experience.md](docs/05-tools-and-system-experience.md) | `tools.py`, MOTD, skel, jail SSH | +| [docs/06-site-and-apache.md](docs/06-site-and-apache.md) | `genlanding.py`, DocumentRoot, TLS | +| [docs/07-public-members-directory.md](docs/07-public-members-directory.md) | `build_directory.py`, `members.json`, privacidade | +| [docs/08-email.md](docs/08-email.md) | Mailgun, legado msmtp, ficheiros de estado | +| [docs/09-terminal-entre.md](docs/09-terminal-entre.md) | Conta `entre`, fila, limites (não provisiona Unix) | +| [docs/10-user-provisioning-and-admin-ops.md](docs/10-user-provisioning-and-admin-ops.md) | `create_runv_user.py`, fluxo de aprovação | +| [docs/11-daily-operations.md](docs/11-daily-operations.md) | Operação corrente | +| [docs/12-security-and-privacy.md](docs/12-security-and-privacy.md) | Confiança, dados sensíveis | +| [docs/13-troubleshooting.md](docs/13-troubleshooting.md) | Erros frequentes | +| [docs/14-smoke-tests-and-validation.md](docs/14-smoke-tests-and-validation.md) | Verificações seguras | +| [docs/15-glossary-and-reference.md](docs/15-glossary-and-reference.md) | Glossário, índice de scripts | +| [docs/diagrams/architecture.mmd](docs/diagrams/architecture.mmd) | Sequência SSH entre → fila (Mermaid) | +| [docs/diagrams/member-flow.mmd](docs/diagrams/member-flow.mmd) | Fluxo pedido → admin → dados públicos (Mermaid) | + +Alteração mínima **fora** de `docs/` para não quebrar referências em código ou templates: + +- [README.md](README.md) (raiz): ponteiro para `docs/README.md`. +- `tools/tools.py`, `site/build_directory.py`, `email/configure_mailgun.py`, `email/configure_msmtp_legacy.py`: docstrings / mensagens apontam para `docs/…` em vez de `.md` removidos nos módulos. +- `terminal/templates/admin_mail.txt`: linha de ajuda ao admin aponta para `docs/10-user-provisioning-and-admin-ops.md` (antes referia `terminal/docs/ADMIN.md`, removido). + +**Nota:** existiu cópia errónea em `dev-notes/DOCS_REBUILD_CHANGELOG.md`; foi removida — a versão canónica é **sempre** este ficheiro na raiz. + +## Fontes de evidência usadas + +- Código Python em `scripts/admin/`, `terminal/`, `site/`, `tools/`, `email/`, `patches/`. +- `terminal/config.example.toml`, exemplos em `site/example-users.json`. +- Documentação modular **antes da remoção**: `INSTALL.md` (raiz), `site/*.md`, `terminal/docs/*.md`, `tools/docs/*.md`, `email/docs/*.md`, `scripts/**/*.md`, `dev-notes/RUNV_CURRENT_STATE_AUDIT.md`. +- `terminal/docs/ARCHITECTURE.md` (fluxo e componentes). +- Diff e defaults nos scripts (caminhos predefinidos, flags). + +## Contradições identificadas e como foram tratadas + +1. **Cron vs refresh “sem cron”** + - `INSTALL.md` sugeria exemplo de cron para `build_directory.py`. + - `site/README.md` enfatizava refresh via `create_runv_user.py` / `genlanding.py` sem cron. + - **Resolução:** em `docs/07-public-members-directory.md` (e operações diárias) ficam explícitos **dois modos válidos**: regeneração automática nos fluxos de provisionamento/landing **ou** cron/manual — não são mutuamente exclusivos. + +2. **`USO.md` inexistente** + - `terminal/docs/ARCHITECTURE.md` referia `USO.md`, que não existia no repositório. + - **Resolução:** descrito em `docs/09-terminal-entre.md` e neste changelog; o fluxo cobre-se nos docs canónicos. + +3. **Múltiplos `INSTALL.md` por módulo** + - Conteúdo sobreposto entre raiz e `tools/`, `email/`, `terminal/`. + - **Resolução:** um único percurso numerado em `docs/01`–`docs/11`, com secções por componente. + +## O que ficou explicitamente **NÃO VERIFICADO** neste ambiente + +- Execução de `--help` e comportamento em runtime de `scripts/admin/create_runv_user.py`, `terminal/setup_entre.py` e `site/genlanding.py` em **Windows**: falham no import (`fcntl` / `grp` inexistentes). **Verificação plena:** correr em **Debian/Linux** alvo. +- Estado real de um servidor de produção (Apache vhosts, TLS, quotas aplicadas, conteúdo de `/var/lib/runv/users.json`): apenas inferência a partir de defaults no código — qualquer deploy concreto deve ser confirmado no servidor. + +## Verificações executadas (2026-03-22, Windows / PowerShell) + +| Comando | Resultado | +|---------|-----------| +| `python -m compileall -q scripts terminal site tools email patches` | Exit 0 | +| `cd email && python -m pytest tests/ -q` | 11 passed | +| `python site/build_directory.py --users-json site/example-users.json --dry-run` | JSON válido no stdout (`username`, `since`, `path`) | +| `python site/build_directory.py --help` | Exit 0 | +| `python email/configure_mailgun.py --help` | Exit 0 | +| `python scripts/admin/create_runv_user.py --help` | **Falha:** `ModuleNotFoundError: fcntl` | +| `python terminal/setup_entre.py --help` | **Falha:** `ModuleNotFoundError: grp` | +| `python site/genlanding.py --help` | **Falha:** `ModuleNotFoundError: grp` | +| `git status -sb` | Registado no momento da passagem (working tree com `docs/` e alterações pendentes) | + +## Pressupostos dependentes do ambiente (operador) + +- **Um único host Debian** com paths tipo `/var/www/runv.club/html`, `/var/lib/runv/`, `/opt/runv/terminal/` — são defaults no código; outros caminhos exigem flags explícitas. +- **Root/sudo** para bootstrap, `tools.py`, email, `entre`, provisionamento de utilizadores. +- **Decisões de segurança** (firewall, TLS, política de passwords SSH) combinam o que o repo automatiza com o que o operador mantém — ver `docs/02` e `docs/12`. + +## Documentação `.md` removida nesta reconstrução + +Removidos de propósito para evitar duplicação e contradições com `docs/` (lista não exaustiva de paths relativos à raiz do repo): + +- `INSTALL.md` +- `dev-notes/RUNV_MEMBER_BUBBLE_CHANGELOG.md` (changelog local; conteúdo operacional relevante está em `docs/07` e `docs/10`) +- `dev-notes/RUNV_CURRENT_STATE_AUDIT.md` +- `site/README.md`, `site/build_directory.md`, `site/genlanding.md`, `site/news/README.md` +- `terminal/README.md`, `terminal/docs/INSTALL.md`, `terminal/docs/ARCHITECTURE.md`, `terminal/docs/ADMIN.md` +- `tools/README.md`, `tools/skel/README.md`, `tools/docs/INSTALL.md`, `tools/docs/ADMIN.md`, `tools/docs/USER_EXPERIENCE.md` +- `email/README.md`, `email/docs/INSTALL.md`, `email/docs/ADMIN.md`, `email/docs/INTEGRATION.md`, `email/docs/TROUBLESHOOTING.md` +- `scripts/starthere.md`, `scripts/skel.md`, `scripts/create_runv_user.md`, `scripts/del-user.md`, `scripts/admin/perm1.md`, `scripts/doom/doom.md`, `scripts/docs/*.md` + +**Não removido:** `email/.pytest_cache/README.md` (artefacto gerado por pytest; não é documentação do produto). + +## Adequação para um operador novo + +A documentação é **utilizável** para alguém que não escreveu o código, desde que leia `docs/README.md` na ordem sugerida e execute verificações em **Debian** onde os scripts Unix são relevantes. Lacunas conhecidas estão marcadas como NÃO VERIFICADO ou recomendação, sem afirmar CI/deploy que não exista no repositório. diff --git a/INSTALL.md b/INSTALL.md @@ -1,184 +0,0 @@ -# Instalação do servidor runv.club - -Este documento descreve a **ordem recomendada** para preparar um servidor Debian (testado em Debian 13 “trixie”) com os scripts deste repositório, e onde aprofundar a configuração de cada módulo. - -**Pré-requisitos** - -- Acesso **root** (ou `sudo`) no servidor. -- Repositório clonado num caminho fixo (ex.: `/root/runv-server` ou `/opt/runv/src`). Os exemplos abaixo assumem que o diretório raiz do clone é `REPO` — substitua pelo caminho real. - -**Convenções de caminhos no servidor** - -| Caminho | Função | -|--------|--------| -| `/var/lib/runv/users.json` | Metadados dos utilizadores (criado na primeira operação que o use) | -| `/var/lib/runv/users.lock` | Lock para escrita segura em `users.json` | -| `/var/lib/runv/entre-queue` | Fila de pedidos do SSH «entre» | -| `/var/log/runv/` | Logs do terminal `entre` | -| `/opt/runv/terminal/` | Cópia instalada do módulo `terminal/` | - ---- - -## Ordem geral (resumo) - -1. **Bootstrap do sistema** — `scripts/admin/starthere.py` (Apache, SSH, firewall, quotas, pacotes base). -2. **Ferramentas e ficheiros globais** — `tools/tools.py` (MOTD, skel, binários, pacotes do manifest). -3. **Site Apache / landing** — `site/genlanding.py`. -4. **Dados públicos da landing** — `site/build_directory.py` (idealmente em **cron**). -5. **Email de saída (Mailgun API, predefinido)** — `email/configure_mailgun.py` (+ documentação em `email/docs/`; legado SMTP: `configure_msmtp_legacy.py`). -6. **SSH restrito «entre»** — `terminal/setup_entre.py`. -7. **Operação** — `create_runv_user.py`, `update_user.py`, `del-user.py` (e só em cenários controlados: `scripts/doom/doom.py`). - -Os passos 2–6 podem ser ajustados conforme já tiveres Apache ou email configurados; a ordem acima minimiza dependências (Apache antes de publicar; `users.json` antes do cron que o lê). - ---- - -## 1. Bootstrap: `starthere.py` - -**Objetivo:** instalar e preparar Apache, OpenSSH, UFW, quotas, pacotes úteis e hardening básico. - -```bash -cd REPO/scripts/admin -sudo python3 starthere.py --help # rever opções -sudo python3 starthere.py # ou com flags que precisares -``` - -**Nota:** este script **não** configura email (Mailgun/msmtp) nem o utilizador `entre`. Consulta também os Markdown em `scripts/docs/` se existirem no teu clone. - ---- - -## 2. Ferramentas globais: `tools/tools.py` - -**Objetivo:** aplicar manifest de pacotes APT, scripts em `/usr/local/bin`, MOTD, skel de utilizador, etc. - -```bash -cd REPO/tools -sudo python3 tools.py --help -sudo python3 tools.py # execução real (requer root conforme o script) -``` - -Detalhes: `tools/docs/INSTALL.md` (se presente). - ---- - -## 3. Landing Apache: `genlanding.py` - -**Objetivo:** virtual host, site estático, opcionalmente Certbot. - -```bash -cd REPO/site -sudo python3 genlanding.py --help -sudo python3 genlanding.py --domain runv.club # exemplo; ajustar domínio e flags -``` - -Garante que o DNS aponta para o servidor antes de TLS com Certbot. - ---- - -## 4. Dados públicos: `build_directory.py` - -**Objetivo:** gerar ficheiros consumidos pela landing a partir de `/var/lib/runv/users.json`. - -```bash -cd REPO/site -python3 build_directory.py --help -``` - -**Cron (exemplo)** — executar como utilizador com permissão de leitura a `users.json` e escrita no destino web: - -```cron -*/15 * * * * cd /caminho/para/REPO/site && /usr/bin/python3 build_directory.py -``` - -Ajusta intervalo e caminhos conforme a política do servidor. - ---- - -## 5. Email (Mailgun API + opcional legado SMTP): `email/configure_mailgun.py` - -**Objetivo:** estado em `/etc/runv-email.json`, segredos em `/etc/runv-email.secrets.json`, envio via **API HTTP Mailgun** (sem msmtp no caminho predefinido). Modo **SMTP/msmtp** apenas com `--legacy-smtp` ou `configure_msmtp_legacy.py`. - -```bash -cd REPO/email -sudo python3 configure_mailgun.py --help -sudo python3 configure_mailgun.py --dry-run # simular -sudo python3 configure_mailgun.py # aplicar (root) -``` - -O ficheiro `configure_msmtp.py` apenas indica os comandos actualizados (Mailgun ou legado). - -Documentação completa: - -- `email/docs/INSTALL.md` -- `email/docs/ADMIN.md`, `TROUBLESHOOTING.md`, `INTEGRATION.md` -- `email/README.md` - -Scripts auxiliares (legado / diagnóstico): `email/scripts/send_test_mail.sh`, `email/scripts/diagnose_msmtp.sh`. - ---- - -## 6. Terminal SSH «entre»: `setup_entre.py` - -**Objetivo:** utilizador `entre`, cópia do módulo para `/opt/runv/terminal`, drop-in `sshd_config`, filas e logs. - -```bash -cd REPO/terminal -sudo python3 setup_entre.py --help -sudo python3 setup_entre.py -``` - -- Coloca chaves em `~entre/.ssh/authorized_keys` antes de confiar em acessos. -- Integração com email (avisos): `email/docs/INTEGRATION.md` e `terminal/docs/INSTALL.md`. - -Arranque do serviço Python (systemd ou manual) está descrito na documentação do terminal. - ---- - -## 7. Operação: contas runv - -Todos em `REPO/scripts/admin/` (executar como **root** salvo indicação em contrário). - -| Script | Uso | -|--------|-----| -| `create_runv_user.py` | Criar utilizador Unix + home + quota + entrada em `users.json` | -| `update_user.py` | Atualizar metadados / quota / estado | -| `del-user.py` | Remover utilizador e limpar metadados (com locks e confirmações) | - -**Ordem típica na vida real:** após infraestrutura (passos 1–6), usas `create_runv_user.py` para cada membro; `build_directory.py` (cron) mantém o site alinhado com `users.json`. - -### `scripts/doom/doom.py` (perigoso) - -Remove **todas** as contas listadas em `users.json` exceto a indicada por `--keep`. Só em ambientes de teste ou com backups e confirmação explícita. - -```bash -cd REPO/scripts/doom -sudo python3 doom.py --help -``` - ---- - -## Verificação rápida (checklist) - -- [ ] `sshd -t` sem erros após drop-in do `entre`. -- [ ] `apache2ctl configtest` / `apachectl configtest` OK após `genlanding.py`. -- [ ] `systemctl status apache2` e `ssh` ativos. -- [ ] Email: `send_test_mail.sh` ou equivalente a partir da documentação do email. -- [ ] Ficheiro `users.json` coerente e `build_directory.py` gera saída esperada. -- [ ] Login SSH como `entre` executa apenas o menu esperado (ForceCommand). - ---- - -## Documentação por pasta - -| Pasta | Documentos | -|-------|------------| -| `email/` | `README.md`, `docs/INSTALL.md`, `ADMIN.md`, … | -| `terminal/` | `docs/INSTALL.md` | -| `tools/` | `docs/INSTALL.md` | -| `scripts/` | `docs/*.md` (conforme o repositório) | - ---- - -## Nota sobre o código Python - -Os scripts usam `subprocess` com **listas de argumentos** (sem `shell=True` nas invocações analisadas), o que reduz risco de injeção de comando. Antes de atualizar em produção, convém correr `python3 -m compileall` na raiz do repositório e testar com `--dry-run` onde o script o suportar. diff --git a/README.md b/README.md @@ -1,3 +1,7 @@ # runv-server -Repositório de automação e documentação para **runv.club** (pubnix Debian). +Automação e conteúdo para **runv.club** (pubnix Debian): bootstrap, site estático Apache, email, SSH `entre`, provisionamento de contas. + +**Documentação canónica:** [docs/README.md](docs/README.md) + +**Changelog desta reconstrução:** [DOCS_REBUILD_CHANGELOG.md](DOCS_REBUILD_CHANGELOG.md) diff --git a/RUNV_CURRENT_STATE_AUDIT.md b/RUNV_CURRENT_STATE_AUDIT.md @@ -1,224 +0,0 @@ -# RUNV_CURRENT_STATE_AUDIT.md - -Auditoria do **estado actual** do repositório **runv-server**, tratado como linha de base nova. -**Não** se inferem regressões a partir do histórico Git; apenas o que está presente **agora** no working tree e o que foi **verificado** por leitura de ficheiros e comandos indicados. - -**Comandos executados nesta passagem** - -| Comando | Resultado | -|---------|-----------| -| `python -m compileall -q scripts terminal site tools email patches` | Exit code **0** (sem erros reportados). | -| `python -m pytest tests/ -q` em `email/` | **11 passed** em ~0,25 s. | -| `python site/build_directory.py --users-json site/example-users.json --dry-run` | Emitiu JSON no stdout (amostra com `alice`); **não verificado** código de saída no shell (PowerShell reportou -1 sem indicar falha lógica do script). | -| `git status -sb` | `## main...origin/main [ahead 1]` e ` D RUNV_SERVER_AUDIT.md`. | - -**Não executado:** `bandit`, `ruff`, `mypy`, testes fora de `email/tests/`, workflows CI (não existem em `.github/` na raiz do repo — **ver secção 10**). - ---- - -## 1. Resumo executivo - -O snapshot actual contém um conjunto **coerente** de módulos Python (stdlib) e documentação para operar um pubnix Debian: separação **explícita** entre o fluxo SSH `entre` (fila) e o provisionamento via `create_runv_user.py`; geração de dados públicos **filtrada** em `build_directory.py`; fila de pedidos com criação de ficheiro **exclusiva** (`O_EXCL`). Não há `shell=True` / `os.system` / `eval` nos `.py` verificados em `scripts/`, `terminal/`, `site/`, `tools/`, `email/`, `patches/` (além de **comentários** em `tools/tools.py` e `email/lib/mailer.py`). A qualidade de barreira automática global é **fraca** (sem CI na raiz, um único pacote de testes em `email/tests/`). A higiene Git no momento da verificação mostra **um ficheiro de auditoria anterior removido** do índice (`RUNV_SERVER_AUDIT.md`) e ramo **ahead 1** face a `origin/main`. A pasta `.cursor/skills/` existe no disco local e **não** está em `.gitignore` — risco de ruído se alguém fizer `git add .`. - ---- - -## 2. Veredito global - -**Coherent enough to continue** (pt: *coerente o suficiente para continuar*). - -**Justificativa:** invariantes arquitecturais principais estão **reflectidas no código actual** e a árvore **compila**; há testes **passando** onde foram corridos. Os problemas são sobretudo **manutenibilidade** (script de provisionamento muito grande, política duplicada), **ausência de CI**, e **higiene/ignore** — não contradições estruturais que impeçam evolução incremental segura com disciplina. - ---- - -## 3. O que está claramente correcto neste momento - -| Item | Evidência | -|------|-----------| -| `entre_app` declara não criar contas Linux | `terminal/entre_app.py` L5–L6. | -| `entre_app` não invoca `adduser`/`useradd` | Grep sem matches em `terminal/entre_app.py`. | -| Provisionamento de **membros** via `adduser` no script canónico | `scripts/admin/create_runv_user.py` L722–L741 (`run_adduser` com lista de argumentos). | -| Fila: ficheiro novo não substitui existente | `terminal/entre_core.py` `save_request_json` L487–L490 (`O_CREAT\|O_EXCL`). | -| Dataset público mínimo | `site/build_directory.py` L96–L105 (`username`, `since`, `path`, opcional `homepage_mtime`). | -| Caminhos default alinhados entre example e docs de produto | `terminal/config.example.toml` L5–7 (`queue_dir`, `log_file`, `templates_dir`); `build_directory.py` default `--users-json` L31–33 (`/var/lib/runv/users.json`). | -| Compilação Python dos pacotes listados | `compileall` exit 0 (**verificado**). | -| Testes do submódulo email | `pytest` **11 passed** (**verificado**). | - ---- - -## 4. Problemas críticos - -**Nenhum bloqueador estrutural** identificado só com base no código e verificações acima: a separação fila vs provisionamento mantém-se; o output público analisado não inclui email nem fingerprint. - -**Atenção operacional (não é “bug” de código, mas risco de uso):** `create_runv_user.py` omite refresh da landing se `--landing-document-root` não existir (help L1565–L1568). Operador pode assumir lista actualizada sem verificar path — **documentação** deve deixar isso explícito em runbook (parcialmente já no help). - ---- - -## 5. Achados por severidade - -### Alta - -- **(Nenhum)** com evidência de falha de segurança directa no código revisto (subprocess inseguro, leak público de campos sensíveis no `build_directory`, overwrite de fila). - -### Média - -- **M1 — Política de validação duplicada:** `USERNAME_PATTERN`, `EMAIL_PATTERN`, listas de nomes reservados e tipos de chave aparecem em `terminal/entre_core.py` (ex. L33–L74) e em `scripts/admin/create_runv_user.py` (ex. L72–80); comentário em `entre_core.py` L31–L32 admite **não** importar em runtime — risco de **deriva**. - -- **M2 — Concentração de complexidade:** `create_runv_user.py` é ficheiro **muito grande** (ordem de ~2000 linhas com docstring inicial extensa) — **difícil** de rever e testar de ponta a ponta sem suite dedicada (**NOT VERIFIED:** contagem exacta de linhas nesta passagem). - -- **M3 — Ausência de CI na raiz do projecto:** glob `.github/*` na raiz devolveu **0** ficheiros; workflows encontram-se apenas sob `.cursor/skills/...` (não fazem parte do produto runv). **NOT VERIFIED:** existência de CI noutro remoto ou branch. - -### Baixa - -- **L1 — `.gitignore` não ignora `.cursor/`:** `.gitignore` actual L1–28 não menciona `.cursor`; se a pasta `skills` for grande, `git add .` pode poluir o índice. - -- **L2 — `site/README.md` vs `INSTALL.md` sobre cron:** `INSTALL.md` recomenda exemplo de cron para `build_directory.py` (L87–91); `site/README.md` L19 menciona regeneração via `create_runv_user` / `genlanding` “(sem cron)”. São **modos alternativos**, mas a redacção pode confundir quem procura uma única verdade operacional. - -- **L3 — Estado Git:** ` D RUNV_SERVER_AUDIT.md` — ficheiro marcado como removido no índice/working tree no momento do `git status`; resolver antes de push (**snapshot only**). - ---- - -## 6. Segurança - -| Tema | Avaliação | -|------|-----------| -| `shell=True` / `os.system` / `eval` em `.py` dos dirs de produto | **Não encontrado** (grep em `scripts`, `terminal`, `site`, `tools`, `email`, `patches`; excepção: linhas de **comentário** em `tools/tools.py` L5, `email/lib/mailer.py` L5). | -| Fila — corrida / overwrite | **Mitigado** por `O_EXCL` (`entre_core.py` L487–L490). | -| Dados sensíveis no JSON da fila | Presentes no **payload** (`entre_core.py` L512–L518: `email`, `public_key_fingerprint`) — **esperado** para revisão admin; **não** copiados para `members.json` pelo `build_directory.py` (L96–L105). | -| `useradd` no terminal | Apenas para criar o utilizador de sistema **`entre`** em `setup_entre.py` L246–254 — **distinto** do provisionamento de membros. | -| Temp files para `ssh-keygen` | `tempfile.mkstemp` + `unlink` em `finally` (`entre_core.py` L218–L242). | -| Config example `entre` | `config.example.toml` L11–14: `admin_email` vazio, remetente `noreply@runv.club` — não expõe segredos; paths padrão sensatos. | -| Análise estática de segurança (bandit) | **NOT VERIFIED** (não executado). | - ---- - -## 7. Operação - -- **Ordem em `INSTALL.md` L22–31** (bootstrap → tools → site → build público → email → terminal → operações) é **logicamente consistente** com dependências típicas (Apache antes de publicar; metadados antes de consumo no site). - -- **Paths hardcoded** (`/var/lib/runv/...`, `/opt/runv/terminal`, `/etc/runv-email.json`) são **consistentes** entre example TOML, defaults em scripts e `until.md` / `INSTALL.md` — adequados a um deploy Debian único; ambientes multi-tenant exigiriam overrides (**fora do escopo verificado**). - -- **Debian / ext4 / quotas:** `starthere.py` docstring L14–18 restringe automatismo de quota a **ext4** — assumido e documentado; **NOT VERIFIED** em VM. - ---- - -## 8. Documentação vs código - -| Tópico | Situação | -|--------|----------| -| `entre` não cria membros | **Alinhado** (`entre_app.py` L5–L6; sem `adduser` no `entre_app`). | -| `create_runv_user` canónico | **Alinhado** (docstring L3–L37; `run_adduser` L722+). | -| Membros públicos filtrados | **Alinhado** (`build_directory.py` L96–L105). | -| Cron vs hooks de refresh | **Nuance:** dois discursos (`INSTALL` vs `site/README`); **não** contradição lógica, falta harmonização de linguagem. | -| Refresh landing após user | **Código** exige DocumentRoot existente (`create_runv_user.py` L1565–L1568, fluxo ~L1872+); **operadores** devem ler o help — risco de suposição errada. | - ---- - -## 9. Qualidade de código / manutenibilidade - -- **Grande:** `scripts/admin/create_runv_user.py` concentra política, I/O, subprocess, jail, quota, metadata, refresh landing — **serviceable** mas **pesado** para onboarding de novos contribuidores. - -- **Duplicação:** regras de validação entre `entre_core.py` e `create_runv_user.py` (**médio** risco de deriva). - -- **Testes:** `email/tests/test_mailgun_client.py` cobre cliente Mailgun (**11** testes, **verificado**). **Sem** suite equivalente visível para `entre_core`, `build_directory`, ou locks de `users.json` (**NOT VERIFIED:** outros testes escondidos). - -- **“Feio mas seguro” vs “perigoso”:** o código analisado cai sobretudo em **feio mas seguro** no que toca a subprocess e dados públicos; **perigoso** seria `shell=True` com entrada do utilizador — **não observado** nos dirs de produto. - ---- - -## 10. Higiene do repositório (snapshot actual) - -- **`git status -sb`:** `main...origin/main [ahead 1]`; ` D RUNV_SERVER_AUDIT.md`. - -- **`.github/workflows` na raiz:** **ausente** (0 ficheiros em `z:/Code/runv-server/.github/*`). - -- **`.cursor/skills`:** pasta presente sob `.cursor/` no ambiente local (**NOT VERIFIED** tamanho total); **não** listada no `.gitignore`. - -- **`.gitignore`:** ignora `terminal/config.toml`, artefactos de news, segredos de email — **razoável** para deploy. - ---- - -## 11. Matriz: manter / corrigir leve / refactor / rebuild - -| Área | Classificação | Nota | -|------|---------------|------| -| **terminal/** | **Refactor** (leve) | Extrair política partilhada ou testes de contrato reduziriam deriva. | -| **scripts/admin/** | **Fix lightly** + **Refactor** (faseado) | Manter como fonte de verdade; quebrar em módulos seria refactor **sem urgência** se houver testes primeiro. | -| **site/** | **Manter** | `build_directory.py` claro. | -| **tools/** | **Manter** | Manifest + cópias; alinhado a comentários de segurança. | -| **email/** | **Manter** | Testes existentes passam. | -| **docs/** | **Fix lightly** | Harmonizar cron vs “sem cron”; realçar pré-requisito do DocumentRoot. | -| **patches/** | **Manter** | Auxiliar. | - -**Rebuild:** **não** justificado pelo estado actual verificado. - ---- - -## 12. Ficheiros que merecem atenção primeiro - -1. `scripts/admin/create_runv_user.py` — tamanho, refresh condicional da landing. -2. `terminal/entre_core.py` + `scripts/admin/create_runv_user.py` — duplicação de regex/listas. -3. `site/README.md` + `INSTALL.md` — narrativa cron / refresh. -4. `.gitignore` — considerar `.cursor/` ou documentar “nunca adicionar skills ao repo runv”. -5. Estado Git — `RUNV_SERVER_AUDIT.md` removido: decidir commit ou restauração. -6. `until.md` — bom índice; manter coerente com defaults dos scripts. - ---- - -## 13. Próximas 10 acções (ordenadas) - -1. Resolver `git status` (commit ou descartar remoção de `RUNV_SERVER_AUDIT.md`; alinhar `ahead 1`). -2. Garantir que `.cursor/` não entra no histórico do runv (ignore ou política de equipa). -3. Adicionar CI mínimo na **raiz**: `python -m compileall -q …` + `pytest email/tests`. -4. Checklist de release: comparar `USERNAME_PATTERN` / reservados entre `entre_core` e `create_runv_user`. -5. Runbook: “DocumentRoot tem de existir para refresh automático de `members.json`”. -6. Unificar parágrafo sobre cron em `site/README.md` / `INSTALL.md`. -7. (Opcional) `bandit -r scripts terminal site tools email patches` e registar resultados. -8. Smoke: `python3 site/build_directory.py --dry-run` com cópia de `users.json` de teste. -9. Smoke: `python3 -m py_compile terminal/entre_app.py terminal/entre_core.py`. -10. Documentar ausência de `.github/workflows` no projecto ou adicionar um workflow simples. - ---- - -## 14. Smoke tests manuais seguros (próximos) - -- `python -m compileall -q scripts terminal site tools email patches` -- `cd email && python -m pytest tests/ -q` -- `python site/build_directory.py --users-json site/example-users.json --dry-run` -- `python scripts/admin/create_runv_user.py --help` (rever flags de landing/quota) -- `python terminal/setup_entre.py --help` (em máquina de desenvolvimento, sem sudo se possível) - ---- - -## 15. Questões abertas / incertezas - -- **Conteúdo do commit “ahead 1”** local face a `origin/main` — **NOT VERIFIED** (`git show` não executado). -- **Bandit / Ruff / mypy** — **NOT VERIFIED**. -- **Páginas HTML em `public/`** cumprem regra de rodapé em 100% dos ficheiros — **NOT VERIFIED** (não revisto ficheiro a ficheiro). -- **Configuração real em produção** (PAM, Mailgun, Apache) — **NOT VERIFIED**. - ---- - -## 16. Apêndice de evidências - -| ID | Afirmação | Evidência | -|----|-----------|-----------| -| E1 | `entre` não provisiona membro no app | `terminal/entre_app.py` L5–L6 | -| E2 | Sem adduser no entre_app | grep vazio em `terminal/entre_app.py` | -| E3 | Fila atómica | `terminal/entre_core.py` L487–L490 | -| E4 | Payload fila com PII técnico | `terminal/entre_core.py` L512–L518 | -| E5 | Público mínimo | `site/build_directory.py` L96–L105 | -| E6 | adduser para membros | `scripts/admin/create_runv_user.py` L729 | -| E7 | useradd só `entre` | `terminal/setup_entre.py` L246–254 | -| E8 | Default users.json | `site/build_directory.py` L31–33 | -| E9 | Defaults fila/log TOML | `terminal/config.example.toml` L5–7 | -| E10 | Landing default path | `scripts/admin/create_runv_user.py` L1562–L1568 | -| E11 | compileall OK | comando executado, exit 0 | -| E12 | pytest email | 11 passed | -| E13 | Sem shell=True (prod dirs) | grep nos seis caminhos de produto | -| E14 | git status snapshot | `main...origin/main [ahead 1]`, ` D RUNV_SERVER_AUDIT.md` | -| E15 | Sem workflows na raiz | glob `z:/Code/runv-server/.github/*` → 0 | -| E16 | .gitignore actual | `.gitignore` L1–28 | - ---- - -*Fim do relatório — estado actual apenas, sem inferência de histórico.* diff --git a/docs/00-overview.md b/docs/00-overview.md @@ -0,0 +1,48 @@ +# Visão geral + +[← Índice](README.md) + +## O que é o runv-server + +Repositório de **scripts (principalmente Python 3, biblioteca padrão)**, conteúdo estático web e documentação para operar um servidor **pubnix** estilo tilde (**runv.club**) em **Debian**. Não é uma aplicação web monolítica (sem `package.json` na raiz do produto). + +## Âmbito do repositório + +- **Infraestrutura:** bootstrap (`starthere.py`), quotas ext4, Apache, UFW, ferramentas globais (`tools.py`). +- **Site público:** landing estática em `site/public/`, geração de Apache (`genlanding.py`), dados públicos de membros (`build_directory.py`). +- **Email transacional:** Mailgun HTTP por defeito; modo legado SMTP/msmtp (`email/`). +- **Pedidos de conta:** fluxo SSH ao utilizador `entre` (`terminal/`) — **fila em JSON**, sem criar contas Unix automaticamente. +- **Provisionamento canónico:** `scripts/admin/create_runv_user.py` cria utilizador Unix, home, jail, quota, metadados. + +## Resumo arquitetural + +| Componente | Responsabilidade | +|------------|------------------| +| `terminal/` | Recolher, validar, enfileirar pedidos; **não** faz `adduser` de membros. | +| `create_runv_user.py` | **Única** fonte canónica do fluxo de criação de conta membro (ordem documentada na docstring). | +| `users.json` | Metadados dos membros no servidor (`/var/lib/runv/users.json`). | +| Fila `entre-queue/` | Pedidos pendentes de revisão humana antes do provisionamento. | +| `build_directory.py` | Lê `users.json` → gera `members.json` **filtrado** para o site. | + +Diagrama: [diagrams/architecture.mmd](diagrams/architecture.mmd). + +## Ciclo de vida de um novo membro + +1. Visitante liga `ssh entre@…` e preenche o fluxo guiado. +2. Gera-se um ficheiro JSON na fila (`/var/lib/runv/entre-queue/`). +3. **Admin** revê o pedido e executa `create_runv_user.py` (root). +4. Actualiza-se `users.json`; opcionalmente regera-se `DocumentRoot/data/members.json` (constelação na landing). +5. O membro aparece na lista pública **só** com campos não sensíveis. + +Diagrama: [diagrams/member-flow.mmd](diagrams/member-flow.mmd). + +## Fronteira dados públicos / privados + +- **Público (`members.json`):** apenas o que `build_directory.py` escreve: `username`, `since`, `path`, opcionalmente `homepage_mtime` (com `--homes-root`). Ver [07-public-members-directory.md](07-public-members-directory.md). +- **Privado:** email, fingerprint de chave, quotas detalhadas, campos internos de `users.json` **não** são copiados para o JSON público (garantido no código de `build_directory.py`). + +## Fontes de verdade + +1. **Código** dos scripts referidos neste índice. +2. O **`INSTALL.md` da raiz** e a documentação `.md` nos módulos foram **substituídos** por esta árvore `docs/` (conteúdo absorvido e harmonizado; ver `DOCS_REBUILD_CHANGELOG.md`). +3. Docstrings de `starthere.py`, `create_runv_user.py`, `setup_entre.py`, `genlanding.py`, etc. diff --git a/docs/01-server-baseline-debian.md b/docs/01-server-baseline-debian.md @@ -0,0 +1,28 @@ +# Baseline do servidor Debian + +[← Índice](README.md) + +## Obrigatório (implícito nos scripts) + +- **Sistema:** **Debian** (o projecto referencia Debian 13 “trixie” em vários README históricos e docstrings; **não verificado** em cada release). +- **Acesso:** capacidade de executar comandos como **root** (`sudo` ou sessão root) para bootstrap, `tools.py`, `genlanding.py`, `setup_entre.py`, `create_runv_user.py`. +- **Python 3** instalado (scripts usam shebang `python3`). + +## Recomendação operacional (não imposta pelo repo) + +- **Hostname** coerente com DNS público se for servir `runv.club` ou outro domínio. +- **Hora:** NTP/chrony para timestamps correctos em logs e `created_at` (o repo **não** configura NTP por si). +- **Locale UTF-8** para terminais e logs legíveis — padrão Debian moderno. + +## Sistema de ficheiros e quotas + +- **`starthere.py`** e a lógica de quota em `create_runv_user.py` assumem **ext4** com **usrquota** no mount que contém `/home` (ou path de sonda). Se o FS não for ext4, a automatização de quota em `starthere.py` **falha de propósito** (mensagem de erro no script). **Evidência:** docstring `scripts/admin/starthere.py` (filesystem ext4). + +## O que o repositório não faz + +- Não escolhe hostname por si. +- Não configura NTP, locale ou timezone como passo dedicado (tratar como **pré-requisito de exploração** ou configuração manual Debian). + +## Próximo passo + +[04-bootstrap-and-base-system.md](04-bootstrap-and-base-system.md) após garantir Debian + root + Python 3. diff --git a/docs/02-admin-access-and-ssh.md b/docs/02-admin-access-and-ssh.md @@ -0,0 +1,31 @@ +# Acesso administrativo e SSH + +[← Índice](README.md) + +## Modelo operacional + +- A maioria dos scripts de infraestrutura exige **root** no servidor alvo. +- O repositório **não** define um “utilizador admin” específico além do que o Debian/OpenSSH já permitem: tipicamente **root com chave SSH** ou utilizador em `sudo`. + +## Facto do repositório + +- **`starthere.py`** documenta que **não** reconfigura SSH além do contexto do bootstrap (ver docstring: não mexe em SSH). +- **`setup_entre.py`** configura SSH **só** para o utilizador especial `entre` (drop-in, PAM opcional, modos de auth documentados no script). + +## Recomendação de segurança (genérica, não codificada no repo) + +- Preferir **autenticação por chave** para a conta que usa para administrar o servidor. +- Desactivar login root por palavra-passe em produção se a política o exigir — **não** é alteração feita automaticamente por estes scripts. + +## Distinção + +| Tema | Origem | +|------|--------| +| Chaves do **admin** no servidor | Política do operador / Debian | +| Chave pública no **pedido `entre`** | Recolhida pelo fluxo `entre`, instalada **só** quando `create_runv_user.py` cria o membro | + +## Relação com scripts + +Sem root/sudo não é possível: `starthere.py`, `tools.py`, `genlanding.py` (sem `--dry-run`), `setup_entre.py`, `create_runv_user.py`. + +Próximo: [03-paths-files-and-state.md](03-paths-files-and-state.md). diff --git a/docs/03-paths-files-and-state.md b/docs/03-paths-files-and-state.md @@ -0,0 +1,32 @@ +# Caminhos, ficheiros e estado + +[← Índice](README.md) + +## Caminhos canónicos no servidor + +| Caminho | Função | Gerado / versionado | +|---------|--------|---------------------| +| `/var/lib/runv/users.json` | Lista de metadados dos membros (fonte para `build_directory.py`) | **Gerado** na primeira operação que use; **nunca** commitar com dados reais | +| `/var/lib/runv/users.lock` | Lock `flock` para escrita segura em `users.json` | Gerado em uso | +| `/var/lib/runv/entre-queue/` | Fila de pedidos JSON do SSH `entre` | Gerado; ficheiros por `request_id` | +| `/var/log/runv/entre.log` | Log do fluxo `entre` (configurável via TOML) | Gerado | +| `/opt/runv/terminal/` | Instalação do módulo `terminal/` (`setup_entre.py`) | Cópia a partir do repo; `config.toml` gerado localmente | +| `/etc/runv-email.json` | Estado público de configuração de email | Gerado por `configure_mailgun.py` | +| `/etc/runv-email.secrets.json` | Segredos (API keys, etc.) | Gerado; **0600**, root; **nunca** commitar | +| `/var/www/runv.club/html` | DocumentRoot **predefinido** em produção (`genlanding.py`, default `--landing-document-root` em `create_runv_user.py`) | Gerado no servidor; não é o mesmo que `site/public/` no clone | + +**Evidência:** `docs/04-bootstrap-and-base-system.md`, `terminal/config.example.toml`, defaults em `site/genlanding.py`, `site/build_directory.py`, `scripts/admin/create_runv_user.py`. + +## No clone do Git + +- **`site/public/data/members.json`:** no repositório deve permanecer lista vazia `[]` (placeholder); dados reais vêm de `build_directory.py` no deploy. +- **`terminal/config.toml`:** no `.gitignore`; usar `config.example.toml` + `gen_config_toml.py` / `setup_entre.py`. + +## O que nunca commitar + +- `runv-email.secrets.json` (qualquer cópia) +- `users.json` com dados reais +- Ficheiros JSON da fila com PII +- Chaves privadas SSH + +Próximo: [04-bootstrap-and-base-system.md](04-bootstrap-and-base-system.md). diff --git a/docs/04-bootstrap-and-base-system.md b/docs/04-bootstrap-and-base-system.md @@ -0,0 +1,27 @@ +# Bootstrap e sistema base + +[← Índice](README.md) + +## Script: `scripts/admin/starthere.py` + +**O que faz** (docstring do script): actualiza APT; instala pacotes úteis; limpeza `autoremove`/`autoclean`; activa Apache2; se UFW inactivo, permite SSH/80/443 e activa UFW; descobre o filesystem que contém `/home`; adiciona `usrquota` ao `fstab` em ext4; remount / quotacheck / quotaon; activa quotas de utilizador. + +**O que não faz** (mesma docstring): não purga pacotes arbitrariamente; **não** configura email; **não** cria utilizadores; **não** mexe no SSH para além do contexto descrito; não instala stack de email. + +## Execução + +```bash +cd REPO/scripts/admin +sudo python3 starthere.py --help +sudo python3 starthere.py +``` + +## Ordem sugerida + +O bootstrap é o **primeiro** passo lógico antes de `tools.py`, site, email e `entre` (ver [00-overview.md](00-overview.md) e ordem em documentação histórica absorvida). + +## Pressupostos + +- **ext4** no volume onde `/home` (ou path de sonda) reside — caso contrário o script aborta a parte de quotas automáticas. + +Próximo: [05-tools-and-system-experience.md](05-tools-and-system-experience.md). diff --git a/docs/05-tools-and-system-experience.md b/docs/05-tools-and-system-experience.md @@ -0,0 +1,32 @@ +# Ferramentas e experiência de sistema + +[← Índice](README.md) + +## Script: `tools/tools.py` + +**Função:** orquestrar no servidor Debian: + +1. Pacotes APT listados em `tools/manifests/apt_packages.txt` (alias `chat` → pacote `weechat`). +2. Cópia de `tools/bin/` para `/usr/local/bin` (`runv-help`, `runv-links`, `runv-status`, `chat`, …). +3. MOTD dinâmico: `tools/motd/60-runv` → `/etc/update-motd.d/60-runv`. +4. Modelos para novas contas: `tools/skel/` → `/etc/skel/`. +5. Drop-in SSH para utilizadores jailed: `tools/sshd/90-runv-jailed.conf` → `/etc/ssh/sshd_config.d/`. + +**Princípios declarados no código:** Python stdlib; **sem `shell=True`** em subprocess. + +## Execução + +```bash +cd REPO/tools +sudo python3 tools.py --help +sudo python3 tools.py --dry-run --verbose # simular +sudo python3 tools.py +``` + +Flags úteis: `--force`, `--skip-apt` (ver `--help`). + +## IRC / patches + +A rede IRC “da casa” e o comando `chat` ligam-se a `patches/patch_irc.py` conforme documentação histórica do módulo (código em `patches/`). + +Próximo: [06-site-and-apache.md](06-site-and-apache.md). diff --git a/docs/06-site-and-apache.md b/docs/06-site-and-apache.md @@ -0,0 +1,28 @@ +# Site público e Apache + +[← Índice](README.md) + +## Conteúdo estático + +- **`site/public/`:** HTML, CSS, JS servidos como DocumentRoot após `genlanding.py`. +- A landing faz `fetch("data/members.json")` **relativo à URL** — o ficheiro efectivo é **`DocumentRoot/data/members.json`** (ver `site/public/assets/app.js`). + +## Script: `site/genlanding.py` + +- Configura VirtualHost Apache, `mod_userdir`, `mod_rewrite`, copia `site/public` → DocumentRoot. +- Modo produção: domínio predefinido `runv.club`, DocumentRoot predefinido `/var/www/runv.club/html`. +- Modo `--dev`: `runv.local`, `/var/www/runv-dev/html`. +- Opcional: `--certbot` (incompatível com `--dev`). +- Após cópia, por omissão chama `build_directory.py` para gravar `data/members.json` no DocumentRoot (`--no-refresh-members` para omitir). +- Versão actual do script: constante `VERSION` no ficheiro (ex.: `0.04`). + +## TLS e DNS + +- **Recomendação:** DNS a apontar para o servidor antes de Certbot (documentado historicamente). + +## Constelação (bolhas) + +- Depende de `members.json` no DocumentRoot. +- Após **`create_runv_user.py`:** se `--landing-document-root` existir como directório, o script tenta regerar `data/members.json` e imprime linha **`constelação (bolhas)`** ou **AVISO** se faltar path ou falhar o refresh (**evidência:** código actual em `create_runv_user.py`). + +Próximo: [07-public-members-directory.md](07-public-members-directory.md). diff --git a/docs/07-public-members-directory.md b/docs/07-public-members-directory.md @@ -0,0 +1,44 @@ +# Directório público de membros + +[← Índice](README.md) + +## Script: `site/build_directory.py` + +- **Entrada:** `--users-json` (default `/var/lib/runv/users.json`) — deve ser uma **lista** JSON de objectos. +- **Saída:** `-o` / `--output` (default no repo: `site/public/data/members.json`; em produção típico: `DocumentRoot/data/members.json`). + +## Schema público (campos escritos) + +Cada elemento do array gerado contém: + +| Campo | Origem / notas | +|-------|------------------| +| `username` | De `users.json` | +| `since` | `created_at` se for string; senão `""` | +| `path` | `"/~username/"` | +| `homepage_mtime` | Opcional; só com `--homes-root` (ex. `/home`) | + +**Privacidade:** o script **não** copia email, fingerprint SSH, quotas nem outros campos internos (**evidência:** lógica em `build_directory.py`, função `main`). + +## Consumo no browser + +- `site/public/assets/app.js`: `validMembers()` exige `username` e `path` (strings); `since` opcional para brilho visual. + +## Quando regenerar + +1. **Hooks:** `create_runv_user.py` (se DocumentRoot existir e refresh activo); `genlanding.py` após cópia (por omissão). +2. **Cron (opcional):** exemplo histórico em `INSTALL` — adequado se quiser actualização periódica sem depender só de criar utilizadores. +3. **Manual:** + +```bash +python3 REPO/site/build_directory.py \ + --users-json /var/lib/runv/users.json \ + -o /var/www/runv.club/html/data/members.json +``` + +## Cron vs hooks (sem contradição) + +- **Hooks** actualizam quando corres `create_runv_user` ou `genlanding`. +- **Cron** é **opcional** para alinhar site com `users.json` mesmo sem novos provisionamentos. + +Próximo: [08-email.md](08-email.md). diff --git a/docs/08-email.md b/docs/08-email.md @@ -0,0 +1,32 @@ +# Email (saída) + +[← Índice](README.md) + +## Arquitectura actual + +- **Predefinição:** envio via **Mailgun HTTP API** (`email/configure_mailgun.py`). +- **Estado:** `/etc/runv-email.json` +- **Segredos:** `/etc/runv-email.secrets.json` (permissões restritas; não versionar). + +## Modo legado + +- SMTP via `msmtp` / `sendmail`: flags `--legacy-smtp` ou `configure_msmtp_legacy.py` (detalhes nas docstrings e `--help` dos scripts em `email/`). + +## Biblioteca + +- `email/lib/mailer.py` — envio reutilizável; templates em `email/templates/`. +- Variável `RUNV_EMAIL_ROOT` ou `email_package_root` no JSON para o fluxo `entre` localizar templates. + +## Integração com `entre` + +- Notificações ao admin usam `admin_email` no `config.toml` do terminal **ou** fallback em `/etc/runv-email.json` (comportamento verificado no código de `terminal/` + `email/lib`). + +## O que o repo não é + +- **Não** é MTA completo (não recebe correio para caixas locais de membros como produto deste repositório). + +## Testes + +- Existem testes em `email/tests/` (ex.: `test_mailgun_client.py`). Ver [14-smoke-tests-and-validation.md](14-smoke-tests-and-validation.md). + +Próximo: [09-terminal-entre.md](09-terminal-entre.md). diff --git a/docs/09-terminal-entre.md b/docs/09-terminal-entre.md @@ -0,0 +1,41 @@ +# Terminal SSH «entre» + +[← Índice](README.md) + +## Papel + +- Utilizador Unix especial **`entre`**: ao ligar por SSH, o OpenSSH executa **`ForceCommand`** → `entre_app.py`. +- **Recolhe** dados (username, email, presença online, chave pública), **valida** (`entre_core.py`), **grava** JSON na fila com criação exclusiva (`O_EXCL`), **regista** log, **opcionalmente** notifica admin por email. + +## Limite explícito (facto de código) + +- **`entre_app.py` / `entre_core.py` não criam contas Linux de membros.** O utilizador `entre` em si é criado por **`setup_entre.py`** com `useradd` — isso é **bootstrap do sistema**, não provisionamento de membro. + +## Ficheiros principais + +| Ficheiro | Função | +|----------|--------| +| `entre_app.py` | UI terminal, passos | +| `entre_core.py` | Config TOML, validação, fila, log, sendmail/Mailgun | +| `setup_entre.py` | Instalação: `entre`, `/opt/runv/terminal`, fila, logs, drop-in sshd, modos de auth | +| `config.example.toml` | Modelo; `config.toml` gerado, não versionado no mesmo sítio | +| `templates/*.txt` | Textos editáveis | +| `systemd/*.path`, `*.service` | Opcional (notificações) | + +## Configuração + +- `queue_dir` default `/var/lib/runv/entre-queue` +- `log_file` default `/var/log/runv/entre.log` +- Ver `terminal/config.example.toml` + +## Modos de autenticação (`setup_entre.py`) + +- Documentados na docstring: `shared-password`, `key-only`, `empty-password` (estilo tilde.town), com avisos de segurança explícitos no código. + +## Documentação histórica + +- O antigo `terminal/docs/ARCHITECTURE.md` referia `USO.md`, que **não existia** neste snapshot. O fluxo operacional está consolidado neste documento e em [10-user-provisioning-and-admin-ops.md](10-user-provisioning-and-admin-ops.md) (a documentação modular em `terminal/docs/` foi removida em favor de `docs/` — ver `DOCS_REBUILD_CHANGELOG.md` na raiz). + +Diagrama de sequência: [diagrams/architecture.mmd](diagrams/architecture.mmd). + +Próximo: [10-user-provisioning-and-admin-ops.md](10-user-provisioning-and-admin-ops.md). diff --git a/docs/10-user-provisioning-and-admin-ops.md b/docs/10-user-provisioning-and-admin-ops.md @@ -0,0 +1,31 @@ +# Provisionamento de utilizadores e operações admin + +[← Índice](README.md) + +## Fonte canónica: `scripts/admin/create_runv_user.py` + +- **Único** script de criação de **membros** com a política completa (docstring longa no ficheiro): `adduser`, chaves, `public_html` / gopher / gemini, permissões, jail (Jailkit), quota, metadados em `users.json`. +- Executar como **root** no servidor Debian. + +## Pós-criação: constelação + +- Flag `--landing-document-root` (default `/var/www/runv.club/html`): se o directório **existir**, corre `build_directory.py` para `data/members.json` (salvo `--no-refresh-landing-members`). +- Saída explícita para o operador: linha de **sucesso** com contagem ou **AVISO** com comando sugerido se path em falta ou falha (**código actual**). + +## Outros scripts admin + +| Script | Uso | +|--------|-----| +| `update_user.py` | Actualizar metadados / quota / estado (`users.json` com lock) | +| `del-user.py` | Remover utilizador e metadados | +| `setup_alt_protocols.py` | Reparar protocolos para contas criadas fora do fluxo | +| `scripts/doom/doom.py` | **Perigoso:** remove contas em massa; só testes / com backup | + +## Fluxo de aprovação + +1. JSON na fila `entre-queue/`. +2. Admin valida manualmente. +3. `create_runv_user.py` com dados aprovados. +4. Refresh público conforme [07](07-public-members-directory.md). + +Próximo: [11-daily-operations.md](11-daily-operations.md). diff --git a/docs/11-daily-operations.md b/docs/11-daily-operations.md @@ -0,0 +1,36 @@ +# Operação diária (dia 2+) + +[← Índice](README.md) + +## Adicionar membro + +1. Pedido via `entre` ou processo interno. +2. `sudo python3 scripts/admin/create_runv_user.py …` (ver `--help` no servidor). +3. Confirmar linha **constelação (bolhas)** ou corrigir com `build_directory.py` manual. + +## Actualizar lista pública sem novo membro + +```bash +sudo python3 REPO/site/build_directory.py \ + --users-json /var/lib/runv/users.json \ + -o /var/www/runv.club/html/data/members.json +``` +(Ajustar paths ao teu DocumentRoot.) + +## Após `git pull` no servidor + +- `sudo python3 tools/tools.py` para MOTD/skel/bin conforme alterações. + +## Notícias + +- Colocar `.md` em `site/news/`, executar `site/news/publish_news.py`; depois voltar a copiar `public/` ou correr `genlanding.py` se aplicável. + +## Wiki + +- Fontes em `site/wiki/` com gerador `build_wiki.py` (estrutura no repo). + +## Email + +- Testes documentados no módulo `email/` (`send_test_mail.sh`, etc., se presentes). + +Próximo: [12-security-and-privacy.md](12-security-and-privacy.md). diff --git a/docs/12-security-and-privacy.md b/docs/12-security-and-privacy.md @@ -0,0 +1,25 @@ +# Segurança e privacidade + +[← Índice](README.md) + +## Factos do código (produto) + +- Uso de `subprocess` com **listas de argumentos** — sem `shell=True` nos módulos Python principais verificados em auditorias recentes (`scripts`, `terminal`, `site`, `tools`, `email`, `patches`). +- Fila `entre`: ficheiros criados com **`O_CREAT|O_EXCL`** para evitar sobrescrever pedidos (`entre_core.py`). +- **`members.json` público:** apenas campos acordados em `build_directory.py` — ver [07-public-members-directory.md](07-public-members-directory.md). + +## Fila vs site público + +- JSONs em `entre-queue/` contêm dados para **revisão admin** (incl. email, chave pública, fingerprint no payload). +- Esses campos **não** devem aparecer no `members.json` servido pelo HTTP — o gerador público não os copia. + +## Segredos + +- `/etc/runv-email.secrets.json`, chaves SSH privadas, tokens: **nunca** em Git; seguir `.gitignore`. + +## Recomendações gerais (não automatizadas pelo repo) + +- Firewall, `sshd_config` global, desactivar root login, etc. — política do operador. +- Modo `empty-password` do `entre` é **deliberadamente fraco** para onboarding; docstring de `setup_entre.py` descreve riscos. + +Próximo: [13-troubleshooting.md](13-troubleshooting.md). diff --git a/docs/13-troubleshooting.md b/docs/13-troubleshooting.md @@ -0,0 +1,33 @@ +# Resolução de problemas + +[← Índice](README.md) + +## Bolhas / constelação não aparecem + +1. Confirmar que existe **`DocumentRoot/data/members.json`** (não só `site/public/data/members.json` no clone). +2. Ver mensagem de **`create_runv_user.py`**: AVISO se DocumentRoot inexistente. +3. Browser: em viewport ≤768px o JS **omitido** de propósito (`app.js`). + +## `members.json` vazio + +- `users.json` inexistente → `build_directory.py` assume `[]` com aviso em stderr. +- JSON inválido → script termina com erro. + +## Email não envia (entre / Mailgun) + +- Verificar `/etc/runv-email.json`, segredos, `admin_email`, `email_package_root` / `RUNV_EMAIL_ROOT`. + +## Apache + +- `apache2ctl configtest` após alterações de vhost. +- `genlanding.py` imprime erros se `build_directory` falhar. + +## Quotas + +- FS não ext4 → automatização de `starthere.py` pode recusar; configurar manualmente ou usar volume ext4. + +## SSH `entre` + +- Sessão fecha de imediato: rever PAM / modo `empty-password` / logs em `/var/log/runv/entre.log`. + +Próximo: [14-smoke-tests-and-validation.md](14-smoke-tests-and-validation.md). diff --git a/docs/14-smoke-tests-and-validation.md b/docs/14-smoke-tests-and-validation.md @@ -0,0 +1,46 @@ +# Smoke tests e validação + +[← Índice](README.md) + +## Sintaxe Python (todo o produto) + +```bash +cd REPO +python3 -m compileall -q scripts terminal site tools email patches +``` + +**Esperado:** código de saída `0`. + +## Submódulo email + +```bash +cd REPO/email +python3 -m pytest tests/ -q +``` + +**Esperado:** testes passam (há `test_mailgun_client.py`). + +## `build_directory.py` + +```bash +python3 site/build_directory.py --users-json site/example-users.json --dry-run +``` + +**Esperado:** JSON no stdout com `username`, `since`, `path`. + +## `--help` (requer Unix) + +Vários scripts importam `fcntl` ou `grp` — **não executáveis** em Windows típico: + +- `scripts/admin/create_runv_user.py --help` +- `terminal/setup_entre.py --help` +- `site/genlanding.py --help` + +Em **Debian:** correr os `--help` acima e guardar a saída para operadores. + +## O que **não** existe no repo (facto) + +- **Sem** workflows `.github/workflows` na raiz do projecto runv (verificado por ausência de `.github/` no clone típico). +- **Sem** suite de testes para `entre_core` ou `build_directory` além do que está em `email/tests/`. + +Próximo: [15-glossary-and-reference.md](15-glossary-and-reference.md). diff --git a/docs/15-glossary-and-reference.md b/docs/15-glossary-and-reference.md @@ -0,0 +1,44 @@ +# Glossário e referência rápida + +[← Índice](README.md) + +## Glossário + +| Termo | Significado | +|-------|-------------| +| **entre** | Utilizador SSH especial para pedidos de entrada; não cria membros. | +| **Fila** | Directório `/var/lib/runv/entre-queue/` com JSON por pedido. | +| **members.json** | Dataset **público** para a constelação na landing. | +| **users.json** | Metadados **internos** dos membros no servidor. | +| **DocumentRoot** | Raiz Apache onde `genlanding.py` copia `site/public/`. | +| **REPO** | Caminho do clone (ex. `/opt/runv/src`). | + +## Índice de scripts (principal) + +| Caminho | Descrição curta | +|---------|-----------------| +| `scripts/admin/starthere.py` | Bootstrap APT, Apache, UFW, quotas ext4 | +| `scripts/admin/create_runv_user.py` | Provisionamento canónico de membro | +| `scripts/admin/update_user.py` | Actualizar membro / metadados | +| `scripts/admin/del-user.py` | Remover membro | +| `tools/tools.py` | APT, MOTD, skel, binários locais | +| `site/genlanding.py` | Apache + cópia landing + refresh members | +| `site/build_directory.py` | users.json → members.json público | +| `email/configure_mailgun.py` | Config email Mailgun / legado | +| `terminal/setup_entre.py` | Instalar fluxo `entre` | +| `terminal/entre_app.py` | App ForceCommand | +| `terminal/entre_core.py` | Núcleo validação/fila | + +## Módulos (pastas) + +- `scripts/admin/` — administração +- `site/` — web estático + geradores +- `tools/` — experiência global Debian +- `email/` — envio +- `terminal/` — SSH entre +- `patches/` — patches auxiliares (ex. IRC) + +## Mapa de documentação + +- **Canónico:** esta pasta `docs/`. +- **Changelog da reconstrução:** `DOCS_REBUILD_CHANGELOG.md` na raiz. diff --git a/docs/README.md b/docs/README.md @@ -0,0 +1,43 @@ +# Documentação runv-server + +Índice canónico do repositório **runv-server** (automação para pubnix Debian / runv.club). Tudo em **pt-BR**. Esta pasta é a **porta de entrada**; não dependa de ficheiros `.md` antigos nos módulos (foram removidos nesta reconstrução — ver `DOCS_REBUILD_CHANGELOG.md` na raiz). + +## Ordem de leitura sugerida + +1. [Visão geral](00-overview.md) +2. [Baseline Debian](01-server-baseline-debian.md) +3. [Acesso admin e SSH](02-admin-access-and-ssh.md) +4. [Caminhos e estado](03-paths-files-and-state.md) +5. [Bootstrap](04-bootstrap-and-base-system.md) +6. [Ferramentas globais](05-tools-and-system-experience.md) +7. [Site e Apache](06-site-and-apache.md) +8. [Membros públicos](07-public-members-directory.md) +9. [Email](08-email.md) +10. [Terminal entre](09-terminal-entre.md) +11. [Provisionamento e admin](10-user-provisioning-and-admin-ops.md) +12. [Operação diária](11-daily-operations.md) +13. [Segurança e privacidade](12-security-and-privacy.md) +14. [Resolução de problemas](13-troubleshooting.md) +15. [Smoke tests](14-smoke-tests-and-validation.md) +16. [Glossário e referência](15-glossary-and-reference.md) + +## Mapa rápido + +| Quero… | Documento | +|--------|-----------| +| Entender o que é o projeto | [00-overview.md](00-overview.md) | +| Preparar o servidor Debian | [01](01-server-baseline-debian.md), [04](04-bootstrap-and-base-system.md) | +| Instalar landing Apache | [06-site-and-apache.md](06-site-and-apache.md) | +| Lista de bolhas / `members.json` | [07-public-members-directory.md](07-public-members-directory.md) | +| Pedidos SSH `entre` | [09-terminal-entre.md](09-terminal-entre.md) | +| Criar conta membro | [10-user-provisioning-and-admin-ops.md](10-user-provisioning-and-admin-ops.md) | +| Email Mailgun / legado | [08-email.md](08-email.md) | + +## Diagramas (Mermaid) + +- [diagrams/architecture.mmd](diagrams/architecture.mmd) +- [diagrams/member-flow.mmd](diagrams/member-flow.mmd) + +## Código-fonte + +A documentação descreve scripts em `scripts/`, `terminal/`, `site/`, `tools/`, `email/`. As docstrings e `--help` dos scripts são fonte de verdade complementar (ver [14-smoke-tests-and-validation.md](14-smoke-tests-and-validation.md)). diff --git a/docs/diagrams/architecture.mmd b/docs/diagrams/architecture.mmd @@ -0,0 +1,21 @@ +%% Fluxo SSH entre + fila (alinhado ao código actual; espelha o antigo terminal/docs/ARCHITECTURE.md, removido) +%% Renderizar com qualquer visualizador Mermaid +sequenceDiagram + participant C as Cliente_SSH + participant S as sshd + participant A as entre_app_py + participant Q as entre_queue + participant L as entre_log + participant M as sendmail_ou_Mailgun + + C->>S: autentica_como_entre + S->>A: ForceCommand + A->>C: formulario_guiado + C->>A: respostas + A->>A: validacao_entre_core + A->>Q: JSON_O_EXCL + A->>L: eventos + opt admin_email_configurado + A->>M: notificacao + end + A->>C: despedida diff --git a/docs/diagrams/member-flow.mmd b/docs/diagrams/member-flow.mmd @@ -0,0 +1,20 @@ +%% Ciclo membro: pedido até presença pública (sem automatizar aprovação) +flowchart LR + subgraph pedido [Pedido] + E[SSH_entre] + F[fila_JSON] + E --> F + end + subgraph admin [Admin] + R[revisao_humana] + C[create_runv_user_py] + R --> C + end + subgraph dados [Dados] + U[users_json] + M[members_json_publico] + U --> M + end + F --> R + C --> U + M --> W[HTTP_landing] diff --git a/email/README.md b/email/README.md @@ -1,68 +0,0 @@ -# Email runv.club — envio via Mailgun HTTP API (predefinido) - -**Aviso: o configurador predefinido foi feito para Mailgun.** Não pré-configura credenciais. - -Subsistema **só de envio** para Debian 13: por omissão grava estado em `/etc/runv-email.json` e segredos em `/etc/runv-email.secrets.json`, e envia mensagens pela **API HTTP Mailgun** (Basic Auth `api` + API key). Opcionalmente mantém um modo **legado** com `msmtp` + `sendmail`. - -## O que faz (predefinido) - -- Configura envio **sem** Postfix/Exim/Dovecot — **não** é um MTA completo. -- **Não** recebe email (sem IMAP, sem caixa local). -- Biblioteca Python reutilizável (`lib/mailer.py`) com templates em texto puro; suporte opcional a corpo **HTML** no `send_mail`. - -## O que instala (APT) — só modo legado SMTP - -| Pacote | Papel | -|--------|-------| -| `msmtp` | Cliente SMTP. | -| `msmtp-mta` | Fornece `/usr/sbin/sendmail`. | -| `ca-certificates` | Confiança TLS. | -| `bsd-mailx` | Comando `mail` para testes em CLI. | - -**Mailgun API (predefinido)** não exige estes pacotes. - -## Execução rápida - -```bash -cd /caminho/runv-server/email -sudo python3 configure_mailgun.py -``` - -Legado SMTP: - -```bash -sudo python3 configure_mailgun.py --legacy-smtp -# ou: sudo python3 configure_msmtp_legacy.py -``` - -O ficheiro `configure_msmtp.py` apenas **indica** estes comandos (substituição do antigo fluxo). - -Flags: `--dry-run`, `--verbose`, `--force`, `--test`, `--legacy-smtp`. Detalhes: [docs/INSTALL.md](docs/INSTALL.md). - -## Documentação - -| Ficheiro | Conteúdo | -|----------|-----------| -| [docs/INSTALL.md](docs/INSTALL.md) | Mailgun vs legado, ficheiros, flags, testes, variáveis de ambiente. | -| [docs/ADMIN.md](docs/ADMIN.md) | Alterar remetente, admin, segredos. | -| [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Falhas comuns. | -| [docs/INTEGRATION.md](docs/INTEGRATION.md) | `lib/mailer.py`, eventos, `entre`. | - -## Biblioteca - -Defina `RUNV_EMAIL_ROOT` para a pasta `email/` do repositório (onde estão `lib/` e `templates/`) e importe `lib.mailer`. O configurador grava também `email_package_root` em `/etc/runv-email.json` para o serviço `entre` encontrar o módulo sem variável de ambiente. - -## Checklist manual de verificação (Mailgun) - -- [ ] `sudo ls -l /etc/runv-email.json /etc/runv-email.secrets.json` — **0600**, root. -- [ ] `sudo python3 configure_mailgun.py --test` — email de teste recebido. -- [ ] `email_package_root` no JSON aponta para a pasta `email/` do deploy (para notificações `entre`). -- [ ] Fluxo `entre` com `admin_email` no `config.toml` — notificação ao admin (Mailgun ou sendmail de fallback). - -## Scripts auxiliares (legado / diagnóstico) - -- `scripts/diagnose_msmtp.sh` — diagnóstico msmtp (modo SMTP). -- `scripts/send_test_mail.sh` — teste via `mail`. -- `scripts/netrc_password.py` — usado por `passwordeval` no msmtp (só legado). - -Versão do módulo alinhada ao repositório runv-server. diff --git a/email/configure_mailgun.py b/email/configure_mailgun.py @@ -4,7 +4,7 @@ Configurador de email runv — Mailgun HTTP API (predefinido). Aviso: este script foi feito para Mailgun. Não pré-configura nenhuma credencial. -Executar como root. Ver email/docs/INSTALL.md. +Executar como root. Ver docs/08-email.md no repositório. """ from __future__ import annotations @@ -219,7 +219,7 @@ def print_summary(public: dict[str, Any], *, dry_run: bool) -> None: if dry_run: print(" (dry-run — ficheiros não gravados)") print() - print("Documentação: email/docs/INSTALL.md") + print("Documentação: docs/08-email.md (repositório)") def main() -> int: diff --git a/email/configure_msmtp_legacy.py b/email/configure_msmtp_legacy.py @@ -5,7 +5,7 @@ LEGADO — Instalador/configurador runv.club: envio via msmtp + sendmail (Debian O caminho predefinido do projeto é Mailgun API (`configure_mailgun.py`). Use este script apenas se precisar de SMTP local/msmtp. -Executar como root. Ver email/docs/INSTALL.md. +Executar como root. Ver docs/08-email.md no repositório. """ from __future__ import annotations @@ -460,7 +460,7 @@ def main() -> int: print(f" netrc: {NETRC_PATH} (credenciais — não partilhar)") print(f" estado: {STATE_PATH}") print(f" sendmail: /usr/sbin/sendmail (msmtp-mta)") - print("\nDocumentação: email/docs/INSTALL.md") + print("\nDocumentação: docs/08-email.md (repositório)") print("Teste posterior: sudo python3 email/configure_msmtp_legacy.py --test") print("Mailgun (recomendado): sudo python3 email/configure_mailgun.py") return 0 diff --git a/email/docs/ADMIN.md b/email/docs/ADMIN.md @@ -1,52 +0,0 @@ -# Administração — email runv.club - -**Predefinição:** Mailgun HTTP API (`configure_mailgun.py`). Secção final: **legado SMTP/msmtp**. - -## Mailgun — alterar remetente (From) - -1. Edite `/etc/runv-email.json` — campo `default_from`. -2. O endereço deve estar autorizado no domínio Mailgun configurado. -3. Valide: `sudo python3 configure_mailgun.py --test`. - -**Não** coloque a API key neste ficheiro. - -## Mailgun — alterar email do administrador - -1. Edite `admin_email` em `/etc/runv-email.json`. -2. Fluxo **entre:** com `admin_email` vazio no `/opt/runv/terminal/config.toml`, o `entre_app` usa o mesmo `admin_email` do JSON — não precisa duplicar. Os avisos do entre usam *From* **`noreply@runv.club`** por omissão (ou `mail_from` no TOML). - -## Mailgun — rodar API key ou região - -1. Para nova key: edite `/etc/runv-email.secrets.json` (0600) **ou** defina `RUNV_MAILGUN_API_KEY` no ambiente do processo. -2. Para mudar domínio/região: edite `/etc/runv-email.json` (`mailgun_domain`, `mailgun_region`, `api_base_url` coerente: `https://api.mailgun.net` vs `https://api.eu.mailgun.net`). -3. Recomendado: voltar a correr `sudo python3 configure_mailgun.py --force` para prompts guiados. - -## Mailgun — reenviar teste - -```bash -sudo python3 /caminho/runv-server/email/configure_mailgun.py --test -``` - -## Legado SMTP — alterar remetente (From) - -1. Edite `/etc/msmtprc` na conta `runv`: linha `from ...`. -2. Actualize `/etc/runv-email.json` campo `default_from`. -3. Valide com `sudo python3 configure_msmtp_legacy.py --test` ou envio via `mail`. - -## Legado SMTP — credenciais - -- Senha/token **só** em `/root/.netrc` (ou `configure_msmtp_legacy.py` com `--force`). -- **Nunca** coloque senhas em `/etc/runv-email.json` em claro. - -## Integrar outros scripts - -Ver [INTEGRATION.md](INTEGRATION.md). Resumo: `RUNV_EMAIL_ROOT` ou `email_package_root` no JSON; usar `lib.mailer.send_mail`. - -## Aliases msmtp (só legado) - -- **msmtp** expande aliases — útil para `mail root` → admin. -- **`newaliases`** (estilo Sendmail) **não** actualiza `/etc/msmtp_aliases`. - -## Log (legado) - -- `/var/log/msmtp.log` quando usa msmtp. diff --git a/email/docs/INSTALL.md b/email/docs/INSTALL.md @@ -1,134 +0,0 @@ -# Instalação — módulo email runv.club - -**Aviso: o configurador predefinido foi feito para Mailgun.** Não embute credenciais, domínios nem chaves — tudo é pedido em tempo de configuração. - -Debian 13 (ou próximo). **Apenas envio** — caminho predefinido **Mailgun HTTP API** (Basic Auth: utilizador `api`, palavra-passe = API key). O modo **SMTP/msmtp + sendmail** permanece disponível como **legado**, desativado por predefinição. - -## O que o predefinido faz (Mailgun) - -- Grava metadados em **`/etc/runv-email.json`** (0600, root): domínio Mailgun, região da API (o configurador fixa **`us`** e `https://api.mailgun.net/`), remetente padrão, email do admin, tipo de chave, caminho da pasta `email/` do repositório (`email_package_root`), etc. **Sem API key neste ficheiro.** -- Grava segredos em **`/etc/runv-email.secrets.json`** (0600, root): apenas `mailgun_api_key`. **Não partilhar nem fazer backup deste ficheiro para repositórios públicos.** - -### API key em variável de ambiente (opcional) - -Em tempo de execução, **`RUNV_MAILGUN_API_KEY`** (se definida) **tem prioridade** sobre o ficheiro de segredos. Útil para systemd ou contentores; o estado público pode continuar a referir `api_key_source: file` — o runtime usa na mesma a env quando presente. - -### Mailgun: SMTP vs HTTP API - -- **Credenciais SMTP** do painel Mailgun são para clientes SMTP (ex.: msmtp); **não** são o mesmo fluxo que a API HTTP. -- A **HTTP API** usa autenticação **HTTP Basic**: username fixo **`api`**, password = **API key** (primary ou domain sending key). -- **US:** `https://api.mailgun.net/v3/<domínio>/messages` (o configurador usa sempre este endpoint; é o mesmo eixo que o SMTP **`smtp.mailgun.org`** nas credenciais SMTP do painel.) -- **EU:** `https://api.eu.mailgun.net/v3/<domínio>/messages` — só para contas/domínios alojados na região UE; nesse caso **edite** `mailgun_region` (`eu`) e `api_base_url` em `/etc/runv-email.json` após correr o script, ou a API devolverá erros de autenticação/domínio. - -### IP allowlist (API) - -Se no painel Mailgun estiver activa a **restrição por IP** para a API, qualquer servidor que chame `api.mailgun.net` tem de ter o **seu IP público** na lista. Sem isso, a API pode responder **401** / «Invalid private key» / **Forbidden** mesmo com chave e domínio correctos. Inclua o IP da VPS (ou desactive a allowlist para testes). - -### Obter uma API key - -1. Painel Mailgun → domínio → **Domain settings** / **Sending API keys**. -2. Preferir **domain sending key** (menor privilégio) se só precisar de enviar desse domínio; **primary API key** também funciona se tiver permissão de envio. - -Para validar a **primary** no painel ou com `curl`, a listagem de domínios usa **`GET /v4/domains`** (US ou EU). A **domain sending key** não serve para esse endpoint; o envio do runv usa **`POST /v3/<domínio>/messages`** (já implementado em `lib/mailgun_client.py`). - -## Executar o configurador (predefinido) - -```bash -cd /caminho/para/runv-server/email -sudo python3 configure_mailgun.py -``` - -No arranque é mostrado o aviso de que o script foi feito para Mailgun e **não** pré-configura credenciais. - -O script pergunta: - -- tipo de chave (domain sending vs primary); -- domínio de envio Mailgun (ex.: `mg.exemplo.com`); -- API key (**ecoada** ao digitar; deve ser introduzida **duas vezes iguais** para continuar — útil para validar cópia/colar; evite terminais partilhados); -- remetente padrão (From); -- email do administrador (notificações / teste); -- caminho da pasta **`email/`** do repositório (para importações, ex. fluxo `entre` — por omissão é a pasta onde está o script). - -A região da API HTTP **não é perguntada**: fica **`us`** (`api.mailgun.net`). Conta só UE: ajuste manualmente o JSON (ver secção «SMTP vs HTTP API» acima). - -## Ficheiros criados (Mailgun) - -| Ficheiro | Descrição | -|----------|-----------| -| `/etc/runv-email.json` | Metadados **sem** API key. **0600** root. | -| `/etc/runv-email.secrets.json` | `mailgun_api_key`. **0600** root. **World-readable proibido.** | - -## Flags (`configure_mailgun.py`) - -| Flag | Efeito | -|------|--------| -| `--dry-run` | Não grava ficheiros; mostra acções. | -| `--verbose` / `-v` | Log DEBUG (nunca inclui a API key). | -| `--force` / `-f` | Sobrescreve estado/segredos sem confirmar. | -| `--test` | Só envia `templates/system_test.txt` via **Mailgun API** (requer estado existente). | -| `--legacy-smtp` | Delega no configurador **SMTP/msmtp** (`configure_msmtp_legacy.py`). | - -## Teste de envio (API) - -```bash -sudo python3 configure_mailgun.py --test -``` - -Em caso de falha, mensagens típicas: - -- **401 / 403** — Chave incorrecta (não é API HTTP / não é do domínio), região errada (US vs EU), ou **IP allowlist** no painel a bloquear o servidor; confira também se o domínio na URL coincide com o domínio verificado. -- **400** — payload inválido; From não autorizado no domínio; campos em falta. -- **404** — domínio errado ou URL/região incorreta (US vs EU). -- **Timeout / erro de rede** — DNS, firewall ou TLS. - -## Modo legado: SMTP + msmtp + sendmail - -Apenas se precisar de relay SMTP clássico: - -```bash -sudo python3 configure_mailgun.py --legacy-smtp -# ou directamente: -sudo python3 configure_msmtp_legacy.py -``` - -Instala `msmtp`, `msmtp-mta`, `ca-certificates`, `bsd-mailx`, gera `/etc/msmtprc`, `/root/.netrc`, `/etc/msmtp_aliases`, e grava `/etc/runv-email.json` com **`backend: sendmail`**. - -**`configure_msmtp.py`** (sem `_legacy`) é apenas um **encaminhamento** com mensagem a indicar os comandos correctos. - -## Verificação rápida (Mailgun) - -```bash -sudo ls -l /etc/runv-email.json /etc/runv-email.secrets.json -# Ambos devem ser -rw------- root root -sudo python3 configure_mailgun.py --test -``` - -Nunca imprima o conteúdo de `runv-email.secrets.json` em chats ou logs públicos. - -## Biblioteca Python (`lib/mailer.py`) - -Com **`backend: mailgun`** no estado, `send_mail` usa a API Mailgun (urllib, stdlib). Com **`backend: sendmail`** ou estado antigo só com `smtp_host`, usa `sendmail -t -i`. - -Defina **`RUNV_EMAIL_ROOT`** para a pasta `email/` ao importar em scripts (ou use `email_package_root` em `/etc/runv-email.json` — o fluxo `entre` tenta ambos). - -Exemplo: - -```bash -sudo RUNV_EMAIL_ROOT=/caminho/runv-server/email python3 -c " -import sys -sys.path.insert(0, '/caminho/runv-server/email') -from lib.mailer import send_mail -send_mail('voce@exemplo.com', 'Teste', 'Corpo.', from_addr='noreply@exemplo.com') -" -``` - -## Variáveis de ambiente úteis - -| Variável | Uso | -|----------|-----| -| `RUNV_EMAIL_ROOT` | Caminho da pasta `email/` (import `lib.*`). | -| `RUNV_EMAIL_STATE_PATH` | Alternativa a `/etc/runv-email.json` (testes). | -| `RUNV_EMAIL_SECRETS_PATH` | Alternativa ao caminho de segredos indicado no estado. | -| `RUNV_MAILGUN_API_KEY` | API key em memória/ambiente (sobrepor ficheiro de segredos). | - -Próximo: [ADMIN.md](ADMIN.md) para operação corrente. diff --git a/email/docs/INTEGRATION.md b/email/docs/INTEGRATION.md @@ -1,92 +0,0 @@ -# Integração — email com o resto do runv-server - -**Predefinição:** envio via **Mailgun HTTP API** quando `/etc/runv-email.json` indica `backend: mailgun` (ou contém `mailgun_domain` + `mailgun_region` sem `backend: sendmail`). Caso contrário, `lib.mailer` usa **sendmail** (msmtp legado). - -## Variável de ambiente - -Defina **`RUNV_EMAIL_ROOT`** como caminho absoluto para a pasta **`email/`** do repositório (a que contém `lib/` e `templates/`). - -```bash -export RUNV_EMAIL_ROOT=/srv/runv-server/email -``` - -O configurador Mailgun grava também **`email_package_root`** em `/etc/runv-email.json`. O fluxo **`entre`** usa esse campo (ou `RUNV_EMAIL_ROOT`) para importar `lib.mailer` e enviar via API quando Mailgun está activo. - -Em Python, antes de importar: - -```python -import os -import sys -ROOT = "/srv/runv-server/email" -os.environ.setdefault("RUNV_EMAIL_ROOT", ROOT) -sys.path.insert(0, ROOT) -from lib.mailer import ( - send_mail, - send_admin_notice, - send_user_notice, - render_template, -) -from lib import templates as T -``` - -**Nunca** use `shell=True` em `subprocess` para envio; a biblioteca usa urllib (Mailgun) ou `sendmail` com lista de argumentos. - -## API resumida (`lib/mailer.py`) - -| Função | Uso | -|--------|-----| -| `render_template(nome, **kwargs)` | Lê `templates/<nome>.txt` e substitui `{placeholders}`. | -| `send_mail(to, subject, body, from_addr=..., html=..., sendmail=..., headers=..., _state=...)` | Texto; `html` opcional (Mailgun). `to` string ou lista. `_state` evita reler disco (testes / entre). | -| `send_admin_notice(..., html_body=...)` | Template → admin. | -| `send_user_notice(..., html_body=...)` | Template → utilizador. | - -Com **Mailgun**, `sendmail` é ignorado para o transporte (usa API). Com **legado**, `sendmail` por defeito: `/usr/sbin/sendmail`. - -## Mapa evento → template → script - -| Evento | Template(s) | Onde disparar | -|--------|-------------|----------------| -| Novo pedido na fila `entre` | Corpo do email: [`terminal/templates/admin_mail.txt`](../../terminal/templates/admin_mail.txt) (não `email/templates/admin_new_request.txt`). Opcional: `user_request_received` existe em `email/templates/` mas **não** está ligado ao `entre`. | Após `save_request_json` em [`terminal/entre_core.py`](../../terminal/entre_core.py) / [`entre_app.py`](../../terminal/entre_app.py). Email admin via `sendmail_notify`; com Mailgun, tenta **primeiro** `lib.mailer.send_mail` se `/etc/runv-email.json` e `email_package_root` / `RUNV_EMAIL_ROOT` forem válidos. | -| Pedido aprovado (manual) | `user_approved` | Processo admin (manual / futuro). | -| Pedido rejeitado | `user_rejected` (+ `reason`) | Idem. | -| Conta criada | `admin_user_created` → admin; `user_account_created` → utilizador | [`scripts/admin/create_runv_user.py`](../../scripts/admin/create_runv_user.py): `--no-welcome-email` / `--no-admin-create-email` para desactivar cada ramo. | -| Conta removida / banimento | `user_account_community_deactivated` → utilizador | [`scripts/admin/del-user.py`](../../scripts/admin/del-user.py): envia por omissão se existir email em `users.json` e `/etc/runv-email.json` válido; `--no-ban-notify-email` desactiva. Templates `admin_user_deleted` / `user_account_removed` existem mas **não** estão ligados a este script. | -| Erro operacional | `admin_error` | Scripts admin / cron. | -| Quota | `user_quota_warning` | Monitorização / quotas. | -| Teste | `system_test` | `configure_mailgun.py --test` (API) ou legado. | - -## Fluxo **entre** (terminal) - -- **`entre_core.sendmail_notify`** tenta primeiro envio **Mailgun** se `/etc/runv-email.json` for compatível e se `email_package_root` ou `RUNV_EMAIL_ROOT` permitir importar `lib.mailer`. -- Se Mailgun não aplicável ou falhar o ramo API, usa **`sendmail -t -i`** como antes (requer msmtp-mta no modo legado). - -### Coerência de configuração - -| Ficheiro | Campos | -|----------|--------| -| `/etc/runv-email.json` | `backend`, `admin_email`, `default_from`, Mailgun (`mailgun_domain`, …) ou SMTP (`smtp_host`, …), `email_package_root`. | -| `/opt/runv/terminal/config.toml` | `admin_email` (opcional se o JSON já tiver), `mail_from` (omissão **noreply@runv.club**), `sendmail_path`. | - -O destinatário dos avisos do **entre** pode vir só do JSON (`admin_email` vazio no TOML). O *From* dos mesmos avisos é **noreply@runv.club** por omissão (não `entre@runv.club`). Garanta esse endereço autorizado no Mailgun se usar API. - -## `create_runv_user.py` / `del-user.py` - -O **`create_runv_user.py`** envia por omissão: - -1. **Boas-vindas** ao utilizador (`user_account_created`), com instruções SSH; `--no-welcome-email` desactiva. -2. **Aviso ao admin** (`admin_user_created` para `admin_email` no JSON); `--no-admin-create-email` desactiva. - -Requer `/etc/runv-email.json` (com `default_from`, `admin_email` para o ramo admin), segredos Mailgun se aplicável, e pasta `email/` acessível (`email_package_root` ou `RUNV_EMAIL_ROOT`). Para o texto de boas-vindas, `--welcome-ssh-host` ou `RUNV_WELCOME_SSH_HOST` define o hostname SSH sugerido. - -Obtenha `admin_email` / `default_from` de `/etc/runv-email.json` — **não** hardcodar. - -O **`del-user.py`** envia **`user_account_community_deactivated`** ao endereço no campo `email` do registo em `/var/lib/runv/users.json` (lido **antes** de apagar o registo), com texto de desativação por descumprimento das normas da comunidade. Requer `default_from` e pasta `email/` acessível (`RUNV_EMAIL_ROOT` ou `email_package_root`). Com `--skip-metadata` ainda tenta ler o ficheiro de metadados para obter o email. - -## Checklist de integração - -- [ ] `RUNV_EMAIL_ROOT` ou `email_package_root` correcto para serviços Python e **entre**. -- [ ] `sudo python3 configure_mailgun.py --test` (ou legado) com sucesso. -- [ ] Templates revistos (português, placeholders). -- [ ] Nenhum segredo em logs ou `print()` (API key só em ficheiro 0600 ou env). - -Roteiro passo a passo no servidor: [VERIFICATION_CHECKLIST.md](VERIFICATION_CHECKLIST.md). diff --git a/email/docs/TROUBLESHOOTING.md b/email/docs/TROUBLESHOOTING.md @@ -1,62 +0,0 @@ -# Resolução de problemas — email runv.club - -## Mailgun API — 401 / 403 - -- API key errada ou revogada; ou key sem permissão para o **domínio** indicado. -- Confirme região **US vs EU** (URL base no JSON deve coincidir com a conta). - -## Mailgun API — 400 - -- `From` não autorizado para o domínio; campos obrigatórios em falta; domínio não verificado no Mailgun. - -## Mailgun API — 404 - -- Domínio incorrecto no path `/v3/.../messages` ou região trocada (US/EU). - -## Mailgun — timeout / rede - -- Firewall de saída, DNS, ou problemas TLS. Teste conectividade HTTPS ao host `api.mailgun.net` ou `api.eu.mailgun.net`. - -## Mailgun — `entre` não envia - -- Confirme `email_package_root` em `/etc/runv-email.json` aponta para a pasta `email/` do deploy **ou** defina `RUNV_EMAIL_ROOT` no ambiente do serviço. -- Confirme `backend` é `mailgun` (ou domínio+região presentes sem `backend: sendmail`). - -## Legado — autenticação SMTP falha - -- Confirme `user` no `msmtprc` e `login` no `.netrc` para o mesmo `machine <host>` que o **host** SMTP. -- Ver `/var/log/msmtp.log` (sem publicar dados sensíveis). - -## Legado — TLS / STARTTLS - -- Porta **587** + `tls on` + `tls_starttls on`; **465** muitas vezes `tls on` + `tls_starttls off`. -- Confirme `ca-certificates` instalado. - -## `sendmail` não encontrado (modo legado) - -- Instale `msmtp-mta`: `apt-get install -y msmtp-mta`. -- Em modo **Mailgun**, `sendmail` não é necessário para `lib.mailer.send_mail`. - -## `mail` não funciona (legado) - -- Instale `bsd-mailx`. - -## Template ausente (`lib/mailer.py`) - -- Defina `RUNV_EMAIL_ROOT` para a pasta **`email/`** do repositório. - -## Permissões em `/root/.netrc` (legado) - -- **600**, root. `sudo chmod 600 /root/.netrc && sudo chown root:root /root/.netrc`. - -## Permissões em segredos Mailgun - -- `/etc/runv-email.secrets.json` deve ser **0600** root. Nunca world-readable. - -## `passwordeval` / `netrc_password.py` (legado) - -- `/usr/local/lib/runv-email/netrc_password.py` — reinstalar com `configure_msmtp_legacy.py`. - -## `--test` diz que falta estado - -- Corra primeiro `configure_mailgun.py` (ou `configure_msmtp_legacy.py` no modo SMTP) **sem** `--test` para criar `/etc/runv-email.json`. diff --git a/scripts/admin/create_runv_user.py b/scripts/admin/create_runv_user.py @@ -1103,10 +1103,11 @@ def try_refresh_landing_members_json( users_json: Path, homes_root: Path | None, log: logging.Logger, -) -> bool: +) -> tuple[bool, int | None]: """ Regenera public/data/members.json no DocumentRoot da landing (build_directory.py). Falhas são apenas registadas — não aborta o provisionamento. + Devolve (sucesso, número de membros no JSON público ou None se não foi possível contar). """ script = _REPO_ROOT / "site" / "build_directory.py" if not script.is_file(): @@ -1114,7 +1115,7 @@ def try_refresh_landing_members_json( "build_directory.py não encontrado em %s; members.json da landing não atualizado", script, ) - return False + return False, None out = document_root / "data" / "members.json" cmd = [ sys.executable, @@ -1135,14 +1136,23 @@ def try_refresh_landing_members_json( r.returncode, err_tail[:2000] if err_tail else "(sem saída)", ) - return False + return False, None log.info("members.json da landing actualizado em %s", out) if r.stderr and r.stderr.strip(): log.debug("build_directory stderr: %s", r.stderr.strip()[:1500]) - return True + n_public: int | None = None + try: + raw = out.read_text(encoding="utf-8") + parsed = json.loads(raw) + if isinstance(parsed, list): + n_public = len(parsed) + log.info("constelação: %s membro(s) no dataset público (%s)", n_public, out) + except (OSError, json.JSONDecodeError, TypeError) as ex: + log.warning("members.json escrito mas não foi possível validar a lista: %s", ex) + return True, n_public except (OSError, subprocess.TimeoutExpired) as e: log.warning("falha ao executar build_directory: %s", e) - return False + return False, None def print_banner() -> None: @@ -1563,8 +1573,9 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: type=Path, default=Path("/var/www/runv.club/html"), help=( - "DocumentRoot da landing Apache; após criar o utilizador, executa site/build_directory.py " - "para gravar data/members.json (bolinhas no site). Se a pasta não existir, o passo é ignorado." + "DocumentRoot da landing Apache (directório existente para actualizar a constelação); " + "após criar o utilizador, executa site/build_directory.py para gravar data/members.json. " + "Se não existir, o refresh é omitido e é impresso um AVISO com o comando sugerido." ), ) p.add_argument( @@ -1869,11 +1880,12 @@ def main(argv: list[str] | None = None) -> int: append_user_metadata(args.metadata_file, args.lock_file, record, log) members_refreshed = False + members_public_count: int | None = None if not args.no_refresh_landing_members and args.landing_document_root: root = args.landing_document_root.resolve() if root.is_dir(): log.info("=== fase: actualizar members.json da landing (%s)", root) - members_refreshed = try_refresh_landing_members_json( + members_refreshed, members_public_count = try_refresh_landing_members_json( document_root=root, users_json=args.metadata_file, homes_root=args.members_homes_root.resolve() @@ -1882,8 +1894,9 @@ def main(argv: list[str] | None = None) -> int: log=log, ) else: - log.info( - "landing document root inexistente (%s); omitindo build_directory.py", + log.warning( + "DocumentRoot da landing inexistente (%s); constelação/bolhas não actualizadas " + "(corra site/genlanding.py antes ou aponte --landing-document-root para o DocumentRoot real).", root, ) @@ -1910,15 +1923,33 @@ def main(argv: list[str] | None = None) -> int: print(f" URL prevista: {args.base_url.rstrip('/')}/~{user}/") print(f" fingerprint: {fingerprint}") print(f" metadados: {args.metadata_file}") - if members_refreshed: - print( - f" landing members: {args.landing_document_root.resolve() / 'data' / 'members.json'}", - ) - elif not args.no_refresh_landing_members and args.landing_document_root: - dr = args.landing_document_root.resolve() - if dr.is_dir(): + dr_resolved = ( + args.landing_document_root.resolve() if args.landing_document_root else None + ) + out_members = (dr_resolved / "data" / "members.json") if dr_resolved else None + if args.no_refresh_landing_members: + print(" constelação (bolhas): omitida (--no-refresh-landing-members)") + elif dr_resolved is not None: + if not dr_resolved.is_dir(): + print( + f" AVISO constelação: DocumentRoot inexistente ({dr_resolved}) — " + "bolhas não actualizadas. Depois de criar o site: " + f"python3 {_REPO_ROOT / 'site' / 'build_directory.py'} " + f"--users-json {args.metadata_file} -o {out_members}", + file=sys.stderr, + ) + elif members_refreshed: + cnt = ( + f", {members_public_count} membro(s) público(s)" + if members_public_count is not None + else "" + ) + print(f" constelação (bolhas): actualizado{cnt} → {out_members}") + else: print( - " landing members: (falha ao regenerar; ver log — corra build_directory.py manualmente)", + f" AVISO constelação: falha ao regenerar members.json (ver log). " + f"Manual: python3 {_REPO_ROOT / 'site' / 'build_directory.py'} " + f"--users-json {args.metadata_file} -o {out_members}", file=sys.stderr, ) if args.no_quota: diff --git a/scripts/admin/perm1.md b/scripts/admin/perm1.md @@ -1,34 +0,0 @@ -# perm1 — jail para contas existentes - -Script **`scripts/admin/perm1.py`** (root): adiciona utilizadores **uid ≥ 1000** ao grupo **`runv-jailed`** e cria o layout Jailkit em **`/srv/jail/<user>`** com **bind mount** da home real, mais linha em **`/etc/fstab`** (idempotente). - -**Excluídos:** `nobody`, `pmurad-admin`, `entre`. - -**Pré-requisitos:** `tools/tools.py` já aplicado (pacote **jailkit**, drop-in **`90-runv-jailed.conf`**, grupo `runv-jailed`). - -## Opções Jailkit - -- **`--jk-profile`** — perfil passado a `jk_init` quando o jail **ainda não tem** `bin/` (default: **`extendedshell`**, mais completo que `basicshell`). Valores: `extendedshell`, `basicshell`. -- **`--no-jk-init`** — **não** executa `jk_init`; só adiciona ao grupo, garante `home/<user>` no jail, bind e fstab. Exige que **`/srv/jail/<user>/bin`** já exista (jail pré-provisionado); caso contrário o script falha com mensagem explícita. - -Se `bin/` já existir, `jk_init` **não** é voltado a correr (idempotente). - -## Reverter (undo) - -O script **`patches/undoperm.py`** (na raiz do repositório) remove o utilizador de `runv-jailed`, desmonta o bind, apaga a linha em `/etc/fstab` e, só com **`--purge-jail-dir`**, remove `/srv/jail/<user>`. **Não** restaura ficheiros alterados por `jk_init`. - -```bash -sudo python3 patches/undoperm.py --verbose --dry-run -sudo python3 patches/undoperm.py --only-user maria -``` - -## Exemplos perm1 - -```bash -sudo python3 scripts/admin/perm1.py --verbose -sudo python3 scripts/admin/perm1.py --only-user maria --dry-run -sudo python3 scripts/admin/perm1.py --jk-profile basicshell -sudo python3 scripts/admin/perm1.py --no-jk-init --only-user maria -``` - -Após aplicar, teste SSH com um utilizador antes de confiar em produção. diff --git a/scripts/create_runv_user.md b/scripts/create_runv_user.md @@ -1,281 +0,0 @@ -# create_runv_user — provisionamento interno (runv.club) - -**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. - -É a **fonte principal** da política de provisionamento: usa `adduser`, mas o fluxo completo (SSH, `public_html`, jail `runv-jailed`, `README.md` opcional, permissões, quota, metadados, log) está centralizado aqui — sem depender de `adduser.local`, `QUOTAUSER` ou regras em `/etc/adduser.conf`. - -**Ambiente:** execute apenas no servidor (ou VM Debian). O script usa `pwd`, `fcntl`, `adduser`, `ssh-keygen`, `findmnt`/`setquota` — **não é suportado no Windows.** - -### O que o script garante (ordem de execução) - -1. **Criar o usuário** — `adduser --disabled-password` (conta Unix). -2. **Instalar a chave** — `~/.ssh/authorized_keys` com chave validada e modos `700` / `600`. -3. **Preparar `public_html`** — diretório `755` e `~/public_html/index.html` estático (sem JavaScript, sem CDN); não sobrescreve sem `--force-index`. -4. **Preparar Gopher e Gemini** — `~/public_gopher/` com `gophermap` e `~/public_gemini/` com `index.gmi` (modelos em português); não sobrescreve sem **`--force-gopher`** / **`--force-gemini`**. Se existir **`/var/gemini/users`**, aplica **bind mount** **`/var/gemini/users/<user>`** ← **`~/public_gemini`** (via `setup_alt_protocols`; **`--force-gemini`** migra symlink legado). Se essa pasta global não existir, regista **aviso** no log — corra **[`setup_alt_protocols.py`](docs/alt_protocols.md)** no servidor. -5. **Skel** — o Debian **copia `/etc/skel` no passo 1**. O skel instalado por **`tools/tools.py`** **não** inclui `README.md`. Só é criado `~/README.md` com **`--with-readme`**; **`--force-readme`** só faz sentido em conjunto (substituir se já existir). -6. **Permissões** — `apply_runv_permissions` reforça home `755`, `.ssh`, sites públicos e, se existir, `README.md`. -7. **Jail SSH** — por omissão: grupo **`runv-jailed`**, **`/srv/jail/<user>`**, `jk_init extendedshell` (perfil Jailkit; idempotente se `bin/` já existir), **bind** de `/home/<user>` em `/srv/jail/<user>/home/<user>`, linha em **`/etc/fstab`** (idempotente). **Não** aplica a **`entre`** nem **`pmurad-admin`**. **`--no-jail`** desliga. Requer **`tools/tools.py`** já aplicado (jailkit + drop-in sshd). Contas **já existentes**: **[`admin/perm1.py`](admin/perm1.md)**; reversão: **`patches/undoperm.py`**. -8. **Quota** (se ativa), verificação final e metadados JSON. - -**Log** em arquivo (e stderr com `--verbose`) com estas fases numeradas, quota, metadados e verificação final. - -### Email de boas-vindas ao utilizador - -Após criar a conta com sucesso (e antes do aviso de quota parcial, se aplicável), o script tenta enviar o template **`user_account_created`** para o endereço de metadado (`--email`), usando `lib.mailer` e o estado global em **`/etc/runv-email.json`** (Mailgun ou SMTP legado — ver `email/docs/INSTALL.md`). - -- Requisitos: configurador de email já executado; pasta `email/` encontrável (`RUNV_EMAIL_ROOT`, `email_package_root` no JSON, ou repositório ao lado de `scripts/`). -- **`--no-welcome-email`** — não envia. -- **`--welcome-ssh-host HOST`** ou **`RUNV_WELCOME_SSH_HOST`** — inclui no corpo um comando `ssh usuario@HOST` concreto; sem isso, o texto usa um placeholder `<hostname>` e pede ao utilizador que confirme o endereço com o administrador. -- Falhas de envio **não** revertem a criação da conta; ficam só no log. - -## Quota ext4 - -O `create_runv_user.py` descobre **automaticamente** o mount que contém `/home/username` (`findmnt` / `admin/runv_mount.py` no repositório) e aplica `setquota` nesse ponto — tanto se a home está na **raiz `/`** como se **`/home`** é um volume **ext4** separado. O filesystem tem de ser **ext4** com **`usrquota`** (ou **`usrjquota=`**) ativo nesse mount. - -- **Não** usa `xfs_quota` nem assume XFS. -- **Não** altera `/etc/fstab`, **não** remonta, **não** reinicia a máquina, **não** executa `quotaon` por si. -- **Apenas verifica** se o ambiente está pronto e, em caso afirmativo, aplica limites ao utilizador recém-criado. - -### Preparar o sistema (Debian 13) - -**Recomendado:** no servidor novo, correr **`admin/starthere.py`** (ver **[starthere.md](starthere.md)**) como root — instala pacotes, pode ativar Apache/UFW e configura **usrquota** no mount detetado a partir de `/home` (o mesmo critério que este script). - -**Alternativa manual** (se não usar `starthere.py`): - -1. Instalar ferramentas: `sudo apt install quota` -2. Em **`/etc/fstab`**, na linha do mount onde está a home (no caso típico, `/`), acrescentar **`usrquota`** (e opcionalmente `grpquota`) nas opções de mount, por exemplo: - `UUID=... / ext4 defaults,usrquota 0 1` -3. Remontar read-write com nova opção ou reiniciar: - `sudo mount -o remount /` -4. Inicializar ficheiros de quota (ajuste o mountpoint se não for `/`): - `sudo quotacheck -cum /` - (pode demorar; em sistemas com quota já ativa use os flags que a sua política recomendar.) -5. Ativar quotas: - `sudo quotaon -v /` -6. Confirmar: - `findmnt -n -o OPTIONS /` deve mostrar `usrquota` ou `usrjquota=...` - -Só depois disto o `create_runv_user.py` conseguirá aplicar limites automaticamente. - -### Política padrão runv (ajustável por flags) - -| Limite | Padrão | -|--------|--------| -| Blocos soft | 450 MiB | -| Blocos hard | 500 MiB | -| Inodes soft | 10000 | -| Inodes hard | 12000 | - -**Unidades:** os flags `--quota-soft-mb` e `--quota-hard-mb` usam o sufixo histórico `-mb`, mas os valores são **MiB** (mebibytes, 1024² bytes), **não** megabytes decimais (10⁶). Internamente convertem para as unidades de **1 KiB** que o `setquota` usa em ext4 (vfsv0): `kib = mib * 1024`. - -### Comportamento e política de falhas (v1) - -| Situação | Comportamento | -|----------|----------------| -| **Padrão** (sem `--no-quota`) | Após criar o utilizador, tenta aplicar quota. Se o sistema **não** estiver preparado ou `setquota` falhar, a conta **permanece**, metadados gravados com `status: partial_quota` e `quota_status: failed` ou `not_configured`, **saída 3** (`EXIT_INCONSISTENT`), mensagem de aviso forte no stderr. | -| **`--require-quota`** | Antes de `adduser`, verifica ext4 + `usrquota`/usrjquota + `setquota`. Se falhar, **aborta sem criar** utilizador (saída 1). | -| **`--no-quota`** | Não chama `setquota`; metadados com `quota_enabled: false`, `quota_status: skipped`. | - -Não há remoção automática da conta quando só a quota falha; o admin decide (ex.: `del-user.py` ou `deluser` manual). - -## Modo interativo (recomendado) - -Sem argumentos, o script entra em **modo interativo**: mostra o cabeçalho (versão e crédito), faz perguntas e você responde no terminal. - -```bash -sudo python3 /usr/local/bin/create_runv_user.py -``` - -Ou explicitamente: - -```bash -sudo python3 /usr/local/bin/create_runv_user.py --interactive -# ou -sudo python3 /usr/local/bin/create_runv_user.py -i -``` - -Fluxo típico: - -1. Nome de usuário Unix -2. Email administrativo (metadado) -3. Chave SSH: colar **uma linha** OpenSSH ou indicar **caminho** de um arquivo `.pub` -4. Dry-run (só validar, sem criar usuário) — sim/não -5. Se for criar de verdade: sobrescrever `index.html` existente — sim/não -6. Se for criar de verdade: sobrescrever `gophermap` existente (`--force-gopher`) — sim/não -7. Se for criar de verdade: sobrescrever `index.gmi` existente (`--force-gemini`) — sim/não -8. Se for criar de verdade: criar `~/README.md` (`--with-readme`) — sim/não (padrão não) -9. Se criar README: sobrescrever se já existir (`--force-readme`) — sim/não -10. Se for criar de verdade: omitir jail SSH (`--no-jail`) — sim/não (padrão não = aplicar jail) -11. Log verboso — sim/não -12. Criar **sem** quota (`--no-quota`) — sim/não (padrão não) -13. Se for com quota: exigir sistema pronto **antes** de criar (`--require-quota`) — sim/não (padrão não) -14. Confirmação final antes de executar - -`Ctrl+C` cancela. Se responder “não” na confirmação final, o script encerra sem alterar o sistema. - -## Modo não interativo (CLI) - -Nos exemplos com caminho **`admin/create_runv_user.py`**, execute a partir do diretório **`scripts/`** do repositório (ou ajuste o caminho). Em produção, use normalmente **`/usr/local/bin/create_runv_user.py`** após `install`. - -### Criação normal com quota (padrão) - -```bash -sudo python3 admin/create_runv_user.py \ - --username alice \ - --email alice@example.com \ - --public-key "ssh-ed25519 AAAA... comentario" -``` - -Opcional: **`--with-readme`**, **`--no-jail`** (conta sem chroot SSH). - -### Sem quota - -```bash -sudo python3 admin/create_runv_user.py \ - --username alice \ - --email alice@example.com \ - --public-key-file /root/alice.pub \ - --no-quota -``` - -### Exigir quota configurada antes de criar - -```bash -sudo python3 admin/create_runv_user.py \ - -u alice \ - --email alice@example.com \ - --public-key "ssh-ed25519 AAAA..." \ - --require-quota -``` - -Se `usrquota` não estiver ativo em `/`, o script termina **sem** chamar `adduser`. - -### Dry-run - -```bash -python3 admin/create_runv_user.py \ - --username alice \ - --email alice@example.com \ - --public-key "ssh-ed25519 AAAA..." \ - --dry-run -``` - -(Não exige root.) - -### Limites personalizados - -```bash -sudo python3 admin/create_runv_user.py \ - -u bob \ - --email bob@example.com \ - --public-key "..." \ - --quota-soft-mb 400 \ - --quota-hard-mb 450 \ - --quota-inode-soft 8000 \ - --quota-inode-hard 9000 -``` - -### Exemplo: falha por quota não habilitada - -Com utilizador criado com sucesso mas mount **sem** `usrquota`: - -- Stderr: aviso forte de conta criada sem quota aplicada. -- Exit code **3**. -- Em `/var/lib/runv/users.json`: `status: partial_quota`, `quota_status: not_configured` (ou `failed` se `setquota` falhou). - -Versão e crédito: - -```bash -python3 admin/create_runv_user.py --version -``` - -## Pré-requisitos no servidor - -- Debian 13 (ou outro Linux com `adduser` e `deluser`) -- Python 3 (`python3`) -- Pacotes: `openssh-client` (`ssh-keygen`), `adduser`, **`quota`** (para `setquota`), **`util-linux`** (`findmnt`) -- Para quota: ext4 com **`usrquota`** (ou **`usrjquota=`**) no mount que contém `/home` -- Apache com `mod_userdir` já configurado (o script não altera o Apache) -- SSH com chaves (o script não altera `sshd_config`) - -## Instalação - -```bash -sudo install -m 755 admin/create_runv_user.py /usr/local/bin/create_runv_user.py -sudo mkdir -p /var/lib/runv -``` - -Log padrão: `/var/log/runv-user-provision.log` -Metadados: `/var/lib/runv/users.json` - -### Landing (`members.json`) - -Após gravar metadados, o script executa por omissão [`site/build_directory.py`](../site/build_directory.md) (via `python3` no repositório ao lado de `site/`) para actualizar **`data/members.json`** no DocumentRoot da landing (padrão **`/var/www/runv.club/html`**), desde que essa pasta exista. Assim a constelação na página reflecte a nova conta **sem cron**. - -- **`--no-refresh-landing-members`** — não chama `build_directory`. -- **`--landing-document-root PATH`** — outro DocumentRoot (default: `/var/www/runv.club/html`). -- **`--members-homes-root PATH`** — passa `--homes-root` ao `build_directory` (ex. `/home` para `homepage_mtime`). - -## Opções úteis (CLI) - -- `--dry-run` — valida tudo e mostra o plano sem criar usuário -- `--verbose` — mais detalhes no stderr -- `--force-index` — sobrescreve `~/public_html/index.html` se já existir -- `--force-gopher` — sobrescreve `~/public_gopher/gophermap` se já existir -- `--force-gemini` — sobrescreve `~/public_gemini/index.gmi` se já existir e corrige o **bind mount** em `/var/gemini/users/<user>` (migra symlink legado) se necessário -- `--force-readme` — sobrescreve `~/README.md` se já existir (útil se o skel do sistema já criou um README) -- `--no-quota` — não aplica `setquota` -- `--require-quota` — falha antes de `adduser` se quota não estiver disponível -- `--quota-soft-mb`, `--quota-hard-mb`, `--quota-inode-soft`, `--quota-inode-hard` — limites (MiB para blocos) -- `--metadata-file`, `--lock-file`, `--log-file` — caminhos alternativos (ex.: testes em VM) -- `--base-url` — URL base no resumo (padrão `http://runv.club`) -- `--landing-document-root`, `--no-refresh-landing-members`, `--members-homes-root` — ver secção *Landing* acima - -## Metadados JSON (campos de quota) - -Cada registo pode incluir: - -- `quota_enabled` (bool) -- `quota_soft_mb`, `quota_hard_mb` (int ou null) -- `quota_inode_soft`, `quota_inode_hard` (int ou null) -- `quota_filesystem`, `quota_mountpoint` (string ou null) -- `quota_applied_at` (ISO 8601 ou null) -- `quota_status`: `skipped` | `applied` | `failed` | `not_configured` - -## Códigos de saída - -| Código | Significado | -|--------|-------------| -| 0 | Sucesso (utilizador criado e, se aplicável, quota aplicada) | -| 1 | Erro de validação ou argumentos (incl. `--require-quota` com sistema não pronto) | -| 2 | Falha de sistema (subprocess, permissões) antes/desde rollback completo | -| 3 | Estado inconsistente: utilizador criado mas quota não aplicada / não configurada; ou rollback falhou | - -## Como testar no Debian 13 (resumo) - -1. Configure quota no `/` conforme a secção “Preparar o sistema”. -2. `sudo python3 admin/create_runv_user.py --username testquota ... --verbose` -3. `sudo quota -u testquota` ou `repquota /` para ver limites. -4. Teste `--dry-run` sem root. -5. Teste `--require-quota` com fstab **sem** usrquota: deve sair **1** sem criar utilizador. -6. Remova o utilizador de teste com a sua ferramenta de banimento (`admin/del-user.py`) quando terminar. - -## Segurança (resumo) - -- Sem `shell=True`; subprocess só com lista de argumentos. -- Username e caminhos validados; sem path traversal. -- Chave pública validada com `ssh-keygen`; fingerprint SHA256 em metadados. -- Email é só metadado administrativo, não conta Unix. - -## Limitações - -- Quota suportada: **ext4** com quota de utilizador tradicional; outros filesystems recusados com mensagem clara. -- Sem remoção de utilizador por este script (use `admin/del-user.py` a partir de `scripts/` no repositório, ou a cópia em `/usr/local/bin` se instalou). -- O script **não** configura automaticamente fstab nem `quotaon`. -- Backup de `/var/lib/runv/users.json` é manual. - -## Dependências Python - -Nenhuma biblioteca PyPI — apenas a biblioteca padrão (ver `requirements.txt`). diff --git a/scripts/del-user.md b/scripts/del-user.md @@ -1,123 +0,0 @@ -# del-user.py — banimento / remoção de conta (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`. - -- **Não** remove nem altera configuração do Apache ou SSH globalmente. -- Opcionalmente remove a entrada correspondente em `/var/lib/runv/users.json` (mesmo formato que `create_runv_user.py`). -- **Gemini:** **umount** do bind em **`/var/gemini/users/<user>`** se estiver montado, remove a linha correspondente em **`/etc/fstab`**, remove symlink legado ou directório vazio. -- Se a home estiver num **ext4** com **usrquota** ativo, tenta **`setquota`** para repor limites a zero **antes** de `deluser` (mount detetado automaticamente, mesma lógica que `create_runv_user.py` / `runv_mount.py`). Se `setquota` falhar, a remoção da conta continua com aviso em stderr. - -**Ambiente:** servidor **Linux** (Debian). Executar como **root** ou `sudo`. No Windows use só para revisão do código. - -## Objetivo - -- Eliminar o utilizador do sistema (`deluser`). -- Remover a pasta home (`--remove-home`) ou, se pedido, todos os ficheiros detidos pelo UID (`--purge-all-files`). -- Manter o registo interno coerente ao apagar o username do JSON de metadados runv (opcional). - -## Segurança - -- **Nunca** remove `root`. -- Recusa contas **reservadas** (ex.: `www-data`, `nobody`) salvo `--force`. -- Recusa UID **&lt; 1000** (contas de sistema típicas) salvo `--force`. -- Confirmação interativa: tem de **digitar o username** à letra (salvo `-y`/`--yes`). -- Sem `shell=True`; usa `subprocess` com lista de argumentos. - -## Requisitos - -- Pacote Debian `adduser` (fornece o comando `deluser`). -- Python 3 (stdlib: `pathlib`, `fcntl`, `json`, etc.). - -## Uso - -Nos exemplos com **`admin/del-user.py`**, execute a partir do diretório **`scripts/`** do repositório. - -### Simular (sem root) - -```bash -python3 admin/del-user.py -u alguem --dry-run -python3 admin/del-user.py -u alguem --dry-run --verbose -``` - -### Remover (interativo) - -```bash -sudo python3 admin/del-user.py --username spammer -``` - -O script pede que escreva de novo o username para confirmar. - -### Remover sem pergunta (automação / scripts) - -```bash -sudo python3 admin/del-user.py -u spammer --yes -``` - -### Remover também ficheiros do utilizador fora da home - -Cuidado: apaga **todos** os ficheiros detidos por esse UID no sistema. - -```bash -sudo python3 admin/del-user.py -u spammer --yes --purge-all-files -``` - -### Não tocar no `users.json` - -```bash -sudo python3 admin/del-user.py -u spammer --yes --skip-metadata -``` - -### Forçar remoção de conta “de sistema” (perigoso) - -```bash -sudo python3 admin/del-user.py -u algum --yes --force -``` - -## Opções - -| Opção | Significado | -|--------|-------------| -| `-u`, `--username` | Utilizador a remover (obrigatório). | -| `--dry-run` | Só mostra o plano; não exige root. | -| `-v`, `--verbose` | Mais saída (comando `deluser`, etc.). | -| `-y`, `--yes` | Não pede confirmação interativa. | -| `--force` | Ignora bloqueio a contas reservadas / UID &lt; 1000. | -| `--purge-all-files` | Usa `deluser --remove-all-files` em vez de `--remove-home`. | -| `--skip-metadata` | Não altera `/var/lib/runv/users.json`. | -| `--metadata-file` | Caminho alternativo ao JSON de metadados. | -| `--lock-file` | Lock `flock` para escrita do JSON (default runv). | - -## Códigos de saída - -- `0` — sucesso. -- `1` — validação / utilizador inexistente / confirmação cancelada. -- `2` — falha de `deluser` ou erro ao gravar metadados. - -## Limitações - -- Se o utilizador tiver sessões ativas ou processos a correr, `deluser` pode falhar ou comportar-se de forma estranha — termine sessões antes, se necessário. -- `--purge-all-files` pode afetar ficheiros em diretórios partilhados se o UID tiver dono em mais sítios; use com consciência. -- O script **não** revoga tokens ou chaves noutros serviços (só o que o SO e os teus processos fizerem com a conta removida). - -## Exemplo de saída (trecho) - -``` -del-user.py — removendo 'spammer' (UID 1005) - - [exec] deluser --remove-home spammer - [ok] deluser concluído para 'spammer' - [metadata] removido registo de 'spammer' em /var/lib/runv/users.json - ---- Resumo --- - Conta removida: 'spammer' - Próximo passo: verificar se não restam processos desse UID ... -``` - -## Relação com outros scripts - -- **`create_runv_user.py`**: cria conta e acrescenta linha ao JSON. -- **`del-user.py`**: remove conta e remove a linha com o mesmo `username` no JSON (salvo `--skip-metadata`). - -— runv.club diff --git a/scripts/docs/1 - begining.md b/scripts/docs/1 - begining.md @@ -1,184 +0,0 @@ -# runv.club – Initial Server Setup (SSH Hardening Guide) - -## Configurações básicas do servidor (Debian) - -Antes do hardening SSH e dos scripts em `scripts/admin/` (`starthere.py`, `create_runv_user.py`, …), convém deixar o sistema **identificável**, com **hora fiável** e **locale** coerente. Executar como **root** ou `sudo` onde indicado. - -### Nome do host (hostname) - -Escolha um nome estável (ex.: `runv-debian`, `runv-prod`). Evite espaços e caracteres estranhos. - -```bash -sudo hostnamectl set-hostname runv-debian -hostnamectl status -``` - -Garanta que o FQDN local resolve (muitas stacks Debian esperam uma linha `127.0.1.1`): - -```bash -grep -E '^127\.0\.1\.1' /etc/hosts || \ - echo "127.0.1.1 runv-debian" | sudo tee -a /etc/hosts -``` - -(Ajuste `runv-debian` ao hostname que definiu.) - -### Fuso horário e relógio (NTP) - -Para servidores é comum **UTC**; se preferir hora local (ex. Portugal): - -```bash -sudo timedatectl set-timezone Europe/Lisbon -# ou: sudo timedatectl set-timezone UTC -sudo timedatectl set-ntp true -timedatectl status -``` - -Confirme que **NTP sync: yes** e que a data/hora estão corretas. Logs e metadados (`create_runv_user` grava timestamps) ficam alinhados. - -### Locale e teclado (opcional mas útil) - -```bash -sudo dpkg-reconfigure locales -``` - -Selecione pelo menos `en_US.UTF-8` ou `pt_PT.UTF-8` (UTF-8). Para consola: - -```bash -sudo dpkg-reconfigure keyboard-configuration -``` - -### Pacotes e índices APT - -Após timezone/locale: - -```bash -sudo apt update -sudo apt full-upgrade -y -``` - -### Notas para o projeto runv-server - -- **Debian recente** (ex. 13): alinhado a `scripts/requirements.txt` e aos guias em `scripts/*.md`. -- **Quotas ext4:** não edite à mão `fstab`/quotas se for usar **`starthere.py`** — ele deteta o mount de `/home` e prepara `usrquota` de forma coerente com **`create_runv_user.py`**. -- **Documentação interna:** anote hostname, IP público/privado e timezone num sítio da equipa (evita confusão entre VPS X / VPS Y). - ---- - -## Overview -This document describes the initial secure setup of the runv.club server on Debian 13. -The goal is to establish a safe baseline before installing pubnix / shared-hosting services for runv.club. - ---- - -## 1. Create Admin User - -```bash -adduser pmurad -adduser pmurad sudo -``` - -Verify: -```bash -id pmurad -``` - -Switch: -```bash -su - pmurad -``` - -Test: -```bash -sudo whoami -``` - ---- - -## 2. Generate SSH Key (Client) - -```powershell -ssh-keygen -t ed25519 -C "runv-sandbox" -f "$env:USERPROFILE\.ssh\runv-sandbox" -``` - ---- - -## 3. Install Public Key - -```bash -mkdir -p /home/pmurad/.ssh -chmod 700 /home/pmurad/.ssh - -cat > /home/pmurad/.ssh/authorized_keys <<'EOF' -<YOUR PUBLIC KEY> -EOF - -chmod 600 /home/pmurad/.ssh/authorized_keys -chown -R pmurad:pmurad /home/pmurad/.ssh -``` - ---- - -## 4. Test SSH Login - -```powershell -ssh -i "$env:USERPROFILE\.ssh\runv-sandbox" pmurad@SERVER_IP -``` - ---- - -## 5. Check SSH Config - -```bash -sudo sshd -T | grep -E 'passwordauthentication|pubkeyauthentication|permitrootlogin' -``` - ---- - -## 6. Disable Root Login - -```bash -sudo mkdir -p /etc/ssh/sshd_config.d - -sudo tee /etc/ssh/sshd_config.d/99-runv-hardening.conf > /dev/null <<'EOF' -PermitRootLogin no -EOF -``` - -Validate: -```bash -sudo sshd -t -sudo systemctl reload ssh -``` - ---- - -## 7. Disable Password Auth - -```bash -sudo tee /etc/ssh/sshd_config.d/99-runv-hardening.conf > /dev/null <<'EOF' -PermitRootLogin no -PasswordAuthentication no -PubkeyAuthentication yes -EOF -``` - -Reload: -```bash -sudo sshd -t -sudo systemctl reload ssh -``` - -Verify: -```bash -sudo sshd -T | grep -E 'passwordauthentication|pubkeyauthentication|permitrootlogin' -``` - ---- - -## Final State - -- Root login: disabled -- Password login: disabled -- Key authentication: enabled - -Secure SSH baseline achieved. diff --git a/scripts/docs/2 - server setup.md b/scripts/docs/2 - server setup.md @@ -1,625 +0,0 @@ -# runv.club – Fase 2: Apache, UserDir e o primeiro utilizador com site pessoal - -## Objetivo - -Este documento continua a configuração inicial do servidor **runv.club** em **Debian 13**. - -Neste ponto, o servidor já tem: - -- um usuário administrador sem root (`pmurad`) -- login por chave SSH funcionando -- login como root desativado no SSH -- autenticação por senha desativada no SSH - -Agora o objetivo é montar o **primeiro caminho real de publicação web por utilizador** (`~username`): - -- instalar o Apache -- habilitar páginas `~username` -- criar um usuário de teste -- fazer `~/public_html` funcionar -- confirmar que `http://SERVIDOR/~testuser/` carrega - -Esta é a primeira prova concreta de que a máquina serve sites pessoais por conta Unix no runv.club. - ---- - -## O que estamos construindo - -Neste desenho, cada utilizador publica um site a partir do diretório home. - -O padrão clássico é: - -- existe conta de usuário -- o usuário tem uma pasta chamada `public_html` -- o Apache serve essa pasta em: - -```text -http://seu-dominio/~username/ -``` - -Por exemplo, no runv.club, o alvo futuro é: - -```text -http://runv.club/~testuser/ -``` - -Para testes em VM local antes do DNS estar pronto, pode ser algo como: - -```text -http://192.168.x.x/~testuser/ -``` - -ou - -```text -http://runv-debian/~testuser/ -``` - -conforme sua rede e resolução de nomes. - ---- - -## Aviso importante antes de começar - -**Não** instale pacotes extras aleatórios ainda. - -Você **não** precisa agora de: - -- PHP -- MariaDB -- PostgreSQL -- Node.js -- Docker -- Certbot -- servidor de e-mail -- BBJ -- ttbp -- botany - -Isso seria prematuro e desnecessário. - -Por enquanto você só precisa do mínimo para provar: - -1. Apache funciona -2. `mod_userdir` funciona -3. permissões estão corretas -4. publicação a partir do home do usuário funciona - -Até isso funcionar, o resto é ruído. - ---- - -## Passo 1 – Atualizar listas de pacotes - -Entre como `pmurad` e execute: - -```bash -sudo apt update -``` - -Em seguida, atualize os pacotes instalados: - -```bash -sudo apt upgrade -y -``` - -Assim a máquina fica atualizada antes de instalar o Apache. - ---- - -## Passo 2 – Instalar o Apache - -Instale o Apache com: - -```bash -sudo apt install -y apache2 -``` - -Após a instalação, verifique se o serviço está em execução: - -```bash -sudo systemctl status apache2 -``` - -Você deve ver algo como: - -```text -active (running) -``` - -Se quiser uma verificação mais curta: - -```bash -systemctl is-active apache2 -``` - -Resultado esperado: - -```text -active -``` - -Se o Apache não estiver em execução, inicie-o: - -```bash -sudo systemctl start apache2 -``` - -E habilite na inicialização: - -```bash -sudo systemctl enable apache2 -``` - ---- - -## Passo 3 – Testar o Apache a partir da própria VM - -Teste localmente primeiro, de dentro do Debian: - -```bash -curl http://localhost -``` - -Você deve receber HTML da página padrão do Apache. - -Se o `curl` não estiver instalado: - -```bash -sudo apt install -y curl -``` - -Se o Apache estiver funcionando, isso confirma que o servidor web está ativo antes de mexer no `UserDir`. - ---- - -## Passo 4 – Liberar HTTP no firewall - -Se você habilitou o UFW antes, é preciso liberar o tráfego web. - -Verifique o status do firewall: - -```bash -sudo ufw status -``` - -Libere o Apache: - -```bash -sudo ufw allow 'Apache' -``` - -Verifique de novo: - -```bash -sudo ufw status -``` - -Você deve ver a porta 80 liberada. - -Se o UFW ainda não estiver habilitado, não é fatal em um sandbox de VM. Mesmo assim, se você pretende usá-lo, este é o momento certo para abrir o HTTP. - ---- - -## Passo 5 – Testar o Apache de outra máquina - -No seu Windows, abra o navegador e tente: - -```text -http://IP_DA_VM/ -``` - -Exemplo: - -```text -http://192.168.50.120/ -``` - -Você deve ver a página padrão do Apache. - -Se **não** vir, o problema é um destes: - -- Apache não está em execução -- firewall bloqueando a porta 80 -- IP da VM errado -- problema de bridge/rede no Proxmox -- o navegador está batendo na máquina errada - -**Não** continue até a página padrão do Apache funcionar. - ---- - -## Passo 6 – Habilitar o módulo UserDir - -É o recurso que permite: - -```text -/~username/ -``` - -Habilite com: - -```bash -sudo a2enmod userdir -``` - -Verifique a sintaxe da configuração do Apache: - -```bash -sudo apache2ctl configtest -``` - -Resultado esperado: - -```text -Syntax OK -``` - -Em seguida, recarregue o Apache: - -```bash -sudo systemctl reload apache2 -``` - -Neste ponto o Apache já conhece `~username`, mas ainda não há conteúdo de usuário para servir. - ---- - -## Passo 7 – Inspecionar a configuração do UserDir - -O pacote Apache do Debian costuma colocar a configuração do módulo em: - -```text -/etc/apache2/mods-available/userdir.conf -``` - -Leia o arquivo: - -```bash -cat /etc/apache2/mods-available/userdir.conf -``` - -Provavelmente você verá algo como: - -```apache -UserDir public_html -<Directory /home/*/public_html> - AllowOverride FileInfo AuthConfig Limit Indexes - Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec - Require method GET POST OPTIONS -</Directory> -``` - -O ponto principal é este: - -```apache -UserDir public_html -``` - -Isso significa que o Apache procurará conteúdo em: - -```text -/home/NOME_DO_USUARIO/public_html -``` - -Ótimo. É o que queremos. - ---- - -## Passo 8 – Criar um usuário de teste - -Crie uma conta de teste não administrativa para o teste de publicação via UserDir. - -**Não** use `pmurad` nesse teste. -Mantenha papéis de admin e usuário comum separados. - -Crie o usuário: - -```bash -sudo adduser testuser -``` - -Você pode dar uma senha temporária simples para uso em laboratório. - -Confirme que o home existe: - -```bash -ls -ld /home/testuser -``` - ---- - -## Passo 9 – Criar public_html e uma página de teste - -Crie a pasta de publicação: - -```bash -sudo -u testuser mkdir -p /home/testuser/public_html -``` - -Crie um arquivo HTML básico: - -```bash -sudo -u testuser tee /home/testuser/public_html/index.html > /dev/null <<'EOF' -<!doctype html> -<html lang="pt-BR"> -<head> - <meta charset="utf-8"> - <title>testuser no runv.club</title> -</head> -<body> - <h1>Funcionou.</h1> - <p>Esta é a primeira página pessoal no runv.club.</p> -</body> -</html> -EOF -``` - -Verifique o arquivo: - -```bash -ls -l /home/testuser/public_html/index.html -``` - ---- - -## Passo 10 – Ajustar permissões do jeito certo - -É aqui que iniciantes costumam errar. - -O Apache precisa conseguir: - -1. entrar em `/home/testuser` -2. entrar em `/home/testuser/public_html` -3. ler `/home/testuser/public_html/index.html` - -Defina as permissões assim: - -```bash -sudo chmod 755 /home/testuser -sudo chmod 755 /home/testuser/public_html -sudo chmod 644 /home/testuser/public_html/index.html -``` - -Confira: - -```bash -namei -l /home/testuser/public_html/index.html -``` - -Se o `namei` não for encontrado, instale o pacote que o fornece: - -```bash -sudo apt install -y util-linux -``` - -O importante é que o usuário do Apache (`www-data`) consiga percorrer e ler o que precisa. - ---- - -## Passo 11 – Testar a página do usuário localmente - -De dentro do Debian: - -```bash -curl http://localhost/~testuser/ -``` - -A saída esperada deve incluir o seu HTML. - -Você também pode testar só o cabeçalho: - -```bash -curl -I http://localhost/~testuser/ -``` - -Um resultado saudável parece com: - -```text -HTTP/1.1 200 OK -``` - -Se aparecer: - -```text -403 Forbidden -``` - -o problema quase certamente são permissões. - -Se aparecer: - -```text -404 Not Found -``` - -o caminho, o nome de usuário ou a configuração do módulo está errado. - ---- - -## Passo 12 – Testar no navegador - -Agora, no Windows, abra: - -```text -http://IP_DA_VM/~testuser/ -``` - -Se a página carregar, você tem o primeiro caminho real de publicação por utilizador. - -Esse é o marco. - ---- - -## Passo 13 – Entender os três modos de falha mais comuns - -### Falha 1 – 403 Forbidden - -Causa: -- `/home/testuser` está muito restritivo -- permissões de `public_html` erradas -- permissões do arquivo erradas - -Correção: -```bash -sudo chmod 755 /home/testuser -sudo chmod 755 /home/testuser/public_html -sudo chmod 644 /home/testuser/public_html/index.html -``` - -### Falha 2 – 404 Not Found - -Causa: -- módulo `userdir` não habilitado -- nome da pasta errado -- nome do arquivo ausente -- erro de digitação no nome de usuário - -Verifique: -```bash -sudo a2query -m userdir -ls -l /home/testuser/public_html -``` - -### Falha 3 – Página do Apache funciona, mas `~testuser` não - -Causa: -- módulo carregado, mas permissões quebradas -- `userdir.conf` alterado incorretamente -- Apache não recarregado após habilitar o módulo - -Correção: -```bash -sudo apache2ctl configtest -sudo systemctl reload apache2 -``` - ---- - -## Passo 14 – Comandos úteis para diagnóstico - -Se algo falhar, estes são os primeiros comandos a usar. - -### Status do Apache -```bash -sudo systemctl status apache2 -``` - -### Verificar sintaxe da configuração -```bash -sudo apache2ctl configtest -``` - -### Confirmar que o módulo está habilitado -```bash -sudo a2query -m userdir -``` - -### Ler o log de erro do Apache -```bash -sudo tail -n 50 /var/log/apache2/error.log -``` - -### Ler o log de acesso -```bash -sudo tail -n 50 /var/log/apache2/access.log -``` - -### Testar localmente -```bash -curl -I http://localhost/~testuser/ -``` - -Esses logs importam. Não adivinhe quando o Apache já está dizendo o que está quebrado. - ---- - -## Passo 15 – Como fica o sucesso - -Você termina esta fase quando **tudo** isto for verdade: - -- Apache instalado -- Apache inicia automaticamente -- página padrão acessível -- módulo `userdir` habilitado -- `testuser` existe -- `/home/testuser/public_html/index.html` existe -- `http://localhost/~testuser/` retorna `200 OK` -- `http://IP_DA_VM/~testuser/` abre no navegador - -Se algum item for falso, a fase não terminou. - ---- - -## Passo 16 – O que vem depois - -Só depois disso funcionando você deve ir para a camada seguinte: - -1. preparar `/etc/skel` -2. definir os arquivos padrão que novos usuários recebem -3. criar um modelo de homepage inicial mais limpo -4. documentar como novos usuários publicam páginas -5. mais tarde: adicionar ttbp, botany e outras ferramentas sociais - -**Não** pule para software de comunidade antes de provar que o caminho de publicação web funciona. - ---- - -## Resumo rápido de comandos - -Para conveniência, o fluxo principal de novo: - -```bash -sudo apt update -sudo apt upgrade -y -sudo apt install -y apache2 curl -sudo systemctl enable apache2 -sudo systemctl start apache2 - -curl http://localhost - -sudo ufw allow 'Apache' - -sudo a2enmod userdir -sudo apache2ctl configtest -sudo systemctl reload apache2 - -sudo adduser testuser - -sudo -u testuser mkdir -p /home/testuser/public_html - -sudo -u testuser tee /home/testuser/public_html/index.html > /dev/null <<'EOF' -<!doctype html> -<html lang="pt-BR"> -<head> - <meta charset="utf-8"> - <title>testuser no runv.club</title> -</head> -<body> - <h1>Funcionou.</h1> - <p>Esta é a primeira página pessoal no runv.club.</p> -</body> -</html> -EOF - -sudo chmod 755 /home/testuser -sudo chmod 755 /home/testuser/public_html -sudo chmod 644 /home/testuser/public_html/index.html - -curl -I http://localhost/~testuser/ -``` - ---- - -## Nota final - -Este documento é propositalmente detalhado porque iniciantes costumam falhar aqui por motivos chatos: - -- permissões erradas -- esquecer de recarregar o serviço -- IP errado -- firewall não aberto -- testar na ordem errada - -Faça na ordem e funciona. -Faça aleatoriamente e você perde tempo. diff --git a/scripts/docs/alt_protocols.md b/scripts/docs/alt_protocols.md @@ -1,190 +0,0 @@ -# Gopher e Gemini — `setup_alt_protocols.py` - -Script em **`scripts/admin/setup_alt_protocols.py`**: instala e configura **gophernicus** (Gopher, porta **70**) e **molly-brown** (Gemini, TLS, porta **1965**) no Debian, alinhado ao runv.club. - -## Modelo de conteúdo - -| Protocolo | Pasta na home | Ficheiro inicial | URL típica | -|-----------|---------------|------------------|------------| -| **HTTP** (já existente) | `~/public_html/` | `index.html` | `http://runv.club/~user/` | -| **Gopher** | `~/public_gopher/` | `gophermap` | `gopher://runv.club/1/~user` | -| **Gemini** | `~/public_gemini/` | `index.gmi` | `gemini://runv.club/~user/` (canónico: path **`/~user/`**, tilde colado ao nome); `gemini://runv.club/~/user/` redirecciona (**v0.11+**) | - -**Gemini (molly-brown):** `DocBase = /var/gemini`, `HomeDocBase = users`. O conteúdo de **`~/public_gemini`** expõe-se em **`/var/gemini/users/<user>`** com **`mount --bind`** e linha em **`/etc/fstab`** (**v0.13+**). O pacote **Molly Debian** recusa **symlinks** cujo destino fica fora do DocBase (`Refusing to follow symlink … outside of DocBase!` no error log); por isso **não** se usam symlinks para `~/public_gemini`. - -### Gopher vs Gemini: formato do endereço - -- **Gopher (gophernicus):** selectors **`~username/…`** (tilde **colado** ao nome), alinhado com URLs como **`gopher://runv.club/1/~user`**. Não há o mesmo «split» de path que no Molly. -- **Gemini (Molly Brown):** o código Go do Molly trata o primeiro segmento do path como **`~username`** (tilde **colado** ao nome), p.ex. **`/~pmurad/`** → `DocBase/users/pmurad/`. O formato **`/~/pmurad/`** (slash entre `~` e o nome) faz o utilizador ficar **vazio** e devolve **51 Not found**. O `.conf` gerado (**v0.11+**) inclui **`[TempRedirects]`** que redirecciona **`/~/user…` → `/~user…`** para compatibilidade com links antigos. - -### URLs Gemini que *não* são capsules de utilizador - -O Molly **não** espelha o HTTP `mod_userdir` no mesmo path: **`gemini://runv.club/pmurad`** (path **`/pmurad`**) **não** aponta para a home. O capsule correcto é **`gemini://runv.club/~pmurad/`** (path **`/~pmurad/`**). **`gemini://runv.club/~/pmurad/`** (slash extra) **não** é o formato que o `resolvePath` entende sozinho; com **v0.11+**, o `.conf` redirecciona (**30**) esse pedido para **`gemini://runv.club/~pmurad/`** (o cliente deve seguir o redirect). - -## Travessia da home (`755` na política runv) - -Apache (`mod_userdir`), **gophernicus** e **molly-brown** precisam de **execução para «others»** (`o+x`, mínimo) em **cada** componente do caminho até a pasta pública (`~/public_html`, `~/public_gopher`, `~/public_gemini`). O utilizador de runtime **não é o mesmo** em todos: no Debian o pacote **molly-brown** usa **`User=molly-brown`** com **`DynamicUser=yes`** (não `www-data`); o **gophernicus** usa o **`User=`** do unit (tipicamente `gophernicus`) — veja `/lib/systemd/system/gophernicus@.service`. Uma home em **`700`** impede a travessia: **HTTP, Gopher e Gemini** deixam de servir conteúdo (p.ex. Gemini **«Not found»** com `index.gmi` presente). - -- **ACL (POSIX):** se `ls -l` mostrar **`+`** nos modos, há entradas **`getfacl`** além do `chmod`. Uma **mask** restritiva ou ausência de leitura efectiva para «other» / utilizador do serviço pode bloquear o Apache, gophernicus ou Molly mesmo com **`644`/`755`** aparentes. Diagnóstico: `getfacl ~/public_gemini/index.gmi` (e directórios no caminho). - -- **Novas contas:** [`create_runv_user.py`](../admin/create_runv_user.py) aplica **`755`** na home em `apply_runv_permissions`. -- **Backfill:** a partir do **v0.07**, [`setup_alt_protocols.py`](../admin/setup_alt_protocols.py) repõe a home do utilizador para **`755`** quando o modo actual é outro (com registo em log). O **v0.08** corrige a detecção de caminhos Let's Encrypt quando `live`/`archive` são **symlinks**. O **v0.09** introduziu redirects Molly baseados numa leitura incorrecta do README upstream. O **v0.11** corrige **`[TempRedirects]`** para **`/~/user…` → `/~user…`** (alinhado ao `resolvePath` em Go). O **v0.12** documenta **ACL POSIX** na travessia e alarga o **WARNING** do `test -r` do `index.gmi` com indicação a `getfacl` quando `ls` mostra `+`. O **v0.13** substitui **symlinks** Gemini por **bind mounts** + **`fstab`** (compatível com o Molly Debian). Validação **`test -r`** do `gophermap` com o utilizador do serviço gophernicus mantém-se. O **v0.14** gera **`/var/gopher/gophermap`** (menu com **`1~user`**) e **`/var/gemini/index.gmi`** na raiz do DocBase para utilizadores com **`~/public_gopher`**, alinhando **`IRC_PATCH_SKIP_USERS`** (incl. **`entre`**, **`pmurad-admin`**) ao bind Gemini e à limpeza com **`--force`**. -- **Conflito:** [`patches/patch_permissions.py`](../../patches/patch_permissions.py) pode aplicar **`chmod 700`** em cada `/home/<user>` por política de privacidade — isso **quebra** a hospedagem em `public_*` até voltar a alinhar permissões (provisionamento ou `chmod` manual). - -## Let's Encrypt e chave TLS (v0.07+; symlinks v0.08+) - -Quando o certificado Gemini está sob a árvore Let's Encrypt (por defeito **`/etc/letsencrypt/live/<domínio>/fullchain.pem`**), o script aplica **antes** de gravar o `.conf` do molly-brown. A partir do **v0.08**, as raízes `live` e `archive` são **resolvidas** (`resolve(strict=False)`): se `/etc/letsencrypt/live` for um **symlink**, o ajuste de `chmod` / `ssl-cert` nos `privkey` continua a aplicar-se ao caminho canónico correcto (deixa de aparecer no log um salto falso «cert não está sob …/live»). - -| Alvo | Acção | -|------|--------| -| `/etc/letsencrypt/live` | `chmod 755` | -| `/etc/letsencrypt/archive` | `chmod 755` | -| `/etc/letsencrypt/live/<domínio>` | `chmod 755` | -| `/etc/letsencrypt/archive/<domínio>` | `chmod 755` (se existir) | -| `archive/<domínio>/privkey*.pem` | `chgrp ssl-cert`, `chmod 640` | - -O `<domínio>` é o nome do directório pai de `fullchain.pem` (igual ao de `--gemini-cert` quando aponta para LE). Caminhos **fora** de `/etc/letsencrypt/live/` **não** são alterados. - -Se o grupo **`ssl-cert`** não existir no sistema, o script regista **WARNING** e não altera os `privkey*.pem` (instale o pacote que fornece esse grupo, p.ex. em Debian). - -**`certbot renew`** pode repor modos mais restritos nos directórios e chaves. Recomenda-se um script em **`/etc/letsencrypt/renewal-hooks/deploy/`** que volte a aplicar a mesma política, ou reexecutar `setup_alt_protocols.py` após renovações (com as flags que fizer sentido: p.ex. `--skip-install --skip-gopher --skip-backfill` se só quiser TLS + Gemini). - -## Validação final (v0.09+) - -No fim da execução, além de verificar ficheiros e **bind mount** Gemini **como root**: - -- Se **`gophernicus.socket`** estiver **`active`**, o script tenta **`runuser -u <User=do_unit> -- test -r`** no **`gophermap`** da primeira conta da lista **fora** de **`IRC_PATCH_SKIP_USERS`** (o `User=` lê-se de `/lib/systemd/system/gophernicus@.service`; fallback **`gophernicus`**). Falha → **WARNING** (home `755`/`o+x`, `public_gopher` `755`, `gophermap` `644`). -- Se **`molly-brown@`** estiver **`active`**, tenta **`runuser -u www-data -- test -r`** no **`index.gmi`** da mesma amostra (heurística: o unit Debian usa **`molly-brown`** dinâmico; ficheiros **`644`** e pastas **`755`** devem permitir leitura a «others» — ou **ACL** compatível; ver nota **ACL** na secção de travessia). Falha → **WARNING** (`public_gemini` `755`, `index.gmi` `644`, bind `/var/gemini/users/<user>`). Se a amostra ainda for **symlink**, regista **WARNING** de migração (**`--force`**). - -Em **`--dry-run`**, só regista os comandos. Sem **`runuser`** (util-linux), estes passos são omitidos. - -## Utilizadores antigos vs novos - -- **Política:** permissões correctas para **HTTP**, **Gopher** e **Gemini** devem existir **à criação** (fluxo [`create_runv_user.py`](../admin/create_runv_user.py): `apply_runv_permissions`) e ser **reaplicadas** no backfill ([`setup_alt_protocols.py`](../admin/setup_alt_protocols.py): home `755`, `public_gopher` / `public_gemini`, bind mounts Gemini). -- **Novos:** modelos via **`/etc/skel`** (após `tools/tools.py`) e **`create_runv_user.py`** quando o provisionador corre. -- **Antigos / contas só `adduser`:** correr **`setup_alt_protocols.py`** (backfill completo) ou pastas/bind Gemini com **`patches/yetgg.py`** (mesma lista que `patch_irc.py`: união JSON + `/home`) se a infraestrutura de sistema já existir; ou reparar com `create_runv_user` e flags `--force-*` onde fizer sentido. - -## Requisitos Gemini - -- **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 aplicar bind mounts (e linhas `fstab` quando o mount corre). - -## 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.06+):** grava `AccessLog` / `ErrorLog` em **`/var/lib/molly-brown/`** (v0.07+ ajuste LE; v0.08+ LE com symlinks + teste `www-data`; ver secções acima). (`runv.club-access.log`, `runv.club-error.log`). Esse caminho coincide com **`StateDirectory=molly-brown`** do unit Debian: o systemd cria o directório com o dono correcto (**`DynamicUser=yes`**) **antes** do `ExecStart`, sem `chown` manual. **Não** pré-cria pastas nem ficheiros de log (evita conflitos com `LogsDirectory` em `/var/log`). -- **Versões antigas (v0.05):** usavam o drop-in `50-runv-logs.conf` com `LogsDirectory=molly-brown`. Se `/var/log/molly-brown` já existia como root, o systemd podia **migrar** para `/var/log/private/molly-brown` e o serviço falhava. O **v0.06+** **remove** esse drop-in e muda os caminhos no `.conf` para `/var/lib/molly-brown/`. -- **Servidor já provisionado:** correr com **`--force`** para regravar o `.conf` e remover o drop-in obsoleto (com backup do drop-in se usar `--force`). Exemplo: - `sudo python3 scripts/admin/setup_alt_protocols.py --verbose --force` -- **Correcção manual rápida (só `.conf`):** `AccessLog` / `ErrorLog` com caminhos absolutos sob **`/var/lib/molly-brown/`**; **sem** `LogsDirectory` extra em drop-in; `sudo systemctl daemon-reload`; `sudo systemctl reset-failed molly-brown@runv.club.service` e `start`. - -## Erro `permission denied` em `/var/log/molly-brown/…` ou migração para `/var/log/private/molly-brown` - -No Debian, **`DynamicUser=yes`** faz o UID de runtime ser dinâmico; `chown` estático não bate. Se viu **`migrating to /var/log/private/molly-brown`** ou **`permission denied`** em `/var/log/molly-brown`, actualize para **v0.06+** com **`--force`**. - -**Limpeza recomendada (serviço parado):** - -```bash -sudo systemctl stop 'molly-brown@runv.club.service' -sudo rm -f /etc/systemd/system/molly-brown@.service.d/50-runv-logs.conf -sudo rm -rf /var/log/molly-brown /var/log/private/molly-brown -sudo systemctl daemon-reload -sudo python3 /opt/runv/src/scripts/admin/setup_alt_protocols.py --verbose --force -``` - -(ajuste o caminho do script ao seu clone). Os logs passam a ficar só em **`/var/lib/molly-brown/`** (legível com `sudo`). - -## Erro `permission denied` em `/var/lib/molly-brown/…` - -Raro se o `.conf` aponta para `/var/lib/molly-brown/` e não há override que desactive `StateDirectory`. Confirme `grep StateDirectory /lib/systemd/system/molly-brown@.service` e caminhos no `.conf`; veja também **TLS** (`privkey` legível pelo grupo `ssl-cert`). - -## Checklist rápido (conf antiga, UFW, «activating») - -1. **Ainda vê `open /-` no journal?** O `/etc/molly-brown/runv.club.conf` no servidor pode continuar com `ErrorLog = "-"` até correr o script com **`--force`** (ou editar à mão). Confirme: `grep -E 'AccessLog|ErrorLog' /etc/molly-brown/runv.club.conf`. -2. **UFW:** o script só executa `ufw allow` automaticamente quando **`ufw status`** mostra o firewall **activo** na altura da execução. Se activou o UFW **depois**, ou usa outro firewall, abra **70/tcp** (Gopher) e **1965/tcp** (Gemini) manualmente: - ```bash - sudo ufw allow 70/tcp comment 'gopher' - sudo ufw allow 1965/tcp comment 'gemini' - sudo ufw reload - ``` - Com `--skip-firewall` ou UFW inactivo, o script regista no log os mesmos comandos sugeridos para copiar. -3. **`molly-brown@runv.club: activating` no fim do script:** **não** indica sucesso — só que o unit ainda não estava `active` nesse instante (p.ex. crash loop). Use `systemctl is-active molly-brown@runv.club.service`, `systemctl is-failed …` e `sudo ss -tlnp | grep 1965`. Se `is-failed` for positivo, veja `journalctl` como abaixo. - -## 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`. -- **Estado e porta:** `sudo systemctl status molly-brown@runv.club.service --no-pager` e `sudo ss -tlnp | grep 1965` (deve haver um processo a escutar em **1965/tcp**). -- **Permissões TLS (frequente):** o Molly corre como utilizador não-root; se `privkey.pem` for só `root:root` `0600`, o arranque falha. Verifique `sudo namei -l /etc/letsencrypt/live/runv.club/privkey.pem` e compare com o utilizador do unit (`systemctl cat molly-brown@runv.club.service`). Soluções típicas: grupo `ssl-cert`, ACL, ou certificados num path legível pelo utilizador do serviço (mantendo segurança). -- **Teste local:** `openssl s_client -connect 127.0.0.1:1965 -servername runv.club </dev/null 2>/dev/null | head -20` -- **Cliente (Lagrange, etc.):** teste **`gemini://runv.club/~user/`** (canónico); `gemini://runv.club/~/user/` deve redireccionar (**v0.11+** no `.conf`) **depois** de `systemctl is-active molly-brown@runv.club.service` devolver `active`. - -## Gemini **51 Not found** e log «Refusing to follow symlink … outside of DocBase!» - -O **Molly Debian** não segue symlinks de `/var/gemini/users/<user>` para fora de `DocBase`. Com **symlink** antigo, o `index.gmi` pode existir e o `test -r` no script passar, mas o servidor devolve **51**. - -- **Correcção:** `sudo python3 scripts/admin/setup_alt_protocols.py --verbose --force` (**v0.13+**) migra para **`mount --bind`** e persiste em **`/etc/fstab`**. -- **Verificar:** `findmnt /var/gemini/users/<user>` e `grep gemini/users /etc/fstab`. - -## Execução (root) - -Use a **raiz do repositório** clonada; o script carrega `patches/patch_irc.py` para a lista de utilizadores (união JSON + `/home`). Sem esse ficheiro, o comando falha com mensagem explícita. - -```bash -cd /caminho/para/runv-server -sudo python3 scripts/admin/setup_alt_protocols.py --dry-run --verbose -sudo python3 scripts/admin/setup_alt_protocols.py --verbose -``` - -### Flags úteis - -| Flag | Efeito | -|------|--------| -| `--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 (exceto **`~/public_gemini/index.gmi`** se já existir). Necessário para **regravar** `/etc/molly-brown/runv.club.conf` (incl. **`[TempRedirects]`** v0.11: **`/~/…` → `/~…`**) e remover o drop-in obsoleto **`50-runv-logs.conf`** (v0.05) ao migrar logs para `/var/lib/molly-brown/`. **v0.14+:** também **regrava** o **`gophermap` raiz** e **`/var/gemini/index.gmi`**; para utilizadores em **`IRC_PATCH_SKIP_USERS`**, remove **bind mount** + linha **`fstab`** + legado em **`/var/gemini/users/<user>`** se ainda existirem. | -| `--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. | -| `--skip-backfill` | Não cria pastas nem bind mounts Gemini por utilizador. | -| `--skip-services` | Não `systemctl enable --now`. | -| `--skip-system-config` | Não escreve `/etc/default/gophernicus`, nem `molly-brown`, nem gophermap raiz. | -| `--users-json PATH` | Parte da fonte de usernames (lista JSON com `username`). Predefinido: `/var/lib/runv/users.json`. | -| `--homes-root PATH` | Parte da fonte de usernames (directórios em `/home` com UID ≥ 1000). O backfill usa a **união** JSON + homes (igual a `patches/patch_irc.py`). | -| `--gemini-hostname HOST` | Predefinido: `runv.club`. | -| `--gemini-cert` / `--gemini-key` | Caminhos PEM para molly-brown. | - -## Descoberta de utilizadores (backfill) - -A lista de contas para criar `~/public_gopher`, `~/public_gemini` e **bind mounts** em `/var/gemini/users/` é a **união** de: - -1. Usernames em **`users.json`** (lista de objetos com campo `username`), quando o ficheiro existe e o JSON é válido; e -2. Nomes em **`--homes-root`** com UID ≥ 1000 e entrada em `passwd`. - -Depois aplicam-se as mesmas exclusões que em **`patches/patch_irc.py`** (**`IRC_PATCH_SKIP_USERS`** — contas de sistema, **`entre`**, **`pmurad-admin`**, etc.; **v0.14+**). Esse conjunto governa também a **lista pública** Gopher/Gemini (menu raiz e **`index.gmi`** DocBase) e impede **bind mount** para esses nomes. Para só pastas/bind Gemini sem reinstalar serviços, pode usar **`patches/yetgg.py`**. - -## Índice raiz Gopher e Gemini (**v0.14+**) - -- **`/var/gopher/gophermap`:** menu explícito (linhas **`i`** de boas-vindas + **`1~username ~username/ host 70`**) só para utilizadores do backfill que tenham **`~/public_gopher`** como directório. Não usa a directiva global **`~`** do gophernicus, para poder **respeitar** **`IRC_PATCH_SKIP_USERS`**. -- **`/var/gemini/index.gmi`:** página Gemtext na raiz do DocBase com links **`=> gemini://<hostname>/~user/`** para o **mesmo** conjunto (backfill + `public_gopher`). Quem está em **`IRC_PATCH_SKIP_USERS`** **não** recebe bind em **`/var/gemini/users/`** e **não** aparece nesse menu. -- **Limpeza:** após remover um nome de **`IRC_PATCH_SKIP_USERS`** ou corrigir mounts antigos, correr **`setup_alt_protocols.py --verbose --force`**. Verificar: `findmnt /var/gemini/users/<user>` vazio para contas excluídas. - -## Relação com outros scripts - -- **`create_runv_user.py`**: após `public_html`, cria `public_gopher`, `public_gemini` e aplica **bind mount** em `/var/gemini/users/<user>` (via `setup_alt_protocols`) **excepto** se o username estiver em **`IRC_PATCH_SKIP_USERS`**. -- **`del-user.py`**: **umount**, remove linha **`fstab`** de bind e remove symlink legado ou directório vazio em `/var/gemini/users/<user>`. -- **`tools/tools.py`**: copia modelos para `/etc/skel` (só contas futuras). - -## Testes manuais sugeridos - -1. `sudo python3 scripts/admin/setup_alt_protocols.py --dry-run --verbose` -2. `sudo python3 scripts/admin/setup_alt_protocols.py --verbose` -3. `dpkg -l gophernicus molly-brown` -4. `systemctl is-active gophernicus.socket` e `systemctl is-active molly-brown@runv.club.service` -5. `ufw status` (se ativo, confirmar 70/tcp e 1965/tcp permitidos) -6. Verificar `/etc/skel/public_gopher` e `public_gemini` após `tools.py` -7. Criar utilizador de teste com `create_runv_user.py` -8. `ls -la /home/teste/public_gopher/gophermap /home/teste/public_gemini/index.gmi` e `findmnt /var/gemini/users/teste` (deve mostrar bind a `~/public_gemini`) -9. Cliente Gopher/Gemini: `gopher://runv.club/1/~teste` e **`gemini://runv.club/~teste/`** (canónico); opcionalmente `gemini://runv.club/~/teste/` → redirect **30** para o canónico (**v0.11+**) - -Versão do script: ver `python3 scripts/admin/setup_alt_protocols.py --version`. diff --git a/scripts/docs/irc_patch.md b/scripts/docs/irc_patch.md @@ -1,69 +0,0 @@ -# IRC no runv.club — comando **`chat`** - -Estilo [tilde.club](https://tilde.club): o utilizador corre só **`chat`** e liga ao IRC da casa com auto-ligação já preparada. - -## Alinhamento (plano / produto) - -- **MOTD** (`tools/motd/60-runv`) e **`runv-help`** referem **apenas o comando `chat`** — sem citar outros nomes de binário ao utilizador. -- **Provisionamento** (`patches/patch_irc.py`) usa **sempre** `weechat-headless` (`-a`, `-r`, `--stdout`): é o fluxo suportado para automatizar `/server add` e `/set` sem editar ficheiros à mão. -- O **cliente interactivo** no terminal é instalado pelos **pacotes globais** em `tools/manifests/apt_packages.txt` (o launcher `chat` escolhe o primeiro binário adequado no PATH); utilizadores continuam a ver só **`chat`**. - -## O que o admin faz - -```bash -cd /caminho/runv-server -sudo python3 patches/patch_irc.py --all-users --verbose -# ou um utilizador: -sudo python3 patches/patch_irc.py --user alice --verbose -``` - -- Instala **`/usr/local/bin/chat`** (salvo `--skip-launcher`). -- Por utilizador: `~/.config/weechat/`, servidor interno **`runv`** (por defeito), nick = **username Unix**, nicks alternativos `user_`, `user__`, `user|away`, autojoin por defeito no canal **`#runv`**. -- O patch aplica também **definições globais** WeeChat (na mesma sessão `weechat-headless`): `irc.look.buffer_switch_join` = `on`, `irc.look.server_buffer` = `independent`, e `buflist.look.display_conditions` = ``${buffer.plugin} == irc && ${type} == channel`` (na buflist aparecem **só canais**, ex. `#runv` — sem a linha do servidor `runv` nem `core.weechat`). Mensagens do servidor continuam acessíveis noutros modos; para voltar a listar tudo, redefine o `display_conditions` na mão. WeeChat antigo ou sem plugin buflist: o `/set buflist.*` pode falhar. -- Com **`--all-users`**, a lista de contas é a **união** de: usernames em `users.json` **e** utilizadores com diretório em `--homes-root` (por omissão `/home`), UID ≥ 1000 e fora da lista interna de contas de sistema — assim contas de administração (ex.: `pmurad-admin`) que não estão no JSON também são provisionadas. -- Exige **`weechat-headless`** no sistema para aplicar o patch; sem esse binário o script falha com mensagem clara (`apt install weechat-headless`). -- Se a config **já existe** mas **não coincide** com o alvo (host, TLS, nicks, autojoin, etc.), o patch **realinha** sozinho (`/server del` + voltar a criar). Não é obrigatório **`--force`** para isso. **`--force`** serve para **reaplicar mesmo quando já estava alinhada** (útil para repor estado conhecido). - -## O que o utilizador faz - -```bash -chat -``` - -Opcional: variável de ambiente **`WEECHAT_HOME`** para outro directório de dados (convénio do cliente IRC). - -## Defaults (ajustáveis por flags) - -| Parâmetro | Default | -|-----------|---------| -| Host IRC | `irc.portalidea.com.br` | -| TLS | ligado (`--tls`; omitir `--no-tls`) | -| Porta | `6697` com TLS, `6667` sem TLS (ou `--port`) | -| Nome do servidor na config | `runv` | -| Autojoin | `#runv`; `--autojoin ""` para nenhum; ou `--autojoin '#canal1,#canal2'` | - -Não há SASL/NickServ automático; no código há comentários para extensão futura com **dados seguros** (sem senhas em texto plano). - -## Flags úteis - -- `--dry-run`, `--verbose`, `--force` (reaplica mesmo com config já igual ao alvo) -- `--skip-launcher`, `--skip-backfill` -- `--users-json`, `--homes-root` -- `--user` **ou** `--all-users` (obrigatório um dos dois) - -## Integração `tools.py` - -Copia **`tools/bin/chat`** → `/usr/local/bin`. O `patches/patch_irc.py` pode reinstalar o mesmo ficheiro se correres o patch sem rerodar `tools.py`. - -## Testes rápidos (Debian 13) - -```bash -sudo python3 patches/patch_irc.py --dry-run --all-users --verbose -sudo python3 patches/patch_irc.py --user "$(logname)" --verbose -command -v chat && ls -l "$(command -v chat)" -command -v weechat-headless -sudo -u USER test -f /home/USER/.config/weechat/irc.conf && grep '^runv\.' /home/USER/.config/weechat/irc.conf -sudo -u USER grep '^runv\.autojoin' /home/USER/.config/weechat/irc.conf -``` - -Substitui `USER` por um utilizador real. diff --git a/scripts/doom/doom.md b/scripts/doom/doom.md @@ -1,88 +0,0 @@ -# `doom.py` — reset em massa de utilizadores runv - -Script de administração que **remove todas as contas runv** registadas em `users.json`, **excepto** um conjunto **protegido**. A remoção real (Unix, quotas, metadados) é feita pelo mesmo fluxo que o banimento manual: chama [`../admin/del-user.py`](../admin/del-user.py) com `-y` para cada utilizador a apagar. - -**Aviso:** operação **irreversível**. Use primeiro `--dry-run` se tiver dúvidas. - -## O que é “protegido” - -O script **nunca** passa ao `del-user.py` ninguém que pertença ao conjunto **protegido**: - -``` -protegido = { conta de referência (--keep / omissão) } ∪ { quem está ligado ao processo } -``` - -### Conta de referência (`keeper`) - -- Com **`--keep USER`**: essa conta é a referência explícita (e entra no protegido). -- Sem `--keep`, com **EUID root** e **`SUDO_USER`** definido (caso típico `sudo python3 doom.py`): a referência é o utilizador em `SUDO_USER`. -- **Root sem `SUDO_USER`**: é **obrigatório** `--keep USER` (evita ambiguidade). -- **Sem ser root** (ex.: só inspeção): a referência é o utilizador do **real UID** (útil em cenários limitados; a remoção real ainda exige root). - -### Quem está ligado ao processo (nunca apagado) - -Mesmo que `--keep` aponte para **outra** conta, o script **não apaga**: - -| Origem | Motivo | -|--------|--------| -| `SUDO_USER` | Quem executou `sudo`. | -| Real UID e **effective** UID | Cobre `sudo -u bob`: protege quem invocou e o utilizador efectivo. | -| `root` | Se o processo corre como root **e** não há `SUDO_USER`, `root` fica protegido se existir no JSON. | - -Os nomes são normalizados para bater com entradas típicas de `users.json` (ex.: minúsculas, regra de username runv). - -## Fonte da lista de utilizadores - -- Lê **apenas** os `username` presentes no ficheiro JSON de metadados (por omissão `/var/lib/runv/users.json`). -- **Não** enumera o `/etc/passwd` por conta própria: o que não está no JSON **não** é alvo do doom (mas contas órfãs no sistema continuam fora deste script). - -## Fluxo de execução - -1. Determina `keeper` e o conjunto **protegido** (referência + quem rodou). -2. Lista todos os utilizadores no JSON **fora** do protegido → **vítimas**. -3. Se não houver vítimas, termina sem chamar `del-user.py`. -4. **Confirmação:** a menos que use `--yes`, é preciso escrever **`DOOM`** em maiúsculas. -5. Em modo real, exige **root** (EUID 0). -6. Para cada vítima, invoca `del-user.py` com `--yes` e os mesmos caminhos de metadata/lock que indicar. - -## Modo dry-run - -`--dry-run` **não exige root**. Mostra quem seria removido e corre o `del-user.py` em dry-run por cada vítima (sem alterações reais). - -## Opções principais - -| Opção | Função | -|--------|--------| -| `--keep USER` | Referência explícita; obrigatório em root puro sem `SUDO_USER`. | -| `--yes` / `-y` | Sem prompt `DOOM` (automação; perigoso). | -| `--dry-run` | Simulação. | -| `--metadata-file`, `--lock-file` | Sobrescreve caminhos do JSON e do lock (útil em testes). | -| `--purge-all-files` | Repassa ao `del-user.py` (além de remover home). | -| `-v` / `--verbose` | Mais saída no `del-user.py`. | - -## Exemplos - -```bash -# Conta a manter = quem fez sudo (ex.: alice); apaga todos os outros no JSON -sudo python3 /caminho/scripts/doom/doom.py - -# Root em consola sem SUDO_USER: dizer explicitamente quem fica como referência -sudo python3 /caminho/scripts/doom/doom.py --keep alice - -# Simular sem apagar nada -python3 /caminho/scripts/doom/doom.py --dry-run - -# Automação (sem confirmação DOOM) -sudo python3 /caminho/scripts/doom/doom.py --yes -``` - -## Nota sobre “ficar só um utilizador” - -O objectivo habitual é deixar **uma** conta runv no JSON (a referência). Se `--keep` for diferente de quem invocou o script, **várias** contas podem permanecer no ficheiro (referência + todas as que o doom é obrigado a proteger). Isto é intencional: **nunca** apagar quem está ligado ao processo. - -## Dependências - -- Python 3, acesso root para execução real. -- `scripts/admin/del-user.py` no repositório, relativo a `doom.py` (`../admin/del-user.py`). - -Versão do script documentada aqui: alinhada a `doom.py` (ver `--version`). diff --git a/scripts/skel.md b/scripts/skel.md @@ -1,145 +0,0 @@ -# skel.py — preparar `/etc/skel` (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. - -- **Não** cria usuários. -- **Não** altera Apache, SSH, chaves ou pacotes. -- Só cria/atualiza ficheiros sob `/etc/skel` (com segurança e idempotência). - -**Ambiente:** execute no servidor Linux como **root** (ou `sudo`). No Windows, use apenas para revisão do código; a execução real é no Debian. - -## Objetivo - -- Garantir `/etc/skel/public_html/` e `/etc/skel/public_html/index.html`. -- Garantir `/etc/skel/README.md` (texto de ajuda para quem entra na shell). -- Aplicar permissões: diretório `755`, ficheiros `644`. -- Ser **idempotente**: voltar a correr não apaga nada; sem `--force`, ficheiros existentes são **preservados**. - -## Requisitos - -- Python 3 (stdlib apenas; usa `pathlib`). -- Permissões de root para escrita em `/etc/skel`. - -## Como executar - -Nos exemplos com **`admin/skel.py`**, execute a partir do diretório **`scripts/`** do repositório. - -Torne o script executável (opcional): - -```bash -sudo chmod +x admin/skel.py -``` - -(a partir do diretório `scripts/` do repositório; ou use o caminho absoluto até `scripts/admin/skel.py`.) - -### Simular (sem root, sem alterar disco) - -```bash -python3 admin/skel.py --dry-run -python3 admin/skel.py --dry-run --verbose -``` - -### Aplicar de verdade - -```bash -sudo python3 admin/skel.py -sudo python3 admin/skel.py --verbose -``` - -### Regenerar templates (sobrescrever) - -Se `index.html` ou `README.md` já existirem em `/etc/skel` e quiser substituir pelo conteúdo embutido no script: - -```bash -sudo python3 admin/skel.py --force -``` - -## Opções - -| Opção | Efeito | -|--------|--------| -| `--dry-run` | Mostra o que seria criado/atualizado; **não** exige root; não escreve ficheiros. | -| `--verbose` | Mais detalhe (ex.: `chmod` explícito). | -| `--force` | Sobrescreve `index.html` e `README.md` se já existirem. Sem `--force`, são preservados. | -| `--version` | Mostra versão do script. | - -## Exemplos de saída - -### Dry-run (trecho) - -``` -Modo dry-run — nenhuma alteração em disco. - - [dry-run] criaria diretório: /etc/skel/public_html - [dry-run] criaria arquivo: /etc/skel/public_html/index.html - [dry-run] criaria arquivo: /etc/skel/README.md - -Resumo: nada foi gravado. Execute sem --dry-run como root para aplicar. -``` - -### Execução real (trecho) - -``` -skel.py — preparando /etc/skel para runv.club - - [dir] criado: /etc/skel/public_html - [file] criado: /etc/skel/public_html/index.html - [file] criado: /etc/skel/README.md - -Aplicando permissões... - ---- Resumo --- - ... -``` - -Segunda execução **sem** `--force`: deve aparecer `preservado` para os ficheiros já existentes. - -## Limitações - -- Não altera `/etc/skel` fora do que o script define (outros ficheiros que o admin adicionar manualmente ficam). -- Não remove ficheiros. -- Não cria `public_html` na home de utilizadores **já** existentes — só para **novos** utilizadores criados **depois** de preparar o skel; utilizadores antigos precisam copiar à mão ou de outro procedimento. - -## Como testar no Debian 13 - -1. **Preparar o skel** - - ```bash - sudo python3 ./admin/skel.py --verbose - ``` - -2. **Criar um utilizador de teste** - - ```bash - sudo adduser testuser - ``` - - (ou `adduser --disabled-password` se o seu fluxo não precisar de password interativa.) - -3. **Verificar cópia a partir de `/etc/skel`** - - Como `testuser` (ou inspecionando a home): - - ```bash - sudo ls -la /home/testuser/ - sudo ls -la /home/testuser/public_html/ - sudo cat /home/testuser/public_html/index.html | head - sudo cat /home/testuser/README.md | head - ``` - -4. **Permissões típicas para publicação web** (o README em `/etc/skel` explica; após `adduser`, confirme se a sua política exige `chmod` na home — muitas instalações já ficam com home `755` e `public_html` herdado de `755`). - -5. **Prova no browser** (se DNS e Apache estiverem corretos): `http://runv.club/~testuser/` - -## Ficheiros geridos pelo script - -| Caminho | Conteúdo | -|---------|-----------| -| `/etc/skel/public_html/index.html` | Página inicial estática em português (CSS embutido, visual simples). | -| `/etc/skel/README.md` | Instruções para o utilizador na shell. | - -## Créditos - -runv.club. diff --git a/scripts/starthere.md b/scripts/starthere.md @@ -1,94 +0,0 @@ -# starthere.py - -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.02** (use `python3 admin/starthere.py --version` a partir do diretório `scripts/` do repositório). - -### Comportamento de `--dry-run` - -O script **consulta sempre o `findmnt` real** (só leitura) para mostrar o estado do mount detetado. Como o `fstab` não é gravado em dry-run, o kernel **não** passa a mostrar `usrquota` até uma execução real; por isso, em dry-run o fluxo **assume** quotas ativas só para completar o plano de quotas, incluindo `quotacheck` / `quotaon` simulados. - -## Quando usar - -- Máquina nova ou recém-instalada, antes de criar utilizadores com [`create_runv_user.py`](create_runv_user.md). -- Quando ainda não existem quotas ativas no filesystem das homes e pretende alinhar ao fluxo de `create_runv_user.md` (ext4 + `setquota`). - -## Requisitos - -- **root** (`sudo` ou sessão root). -- **Debian** (ou derivado com `apt-get`). -- O path de sonda (predefinido **`/home`**) tem de residir num filesystem **`ext4`**. Se `/home` estiver noutro tipo (btrfs, xfs, …), o script aborta — configure quotas manualmente ou use layout ext4 para as homes. -- Acesso à rede para `apt-get update` / `install` (exceto em `--dry-run`, que não executa APT mas ainda pode ler `/etc/fstab` nos passos de quota). - -## O que o script faz - -1. **`apt-get update`** (a menos que `--no-install`). -2. **`apt-get install -y`** de um conjunto fixo de pacotes (personalizável com `--packages`; ver lista em `BASE_PACKAGES` no código). -3. **Limpeza segura**: `apt-get autoremove` e `autoclean` (desligável com `--no-cleanup`). -4. **Serviços** (desligável com `--no-services`): - - `systemctl enable --now apache2` - - Se o UFW estiver **inativo**: `ufw allow OpenSSH`, `80/tcp`, `443/tcp`, depois `ufw --force enable` (não altera regras se o UFW já estiver ativo). -5. **Quotas** (desligável com `--no-quota`): - - **Deteta** o mountpoint com `find_mount_triple` sobre `--quota-probe` (predefinido `/home`). - - Garante `usrquota` na linha **ext4** correspondente a esse mountpoint em `/etc/fstab`. - - Backup de `/etc/fstab` em `/root/runv-fstab-backups/fstab.<timestamp>.bak`. - - **`mount -o remount,usrquota <mount>`** e, se preciso, **`mount -o remount <mount>`** (saltável com `--skip-remount`). - - **`quotacheck`** / **`quotaon`** nesse mesmo `<mount>`. - -## O que o script não faz - -- Não remove pacotes em massa nem faz `purge` agressivo. -- Não altera vhosts Apache nem cria utilizadores (só enable/start do serviço `apache2`). -- Não altera configuração SSH além do que o pacote `openssh-server` já traz. -- Não instala stack de correio. -- Não define limites por utilizador — isso fica para `create_runv_user.py` / `setquota` manual. - -## Opções de linha de comandos - -| Opção | Efeito | -|--------|--------| -| `--dry-run` | Mostra comandos e plano; não executa subprocessos reais (saídas simuladas onde aplicável). | -| `--verbose` | Ecoa comandos e saída de stderr/stdout dos programas. | -| `--packages` | Lista explícita de pacotes a instalar (substitui o padrão). | -| `--no-install` | Não corre APT; apenas lógica de quotas (útil se os pacotes já estiverem instalados). | -| `--no-cleanup` | Não corre `autoremove` / `autoclean`. | -| `--no-quota` | Só instala/limpa; não mexe em `fstab` nem quotas. | -| `--quota-probe PATH` | Caminho para descobrir o FS de quotas (predefinido `/home`; deve bater com onde o `create_runv_user` cria homes). | -| `--skip-remount` | Não tenta `remount` após editar `fstab`. | -| `--allow-live-scan` | Usa apenas **`quotacheck -cuM`** (não tenta antes `-cu`). | -| `--no-services` | Não ativa Apache nem configura/ativa UFW. | -| `--version` | Mostra versão e sai. | - -Códigos de saída: **0** sucesso; **2** erro operacional (`BootstrapError`); **130** interrupção (Ctrl+C). - -## Pacotes base (padrão) - -Incluem, entre outros: `apache2`, `openssh-server`, `sudo`, `ufw`, `quota`, ferramentas de rede e ficheiros (`curl`, `wget`, `git`, `rsync`), consola (`tmux`, `htop`, `vim`, …), `jq`, `acl`, `build-essential`, `python3-venv`, `python3-pip`, `ripgrep`, `shellcheck`. A lista completa está em `BASE_PACKAGES` em [`starthere.py`](admin/starthere.py). - -## Quotas: comportamento esperado e problemas comuns - -- Depois de editar `fstab`, o **remount** pode falhar em alguns ambientes (cloud-init, segurança do kernel). O script sugere **reiniciar a VM** e voltar a executar o script. -- **`quotacheck` com filesystem montado**: o script tenta **`-cu`**, depois **`-cuM`**, e se ainda falhar (p.ex. quotas já ativas após remount — mensagem «use -f»), **`-cuM -f`** e **`-cu -f`**. **`--allow-live-scan`** começa por **`-cuM`** e, se preciso, **`-cuM -f`**. -- **`quotaon -vu`**: se o kernel já tiver quotas de utilizador activas neste mount (comum após `remount,usrquota` + `quotacheck`), o `quotaon` pode devolver **Device or resource busy**. O script trata isso como **sucesso** e imprime um aviso — confirme com **`quota -vs`** ou **`sudo repquota -s <mount>`** (`repquota` costuma estar em `/usr/sbin/`). -- Confirme com `mount | grep ' on <mount> '` (o `<mount>` é o indicado no resumo do script, ex. `/` ou `/home`) e `quota -vs`. -- **Reinício:** em muitas contas normais o comando `reboot` não está no `PATH`; use `sudo reboot` ou `/sbin/reboot`. - -### Avisos «external quota files» e `tune2fs -O quota` - -O `quotacheck` e o `quotaon` podem imprimir avisos dizendo que o kernel prefere a **feature interna `quota` do ext4** e que os ficheiros clássicos (`aquota.user` / «external quota files») estão **deprecated**. - -- **Isto não invalida o que o script fez:** com `usrquota` no mount e `quota -vs` a mostrar o filesystem, as quotas estão ativas; `setquota` (como em `create_runv_user.py`) funciona neste modo. -- O script usa o caminho suportado em **/** montado: `usrquota` + ficheiros de quota geridos pelas ferramentas `quota` — é o esperado quando a feature `quota` do ext4 **não** foi ligada no superbloco. -- **Migrar** para quotas «só no ext4» (sem aquele aviso) implica, em geral, **desmontar** o volume (para `/` isso significa **modo rescue/live** ou VM parada), correr algo como `tune2fs -O quota <dispositivo>`, voltar a montar e rever opções de mount/documentação do seu Debian — fora do âmbito automático deste bootstrap. -- O pacote **`e2fsprogs`** (inclui `tune2fs`, `dumpe2fs`) está na lista base para inspeção manual; após um run bem-sucedido, o script pode imprimir uma **nota** em stderr se detectar que a feature interna ainda não está ativa. - -## Relação com outros scripts - -- Após este bootstrap, use **[create_runv_user.md](create_runv_user.md)** para criar contas com quota e `public_html`. -- **[skel.md](skel.md)** e **[del-user.md](del-user.md)** cobrem esqueleto de ficheiros e remoção de utilizadores. - -## Segurança e operações - -- Alterações em **`/etc/fstab`** são precedidas de backup em **`/root/runv-fstab-backups/`**. -- O script foi pensado para **ext4 no volume das homes**; não extrapola para outros filesystems. -- Revise sempre `--dry-run` / `--verbose` num ambiente de teste antes de produção. diff --git a/site/README.md b/site/README.md @@ -1,104 +0,0 @@ -# Site público (landing runv.club) - -Conteúdo estático inspirado em [tilde.town](https://tilde.town) e [tilde.club](https://tilde.club): landing com constelação de links por membro (`members.json`), **`/news/`** (lista dinâmica via `data/news.json` + RSS), **`/wiki/`**, e **`/junte-se/`** — guia de chave SSH (Linux, macOS, Windows) e acesso a **`entre@runv.club`**. - -## O que significa “membro” na página - -- **Membro listado** = conta presente em `/var/lib/runv/users.json` (criada por `create_runv_user.py`). -- **Não** é “sessão SSH ativa neste momento” nem “logged in”; isso exigiria outra fonte de dados (ex. `lastlog`). - -## Privacidade - -- `build_directory.py` **filtra** o JSON interno: só escreve `username`, `since` (data de criação), `path` (`/~user/`) e, opcionalmente, `homepage_mtime` se você usar `--homes-root`. -- **Nunca** copia email, fingerprint de chave nem quotas para `members.json`. - -## Stack - -- **HTML/CSS/JS** estáticos em `public/`. -- **Rodapé:** em todas as páginas HTML em `public/` deve constar o **contato** da administração — `admin@runv.club` (bloco `<footer class="site-footer">` como em `index.html`). -- **Geração de dados**: Python 3 (stdlib); `members.json` é regenerado por `create_runv_user.py` e por `genlanding.py` (sem cron); sem CGI. - -## Gerar `public/data/members.json` - -**No Git**, `public/data/members.json` fica **`[]`**: a landing não deve mostrar utilizadores fictícios. Quem aparece na constelação vem **só** de `build_directory.py` a ler **`/var/lib/runv/users.json`**, invocado automaticamente após **`create_runv_user.py`** e após **`genlanding.py`** (podes ainda correr o script à mão). Em desenvolvimento, usa uma cópia de teste com **`--users-json site/example-users.json`**. Se **`users.json` ainda não existir** no servidor, o `build_directory.py` assume **zero membros** (aviso em stderr) em vez de falhar. - -Manual detalhado do script: **[`build_directory.md`](build_directory.md)**. - -No servidor (como root), após provisionar contas: - -```bash -sudo python3 /caminho/ao/repo/site/build_directory.py \ - --users-json /var/lib/runv/users.json \ - --homes-root /home \ - -o /caminho/deploy/public/data/members.json -``` - -Sem acesso a `/home` (ex.: build na **sua** máquina só para pré-visualizar): - -```bash -python3 site/build_directory.py \ - --users-json site/example-users.json \ - -o site/public/data/members.json -``` - -Dry-run: - -```bash -python3 site/build_directory.py --users-json site/example-users.json --dry-run -``` - -## Publicar notícias (`news/publish_news.py`) - -Coloque um `.md` em **`site/news/`** (linha 1 = título; resto = corpo), execute `python3 site/news/publish_news.py`. Isto gera **`public/news/data/news.json`**, **`public/news/feed.rss`** e actualiza **`lastmod`** de `/news/` no `sitemap.xml`. O `.md` é removido após publicar. - -`news.json` **não** é versionado (`.gitignore`) para não conflitar com `git pull` no servidor. Manual: **[`news/README.md`](news/README.md)**. - -Depois, volte a copiar `public/` para o `DocumentRoot` (`genlanding.py` ou deploy manual). - -## Configurar Apache (`genlanding.py`) - -Para **gerar o VirtualHost**, **ativar** `mod_userdir` / `mod_rewrite`, copiar **`public/`** para o `DocumentRoot` e (opcional) rodar **Certbot**, use o script **[`genlanding.py`](genlanding.py)**. Manual completo: **[`genlanding.md`](genlanding.md)**. - -Exemplos: - -```bash -# Produção (runv.club, /var/www/runv.club/html) -sudo python3 site/genlanding.py - -# Pré-visualização -python3 site/genlanding.py --dry-run - -# VM / teste local (runv.local; por padrão desativa 000-default para IP servir a landing) -sudo python3 site/genlanding.py --dev -# Manter página Debian em paralelo: --dev --keep-default-site - -# TLS após HTTP correto (não combinar com --dev) -sudo python3 site/genlanding.py --certbot -``` - -## Deploy no Apache (manual) - -Alternativa ao genlanding: copiar o conteúdo de **`public/`** para o `DocumentRoot` do VirtualHost do domínio (ex. `/var/www/runv.club/html/`), ou configurar `DocumentRoot` para apontar diretamente para esta pasta. - -**Certifique-se** de que `mod_userdir` continua a servir `~/public_html` para cada **usuário**; a landing é só a **raiz** do site. - -**`members.json`:** não é necessário cron. Com **`genlanding.py`**, a cópia de `public/` é seguida por `build_directory.py` no `DocumentRoot` (use `--no-refresh-members` para omitir). Com **`create_runv_user.py`**, após criar a conta o mesmo ficheiro é regenerado por omissão (`--no-refresh-landing-members` para omitir). - -## Arquivos - -| Caminho | Função | -|---------|--------| -| `genlanding.py` | Configura Apache (vhost, cópia de `public/`, opcional Certbot); ver `genlanding.md` | -| `build_directory.py` | Gera `members.json` público; ver **`build_directory.md`** | -| `build_directory.md` | Como usar `build_directory.py` (flags, integração com genlanding/create_runv_user) | -| `public/index.html` | Landing | -| `public/faq/index.html` | FAQ (texto estático; link discreto no rodapé) | -| `public/junte-se/index.html` | Pedir entrada: gerar chave SSH e `ssh entre@runv.club` | -| `public/assets/style.css` | Estilos | -| `public/assets/app.js` | Constelação, lista, filtro, shuffle | -| `public/assets/news-page.js` | Lista de notícias a partir de `news/data/news.json` | -| `news/publish_news.py` | Ingere `.md` e gera `news.json`, RSS e `sitemap` | -| `public/news/data/news.json` | Gerado localmente / no servidor (ignorado pelo git) | -| `public/news/feed.rss` | Feed RSS (stub no repo; regerado pelo script) | -| `public/data/members.json` | Dados públicos (regenerado; exemplo no repo) | -| `example-users.json` | Amostra para testes locais | diff --git a/site/build_directory.md b/site/build_directory.md @@ -1,126 +0,0 @@ -# build_directory.py — gerar `members.json` para a landing - -O script [`build_directory.py`](build_directory.py) lê o ficheiro interno **`users.json`** (criado pelo [`create_runv_user.py`](../scripts/create_runv_user.md)) e gera um JSON **público** consumido pelo JavaScript da landing (`public/assets/app.js`): posiciona os **pontos** (links para `/~utilizador/`) com base em `username`, `since` e `path`. - -- **Python 3**, só biblioteca padrão (sem PyPI). -- **Não** é um servidor web: corre na linha de comando. Em produção é invocado automaticamente por [`create_runv_user.py`](../scripts/create_runv_user.md) (após criar conta) e por [`genlanding.py`](genlanding.md) (após copiar `public/` para o DocumentRoot), salvo flags para desactivar. - -Visão geral do `site/`: [README.md](README.md). - -## O que entra e o que sai - -### Entrada (`--users-json`) - -Caminho para o JSON do provisionador (por omissão no servidor: `/var/lib/runv/users.json`). O ficheiro deve ser uma **lista** de objectos; cada entrada com `username` (string) é considerada. - -Se o caminho **ainda não existir** (bootstrap antes do primeiro `create_runv_user.py`), o script **não falha**: emite um aviso em stderr, assume **lista vazia** e gera `members.json` com `[]`. Podes também criar manualmente `/var/lib/runv/users.json` com conteúdo `[]` se preferires. - -O script **ignora** linhas que não sejam dicionários ou sem `username` válido. Se o ficheiro **existir** mas o JSON for inválido ou não for uma lista, o script termina com erro. - -### Saída (`-o` / `--output`) - -Um único ficheiro JSON (por omissão: **`site/public/data/members.json`**, relativo à pasta onde está o script). - -Cada elemento do array público tem: - -| Campo | Significado | -|--------|-------------| -| `username` | Nome Unix do membro | -| `since` | Valor de `created_at` no `users.json`, se existir e for string (senão `""`) | -| `path` | URL do site pessoal, ex. `"/~alice/"` | -| `homepage_mtime` | *(Opcional)* Só se usares `--homes-root`: ISO UTC da última modificação de `public_html/index.html` desse utilizador | - -### Privacidade - -**Nunca** são copiados para o ficheiro público: email, fingerprint SSH, quotas, nem outros campos internos do `users.json`. - -## Opções da linha de comando - -| Opção | Curto | Por omissão | Descrição | -|--------|------|-------------|-----------| -| `--users-json` | — | `/var/lib/runv/users.json` | Ficheiro fonte (lista JSON) | -| `--output` | `-o` | `site/public/data/members.json`* | Onde gravar o JSON público | -| `--homes-root` | — | *(não definido)* | Se definires (ex. `/home`), tenta acrescentar `homepage_mtime` por utilizador | -| `--dry-run` | — | — | Imprime o JSON no **stdout**; não grava ficheiro | - -\*O caminho por omissão é relativo ao directório do script: `<pasta_do_build_directory.py>/public/data/members.json`. - -## Como executar (a partir da raiz do repositório) - -### 1. Servidor em produção - -Com acesso a `users.json` e, de preferência, a `/home` para `homepage_mtime`: - -```bash -cd /caminho/ao/runv-server -sudo python3 site/build_directory.py \ - --users-json /var/lib/runv/users.json \ - --homes-root /home \ - -o /var/www/runv.club/html/data/members.json -``` - -Ajusta `-o` ao **DocumentRoot** real (o mesmo que usaste com [`genlanding.py`](genlanding.md), ex. `/var/www/runv.club/html/data/members.json`). - -### 2. Máquina local (sem `/var/lib/runv`) - -Usa o exemplo do repo ou uma cópia sanitizada do `users.json`: - -```bash -cd /caminho/ao/runv-server -python3 site/build_directory.py \ - --users-json site/example-users.json \ - -o site/public/data/members.json -``` - -Assim podes editar a landing e recarregar o browser sem tocar no servidor. - -### 3. Pré-visualizar no terminal (dry-run) - -```bash -python3 site/build_directory.py \ - --users-json site/example-users.json \ - --dry-run -``` - -Útil para validar o JSON sem sobrescrever ficheiros. - -### 4. Sem `--homes-root` - -Se não quiseres (ou não puderes) ler as homes: - -```bash -sudo python3 site/build_directory.py \ - --users-json /var/lib/runv/users.json \ - -o /var/www/runv.club/html/data/members.json -``` - -A lista aparece na landing; não haverá `homepage_mtime` (o JS deve tolerar campo em falta). - -## Erros comuns - -| Mensagem / situação | Causa provável | -|---------------------|----------------| -| `Ficheiro inexistente` | `--users-json` aponta para um path errado ou ficheiro ainda não criado | -| `Formato inválido: esperada lista JSON` | O ficheiro não é um array JSON no topo | -| Permissão negada ao gravar `-o` | Corre com `sudo` ou escolhe um `-o` onde o teu utilizador possa escrever | -| `homepage_mtime` nunca aparece | Falta `--homes-root` ou não existe `~/public_html/index.html` legível para esse user | -| «Escritos N membros» mas a página não mostra pontos | Gravaste em `site/public/data/` no repo; o **site público** usa o **DocumentRoot** do Apache (ex. `/var/www/runv.club/html/`). Usa `-o` para esse path, ou corre `genlanding.py` (regenera `members.json` por omissão após a cópia). | - -## Fluxo em produção (sem cron) - -Não é necessário agendar `build_directory.py` no cron. A lista pública actualiza-se quando: - -1. **`create_runv_user.py`** cria uma conta — por omissão chama `build_directory.py` com `-o <DocumentRoot>/data/members.json` se `--landing-document-root` existir no disco (padrão `/var/www/runv.club/html`). Use `--no-refresh-landing-members` para omitir. -2. **`genlanding.py`** copia a landing — por omissão volta a executar `build_directory.py` no mesmo DocumentRoot. Use `--no-refresh-members` para omitir. - -Podes continuar a correr o script **manualmente** com os exemplos desta página (útil para reparos ou ambientes sem esses passos). - -## Relação com outros ficheiros - -| Ferramenta | Papel | -|------------|--------| -| [`create_runv_user.py`](../scripts/create_runv_user.md) | Mantém `/var/lib/runv/users.json`; opcionalmente regenera `members.json` na landing | -| [`genlanding.py`](genlanding.md) | Copia `public/` para o Apache; por omissão regenera `data/members.json` a partir de `users.json` | -| `public/assets/app.js` | Faz `fetch` a `data/members.json` (caminho relativo à página) | - -Depois de alterar `members.json` no servidor, não é obrigatório recarregar o Apache — é ficheiro estático servido como qualquer outro. diff --git a/site/build_directory.py b/site/build_directory.py @@ -122,7 +122,7 @@ def main() -> None: "Nota: com membros > 0, confirme que este path é o servido pelo HTTP " "(<DocumentRoot>/data/members.json). Se a landing em produção não mostrar os pontos, " "use -o ex.: /var/www/runv.club/html/data/members.json ou copie o ficheiro para lá " - "(ou genlanding.py). Ver site/build_directory.md.", + "(ou genlanding.py). Ver docs/07-public-members-directory.md no repositório.", file=sys.stderr, ) diff --git a/site/genlanding.md b/site/genlanding.md @@ -1,115 +0,0 @@ -# genlanding.py — Apache para a landing runv - -Script em [`genlanding.py`](genlanding.py) (Python 3, stdlib) que configura o **Apache** em Debian para: - -- servir a landing estática (`site/public/`) num **VirtualHost** dedicado; -- activar **`mod_userdir`** e **`mod_rewrite`** (redirect **www → apex** em HTTP); -- em **produção** e em **`--dev`**: por omissão desactiva `000-default.conf` (salvo `--keep-default-site`) para pedidos por IP servirem a landing; opcional **Certbot** em produção (`--certbot`). - -Não substitui o manual de [`scripts/docs/2 - server setup.md`](../scripts/docs/2%20-%20server%20setup.md) para aprender permissões e diagnóstico; automatiza o caminho habitual após DNS e pacotes base. - -**SEO:** canonical, Open Graph, Twitter Card, JSON-LD, `robots.txt` e `sitemap.xml` vivem em [`public/`](public/) (sobretudo [`public/index.html`](public/index.html)); o `genlanding.py` apenas copia essa árvore para o `DocumentRoot`. - -**Notícias:** após correr [`news/publish_news.py`](news/publish_news.py) (gera `public/news/data/news.json` e `feed.rss`), execute de novo o `genlanding.py` (ou copie `public/`) para o servidor servir os ficheiros actualizados. - -**FAQ:** conteúdo em [`public/faq/index.html`](public/faq/index.html); o deploy copia `public/` inteiro, logo o FAQ segue automaticamente. Link discreto no rodapé das páginas. - -**Wiki:** ficheiros-fonte em [`wiki/*.txt`](wiki/) (`NN_slug.txt`). Em **local**, antes do deploy, gere o HTML em [`public/wiki/`](public/wiki/) com `python3 site/wiki/build_wiki.py` (actualiza também as entradas da wiki em [`public/sitemap.xml`](public/sitemap.xml) entre os comentários `<!-- wiki:gerado -->`). O `genlanding.py` **só copia** `site/public/` — **não** executa este gerador no servidor; ficheiros em `site/wiki/` (excepto o que estiver dentro de `public/`) **não** entram no `DocumentRoot`. - -## Pré-requisitos - -- **Debian** com `apache2` instalado (recomendado: [`scripts/admin/starthere.py`](../scripts/admin/starthere.py) antes). -- **Produção:** DNS de `runv.club` e `www.runv.club` a apontar para o servidor; porta **80** acessível se fores usar **Certbot**. -- Executar como **root** (`sudo`), excepto `--dry-run` (permite pré-visualizar noutra máquina). - -## Uso rápido - -```bash -cd /caminho/ao/runv-server -sudo python3 site/genlanding.py -``` - -Produção (valores por omissão: `ServerName` **runv.club**, `DocumentRoot` **`/var/www/runv.club/html`**, ficheiro **`/etc/apache2/sites-available/runv.club.conf`**). - -Pré-visualizar sem alterar nada: - -```bash -python3 site/genlanding.py --dry-run -``` - -## Flags principais - -| Flag | Descrição | -|------|-----------| -| `--dev` | Modo **teste local**: `runv.local`, `DocumentRoot` `/var/www/runv-dev/html`, ficheiro `runv-dev.conf`; por omissão **desactiva** `000-default` (igual à produção). | -| `--domain NAME` | Substitui o `ServerName` (e `www.NAME` como alias). | -| `--document-root PATH` | Substitui o `DocumentRoot`. | -| `--source PATH` | Origem da landing (default: `site/public` relativo ao script). | -| `--keep-default-site` | Mantém `000-default.conf` activo (**produção** e **`--dev`**). Com `000-default` activo, pedidos por **IP** não casam com `ServerName` e continuam a mostrar a página Debian; ver secção abaixo. | -| `--certbot` | Depois de configurar HTTP, executa `certbot --apache -d <domínio> -d www.<domínio>`. **Incompatível com `--dev`.** | -| `--dry-run` | Mostra o VirtualHost e comandos; não exige root. | -| `--no-refresh-members` | Não executar `build_directory.py` após copiar `public/` (omitir `data/members.json`). | -| `--members-users-json PATH` | Fonte para `build_directory` (default: `/var/lib/runv/users.json`). | -| `--members-homes-root PATH` | Opcional: `--homes-root` para `build_directory` (ex. `/home`). | - -## Pedidos por IP vs `ServerName` - -Com **vários** `VirtualHost *:80`, o Apache escolhe o vhost pelo cabeçalho **`Host`**. Se pedires `http://192.168.50.85/`, o `Host` é o IP (ou não coincide com `runv.local`) → o servidor usa o vhost **por defeito** na porta 80, que no Debian costuma ser **`000-default`** (`/var/www/html`, página “It works!”). - -- **Por omissão** (`--dev` ou produção **sem** `--keep-default-site`): o script desactiva `000-default`; o vhost runv fica como único (ou primeiro) em `:80` e **pedidos por IP** passam a servir a **landing** no `DocumentRoot` configurado. -- Com **`--keep-default-site`**: mantém-se `000-default`; para ver a landing usa **`http://runv.local/`** (com `/etc/hosts`) ou força o host no cliente: - - ```bash - curl -sI -H 'Host: runv.local' http://192.168.50.85/ - ``` - -Se `curl http://runv.local/` não devolver nada na VM, confirma que **`runv.local`** está em **`/etc/hosts`** a apontar para o IP correcto (ex. `127.0.0.1` ou o IP da interface). - -## Modo `--dev` (VM ou laptop) - -1. Correr: `sudo python3 site/genlanding.py --dev` (por omissão desactiva `000-default`; usa `--keep-default-site` se quiseres manter a página Debian em paralelo). -2. Opcional: no **cliente** ou na VM, editar `/etc/hosts` para nome bonito: - - ```text - 127.0.0.1 runv.local www.runv.local - ``` - - (Se o Apache estiver noutra máquina, usa o IP dessa máquina em vez de `127.0.0.1`.) - -3. Abrir `http://runv.local/` ou `http://IP_DA_VM/` no browser (sem `--keep-default-site`). O redirect **www → apex** usa **HTTP** (não uses Certbot em `--dev`). - -## Ordem sugerida (produção) - -1. `starthere.py` — pacotes, Apache a correr, quotas, etc. -2. `genlanding.py` — VirtualHost + cópia da landing. -3. Opcional: `genlanding.py --certbot` **numa segunda execução** (ou a primeira já com `--certbot` se tudo estiver pronto), **depois** de confirmar HTTP no domínio. -4. Lista de membros: após o passo 2, o script **já** corre [`build_directory.py`](build_directory.py) por omissão (salvo `--no-refresh-members`). Novas contas também disparam o mesmo via [`create_runv_user.py`](../scripts/create_runv_user.md). - -## Relação com `build_directory.py` - -- `genlanding.py` **copia** o conteúdo actual de `public/` e, por omissão, executa `site/build_directory.py` com `-o` em `<DocumentRoot>/data/members.json` e `--users-json` em `/var/lib/runv/users.json`, para a constelação reflectir contas reais **sem** depender de cron. -- **`--no-refresh-members`** omite esse passo (útil se `users.json` ainda não existir e quiseres evitar o aviso, ou fluxos especiais). - -### Lista pública (só utilizadores reais) - -- **`public/data/members.json`** no repositório deve ser **`[]`** (placeholder). **Não** versionar nomes fictícios como membros da comunidade; a única fonte de verdade para quem aparece no site é **`/var/lib/runv/users.json`**, filtrada por `build_directory.py`. -- **`site/example-users.json`** existe só para desenvolvimento / testes locais com `build_directory.py --users-json`, não para ship em produção como se fossem contas reais. -- **Deploy:** cada `genlanding.py` substitui o `DocumentRoot`; o passo integrado de `build_directory` **repõe** `members.json` a partir de `users.json`, evitando ficar preso ao `[]` do repo. - -## O que o script não faz - -- Não cria utilizadores Unix nem mexe em `users.json`. -- Não configura **firewall** nem **DNS**. -- Não valida certificados além do que o **Certbot** fizer se invocares `--certbot`. - -## Ficheiros tocados - -| Caminho | Acção | -|---------|--------| -| `/etc/apache2/sites-available/runv.club.conf` ou `runv-dev.conf` | Criado / sobrescrito | -| `DocumentRoot` (ex. `/var/www/runv.club/html`) | Conteúdo substituído pela cópia de `public/` | -| `a2enmod userdir`, `rewrite` | Activados | -| `a2dissite 000-default` | Sem `--keep-default-site` (produção ou `--dev`); falha silenciosa se já desactivado | -| `a2ensite` | Activa o site runv | - -Versão do script: ver `python3 site/genlanding.py --version`. diff --git a/site/genlanding.py b/site/genlanding.py @@ -9,13 +9,14 @@ depois volte a correr este script para copiar. Executar como root (excepto --dry-run). Apenas biblioteca padrão Python 3. -Versão 0.03 — runv.club +Versão 0.04 — runv.club """ from __future__ import annotations import argparse import grp +import json import os import pwd import re @@ -25,7 +26,7 @@ import sys from pathlib import Path from typing import Final -VERSION: Final[str] = "0.03" +VERSION: Final[str] = "0.04" EXIT_OK: Final[int] = 0 EXIT_USAGE: Final[int] = 1 EXIT_ERROR: Final[int] = 2 @@ -159,6 +160,11 @@ def refresh_members_json_in_document_root( f"({users_json} → {document_root / 'data' / 'members.json'})", ) return + if not document_root.is_dir(): + eprint( + f"Erro: DocumentRoot inexistente ({document_root}); não é possível gravar data/members.json." + ) + return script = SCRIPT_DIR / "build_directory.py" if not script.is_file(): eprint(f"Aviso: {script} não encontrado; members.json não regenerado.") @@ -187,6 +193,17 @@ def refresh_members_json_in_document_root( if r.stderr.strip(): for line in r.stderr.strip().splitlines()[:5]: print(f" {line}") + try: + data = json.loads(out.read_text(encoding="utf-8")) + if isinstance(data, list): + print( + f" [ok] constelação (bolhas): {len(data)} membro(s) — " + "o index.html faz fetch a data/members.json (relativo ao DocumentRoot)." + ) + else: + eprint("Aviso: members.json não é uma lista JSON; verifique build_directory.py.") + except (OSError, json.JSONDecodeError, TypeError) as e: + eprint(f"Aviso: não foi possível confirmar o conteúdo de members.json: {e}") def chown_www_data(path: Path, *, dry_run: bool) -> None: diff --git a/site/news/README.md b/site/news/README.md @@ -1,25 +0,0 @@ -# Publicar notícias (`publish_news.py`) - -1. Crie um ficheiro **`.md`** nesta pasta (`site/news/`) com **qualquer nome** (excepto `_*`, que são ignorados). O ficheiro **`README.md`** nunca é publicado. -2. **Linha 1:** título da notícia. -3. **Linhas seguintes:** corpo em Markdown leve: - - `**negrito**` - - `*itálico*` ou `_itálico_` - - `++sublinhado++` -4. No servidor (ou no clone), a partir da raiz do repositório: - - ```bash - python3 site/news/publish_news.py --verbose - ``` - -5. O script: - - acrescenta a notícia a `site/public/news/data/news.json` (data **DD-MM-AAAA**, fuso `America/Sao_Paulo` quando o pacote **tzdata** está disponível; caso contrário usa **UTC−3** fixo); - - regera `site/public/news/feed.rss`; - - actualiza `lastmod` da URL `/news/` em `site/public/sitemap.xml`; - - **apaga** o `.md` processado. - -**Git:** `news.json` está em `.gitignore` para evitar conflitos em `git pull` no servidor. No repositório há só `site/public/news/data/news.json.example` (lista vazia). Em produção, após o primeiro `publish_news.py`, copie o `DocumentRoot` com `genlanding.py` ou mantenha `news.json` só no servidor. - -**Windows:** instale `tzdata` (`pip install tzdata`) para o fuso `America/Sao_Paulo` exacto. - -**Modelo:** veja `_exemplo.md` (não é publicado). diff --git a/terminal/README.md b/terminal/README.md @@ -1,65 +0,0 @@ -# terminal — pedido de entrada SSH (`entre@runv.club`) - -Módulo **runv.club** para quem se liga por SSH ao utilizador Unix **`entre`**: em vez de shell normal, corre uma experiência **textual guiada** que recolhe nome de utilizador, email, **sítios ou perfis online** (onde te possamos ver) e chave pública SSH, grava um JSON na **fila local** e (opcionalmente) notifica o administrador por **sendmail**. - -**Não cria contas Linux.** O provisionamento continua a ser manual (ou via [`scripts/admin/create_runv_user.py`](../scripts/admin/create_runv_user.md)). - -## Ficheiros principais - -| Ficheiro | Função | -|----------|--------| -| `entre_app.py` | Programa principal (ForceCommand SSH). | -| `entre_core.py` | Validação, fila JSON, log, email. | -| `setup_entre.py` | Instalação no servidor (root): utilizador `entre`, shell `/bin/sh`, `--auth-mode` (`shared-password` \| `key-only` \| `empty-password` estilo tilde.town), PAM opcional, drop-in SSH, `sshd -t` + `sshd -T -C`, reload. | -| `config.example.toml` | Modelo versionado; **não** editar como `config.toml` no git. | -| `gen_config_toml.py` | Gera `config.toml` a partir do example (evita conflitos em `git pull`). | -| `templates/*.txt` | Textos da experiência e do email ao admin. | -| `docs/USO.md` | **Instalação + uso** (admin, visitante, testes, checklist). | -| `docs/INSTALL.md` | Guia de instalação detalhado (Debian 13). | -| `docs/ADMIN.md` | Operação e aprovação de pedidos. | -| `docs/ARCHITECTURE.md` | Desenho e segurança. | - -## Instalação e uso (resumo) - -Guia unificado: **[`docs/USO.md`](docs/USO.md)**. - -Em linhas: - -1. Como root: `python3 setup_entre.py` (ou `scripts/install.sh`) — por omissão `--auth-mode shared-password`, shell `/bin/sh`, validação `sshd -T -C …`. -2. **Onboarding sem senha (estilo tilde.town):** `sudo python3 setup_entre.py --auth-mode empty-password` — **PAM** por omissão; SSH por omissão **keyboard-interactive** (melhor no **OpenSSH do Windows**). Teste: `ssh entre@runv.club`. Se a sessão fechar, veja PAM e logs em [INSTALL.md](docs/INSTALL.md). Para o modo README tilde (**password** + senha vazia), use **`--empty-password-tilde-password-auth`** (Linux/Git Bash). -3. Gerar ou ajustar `/opt/runv/terminal/config.toml` com `python3 gen_config_toml.py --install-root /opt/runv/terminal` (ou `--force` para repor o example). O ficheiro está em `.gitignore` no clone; só o **example** é versionado. -4. Modo default: `sudo passwd entre`. Modo `key-only`: `authorized_keys`. -5. Visitante: `ssh entre@runv.club` e seguir o fluxo até à despedida. - -Opcional: `--skip-sshd` para aplicar o bloco `Match User entre` à mão (`INSTALL.md`). - -## Teste local (sem SSH) - -```bash -chmod +x scripts/test_local.sh -./scripts/test_local.sh -``` - -Usa `terminal/data/queue` e `config.example.toml`. Exige **`ssh-keygen`** no PATH (validação da chave). - -## Variáveis de ambiente (opcional) - -| Variável | Efeito | -|----------|--------| -| `RUNV_ENTRE_ROOT` | Raiz do módulo (default: pasta do `entre_core.py`). | -| `RUNV_ENTRE_CONFIG` | Caminho absoluto do `config.toml`. | -| `RUNV_ENTRE_QUEUE_DIR` | Sobrepõe `queue_dir` do TOML. | -| `RUNV_ENTRE_LOG_FILE` | Sobrepõe `log_file` do TOML. | -| `RUNV_ENTRE_TEMPLATES_DIR` | Sobrepõe `templates_dir`. | - -## Checklist manual de teste - -- [ ] `python3 -m py_compile entre_app.py entre_core.py setup_entre.py` -- [ ] `./scripts/test_local.sh` — percorrer fluxo até gravar JSON em `data/queue/` -- [ ] Confirmar que **não** sobrescreve se repetir o mesmo `request_id` (colisão improvável; o código regera UUID) -- [ ] Com `admin_email` preenchido e `mailutils`/`sendmail`: pedido gera tentativa de email (ver log) -- [ ] No servidor: após `setup_entre.py`, `sshd -t` OK e ficheiro `runv-entre.conf` (ou equivalente manual com `--skip-sshd`) -- [ ] `ssh entre@servidor` — fluxo completo e ficheiro em `/var/lib/runv/entre-queue/` -- [ ] Após aprovação: correr `create_runv_user.py` com dados do JSON (ver `docs/ADMIN.md`) - -Versão da app: ver `python3 entre_app.py --version`. diff --git a/terminal/docs/ADMIN.md b/terminal/docs/ADMIN.md @@ -1,103 +0,0 @@ -# Operação — fila de pedidos `entre` (runv.club) - -Fluxo geral de instalação e utilização: **[USO.md](USO.md)**. - -## Onde ficam os pedidos - -- Directório: **`/var/lib/runv/entre-queue/`** -- Um ficheiro **`{request_id}.json`** por pedido (UUID v4). -- Permissões: directório `0700`, dono **`entre`**; ficheiros `0640` na criação. - -## Conteúdo típico do JSON - -| Campo | Descrição | -|-------|-----------| -| `request_id` | Identificador único. | -| `username` | Nome Unix desejado pelo candidato. | -| `email` | Contacto. | -| `online_presence` | Texto livre com sítios/perfis indicados pelo candidato. | -| `public_key` | Linha OpenSSH normalizada. | -| `public_key_fingerprint` | SHA256 (formato OpenSSH). | -| `submitted_at` | ISO 8601 UTC. | -| `remote_addr` | Endereço remoto, se `SSH_CONNECTION`/`SSH_CLIENT` existir. | -| `tty` | `SSH_TTY`, se existir. | -| `source` | `entre-ssh`. | -| `status` | Inicialmente `pending`. | -| `app_version` | Versão do `entre_app`. | - -## Ler e filtrar - -```bash -sudo ls -1 /var/lib/runv/entre-queue/ -sudo jq -r '"\(.submitted_at) \(.username) \(.email) \(.status)"' /var/lib/runv/entre-queue/*.json -``` - -## Revisão manual - -1. Abrir o JSON e confirmar que username, email, `online_presence` e chave são plausíveis. -2. Procurar duplicados (mesmo email ou mesma fingerprint com pedidos `pending`). -3. Decidir: aprovar, rejeitar ou pedir mais informação por email **fora** deste sistema. - -## Aprovar e criar a conta real - -Use o provisionador interno **[`scripts/admin/create_runv_user.py`](../../scripts/admin/create_runv_user.py)** (no servidor, como root): - -```bash -sudo python3 /caminho/create_runv_user.py \ - --username "NOME_DO_JSON" \ - --email "EMAIL_DO_JSON" \ - --public-key 'LINHA_EXACTA_DO_JSON' -``` - -Ou modo interactivo sem flags e colar os dados. O script valida de novo (regex, chave, utilizador ainda inexistente, etc.). - -**Importante:** os dados do JSON são **proposta**; a última palavra é sempre o operador e o `create_runv_user.py`. - -## Marcar pedidos no JSON - -Não há base de dados: o operador pode: - -- Acrescentar campos manualmente, por exemplo: - - `"reviewed_at": "2026-03-20T12:00:00+00:00"` - - `"status": "approved"` | `"rejected"` | `"archived"` - - `"reviewer": "admin"` -- Ou mover ficheiros para subpastas (`approved/`, `rejected/`) se criar essa convenção localmente. - -Sugestão mínima: manter o ficheiro no sítio e só alterar `status` para auditoria simples. - -## Notificação ao administrador - -1. **Obrigatória:** novo ficheiro na fila. -2. **Log:** `/var/log/runv/entre.log` (ou o caminho em `config.toml`); também um resumo curto (`admin_console_notice`) na mesma sessão. -3. **Email:** o `entre_app.py` envia o corpo definido em `templates/admin_mail.txt` quando há destinatário válido: - - **Prioridade:** `admin_email` em `config.toml`. - - **Fallback:** se `admin_email` no TOML estiver vazio, usa `admin_email` de `/etc/runv-email.json` (o mesmo ficheiro do Mailgun / `configure_mailgun.py`). - - **Transporte:** [`entre_core.sendmail_notify`](../entre_core.py) tenta **primeiro** a API **Mailgun** via `lib.mailer.send_mail` quando o JSON global indica Mailgun; caso contrário usa `sendmail_path` (por omissão `/usr/sbin/sendmail`). Requisitos Mailgun: `email_package_root` ou variável `RUNV_EMAIL_ROOT` a apontar para a pasta `email/` do repositório. - - **Remetente:** por omissão **`noreply@runv.club`** (`mail_from` no TOML ou constante no código). Não usar `entre@runv.club` no *From* (conta SSH). Outro endereço: defina `mail_from` no `config.toml`. - -### Reenviar notificação - -Não há botão. Opções: - -- Copiar o JSON e enviar email manualmente. -- Script local que relê o JSON e chama `sendmail` com o mesmo formato que `templates/admin_mail.txt`. - -### Depuração de email - -- Ver log: `grep -E 'notificação|Mailgun|sendmail' /var/log/runv/entre.log`. -- **Mailgun:** confirmar `/etc/runv-email.json` + chave em `/etc/runv-email.secrets.json`; IP allowlist no painel Mailgun; `email_package_root` ou `RUNV_EMAIL_ROOT`. -- **Legado (MTA):** testar `echo test | mail -s test root` (conforme o servidor); `ls -l /usr/sbin/sendmail`. - -## Pedidos inválidos ou spam - -- Marcar `status` como `rejected` ou arquivar. -- Não apagar de imediato se quiseres trilho de auditoria; podes mover para `archive/` depois de um tempo. -- **Rate limiting** avançado está fora de âmbito deste módulo; pode ser feito à frente (fail2ban, firewall, etc.). - -## Logs e privacidade - -Os JSONs contêm dados pessoais e chave pública. Restringe acesso ao directório da fila e rotações de log conforme a política da runv. - -## Ligação com o site / documentação pública - -Se existir página “Junte-se a nós” no site estático, deve apontar para **`ssh entre@runv.club`** e explicar geração de chaves — mantém coerência com este fluxo. diff --git a/terminal/docs/ARCHITECTURE.md b/terminal/docs/ARCHITECTURE.md @@ -1,65 +0,0 @@ -# Arquitectura — módulo `terminal` (entre SSH) - -Instalação e uso operacional: **[USO.md](USO.md)**. - -## Fluxo ponta a ponta - -```mermaid -sequenceDiagram - participant C as Cliente_SSH - participant S as sshd - participant A as entre_app.py - participant Q as entre_queue - participant L as entre.log - participant M as sendmail - - C->>S: autentica como entre - S->>A: ForceCommand - A->>C: splash, intro curta + aviso chave - A->>C: formulário (4 passos: user, email, presença online, pubkey) - C->>A: respostas por ecrã (presença online: várias linhas até .) - A->>A: validação (entre_core) - A->>Q: JSON O_EXCL - A->>L: eventos - opt admin_email configurado - A->>M: email resumo - end - A->>C: despedida -``` - -## Componentes - -| Peça | Papel | -|------|--------| -| `entre_app.py` | Orquestra etapas, I/O terminal, confirmação. | -| `entre_core.py` | Config TOML, validação (alinhada ao `create_runv_user.py`), escrita atómica do JSON, logging, `sendmail`. | -| `setup_entre.py` | Bootstrap: utilizador `entre`, árvore em `/opt/runv/terminal`, permissões, snippet SSH impresso. | -| `templates/*.txt` | Conteúdo editável sem alterar código. | -| `systemd/*.path` + `*.service` | Gatilho opcional em alterações da fila. | - -## Decisões de segurança - -- **Sem `shell=True`:** `subprocess.run([...], ...)` apenas com listas literais. -- **Sem criação de utilizadores** no `entre_app.py` / `entre_core.py`. -- **Sem alteração de Apache ou sshd** pelo código de aplicação. -- **Fila:** criação com `O_CREAT|O_EXCL` para não sobrescrever ficheiros existentes. -- **Entrada:** limites de tamanho; chave numa linha; rejeição de marcadores de chave privada. -- **SSH:** `DisableForwarding` e afins recomendados no `Match User entre` para limitar túneis e agent forwarding. -- **Utilizador `entre`:** shell `nologin` reduz superfície se alguma configuração falhar (ainda assim, o essencial é o `ForceCommand` correcto). - -## Por que a conta não é criada na hora - -- **Revisão humana:** pubnix/tilde costuma evitar contas automáticas abertas a abuso. -- **Coerência com o projeto:** o provisionamento oficial e quotas/metadata estão centralizados em `create_runv_user.py`. -- **Auditoria:** JSONs imutáveis na entrada (novo `request_id` por tentativa gravada) facilitam rastrear o que foi pedido. - -## Pontos de extensão futura - -- Campo opcional “mensagem ao admin” no JSON. -- Script que promove `pending` → `approved` e chama `create_runv_user.py`. -- Notificação via webhook ou Matrix no `runv-entre-notify.service`. -- Base de dados: substituir fila por tabela **mudaria** este módulo; hoje é deliberadamente ficheiro-only. - -## Alinhamento com `create_runv_user.py` - -Regex de username/email (inclui checagem explícita de um único `@` antes do regex), tipos de chave e normalização da linha pública seguem a mesma filosofia que [`scripts/admin/create_runv_user.py`](../../scripts/admin/create_runv_user.py). O código **não** importa esse ficheiro em runtime (evita dependência de path do repositório em `/opt/runv/terminal`); comentários no código referem a necessidade de manter políticas sincronizadas. diff --git a/terminal/docs/INSTALL.md b/terminal/docs/INSTALL.md @@ -1,241 +0,0 @@ -# Instalação — fluxo SSH `entre` (runv.club) - -Guia para **Debian 13** (ou derivado próximo). Por defeito, `setup_entre.py` instala **`/etc/ssh/sshd_config.d/runv-entre.conf`**, corre **`sshd -t`** e **`systemctl reload ssh`** (com backup do drop-in anterior se existir). Use **`--skip-sshd`** se preferir aplicar o bloco à mão. - -Para um único documento com **instalação + uso** (visitante e admin), ver também **[USO.md](USO.md)**. - -## 1. Dependências - -```bash -sudo apt update -sudo apt install -y python3 openssh-server openssh-client mailutils -``` - -- **python3** — interpretador (stdlib: `tomllib`, `email`, etc.). -- **openssh-client** — binário `ssh-keygen` usado para validar fingerprint da chave pública. -- **openssh-server** — serviço SSH. -- **mailutils** (ou outro MTA com **sendmail** em `/usr/sbin/sendmail`) — **opcional**, só para email ao admin. - -**Recomendado para o servidor runv.club:** configurar envio **sem Postfix/Exim** com o módulo do repositório **[`email/`](../email/README.md)** (`msmtp` + `msmtp-mta` + `bsd-mailx`). Depois disso, `/usr/sbin/sendmail` encaminha para o seu SMTP externo e o `entre` continua a usar `sendmail_path = "/usr/sbin/sendmail"` no `config.toml`. - -## 2. Obter o código - -A partir do repositório `runv-server`, a pasta relevante é `terminal/`. - -## 3. Executar o setup (root) - -```bash -cd /caminho/do/repositório/terminal -sudo python3 setup_entre.py -``` - -Ou: - -```bash -sudo sh scripts/install.sh -``` - -O script: - -- cria o utilizador **`entre`** (se não existir), com home por omissão `/home/entre` e shell **`/bin/sh`** (o OpenSSH precisa de shell funcional para o contexto do **ForceCommand**; `nologin` impede o fluxo); -- alinha o shell com **`chsh`** se `entre` já existir com outro shell; -- garante **`~entre/.ssh`** e **`authorized_keys`** (vazio; útil sobretudo em `--auth-mode key-only`); -- cria **`/var/lib/runv/entre-queue`** (dono `entre`, modo `0700`); -- garante **`/var/log/runv/`** e o ficheiro **`entre.log`** (dono `entre`, leitura/escrita para append); -- copia o módulo para **`/opt/runv/terminal`** e, se não existir `config.toml`, gera-o a partir de `config.example.toml`; -- **OpenSSH (por defeito):** escreve o drop-in conforme **`--auth-mode`** (omissão: **`shared-password`**); **`sshd -t`**, validação **`sshd -T -C …`**, **`systemctl reload ssh`** (em falha, reverte o drop-in). - -Opções úteis: - -- `--auth-mode shared-password` | `key-only` | `empty-password` — método para `entre`. -- **`empty-password` (onboarding estilo [tilde.town](https://tilde.town) / `join@tilde.town`):** cria grupo **`entre-open`**, mete `entre` no grupo, **`passwd -d`**, valida **NP**. **Por omissão** o drop-in usa **`AuthenticationMethods keyboard-interactive`** + **`KbdInteractiveAuthentication yes`** (PAM **`pam_succeed_if`** sem prompts) — melhor com **OpenSSH do Windows**, que em geral não envia palavra-passe vazia no método **`password`**. **`--empty-password-tilde-password-auth`** volta ao esquema README tilde (**`password`** + **`PermitEmptyPasswords yes`**). **Por omissão** altera **`/etc/pam.d/sshd`**: backup e linha **`pam_succeed_if … user ingroup …`** antes de **`@include common-auth`**. Sem isto, no Debian o **PAM** pode recusar o fluxo → **«Connection closed»**. **Não** é ausência total de autenticação: é política explícita só para `entre`. -- `--empty-password-group` — nome do grupo suplementar (default: `entre-open`). -- **`--empty-password-tilde-password-auth`** — só com **`empty-password`**: drop-in estilo README tilde (**`password`** + **`PermitEmptyPasswords yes`**); omissão = keyboard-interactive (recomendado para Windows). -- **`--skip-pam-empty-password-rule`** — não mexer no PAM (só para quem configura à mão; em geral **não** use em `empty-password` em Debian). -- `--sshd-test-connection` — argumento `-C` para `sshd -T` (deve bater com o `Match`, ex.: `user=entre,host=runv.club,addr=127.0.0.1`). -- `--dry-run` — apenas mensagens, sem alterações. -- **Reexecução:** se já existir **`/opt/runv/terminal/entre_app.py`**, em terminal interactivo o script pergunta se deseja continuar (actualiza `entre_app.py`, `entre_core.py`, `templates/`, etc.). Responder **não** cancela tudo. Em seguida, se **`config.toml`** já existir, pergunta se deve **substituí-lo** pelo example (omissão: **não**, para não perder `admin_email`). -- `-y` / `--yes` — não mostrar esses prompts (útil em scripts); **`config.toml`** continua preservado salvo **`--force-config`**. -- `--force-config` — repõe `config.toml` a partir do example (sem segundo prompt). -- `--skip-copy` — só directórios/utilizador (sem copiar ficheiros). -- `--skip-sshd` — não toca no SSH; imprime o bloco `Match User entre` para cópia manual. -- `--no-reload` — grava o drop-in e corre `sshd -t` + validação `-T`, mas não recarrega o serviço (útil para rever antes). - -## 4. Configuração (`config.toml`) - -O **`config.toml`** não deve ser versionado no repositório (está em **`.gitignore`** em `terminal/config.toml` no clone). Gere-o a partir do modelo: - -```bash -sudo python3 /opt/runv/src/terminal/gen_config_toml.py --install-root /opt/runv/terminal -``` - -- **`--force`** — sobrescreve um `config.toml` já existente (perde edições locais nesse ficheiro). -- O **`setup_entre.py`** chama a mesma lógica na primeira instalação (ou com **`--force-config`**). - -Edite **`/opt/runv/terminal/config.toml`** quando precisar de valores que não vêm do example: - -- **`admin_email`** — endereço para notificações. Pode ficar vazio no TOML se **`admin_email`** estiver definido em **`/etc/runv-email.json`** (fallback usado pelo `entre_app.py`). Se ambos estiverem vazios, só fila + log. -- **`mail_from`** — remetente do email (cabeçalho `From`); por omissão **`noreply@runv.club`** (não use **`entre@runv.club`**: essa conta é só SSH). Valores antigos `entre@runv.club` no TOML são normalizados para noreply. Para outro remetente verificado no Mailgun, defina explicitamente no TOML. -- **`sendmail_path`** — normalmente `/usr/sbin/sendmail` (ramo legado; com Mailgun configurado, o envio pode ser pela API sem precisar de MTA). - -## 5. Autenticação SSH para o utilizador `entre` - -O OpenSSH **exige** sempre **alguma** credencial; **não** existe “`ssh` e entrou” sem palavra-passe nem chave no protocolo. - -**Modo recomendado (`--auth-mode shared-password`):** palavra-passe Unix **partilhada**, definida **só pelo root** (`sudo passwd entre`, etc.), com **`AuthenticationMethods password`**, **`PubkeyAuthentication no`** e **`KbdInteractiveAuthentication no`** no `Match User entre`, para acesso sem chave pré-registada. - -**`key-only`:** só chave pública em **`authorized_keys`**; sem palavra-passe. - -**`empty-password`:** `passwd -d entre`, grupo **`entre-open`**, regra PAM **`pam_succeed_if user ingroup entre-open`** (recomendado no Debian). **Por omissão** o SSH usa **`keyboard-interactive`** (PAM resolve sem prompts; compatível com Windows). Com **`--empty-password-tilde-password-auth`**, **`AuthenticationMethods password`** + **`PermitEmptyPasswords yes`** (como muitos README tilde). **Menos seguro** que palavra-passe ou chave; usar só para onboarding do utilizador especial `entre`, não para contas normais. - -O fluxo **`entre_app`** (historinha + formulário) **não** altera a senha Unix; só recolhe o pedido de conta. - -O visitante **não** obtém shell interactivo normal: o **`ForceCommand`** substitui o comando remoto (o shell em passwd é apenas o contexto mínimo exigido pelo OpenSSH). - -## 6. OpenSSH (`runv-entre.conf`) - -O setup coloca o ficheiro **`/etc/ssh/sshd_config.d/runv-entre.conf`** com o mesmo conteúdo lógico que abaixo (o caminho de `python3` vem de `which python3` no servidor). Confirme que **`/etc/ssh/sshd_config`** inclui algo como `Include /etc/ssh/sshd_config.d/*.conf` (comum no Debian). - -Exemplo equivalente: - -``` -Match User entre - AuthenticationMethods password - PasswordAuthentication yes - KbdInteractiveAuthentication no - PubkeyAuthentication no - PermitEmptyPasswords no - ForceCommand /usr/bin/python3 /opt/runv/terminal/entre_app.py - PermitTTY yes - PermitUserRC no - X11Forwarding no - AllowAgentForwarding no - AllowTcpForwarding no - PermitTunnel no - DisableForwarding yes -``` - -Exemplo **`--auth-mode empty-password`** (omissão; keyboard-interactive + PAM — recomendado para Windows): - -``` -Match User entre - AuthenticationMethods keyboard-interactive - PasswordAuthentication no - KbdInteractiveAuthentication yes - PubkeyAuthentication no - PermitEmptyPasswords no - ForceCommand /usr/bin/python3 /opt/runv/terminal/entre_app.py - PermitTTY yes - PermitUserRC no - X11Forwarding no - AllowAgentForwarding no - AllowTcpForwarding no - PermitTunnel no - DisableForwarding yes -``` - -Com **`--empty-password-tilde-password-auth`** (README tilde; `PermitEmptyPasswords yes`): - -``` -Match User entre - AuthenticationMethods password - PasswordAuthentication yes - KbdInteractiveAuthentication no - PubkeyAuthentication no - PermitEmptyPasswords yes - ForceCommand /usr/bin/python3 /opt/runv/terminal/entre_app.py - PermitTTY yes - PermitUserRC no - X11Forwarding no - AllowAgentForwarding no - AllowTcpForwarding no - PermitTunnel no - DisableForwarding yes -``` - -Em **`empty-password`**, o script faz backup de **`/etc/pam.d/sshd`** e insere antes de `@include common-auth` (ou primeira linha `auth`), salvo **`--skip-pam-empty-password-rule`**: - -``` -auth [success=done default=ignore] pam_succeed_if.so user ingroup entre-open -``` - -(Ajuste `entre-open` com **`--empty-password-group`** se mudar o nome do grupo.) - -Se usou **`--skip-sshd`**, crie o ficheiro à mão e depois: - -```bash -sudo sshd -t -sudo systemctl reload ssh -``` - -Confirme que o caminho de **`python3`** e de **`entre_app.py`** coincidem com o servidor (`which python3`). - -## 7. Teste local do programa (sem SSH) - -Na máquina de desenvolvimento (com `ssh-keygen` disponível): - -```bash -cd terminal -chmod +x scripts/test_local.sh -./scripts/test_local.sh -``` - -Os pedidos ficam em `terminal/data/queue/`. - -## 8. Teste via SSH - -A partir de um cliente: - -```bash -ssh entre@runv.club -``` - -(Substitua o host.) Percorra o fluxo até ao fim e verifique: - -```bash -sudo ls -la /var/lib/runv/entre-queue/ -sudo jq . /var/lib/runv/entre-queue/<request_id>.json -``` - -## 9. Teste de notificação por email - -1. Defina o destinatário: **`admin_email`** no `config.toml` **ou** (se o TOML estiver vazio) **`admin_email`** em **`/etc/runv-email.json`**. -2. **Mailgun:** estado e segredos correctos; `email_package_root` ou `RUNV_EMAIL_ROOT`; teste com `email/configure_mailgun.py --test` no servidor. -3. **Legado:** **`sendmail`** e MTA a aceitar relay ou mail local. -4. Opcional: inspeccionar o formato com: - -```bash -sh scripts/test_mail.sh -``` - -Se o email falhar, o pedido **mantém-se** na fila e o log regista o aviso (`notificação Mailgun falhou`, `sendmail falhou`, etc.). - -## 10. systemd.path (opcional) - -Para reagir a alterações na fila (log extra, hook próprio): - -```bash -sudo cp systemd/runv-entre-notify.path /etc/systemd/system/ -sudo cp systemd/runv-entre-notify.service /etc/systemd/system/ -sudo systemctl daemon-reload -sudo systemctl enable --now runv-entre-notify.path -``` - -Edite **`runv-entre-notify.service`** se quiser outro `ExecStart` (sem depender do Python do módulo para notificações simples). - -## 11. Segurança e reversão do drop-in - -A instalação automática faz **backup** do ficheiro anterior (`runv-entre.conf.bak.<timestamp>`), valida com **`sshd -t`** e só então recarrega o serviço. Se o teste falhar, o script **reverte** (ou remove o ficheiro numa primeira instalação). Para ambientes onde qualquer alteração ao SSH exige revisão prévia, use **`--no-reload`** ou **`--skip-sshd`**. - -## Problemas frequentes - -| Sintoma | Verificação | -|---------|-------------| -| `entre_app.py` não arranca | Permissões em `/opt/runv/terminal`, dono `entre`, `python3` no caminho. | -| Erro ao gravar fila | Dono e modo de `/var/lib/runv/entre-queue`. | -| Log vazio / permissão | Dono de `/var/log/runv/entre.log`. | -| Chave rejeitada | `ssh-keygen` instalado; chave numa linha; tipo permitido. | -| Sessão SSH fecha logo | Autenticação de `entre` falhou antes do ForceCommand. | -| Email do novo pedido não chega | `admin_email` no TOML ou no `/etc/runv-email.json`; Mailgun: allowlist de IP, chave HTTP, `email_package_root` / `RUNV_EMAIL_ROOT`; legado: `sendmail_path` e MTA. Ver log `entre`. | - -Documentação de operação: **[ADMIN.md](ADMIN.md)**. Desenho: **[ARCHITECTURE.md](ARCHITECTURE.md)**. diff --git a/terminal/templates/admin_mail.txt b/terminal/templates/admin_mail.txt @@ -18,4 +18,4 @@ Chave pública (uma linha): --- Fila: /var/lib/runv/entre-queue/{request_id}.json -Aprovar com create_runv_user.py (ver terminal/docs/ADMIN.md). +Aprovar com create_runv_user.py (ver docs/10-user-provisioning-and-admin-ops.md no repositório runv-server). diff --git a/tools/README.md b/tools/README.md @@ -1,63 +0,0 @@ -# tools — experiência base runv.club (Debian) - -Módulo para **automatizar** no servidor Debian 13 (ou compatível): - -1. **Pacotes globais** via `apt` (lista em `manifests/apt_packages.txt`) — para todos os usuários, **sem** passar pelo `/etc/skel`. -2. **Comandos locais** em `/usr/local/bin`: `runv-help`, `runv-links`, `runv-status`, **`chat`** (IRC; rede da casa provisionada com **`patches/patch_irc.py`** — utilizadores usam só `chat`). -3. **MOTD dinâmico** em `/etc/update-motd.d/60-runv` (arte ASCII verde, texto em português). -4. **Arquivos padrão** copiados para `/etc/skel/` (README, `.bash_aliases`, `public_html/index.html`, `public_gopher/gophermap`, `public_gemini/index.gmi`) — **somente modelos de home**, nunca instaladores de sistema. - -## Regras - -- **`/etc/skel`** = apenas arquivos que **novas contas** recebem na home (via `adduser`). **Não** instala programas. -- **Programas** = sempre **`apt`** (globais). -- **Scripts do projeto** = **`/usr/local/bin`**. -- **MOTD** = script executável em **`/etc/update-motd.d/`**. -- Python **stdlib** apenas; **`subprocess` sem `shell=True`**; sem Docker, sem web, sem DB. - -## Execução rápida - -No servidor, a partir da raiz do repositório (ou com caminho absoluto): - -```bash -sudo python3 tools/tools.py -``` - -Simular sem alterar nada: - -```bash -sudo python3 tools/tools.py --dry-run --verbose -``` - -Sem `--force`, o script **atualiza** MOTD, `bin/` e skel quando o ficheiro no repositório **mudou** em relação ao destino. Para sobrescrever **sempre** (mesmo idêntico): - -```bash -sudo python3 tools/tools.py --force -``` - -Reaplicar só scripts/MOTD/skel **sem** rodar `apt`: - -```bash -sudo python3 tools/tools.py --skip-apt -``` - -## Conteúdo - -| Caminho | Função | -|---------|--------| -| `tools.py` | Orquestra apt, cópias e permissões | -| `manifests/apt_packages.txt` | Um pacote Debian por linha | -| `bin/` | Scripts shell instalados em `/usr/local/bin` | -| `motd/60-runv` | Fragmento MOTD (verde, pubnix) | -| `skel/` | Modelos copiados para `/etc/skel/` | -| `docs/` | Instalação, administração, experiência do usuário | - -## Byobu - -O pacote **byobu** é instalado **globalmente** com os demais, mas **não** é ativado automaticamente para todos os usuários. Quem quiser pode habilitar depois com **`byobu-enable`** na própria conta. Integrar isso ao fluxo de **provisionamento** (`create_runv_user` / onboarding) fica para uma etapa futura — não é papel deste módulo forçar Byobu no login. - -## Documentação - -- **[docs/INSTALL.md](docs/INSTALL.md)** — dependências, flags, verificação. -- **[docs/ADMIN.md](docs/ADMIN.md)** — operação e manutenção. -- **[docs/USER_EXPERIENCE.md](docs/USER_EXPERIENCE.md)** — o que o usuário vê e recebe. diff --git a/tools/docs/ADMIN.md b/tools/docs/ADMIN.md @@ -1,107 +0,0 @@ -# Administração — módulo `tools/` - -Operação contínua do runv.club em **Debian**. - -## Atualizar a lista de pacotes - -1. Edite **`tools/manifests/apt_packages.txt`** (um pacote por linha; comentários com `#`). -2. No servidor: - -```bash -sudo python3 tools/tools.py --verbose -``` - -Use **`--skip-apt`** se quiser **não** rodar o apt nesta passada (por exemplo, durante janela de manutenção em que só atualiza arquivos). - -## Trocar textos do MOTD - -- Edite **`tools/motd/60-runv`** no repositório (shell `sh`, sem `figlet`). O logótipo **RUNV** usa as mesmas linhas UTF-8 que a landing e o `entre_app.py`; só esse bloco leva ANSI verde (`%b` + literais `\033`, não `echo -e`). -- Reaplique: - -```bash -sudo python3 tools/tools.py --force --skip-apt -``` - -(`--force` força cópia mesmo sem mudança no conteúdo; sem ele, basta alterar o ficheiro no repo e rodar `tools.py`.) - -**Boas práticas:** mantenha fallbacks (`command -v` / redirecionar stderr) para não quebrar o login se algum binário sumir. - -## Editar `runv-help`, `runv-links`, `runv-status` - -- **`runv-status`** verifica o nome de login (`id -un`); por omissão só **`pmurad-admin`** passa. Para outro admin, edite a variável **`RUNV_STATUS_USER`** no script em **`tools/bin/runv-status`** antes de reaplicar. - -1. Altere os arquivos em **`tools/bin/`**. -2. Instale de novo: - -```bash -sudo python3 tools/tools.py --force --skip-apt -``` - -Confirme permissões **755** em `/usr/local/bin/`. - -## Reaplicar tudo com `tools.py` - -Instalação completa (apt + arquivos): - -```bash -sudo python3 tools/tools.py --force --verbose -``` - -Só arquivos (sem apt): - -```bash -sudo python3 tools/tools.py --force --skip-apt -``` - -## Remover um script - -O `tools.py` **não remove** arquivos do sistema. Para retirar, por exemplo, `runv-help`: - -```bash -sudo rm -f /usr/local/bin/runv-help -``` - -Para o MOTD: - -```bash -sudo rm -f /etc/update-motd.d/60-runv -``` - -Para modelos no skel (cuidado — afeta **novas** contas, não apaga homes existentes): - -```bash -sudo rm -f /etc/skel/README.md -# etc. -``` - -Depois, se quiser reinstalar só a partir do repositório: - -```bash -sudo python3 tools/tools.py --force --skip-apt -``` - -## Ajustar permissões manualmente - -Se algo ficou com modo errado: - -```bash -sudo chmod 755 /usr/local/bin/runv-help /usr/local/bin/runv-links /usr/local/bin/runv-status -sudo chmod 755 /etc/update-motd.d/60-runv -sudo chmod 644 /etc/skel/README.md /etc/skel/.bash_aliases /etc/skel/public_html/index.html -sudo chmod 755 /etc/skel/public_html -``` - -Dono típico: **root:root** (o script tenta `chown` após copiar). - -## Byobu - -- **Instalado** globalmente com o apt deste módulo. -- **Não** habilitado automaticamente para todos (evita surpresas no login). -- Usuários podem usar **`byobu-enable`** quando quiserem. -- Documentar ou automatizar no **onboarding** / `create_runv_user` é decisão futura — ver **`tools/README.md`**. - -## Idempotência - -- Rodar **`tools.py`** **sem `--force`** compara origem e destino: se forem **idênticos**, pula; se o repo tiver **versão nova**, copia e atualiza. -- **`apt-get install`** já é idempotente para pacotes instalados. -- Use **`--force`** para sobrescrever **sempre** (mesmo conteúdo igual), por exemplo para repor dono/permissões. diff --git a/tools/docs/INSTALL.md b/tools/docs/INSTALL.md @@ -1,133 +0,0 @@ -# Instalação — módulo `tools/` (runv.club) - -Guia em **português** para administradores. Ambiente alvo: **Debian 13** (ou Debian estável recente). - -## Dependências - -- **root** no servidor (sudo). -- **Python 3** do Debian (sem PyPI obrigatório). -- **`apt`** funcional (`apt-get`). -- Rede para `apt-get update` / `install` (ou espelho local configurado). - -Não é necessário Docker, banco de dados nem painel web. - -## O que o `tools.py` faz - -1. Valida execução como **root** (exceto em `--dry-run`, que só simula). -2. Lê **`manifests/apt_packages.txt`** (ignora linhas vazias e `#`). -3. Executa **`apt-get update -qq`** e **`apt-get install -y --no-install-recommends`** com esses pacotes. -4. Copia **`bin/runv-help`**, **`runv-links`**, **`runv-status`**, **`bin/chat`** → **`/usr/local/bin/`** com modo **755** (`chat` abre o IRC com config em `~/.config/weechat`; ver **`scripts/docs/irc_patch.md`**). -5. Copia **`motd/60-runv`** → **`/etc/update-motd.d/60-runv`** com modo **755**. -6. Garante **Jailkit + SSH chroot** (idempotente): grupo **`runv-jailed`**, remove **`pmurad-admin`** desse grupo se estiver, instala **`/etc/ssh/sshd_config.d/90-runv-jailed.conf`** a partir do repo, **`sshd -t`** e **`systemctl reload ssh`** (ou `sshd`). -7. Copia o **`skel/`** do repositório para **`/etc/skel/`**: - - `.bash_aliases` → **644** - - `public_html/index.html` → diretório **`public_html` 755**, arquivo **644** - - `public_gopher/gophermap` → diretório **`public_gopher` 755**, arquivo **644** - - `public_gemini/index.gmi` → diretório **`public_gemini` 755**, arquivo **644** - - O **`README.md` não** é copiado para `/etc/skel` (política runv: contas novas sem README na home por defeito). Se existir **`/etc/skel/README.md`** de uma instalação antiga, o `tools.py` **remove** esse ficheiro ao sincronizar o skel. - -O **`/etc/skel`** só afeta **contas novas** criadas depois da cópia (o Debian copia o skel no `adduser`). Utilizadores **já existentes** não recebem automaticamente estes ficheiros: use **[`scripts/admin/setup_alt_protocols.py`](../../scripts/docs/alt_protocols.md)** (backfill) ou crie `~/public_gopher` e `~/public_gemini` manualmente. - -Se o destino **já existir** e for **idêntico** (conteúdo byte-a-byte) à origem no repositório, a cópia é **ignorada**. Se o ficheiro no repo **mudou**, o `tools.py` **atualiza** o destino mesmo sem **`--force`**. Use **`--force`** para sobrescrever sempre (útil para repor permissões/mtime ou forçar cópia igual). - -## Execução - -```bash -cd /caminho/para/runv-server -sudo python3 tools/tools.py -``` - -### Flags - -| Flag | Efeito | -|------|--------| -| `--dry-run` | Não grava nem chama apt de verdade; mostra o que seria feito. | -| `--verbose` | Log detalhado no stderr. | -| `--force` | Sobrescreve sempre, mesmo quando origem e destino são idênticos. | -| `--skip-apt` | Pula `apt-get` (útil para atualizar só MOTD/bin/skel). | - -Exemplo seguro antes da primeira aplicação: - -```bash -sudo python3 tools/tools.py --dry-run --verbose -``` - -## Verificar pacotes instalados - -```bash -dpkg -l wtmpdb jailkit byobu tmux lynx weechat weechat-headless mutt bsdgames tree less curl wget git -``` - -Ou: - -```bash -apt list --installed 2>/dev/null | grep -E 'wtmpdb|jailkit|byobu|tmux|lynx|weechat|mutt|bsdgames|tree|less|curl|wget|git' -``` - -**Importante:** esses programas são **globais**. **Não** dependem do `/etc/skel`. Qualquer usuário com shell pode usá-los após a instalação (e após login, se o pacote estiver no `PATH`). - -## Verificar comandos em `/usr/local/bin` - -```bash -ls -l /usr/local/bin/runv-help /usr/local/bin/runv-links /usr/local/bin/runv-status /usr/local/bin/chat -/usr/local/bin/runv-help -``` - -Devem ser executáveis (**`-rwxr-xr-x`**) e imprimir texto em português com cores. - -## Verificar MOTD - -O Debian monta o MOTD com scripts em `/etc/update-motd.d/`. Para testar **só** o fragmento runv: - -```bash -sudo chmod +x /etc/update-motd.d/60-runv # se ainda não estiver -/etc/update-motd.d/60-runv -``` - -Para ver a sequência completa (pode ser longa): - -```bash -run-parts /etc/update-motd.d/ -``` - -Em novo login SSH você deve ver o bloco **verde** com arte **RUNV**, a tagline, a lista de comandos úteis e a dica **“digite runv-help para começar”**. Estatísticas do servidor (**`runv-status`**) não aparecem no MOTD nem em `runv-help`; só o utilizador **`pmurad-admin`** pode executar `runv-status`. - -## Verificar `/etc/skel` - -```bash -ls -la /etc/skel/ -ls -la /etc/skel/public_html/ -``` - -Esperado: - -- `.bash_aliases` com permissões **644** (arquivo). -- **Sem** `README.md` em `/etc/skel` após sincronização. -- `public_html` como diretório **755**. -- `public_html/index.html` **644**. - -Novas contas criadas com `adduser` **depois** desta instalação recebem esses arquivos na home (junto com o restante do skel padrão do Debian, como `.bashrc`, se existir no sistema). - -## SSH: grupo `runv-jailed` (chroot) - -O drop-in **`90-runv-jailed.conf`** aplica `ChrootDirectory /srv/jail/%u` a utilizadores no grupo **`runv-jailed`**. A conta **`pmurad-admin`** **não** deve estar nesse grupo (administração fora do chroot). Novas contas normais recebem jail e grupo via **`scripts/admin/create_runv_user.py`**; contas já existentes via **`scripts/admin/perm1.py`**. - -Após alterar o sshd, confirme **`sshd -t`** e teste login com um utilizador de staging antes de aplicar em produção. - -## Instruções de teste (checklist) - -1. **Dry-run:** `sudo python3 tools/tools.py --dry-run --verbose` — revisar saída. -2. **Aplicar:** `sudo python3 tools/tools.py --verbose`. -3. **Segunda execução** sem `--force` com repo **inalterado** — deve **pular** ficheiros já iguais; após **editar** MOTD/bin/skel no repo, a mesma execução deve **copiar de novo**. -4. **`runv-help` / `runv-links`** — qualquer utilizador; **`runv-status`** — apenas como **`pmurad-admin`**. -5. **MOTD:** rodar `/etc/update-motd.d/60-runv` ou novo login SSH. -6. **Skel:** criar usuário de teste com `adduser` e conferir **ausência** de `~usuario/README.md` e presença de `~/public_html/index.html` (se o skel runv tiver sido aplicado). - -## Problemas comuns - -- **apt-get update falha:** corrija espelhos/rede; o script registra erro e ainda pode copiar bin/MOTD/skel. -- **Permissão negada:** execute com `sudo` / root. -- **MOTD não aparece:** em alguns setups o display do MOTD depende de `pam_motd` e SSH; confira configuração do `sshd` e PAM no Debian. -- **MOTD sem grelha `last`:** em **Debian 13+**, o comando **`last`** vem do pacote **`wtmpdb`**, não de `util-linux`. Instale com `apt install wtmpdb`. O fragmento `60-runv` tenta `last` em PATH, `/usr/bin/last` e `/bin/last`. A mensagem *sem registos recentes em wtmp* indica wtmp vazio, não falta do binário. -- **Gemini (`molly-brown`) inactivo ou «activating»:** guia de diagnóstico (journalctl com `sudo`, porta 1965, permissões da chave TLS) em **`scripts/docs/alt_protocols.md`** — secção *Molly não sobe ou fica em «activating»*. diff --git a/tools/docs/USER_EXPERIENCE.md b/tools/docs/USER_EXPERIENCE.md @@ -1,57 +0,0 @@ -# Experiência do usuário — runv.club (`tools/`) - -Visão para **quem entra no servidor** pela primeira vez (e para quem documenta suporte). - -## O que aparece no login - -1. **MOTD** — O Debian executa os scripts em `/etc/update-motd.d/`. O fragmento **`60-runv`** mostra: - - logótipo **RUNV** (mesmo desenho UTF-8 da landing) **só nesse bloco** em verde; - - tagline `.club — um computador para compartilhar` (sem estatísticas no MOTD; o comando **`runv-status`** existe mas **não** é listado aqui e só o utilizador **`pmurad-admin`** pode executá-lo); - - **Comandos úteis** em lista, com nome a verde e descrição a cinza (ANSI), alinhada ao texto do `runv-help`; - - secção **Últimos usuários online**: grelha **3×3** com até **9 nomes únicos** (fonte: **`last`** / wtmp; ordem = atividade recente; cada utilizador só aparece **uma** vez; ignora linhas `reboot` / `wtmp` e os utilizadores **`entre`** e **`root`**). Em **Debian 13+**, o binário **`last`** vem do pacote **`wtmpdb`** (o `tools.py` instala-o). O fragmento tenta **`/usr/bin/last`** se o PATH de `update-motd.d` não incluir `last`. Se aparecer *sem registos recentes em wtmp*, o ficheiro de logins ainda não tem entradas (ex.: sem logins SSH registados). - - linha final: **digite `runv-help` para começar**. - -2. **Prompt da shell** — Depende do shell padrão (geralmente Bash no Debian). O que o usuário **herda** da home vem do **`/etc/skel`** no momento em que a conta foi criada. - -## Comandos locais do runv - -| Comando | Função | -|---------|--------| -| **`runv-help`** | Texto de ajuda: o que é o runv, comandos úteis, dicas, link do site. | -| **`runv-links`** | Links: runv.club, Portal IDEA, etc. | -| **`runv-status`** | (Só **`pmurad-admin`**) hostname, uptime, memória, disco, `who`. Não aparece no MOTD nem em `runv-help`. | - -Todos são **shell scripts** em **`/usr/local/bin`**, com cores ANSI simples, texto em **português**. Não dependem de Python na sessão do usuário. - -## O que o usuário recebe na home (contas novas) - -Quando um administrador cria a conta com **`adduser`**, o Debian copia **`/etc/skel`** para a home. Depois de rodar o módulo **`tools/`**, o skel inclui (entre o que o Debian já traz, como `.bashrc` quando aplicável): - -- **`.bash_aliases`** — atalhos (`ll`, `la`, `l`, `help-runv`). -- **`public_html/index.html`** — página inicial mínima em HTML estático (sem JS, sem CDN), em português. - -**Não** há **`README.md`** no skel runv: orientação inicial está no **MOTD** e no comando **`runv-help`**. Quem quiser um README na home pode criar manualmente ou o admin pode usar **`create_runv_user.py --with-readme`** ao provisionar. - -**Observação:** no Bash do Debian, o arquivo **`~/.bashrc`** costuma ter (por padrão) um bloco que carrega **`~/.bash_aliases`** se existir. Se o usuário remover esse trecho do `.bashrc`, os aliases deixam de carregar — isso é comportamento padrão do Debian, não do runv. - -## Programas globais (apt) - -Pacotes listados em **`manifests/apt_packages.txt`** (incluindo ferramentas de terminal e IRC) ficam **instalados no sistema**. O comando global **`chat`** em `/usr/local/bin` é o único nome que o utilizador precisa para IRC na rede da casa; a config é aplicada pelo admin com **`patches/patch_irc.py`**. O usuário **não** precisa de nada no skel para **executá-los**: após o admin rodar `tools.py`, eles passam a existir no `PATH`. Ou seja: - -- **Skel** ≠ instalar programas. -- **Skel** = arquivos iniciais na home. -- **apt** = programas para todos. - -## Byobu - -- Está **disponível** após a instalação dos pacotes. -- **Não** abre sozinho para todos no login. -- Quem quiser pode rodar **`byobu-enable`** na própria conta, quando fizer sentido. - -## Como isso ajuda iniciantes - -- **MOTD** orienta na hora do login (**`runv-help`**). -- **`runv-help`** e **`runv-links`** explicam o pubnix, permissões e URLs oficiais. -- Administradores com a conta **`pmurad-admin`** podem usar **`runv-status`** para contexto do servidor (outros utilizadores recebem recusa explícita). - -Juntos, reduzem fricção para quem nunca usou pubnix ou SSH no dia a dia. diff --git a/tools/skel/README.md b/tools/skel/README.md @@ -1,58 +0,0 @@ -# Bem-vindo(a) ao runv.club - -O **runv.club** é um servidor compartilhado (pubnix) pensado para a comunidade brasileira: você acessa por **SSH**, usa a **shell** e pode publicar uma **página web** simples. - -## Sua página na internet - -- Os arquivos públicos do site ficam em **`~/public_html/`**. -- A página principal é **`~/public_html/index.html`** (HTML estático). -- A URL pública será algo como **`https://runv.club/~seu_usuario/`** (o nome após `~` é o seu login). - -Edite o HTML com um editor no terminal, por exemplo: - -```bash -nano ~/public_html/index.html -``` - -## Permissões (importante) - -Para o servidor web enxergar seu site: - -| Local | Modo sugerido | -|-------|----------------| -| Sua home (`~`) | `755` | -| `~/public_html` | `755` | -| Arquivos dentro de `public_html` | `644` | - -Exemplo: - -```bash -chmod 755 ~ ~/public_html -chmod 644 ~/public_html/index.html -``` - -## Ajuda rápida no servidor - -Digite no terminal: - -```bash -runv-help -``` - -Você verá uma lista de **comandos úteis** (navegação no terminal, e-mail, IRC, jogos, etc.) e dicas para quem está começando. - -Outros comandos locais: - -- **`runv-links`** — links do projeto e do mantenedor. - -## Arquivos públicos - -Tudo o que você colocar em **`public_html`** pode ser lido pelo mundo via HTTP. **Não coloque** chaves privadas, senhas ou dados sensíveis nessa pasta. - -## Aliases - -Este diretório pode incluir um arquivo **`.bash_aliases`** (já sugerido no skel) com atalhos como `ll` e `help-runv`. Se o seu shell for Bash, ele costuma carregar aliases desse arquivo se a linha correspondente existir no `~/.bashrc` (no Debian isso costuma vir comentado — você pode descomentar). - ---- - -Seja gentil com a máquina e com a comunidade. Bom uso do runv.club. diff --git a/tools/tools.py b/tools/tools.py @@ -3,7 +3,7 @@ runv.club — ferramentas globais, MOTD, Jailkit/SSH runv-jailed, comandos em /usr/local/bin e /etc/skel. Debian 13 · Python 3 stdlib apenas · sem shell=True. -Execute como root. Ver tools/README.md e tools/docs/INSTALL.md. +Execute como root. Ver docs/05-tools-and-system-experience.md no repositório. """ from __future__ import annotations