From f0499c32f6586098424e31b49e829ff4e7819520 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Thu, 28 May 2026 08:59:54 +0800 Subject: [PATCH] Tighten CLI option diagnostics --- Makefile | 3 +- README.md | 5 ++- docs/CHANGELOG.md | 9 +++++ docs/EASY_SETUP.md | 8 +++- src/main.c | 67 +++++++++++++++++++++++--------- src/tntctl.c | 11 +----- tests/test_cli_options.sh | 82 +++++++++++++++++++++++++++++++++++++++ tests/test_tntctl_cli.sh | 11 ++++++ 8 files changed, 165 insertions(+), 31 deletions(-) create mode 100755 tests/test_cli_options.sh diff --git a/Makefile b/Makefile index c5281c9..a7de198 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o) DEPS = $(OBJECTS:.o=.d) $(CTL_OBJECTS:.o=.d) TARGET = tnt CTL_TARGET = tntctl -CTL_OBJECTS = $(OBJ_DIR)/tntctl.o +CTL_OBJECTS = $(OBJ_DIR)/tntctl.o $(OBJ_DIR)/exec_catalog.o $(OBJ_DIR)/common.o TARGETS = $(TARGET) $(CTL_TARGET) PREFIX ?= /usr/local @@ -122,6 +122,7 @@ unit-test: script-test: all @echo "Running script tests..." + @cd tests && ./test_cli_options.sh @cd tests && ./test_logrotate.sh @cd tests && ./test_message_log_tool.sh diff --git a/README.md b/README.md index 202fe2f..7adf3e0 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,12 @@ PORT=3333 tnt # via env var ### Connecting ```sh -ssh -p 2222 chat.example.com +ssh -p 2222 localhost ``` +For a deployed server, replace `localhost` with your public host, for example +`chat.example.com`. + **Anonymous access by default**: Users can connect with ANY username/password (or empty password). No SSH keys required. Perfect for public chat servers. ## Usage diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 73d689b..6889382 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -100,6 +100,13 @@ source release. - Release documentation now creates the local tag before strict release checks, matching the strict gate's tag-at-HEAD requirement. +- Startup option parsing now reports missing values for `--bind`, `-p`, + `--idle-timeout`, and related flags with the localized + "option requires argument" diagnostic instead of treating the option as + unknown. +- `tntctl` now reuses the SSH exec command matcher for local command + validation, so `tntctl host --help` reaches the server-side exec help alias + instead of being rejected locally. - Split UI-language parsing from localized text lookup: `src/i18n.c` now owns locale/code parsing, while `src/i18n_text.c` owns the table-driven text catalog with coverage checks for every message ID. @@ -121,6 +128,8 @@ reducing duplicate command knowledge in `src/exec.c`. - Replaced hard-coded `chat.m1ng.space` examples with `chat.example.com` so public documentation does not imply a specific production host. +- First-run connection examples now use `localhost`, keeping + `chat.example.com` for deployed public-host examples. - Moved SSH exec usage text and argument-shape checks into `exec_catalog`, so `src/exec.c` no longer duplicates `--json` and required-message validation. - Moved interactive command usage text and first-pass argument-shape checks diff --git a/docs/EASY_SETUP.md b/docs/EASY_SETUP.md index 7af0f4d..9af0b91 100644 --- a/docs/EASY_SETUP.md +++ b/docs/EASY_SETUP.md @@ -37,9 +37,11 @@ tnt -p 2222 -d /var/lib/tnt ## Connect ```sh -ssh -p 2222 chat.example.com +ssh -p 2222 localhost ``` +For a deployed server, replace `localhost` with your public host. + Default access rules: - Any SSH username is accepted. @@ -199,9 +201,11 @@ tnt ### 连接 ```sh -ssh -p 2222 chat.example.com +ssh -p 2222 localhost ``` +部署到公网后,将 `localhost` 替换为你的域名。 + 默认情况下,任意 SSH 用户名和空密码都可以连接。进入后 TNT 会询问显示名称。 ### 常用操作 diff --git a/src/main.c b/src/main.c index 81ea22f..b1f8058 100644 --- a/src/main.c +++ b/src/main.c @@ -75,6 +75,16 @@ static int set_numeric_env_option(const char *env_name, const char *opt_name, return TNT_EXIT_OK; } +static bool require_option_arg(int argc, char **argv, int index, + ui_lang_t lang) { + if (index + 1 >= argc || argv[index + 1][0] == '\0') { + fprintf(stderr, cli_text_option_requires_arg_format(lang), + argv[index]); + return false; + } + return true; +} + int main(int argc, char **argv) { int port = DEFAULT_PORT; ui_lang_t lang = i18n_default_ui_lang(); @@ -93,9 +103,11 @@ int main(int argc, char **argv) { /* Parse command line arguments */ for (int i = 1; i < argc; i++) { - if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) && - i + 1 < argc) { + if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) { int val; + if (!require_option_arg(argc, argv, i, lang)) { + return TNT_EXIT_USAGE; + } if (!parse_int_arg(argv[i + 1], 1, 65535, &val)) { fprintf(stderr, cli_text_invalid_port_format(lang), argv[i + 1]); @@ -103,18 +115,19 @@ int main(int argc, char **argv) { } port = val; i++; - } else if ((strcmp(argv[i], "-d") == 0 || - strcmp(argv[i], "--state-dir") == 0) && i + 1 < argc) { - if (argv[i + 1][0] == '\0') { - fprintf(stderr, cli_text_invalid_value_format(lang), - argv[i], argv[i + 1]); + } else if (strcmp(argv[i], "-d") == 0 || + strcmp(argv[i], "--state-dir") == 0) { + if (!require_option_arg(argc, argv, i, lang)) { return TNT_EXIT_USAGE; } if (set_env_option("TNT_STATE_DIR", argv[i + 1]) != 0) { return TNT_EXIT_ERROR; } i++; - } else if (strcmp(argv[i], "--bind") == 0 && i + 1 < argc) { + } else if (strcmp(argv[i], "--bind") == 0) { + if (!require_option_arg(argc, argv, i, lang)) { + return TNT_EXIT_USAGE; + } if (!is_config_token(argv[i + 1])) { fprintf(stderr, cli_text_invalid_value_format(lang), argv[i], argv[i + 1]); @@ -124,7 +137,10 @@ int main(int argc, char **argv) { return TNT_EXIT_ERROR; } i++; - } else if (strcmp(argv[i], "--public-host") == 0 && i + 1 < argc) { + } else if (strcmp(argv[i], "--public-host") == 0) { + if (!require_option_arg(argc, argv, i, lang)) { + return TNT_EXIT_USAGE; + } if (!is_config_token(argv[i + 1])) { fprintf(stderr, cli_text_invalid_value_format(lang), argv[i], argv[i + 1]); @@ -134,8 +150,10 @@ int main(int argc, char **argv) { return TNT_EXIT_ERROR; } i++; - } else if (strcmp(argv[i], "--max-connections") == 0 && - i + 1 < argc) { + } else if (strcmp(argv[i], "--max-connections") == 0) { + if (!require_option_arg(argc, argv, i, lang)) { + return TNT_EXIT_USAGE; + } int rc = set_numeric_env_option("TNT_MAX_CONNECTIONS", argv[i], argv[i + 1], 1, MAX_CONFIGURED_CLIENTS, lang); @@ -143,8 +161,10 @@ int main(int argc, char **argv) { return rc; } i++; - } else if (strcmp(argv[i], "--max-conn-per-ip") == 0 && - i + 1 < argc) { + } else if (strcmp(argv[i], "--max-conn-per-ip") == 0) { + if (!require_option_arg(argc, argv, i, lang)) { + return TNT_EXIT_USAGE; + } int rc = set_numeric_env_option("TNT_MAX_CONN_PER_IP", argv[i], argv[i + 1], 1, MAX_CONFIGURED_CLIENTS, lang); @@ -152,8 +172,10 @@ int main(int argc, char **argv) { return rc; } i++; - } else if (strcmp(argv[i], "--max-conn-rate-per-ip") == 0 && - i + 1 < argc) { + } else if (strcmp(argv[i], "--max-conn-rate-per-ip") == 0) { + if (!require_option_arg(argc, argv, i, lang)) { + return TNT_EXIT_USAGE; + } int rc = set_numeric_env_option("TNT_MAX_CONN_RATE_PER_IP", argv[i], argv[i + 1], 1, MAX_CONFIGURED_CLIENTS, lang); @@ -161,21 +183,30 @@ int main(int argc, char **argv) { return rc; } i++; - } else if (strcmp(argv[i], "--rate-limit") == 0 && i + 1 < argc) { + } else if (strcmp(argv[i], "--rate-limit") == 0) { + if (!require_option_arg(argc, argv, i, lang)) { + return TNT_EXIT_USAGE; + } int rc = set_numeric_env_option("TNT_RATE_LIMIT", argv[i], argv[i + 1], 0, 1, lang); if (rc != TNT_EXIT_OK) { return rc; } i++; - } else if (strcmp(argv[i], "--idle-timeout") == 0 && i + 1 < argc) { + } else if (strcmp(argv[i], "--idle-timeout") == 0) { + if (!require_option_arg(argc, argv, i, lang)) { + return TNT_EXIT_USAGE; + } int rc = set_numeric_env_option("TNT_IDLE_TIMEOUT", argv[i], argv[i + 1], 0, 86400, lang); if (rc != TNT_EXIT_OK) { return rc; } i++; - } else if (strcmp(argv[i], "--ssh-log-level") == 0 && i + 1 < argc) { + } else if (strcmp(argv[i], "--ssh-log-level") == 0) { + if (!require_option_arg(argc, argv, i, lang)) { + return TNT_EXIT_USAGE; + } int rc = set_numeric_env_option("TNT_SSH_LOG_LEVEL", argv[i], argv[i + 1], 0, 4, lang); if (rc != TNT_EXIT_OK) { diff --git a/src/tntctl.c b/src/tntctl.c index 9d21850..673e8b8 100644 --- a/src/tntctl.c +++ b/src/tntctl.c @@ -1,4 +1,5 @@ #include "common.h" +#include "exec_catalog.h" #include #include @@ -73,15 +74,7 @@ static bool is_host_key_checking_mode(const char *value) { } static bool is_known_exec_command(const char *command) { - return command && - (strcmp(command, "health") == 0 || - strcmp(command, "stats") == 0 || - strcmp(command, "users") == 0 || - strcmp(command, "tail") == 0 || - strcmp(command, "dump") == 0 || - strcmp(command, "post") == 0 || - strcmp(command, "help") == 0 || - strcmp(command, "exit") == 0); + return exec_catalog_match(command, NULL, NULL); } static int build_remote_command(char *buffer, size_t buf_size, int argc, diff --git a/tests/test_cli_options.sh b/tests/test_cli_options.sh new file mode 100755 index 0000000..0deb788 --- /dev/null +++ b/tests/test_cli_options.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# CLI option parsing regression tests. + +BIN="../tnt" +PASS=0 +FAIL=0 + +pass() { + echo "✓ $1" + PASS=$((PASS + 1)) +} + +fail() { + echo "✗ $1" + if [ -n "$2" ]; then + printf '%s\n' "$2" + fi + FAIL=$((FAIL + 1)) +} + +if [ ! -f "$BIN" ]; then + echo "Error: Binary $BIN not found. Run make first." + exit 1 +fi + +expect_missing_arg() { + opt="$1" + output=$("$BIN" "$opt" 2>&1) + status=$? + + if [ "$status" -eq 64 ] && + printf '%s\n' "$output" | grep -q "Option requires argument: $opt"; then + pass "$opt reports missing argument" + else + fail "$opt missing argument diagnostic unexpected" "$output" + fi +} + +echo "=== TNT CLI Option Tests ===" + +for opt in \ + -p \ + --port \ + -d \ + --state-dir \ + --bind \ + --public-host \ + --max-connections \ + --max-conn-per-ip \ + --max-conn-rate-per-ip \ + --rate-limit \ + --idle-timeout \ + --ssh-log-level \ + --log-check \ + --log-recover +do + expect_missing_arg "$opt" +done + +ZH_OUTPUT=$(TNT_LANG=zh "$BIN" --bind 2>&1) +ZH_STATUS=$? +if [ "$ZH_STATUS" -eq 64 ] && + printf '%s\n' "$ZH_OUTPUT" | grep -q '选项需要参数: --bind'; then + pass "missing argument diagnostic follows TNT_LANG" +else + fail "localized missing argument diagnostic unexpected" "$ZH_OUTPUT" +fi + +BAD_PORT_OUTPUT=$("$BIN" --port abc 2>&1) +BAD_PORT_STATUS=$? +if [ "$BAD_PORT_STATUS" -eq 64 ] && + printf '%s\n' "$BAD_PORT_OUTPUT" | grep -q 'Invalid port: abc'; then + pass "invalid port still reports invalid value" +else + fail "invalid port diagnostic unexpected" "$BAD_PORT_OUTPUT" +fi + +echo "" +echo "PASSED: $PASS" +echo "FAILED: $FAIL" +[ "$FAIL" -eq 0 ] && echo "All tests passed" || echo "Some tests failed" +exit "$FAIL" diff --git a/tests/test_tntctl_cli.sh b/tests/test_tntctl_cli.sh index 12b66e0..face0e0 100755 --- a/tests/test_tntctl_cli.sh +++ b/tests/test_tntctl_cli.sh @@ -119,6 +119,17 @@ else FAIL=$((FAIL + 1)) fi +run_ok "remote help alias is accepted" "$BIN" example.com --help +grep -q '^--help$' "$SSH_LOG" +if [ $? -eq 0 ]; then + echo "✓ --help after host is forwarded as exec help" + PASS=$((PASS + 1)) +else + echo "✗ remote --help command unexpected" + cat "$SSH_LOG" + FAIL=$((FAIL + 1)) +fi + PATH="$FAKE_BIN:$PATH" TNTCTL_SSH_LOG="$SSH_LOG" "$BIN" example.com users --xml >/dev/null 2>&1 REMOTE_STATUS=$? if [ "$REMOTE_STATUS" -eq 64 ]; then