Tighten CLI option diagnostics

This commit is contained in:
m1ngsama 2026-05-28 08:59:54 +08:00
parent 797ecbb992
commit f0499c32f6
8 changed files with 165 additions and 31 deletions

View file

@ -25,7 +25,7 @@ OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
DEPS = $(OBJECTS:.o=.d) $(CTL_OBJECTS:.o=.d) DEPS = $(OBJECTS:.o=.d) $(CTL_OBJECTS:.o=.d)
TARGET = tnt TARGET = tnt
CTL_TARGET = tntctl 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) TARGETS = $(TARGET) $(CTL_TARGET)
PREFIX ?= /usr/local PREFIX ?= /usr/local
@ -122,6 +122,7 @@ unit-test:
script-test: all script-test: all
@echo "Running script tests..." @echo "Running script tests..."
@cd tests && ./test_cli_options.sh
@cd tests && ./test_logrotate.sh @cd tests && ./test_logrotate.sh
@cd tests && ./test_message_log_tool.sh @cd tests && ./test_message_log_tool.sh

View file

@ -48,9 +48,12 @@ PORT=3333 tnt # via env var
### Connecting ### Connecting
```sh ```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. **Anonymous access by default**: Users can connect with ANY username/password (or empty password). No SSH keys required. Perfect for public chat servers.
## Usage ## Usage

View file

@ -100,6 +100,13 @@
source release. source release.
- Release documentation now creates the local tag before strict release checks, - Release documentation now creates the local tag before strict release checks,
matching the strict gate's tag-at-HEAD requirement. 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 - 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 locale/code parsing, while `src/i18n_text.c` owns the table-driven text
catalog with coverage checks for every message ID. catalog with coverage checks for every message ID.
@ -121,6 +128,8 @@
reducing duplicate command knowledge in `src/exec.c`. reducing duplicate command knowledge in `src/exec.c`.
- Replaced hard-coded `chat.m1ng.space` examples with `chat.example.com` so - Replaced hard-coded `chat.m1ng.space` examples with `chat.example.com` so
public documentation does not imply a specific production host. 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 - Moved SSH exec usage text and argument-shape checks into `exec_catalog`, so
`src/exec.c` no longer duplicates `--json` and required-message validation. `src/exec.c` no longer duplicates `--json` and required-message validation.
- Moved interactive command usage text and first-pass argument-shape checks - Moved interactive command usage text and first-pass argument-shape checks

View file

@ -37,9 +37,11 @@ tnt -p 2222 -d /var/lib/tnt
## Connect ## Connect
```sh ```sh
ssh -p 2222 chat.example.com ssh -p 2222 localhost
``` ```
For a deployed server, replace `localhost` with your public host.
Default access rules: Default access rules:
- Any SSH username is accepted. - Any SSH username is accepted.
@ -199,9 +201,11 @@ tnt
### 连接 ### 连接
```sh ```sh
ssh -p 2222 chat.example.com ssh -p 2222 localhost
``` ```
部署到公网后,将 `localhost` 替换为你的域名。
默认情况下,任意 SSH 用户名和空密码都可以连接。进入后 TNT 会询问显示名称。 默认情况下,任意 SSH 用户名和空密码都可以连接。进入后 TNT 会询问显示名称。
### 常用操作 ### 常用操作

View file

