Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/ssh_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ typedef struct client {
char ssh_login[MAX_USERNAME_LEN];
char session_id[32]; /* Opaque id an identity module sees */
char key_fingerprint[TNT_IDENTITY_FINGERPRINT_SIZE]; /* Signed SSH key */
bool has_client_lang; /* The client sent LANG or LC_* */
ui_lang_t client_lang;
time_t connect_time;
time_t last_active;
atomic_bool redraw_pending;
Expand Down
28 changes: 28 additions & 0 deletions src/bootstrap.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "bootstrap.h"
#include "client.h"
#include "common.h"
#include "i18n.h"
#include "identity.h"
#include "input.h"
#include "ratelimit.h"
Expand All @@ -27,6 +28,8 @@ typedef struct {
char client_ip[INET6_ADDRSTRLEN];
char requested_user[MAX_USERNAME_LEN];
char key_fingerprint[TNT_IDENTITY_FINGERPRINT_SIZE];
int lang_rank; /* 0 none, 1 LANG, 2 LC_MESSAGES, 3 LC_ALL */
ui_lang_t lang;
int pty_width;
int pty_height;
char exec_command[MAX_EXEC_COMMAND_LEN];
Expand Down Expand Up @@ -422,6 +425,28 @@ static int channel_pty_window_change(ssh_session session, ssh_channel channel,
return SSH_OK;
}

/* OpenSSH clients send LANG and LC_* by default (SendEnv), which lets each
* session start in its own language. Nothing is exported to the process. */
static int channel_env_request(ssh_session session, ssh_channel channel,
const char *name, const char *value,
void *userdata) {
(void)session;
(void)channel;
session_context_t *ctx = (session_context_t *)userdata;
int rank = !name ? 0
: strcmp(name, "LC_ALL") == 0 ? 3
: strcmp(name, "LC_MESSAGES") == 0 ? 2
: strcmp(name, "LANG") == 0 ? 1
: 0;
ui_lang_t lang;

if (rank > ctx->lang_rank && i18n_try_parse_ui_lang(value, &lang)) {
ctx->lang_rank = rank;
ctx->lang = lang;
}
return SSH_OK;
}

/* Shell request callback */
static int channel_shell_request(ssh_session session, ssh_channel channel,
void *userdata) {
Expand Down Expand Up @@ -478,6 +503,7 @@ static void setup_session_channel_callbacks(ssh_channel channel,
ctx->channel_cb->channel_shell_request_function = channel_shell_request;
ctx->channel_cb->channel_pty_window_change_function = channel_pty_window_change;
ctx->channel_cb->channel_exec_request_function = channel_exec_request;
ctx->channel_cb->channel_env_request_function = channel_env_request;

ssh_set_channel_callbacks(channel, ctx->channel_cb);
}
Expand Down Expand Up @@ -681,6 +707,8 @@ void *bootstrap_run(void *arg) {
}
snprintf(client->key_fingerprint, sizeof(client->key_fingerprint), "%s",
ctx->key_fingerprint);
client->has_client_lang = ctx->lang_rank > 0;
client->client_lang = ctx->lang;
if (ctx->exec_command[0] != '\0') {
strncpy(client->exec_command, ctx->exec_command,
sizeof(client->exec_command) - 1);
Expand Down
2 changes: 1 addition & 1 deletion src/input.c
Original file line number Diff line number Diff line change
Expand Up @@ -1642,7 +1642,7 @@ void input_run_session(client_t *client) {
* leaves `ssh vim@host` free to mean "give me the vim keys". */
client->keymap = tnt_keymap_from_login(client->ssh_login, g_default_keymap);
client->follow_tail = true;
client->ui_lang = g_default_ui_lang;
client->ui_lang = client->has_client_lang ? client->client_lang : g_default_ui_lang;
client->connected = true;
client->command_history_count = 0;
client->command_history_pos = 0;
Expand Down
6 changes: 5 additions & 1 deletion tests/run_suites.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
set -u

BASE_PORT=${PORT:-2222}
# ssh forwards these (SendEnv), and a session follows them over the server's
# TNT_LANG, so a developer's locale would change what the suites see.
unset LANG LC_ALL LC_MESSAGES
JOBS=${INTEGRATION_JOBS:-4}

# Slowest first, so the last job slot never waits on a long suite.
Expand All @@ -23,7 +26,8 @@ vim_keymap 12
wide_text_view 9
module_privileged 16
identity 17
module_private 18"
module_private 18
client_lang 19"

WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tnt-suites.XXXXXX")
trap 'rm -rf "$WORK_DIR"' EXIT
Expand Down
83 changes: 83 additions & 0 deletions tests/test_client_lang.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/bin/bash
# Each session starts in the language its SSH client sends with LANG or LC_*.

BIN="../tnt"
PORT=${PORT:-2222}
PASS=0
FAIL=0
STATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tnt-client-lang-test.XXXXXX")
SERVER_PID=""

cleanup() {
if [ -n "$SERVER_PID" ]; then
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
rm -rf "$STATE_DIR"
}
trap cleanup EXIT

if [ ! -f "$BIN" ]; then
echo "Error: Binary $BIN not found."
exit 1
fi

SSH_BASE="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PubkeyAuthentication=no -o ConnectionAttempts=3 -o ConnectTimeout=15 -o SendEnv=-* -p $PORT"

wait_for_health() {
for _ in $(seq 1 15); do
[ "$(ssh -n $SSH_BASE localhost health 2>/dev/null)" = "ok" ] && return 0
sleep 1
done
return 1
}

# Prints which prompt language the session showed: zh, en or none.
prompt_language() {
local name="$1"
shift
local script="$STATE_DIR/$name.expect"
cat >"$script" <<EOF
set timeout 10
spawn env -u LANG -u LC_ALL -u LC_MESSAGES ssh $SSH_BASE $* localhost
expect {
"请输入用户名" { puts "\nLANGUAGE=zh"; send -- "\003"; exit 0 }
"Enter display name" { puts "\nLANGUAGE=en"; send -- "\003"; exit 0 }
timeout { puts "\nLANGUAGE=none"; exit 0 }
}
EOF
expect "$script" 2>&1 | sed -n 's/^LANGUAGE=//p' | tr -d '\r'
}

check() {
local description="$1" want="$2" got="$3"
if [ "$got" = "$want" ]; then
echo "✓ $description"
PASS=$((PASS + 1))
else
echo "✗ $description (want $want, got $got)"
FAIL=$((FAIL + 1))
fi
}

echo "=== TNT Client Language Tests ==="

TNT_LANG=en TNT_RATE_LIMIT=0 "$BIN" -p "$PORT" -d "$STATE_DIR" >"$STATE_DIR/server.log" 2>&1 &
SERVER_PID=$!
if ! wait_for_health; then
echo "✗ server did not start"
cat "$STATE_DIR/server.log"
exit 1
fi

check "LANG=zh_CN.UTF-8 starts in Chinese" zh "$(prompt_language zh -o SetEnv=LANG=zh_CN.UTF-8)"
check "LC_ALL wins over LANG" zh "$(prompt_language lcall '-o "SetEnv=LANG=en_US.UTF-8 LC_ALL=zh_CN.UTF-8"')"
check "LANG=en_US.UTF-8 keeps English" en "$(prompt_language en -o SetEnv=LANG=en_US.UTF-8)"
check "an unknown locale falls back to the server default" en "$(prompt_language other -o SetEnv=LANG=fr_FR.UTF-8)"
check "no locale falls back to the server default" en "$(prompt_language none)"

echo ""
echo "PASSED: $PASS"
echo "FAILED: $FAIL"
[ "$FAIL" -eq 0 ] && echo "All tests passed"
exit "$FAIL"
6 changes: 6 additions & 0 deletions tnt-chat.7
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,12 @@ The default is
.TQ
.BI ":language " [en|zh]
Show or set the session interface language.
A session starts in the language the SSH client sends in
.BR LC_ALL ,
.B LC_MESSAGES
or
.BR LANG ,
when it is English or Chinese, and in the server's language otherwise.
.TP
.B :help
Open the concise interactive manual.
Expand Down
4 changes: 4 additions & 0 deletions tnt.8
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ An invalid explicit value selects English.
Select the language when
.B TNT_LANG
is unset, in the precedence shown.
A session whose SSH client sends one of these variables, as OpenSSH does by
default with
.BR SendEnv ,
starts in that language instead; otherwise it uses the server's.
.TP
.B TNT_MAX_CONNECTIONS
Global connection limit.
Expand Down
Loading