diff --git a/docs/DDIAL.md b/docs/DDIAL.md index a9a725c..18f9b28 100644 --- a/docs/DDIAL.md +++ b/docs/DDIAL.md @@ -157,16 +157,30 @@ its own upstream hub: | `CHATTER_DDIAL_HUB_LISTEN` | `port` or `addr:port` (`[v6addr]:port`) to accept station links on. Unset = off. | | `CHATTER_DDIAL_STATION` | Name shown in the welcome line and the `}}}` user list. | -Operators manage keys from the Chatter room: - -- `/ddial key new ` prints a new nine-digit station key. - **It is shown once.** Give it to that station's operator; it is their - link password (`CHATTER_DDIAL_KEY` on another ssh-chatter). +Operators manage requests and keys from the Chatter room: + +- `/ddial requests` lists pending inbound station requests with their ids, + names and source addresses. +- `/ddial approve ` prints a new nine-digit station key and + removes the request. **The key is shown once.** Give it to that station's + operator; it is their link password (`CHATTER_DDIAL_KEY` on another + ssh-chatter). +- `/ddial deny ` removes a request without issuing a key. +- `/ddial key new ` still creates a key directly when no + inbound request is needed. - `/ddial key list` shows each key's id, name, creation date and last use. - `/ddial key revoke ` deletes a key and drops a station linked with it. Ids are never reused. - `/ddial hub` lists the linked stations. +A station without a key sends `@request:` at the password +prompt (for example, `CHATTER_DDIAL_KEY='@request:RemoteOne'`). The hub +records the request and closes that login attempt; the station can use the +key after an operator approves it. Repeating a request from the same address +and name keeps its original id. Pending requests are stored in +`ddial_station_requests.dat` next to the member files and rewritten +atomically whenever they change. + Keys are stored as PBKDF2-SHA256 hashes in `ddial_station_keys.tsv` next to the member files (mode 0600). diff --git a/src/host/core/command_messages.c b/src/host/core/command_messages.c index 1da0a69..33ee1df 100644 --- a/src/host/core/command_messages.c +++ b/src/host/core/command_messages.c @@ -1623,6 +1623,126 @@ static const session_command_message_t kSessionCommandMessages[] = { [SESSION_UI_LANGUAGE_PL] = "Wątek wiadomości #%s:", }, }, + { + "Only operators can manage DDial station links.", + { + [SESSION_UI_LANGUAGE_KO] = "DDial 스테이션 링크는 운영자만 관리할 수 있습니다.", + [SESSION_UI_LANGUAGE_JP] = "DDial ステーションリンクを管理できるのはオペレーターのみです。", + [SESSION_UI_LANGUAGE_ZH] = "只有管理员可以管理 DDial 站点链接。", + [SESSION_UI_LANGUAGE_RU] = "Управлять связями станций DDial могут только операторы.", + [SESSION_UI_LANGUAGE_DE] = "Nur Operatoren können DDial-Stationsverbindungen verwalten.", + [SESSION_UI_LANGUAGE_FR] = "Seuls les opérateurs peuvent gérer les liaisons de stations DDial.", + [SESSION_UI_LANGUAGE_PL] = "Tylko operatorzy mogą zarządzać połączeniami stacji DDial.", + }, + }, + { + "No pending station requests.", + { + [SESSION_UI_LANGUAGE_KO] = "대기 중인 스테이션 요청이 없습니다.", + [SESSION_UI_LANGUAGE_JP] = "保留中のステーション要求はありません。", + [SESSION_UI_LANGUAGE_ZH] = "没有待处理的站点请求。", + [SESSION_UI_LANGUAGE_RU] = "Нет ожидающих запросов станций.", + [SESSION_UI_LANGUAGE_DE] = "Keine ausstehenden Stationsanfragen.", + [SESSION_UI_LANGUAGE_FR] = "Aucune demande de station en attente.", + [SESSION_UI_LANGUAGE_PL] = "Brak oczekujących zgłoszeń stacji.", + }, + }, + { + "Pending station requests:", + { + [SESSION_UI_LANGUAGE_KO] = "대기 중인 스테이션 요청:", + [SESSION_UI_LANGUAGE_JP] = "保留中のステーション要求:", + [SESSION_UI_LANGUAGE_ZH] = "待处理的站点请求:", + [SESSION_UI_LANGUAGE_RU] = "Ожидающие запросы станций:", + [SESSION_UI_LANGUAGE_DE] = "Ausstehende Stationsanfragen:", + [SESSION_UI_LANGUAGE_FR] = "Demandes de stations en attente :", + [SESSION_UI_LANGUAGE_PL] = "Oczekujące zgłoszenia stacji:", + }, + }, + { + "Usage: /ddial approve ", + { + [SESSION_UI_LANGUAGE_KO] = "사용법: /ddial approve <요청ID>", + [SESSION_UI_LANGUAGE_JP] = "使い方: /ddial approve <要求ID>", + [SESSION_UI_LANGUAGE_ZH] = "用法:/ddial approve <请求ID>", + [SESSION_UI_LANGUAGE_RU] = "Использование: /ddial approve ", + [SESSION_UI_LANGUAGE_DE] = "Verwendung: /ddial approve ", + [SESSION_UI_LANGUAGE_FR] = "Utilisation : /ddial approve ", + [SESSION_UI_LANGUAGE_PL] = "Użycie: /ddial approve ", + }, + }, + { + "Usage: /ddial deny ", + { + [SESSION_UI_LANGUAGE_KO] = "사용법: /ddial deny <요청ID>", + [SESSION_UI_LANGUAGE_JP] = "使い方: /ddial deny <要求ID>", + [SESSION_UI_LANGUAGE_ZH] = "用法:/ddial deny <请求ID>", + [SESSION_UI_LANGUAGE_RU] = "Использование: /ddial deny ", + [SESSION_UI_LANGUAGE_DE] = "Verwendung: /ddial deny ", + [SESSION_UI_LANGUAGE_FR] = "Utilisation : /ddial deny ", + [SESSION_UI_LANGUAGE_PL] = "Użycie: /ddial deny ", + }, + }, + { + "No station request with that id.", + { + [SESSION_UI_LANGUAGE_KO] = "해당 ID의 스테이션 요청이 없습니다.", + [SESSION_UI_LANGUAGE_JP] = "その ID のステーション要求はありません。", + [SESSION_UI_LANGUAGE_ZH] = "没有该 ID 的站点请求。", + [SESSION_UI_LANGUAGE_RU] = "Запрос станции с таким id не найден.", + [SESSION_UI_LANGUAGE_DE] = "Keine Stationsanfrage mit dieser ID.", + [SESSION_UI_LANGUAGE_FR] = "Aucune demande de station avec cet identifiant.", + [SESSION_UI_LANGUAGE_PL] = "Brak zgłoszenia stacji o tym identyfikatorze.", + }, + }, + { + "Could not approve station request.", + { + [SESSION_UI_LANGUAGE_KO] = "스테이션 요청을 승인하지 못했습니다.", + [SESSION_UI_LANGUAGE_JP] = "ステーション要求を承認できませんでした。", + [SESSION_UI_LANGUAGE_ZH] = "无法批准站点请求。", + [SESSION_UI_LANGUAGE_RU] = "Не удалось одобрить запрос станции.", + [SESSION_UI_LANGUAGE_DE] = "Stationsanfrage konnte nicht genehmigt werden.", + [SESSION_UI_LANGUAGE_FR] = "Impossible d'approuver la demande de station.", + [SESSION_UI_LANGUAGE_PL] = "Nie udało się zatwierdzić zgłoszenia stacji.", + }, + }, + { + "Could not deny station request.", + { + [SESSION_UI_LANGUAGE_KO] = "스테이션 요청을 거절하지 못했습니다.", + [SESSION_UI_LANGUAGE_JP] = "ステーション要求を拒否できませんでした。", + [SESSION_UI_LANGUAGE_ZH] = "无法拒绝站点请求。", + [SESSION_UI_LANGUAGE_RU] = "Не удалось отклонить запрос станции.", + [SESSION_UI_LANGUAGE_DE] = "Stationsanfrage konnte nicht abgelehnt werden.", + [SESSION_UI_LANGUAGE_FR] = "Impossible de refuser la demande de station.", + [SESSION_UI_LANGUAGE_PL] = "Nie udało się odrzucić zgłoszenia stacji.", + }, + }, + { + "Station request #%u denied.", + { + [SESSION_UI_LANGUAGE_KO] = "스테이션 요청 #%u을(를) 거절했습니다.", + [SESSION_UI_LANGUAGE_JP] = "ステーション要求 #%u を拒否しました。", + [SESSION_UI_LANGUAGE_ZH] = "已拒绝站点请求 #%u。", + [SESSION_UI_LANGUAGE_RU] = "Запрос станции #%u отклонён.", + [SESSION_UI_LANGUAGE_DE] = "Stationsanfrage #%u abgelehnt.", + [SESSION_UI_LANGUAGE_FR] = "Demande de station #%u refusée.", + [SESSION_UI_LANGUAGE_PL] = "Odrzucono zgłoszenie stacji #%u.", + }, + }, + { + "Station request #%u approved. Station key #%u for '%s': %s (shown once; give it to that station's operator)", + { + [SESSION_UI_LANGUAGE_KO] = "스테이션 요청 #%u을(를) 승인했습니다. 스테이션 키 #%u ('%s'): %s (한 번만 표시되니 해당 스테이션 운영자에게 전달하세요)", + [SESSION_UI_LANGUAGE_JP] = "ステーション要求 #%u を承認しました。ステーションキー #%u ('%s'): %s (一度だけ表示されます。ステーションのオペレーターに渡してください)", + [SESSION_UI_LANGUAGE_ZH] = "已批准站点请求 #%u。站点密钥 #%u('%s'):%s(仅显示一次,请交给该站点管理员)", + [SESSION_UI_LANGUAGE_RU] = "Запрос станции #%u одобрен. Ключ станции #%u для «%s»: %s (показывается один раз; передайте его оператору станции)", + [SESSION_UI_LANGUAGE_DE] = "Stationsanfrage #%u genehmigt. Stationsschlüssel #%u für „%s“: %s (wird nur einmal angezeigt; an den Stationsoperator weitergeben)", + [SESSION_UI_LANGUAGE_FR] = "Demande de station #%u approuvée. Clé de station #%u pour « %s » : %s (affichée une seule fois ; transmettez-la à l'opérateur de cette station)", + [SESSION_UI_LANGUAGE_PL] = "Zatwierdzono zgłoszenie stacji #%u. Klucz stacji #%u dla „%s”: %s (widoczny tylko raz; przekaż go operatorowi tej stacji)", + }, + }, }; // Returns the message in the session's UI language, or the English text when diff --git a/src/host/core/help_entries.c b/src/host/core/help_entries.c index a3c2560..21eccb4 100644 --- a/src/host/core/help_entries.c +++ b/src/host/core/help_entries.c @@ -2273,6 +2273,38 @@ static const session_help_entry_t kSessionHelpOperator[] = { .label_arg_count = 0U, .label_args = {}, }, + { + .kind = SESSION_HELP_ENTRY_COMMAND, + .label = "ddial |deny >", + .description = + { + "Review inbound station requests. approve issues a key shown " + "once and removes the request; deny removes it without " + "issuing a key (operator only).", + "들어온 스테이션 요청을 검토합니다. approve는 한 번만 표시되는 " + "키를 발급하고 요청을 삭제하며, deny는 키 없이 요청을 삭제합니다 " + "(운영자 전용).", + "受信したステーション要求を確認します。approve は一度だけ表示" + "されるキーを発行して要求を削除し、deny はキーを発行せず要求を" + "削除します(オペレーター専用)。", + "查看收到的站点请求。approve 签发仅显示一次的密钥并删除请求;" + "deny 不签发密钥,直接删除请求(仅限管理员)。", + "Просмотр входящих запросов станций. approve выдаёт ключ, " + "показываемый один раз, и удаляет запрос; deny удаляет запрос " + "без выдачи ключа (только оператор).", + "Eingehende Stationsanfragen prüfen. approve erstellt einen nur " + "einmal angezeigten Schlüssel und löscht die Anfrage; deny " + "löscht sie ohne Schlüssel (nur Operatoren).", + "Examiner les demandes de stations entrantes. approve génère " + "une clé affichée une seule fois et supprime la demande ; deny " + "la supprime sans clé (opérateur uniquement).", + "Przeglądaj przychodzące zgłoszenia stacji. approve wydaje klucz " + "widoczny tylko raz i usuwa zgłoszenie; deny usuwa je bez " + "wydawania klucza (tylko operator).", + }, + .label_arg_count = 0U, + .label_args = {}, + }, { .kind = SESSION_HELP_ENTRY_COMMAND, .label = "resetpw ", diff --git a/src/host/ddial/hub.c b/src/host/ddial/hub.c index 0a65b5f..e62bef0 100644 --- a/src/host/ddial/hub.c +++ b/src/host/ddial/hub.c @@ -10,7 +10,9 @@ * 1. The station connects to CHATTER_DDIAL_HUB_LISTEN ([addr:]port). * 2. We print "Enter Password or [RETURN]: ". * 3. The station sends a station key: nine digits generated here with - * /ddial key new