@ -75,6 +75,16 @@ static int set_numeric_env_option(const char *env_name, const char *opt_name,
return TNT_EXIT_OK; 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 main(int argc, char **argv) {
int port = DEFAULT_PORT; int port = DEFAULT_PORT;
ui_lang_t lang = i18n_default_ui_lang(); ui_lang_t lang = i18n_default_ui_lang();
@ -93,9 +103,11 @@ int main(int argc, char **argv) {
/* Parse command line arguments */ /* Parse command line arguments */
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) && if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) {
i + 1 < argc) {
int val; int val;
if (!require_option_arg(argc, argv, i, lang)) {
return TNT_EXIT_USAGE;
}
if (!parse_int_arg(argv[i + 1], 1, 65535, &val)) { if (!parse_int_arg(argv[i + 1], 1, 65535, &val)) {
fprintf(stderr, cli_text_invalid_port_format(lang), fprintf(stderr, cli_text_invalid_port_format(lang),
argv[i + 1]); argv[i + 1]);
@ -103,18 +115,19 @@ int main(int argc, char **argv) {
} }
port = val; port = val;
i++; i++;
} else if ((strcmp(argv[i], "-d") == 0 || } else if (strcmp(argv[i], "-d") == 0 ||
strcmp(argv[i], "--state-dir") == 0) && i + 1 < argc) { strcmp(argv[i], "--state-dir") == 0) {
if (argv[i + 1][0] == '\0') { if (!require_option_arg(argc, argv, i, lang)) {
fprintf(stderr, cli_text_invalid_value_format(lang),
argv[i], argv[i + 1]);
return TNT_EXIT_USAGE; return TNT_EXIT_USAGE;
} }
if (set_env_option("TNT_STATE_DIR", argv[i + 1]) != 0) { if (set_env_option("TNT_STATE_DIR", argv[i + 1]) != 0) {
return TNT_EXIT_ERROR; return TNT_EXIT_ERROR;
} }
i++; 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])) { if (!is_config_token(argv[i + 1])) {
fprintf(stderr, cli_text_invalid_value_format(lang), fprintf(stderr, cli_text_invalid_value_format(lang),
argv[i], argv[i + 1]); argv[i], argv[i + 1]);
@ -124,7 +137,10 @@ int main(int argc, char **argv) {
return TNT_EXIT_ERROR; return TNT_EXIT_ERROR;
} }
i++; 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])) { if (!is_config_token(argv[i + 1])) {
fprintf(stderr, cli_text_invalid_value_format(lang), fprintf(stderr, cli_text_invalid_value_format(lang),
argv[i], argv[i + 1]); argv[i], argv[i + 1]);
@ -134,8 +150,10 @@ int main(int argc, char **argv) {
return TNT_EXIT_ERROR; return TNT_EXIT_ERROR;
} }
i++; i++;
} else if (strcmp(argv[i], "--max-connections") == 0 && } else if (strcmp(argv[i], "--max-connections") == 0) {
i + 1 < argc) { if (!require_option_arg(argc, argv, i, lang)) {
return TNT_EXIT_USAGE;
}
int rc = set_numeric_env_option("TNT_MAX_CONNECTIONS", argv[i], int rc = set_numeric_env_option("TNT_MAX_CONNECTIONS", argv[i],
argv[i + 1], 1, argv[i + 1], 1,
MAX_CONFIGURED_CLIENTS, lang); MAX_CONFIGURED_CLIENTS, lang);
@ -143,8 +161,10 @@ int main(int argc, char **argv) {
return rc; return rc;
} }
i++; i++;
} else if (strcmp(argv[i], "--max-conn-per-ip") == 0 && } else if (strcmp(argv[i], "--max-conn-per-ip") == 0) {
i + 1 < argc) { 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], int rc = set_numeric_env_option("TNT_MAX_CONN_PER_IP", argv[i],
argv[i + 1], 1, argv[i + 1], 1,
MAX_CONFIGURED_CLIENTS, lang); MAX_CONFIGURED_CLIENTS, lang);
@ -152,8 +172,10 @@ int main(int argc, char **argv) {
return rc; return rc;
} }
i++; i++;
} else if (strcmp(argv[i], "--max-conn-rate-per-ip") == 0 && } else if (strcmp(argv[i], "--max-conn-rate-per-ip") == 0) {
i + 1 < argc) { if (!require_option_arg(argc, argv, i, lang)) {
return TNT_EXIT_USAGE;
}
int rc = set_numeric_env_option("TNT_MAX_CONN_RATE_PER_IP", int rc = set_numeric_env_option("TNT_MAX_CONN_RATE_PER_IP",
argv[i], argv[i + 1], 1, argv[i], argv[i + 1], 1,
MAX_CONFIGURED_CLIENTS, lang); MAX_CONFIGURED_CLIENTS, lang);
@ -161,21 +183,30 @@ int main(int argc, char **argv) {
return rc; return rc;
} }
i++; 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], int rc = set_numeric_env_option("TNT_RATE_LIMIT", argv[i],
argv[i + 1], 0, 1, lang); argv[i + 1], 0, 1, lang);
if (rc != TNT_EXIT_OK) { if (rc != TNT_EXIT_OK) {
return rc; return rc;
} }
i++; 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], int rc = set_numeric_env_option("TNT_IDLE_TIMEOUT", argv[i],
argv[i + 1], 0, 86400, lang); argv[i + 1], 0, 86400, lang);
if (rc != TNT_EXIT_OK) { if (rc != TNT_EXIT_OK) {
return rc; return rc;
} }
i++; 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], int rc = set_numeric_env_option("TNT_SSH_LOG_LEVEL", argv[i],
argv[i + 1], 0, 4, lang); argv[i + 1], 0, 4, lang);
if (rc != TNT_EXIT_OK) { if (rc != TNT_EXIT_OK) {

View file

@ -1,4 +1,5 @@
#include "common.h" #include "common.h"
#include "exec_catalog.h"
#include <ctype.h> #include <ctype.h>
#include <errno.h> #include <errno.h>
@ -73,15 +74,7 @@ static bool is_host_key_checking_mode(const char *value) {
} }
static bool is_known_exec_command(const char *command) { static bool is_known_exec_command(const char *command) {
return command && return exec_catalog_match(command, NULL, NULL);
(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);
} }
static int build_remote_command(char *buffer, size_t buf_size, int argc, static int build_remote_command(char *buffer, size_t buf_size, int argc,

82
tests/test_cli_options.sh Executable file
View file

@ -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"

View file

@ -119,6 +119,17 @@ else
FAIL=$((FAIL + 1)) FAIL=$((FAIL + 1))
fi 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 PATH="$FAKE_BIN:$PATH" TNTCTL_SSH_LOG="$SSH_LOG" "$BIN" example.com users --xml >/dev/null 2>&1
REMOTE_STATUS=$? REMOTE_STATUS=$?
if [ "$REMOTE_STATUS" -eq 64 ]; then if [ "$REMOTE_STATUS" -eq 64 ]; then