Commit graph

122 commits

Author SHA1 Message Date
169ba1a150 tui: preserve ansi styling when truncating output 2026-05-21 12:12:14 +08:00
67d21ad0e9 tui: improve history browsing and support guide 2026-05-21 11:57:59 +08:00
87d6572156 chat: whisper inbox with :inbox view + ✉ unread chip (UX-12)
Whispers used to flash on the recipient's terminal and disappear with
the next redraw.  No history, no record, no signal if you weren't
looking.

Now whispers are stored per-recipient in a bounded inbox (16 slots,
FIFO eviction):

  typedef struct { time_t timestamp; char from[]; char content[]; } whisper_t;
  whisper_t whisper_inbox[16];
  int       whisper_inbox_count;
  _Atomic int unread_whispers;

Sender side (:msg / :w):
  - resolves target as before
  - pushes the whisper into target->whisper_inbox under target->io_lock
    (so two simultaneous senders to the same recipient don't tear the
    ring)
  - bumps target->unread_whispers atomically
  - sends a single \a bell + triggers redraw_pending
  - no longer writes whisper text directly to the channel (which used
    to get clobbered by the next redraw)

Recipient side:
  - new :inbox command in COMMAND mode prints the snapshot under
    io_lock, in M7 chat-list style:

      悄悄话 · whispers  · 3
        05-17 13:42  alice: 一会儿要不要喝咖啡
        ...

  - viewing :inbox resets unread_whispers to 0

Title bar (extends UX-11):
  - bright magenta "✉ N" chip alongside the yellow "★ N" mention chip
  - same priority / degradation rules as ★

Whispers remain private — they're never broadcast to the room and
never persisted to messages.log.  The inbox lives only in client_t,
so disconnecting drops it.
2026-05-17 14:35:16 +08:00
ddcecbea81 tui: persistent @mention unread counter in title bar (UX-11)
The bell + brief yellow highlight on the chat line meant that if you
weren't looking at the screen the moment someone @-mentioned you, you
had no way to know.

Now the title bar carries a sticky chip:

    tester · 在线 3 · NORMAL  ★ 2                ? 帮助

- bright yellow "★ N" appears whenever client->unread_mentions > 0
- count is bumped atomically in notify_mentions() for each target
- cleared automatically when the user returns to attention:
  * pressing 'i' in NORMAL to re-enter INSERT mode
  * pressing 'G' in NORMAL to jump to the live tail
- never dropped by the narrow-terminal degradation (UX-6) unless
  every other optional chip has already been shed — it's the highest
  priority signal in the bar

Counter is _Atomic int so the cross-thread bump in notify_mentions
doesn't tear against the local thread's reads / resets.
2026-05-17 14:27:46 +08:00
6a36cbcb82 input: Tab completes @mentions in INSERT mode (UX-9)
Typing @al<Tab> in INSERT mode now resolves to @alice and appends a
trailing space so the next word starts cleanly.

Algorithm:
1. walk back from end-of-input until '@' or ' ' is seen
2. '@' counts as a mention start only when at start-of-input or
   preceded by a space (avoids matching e.g. email@host)
3. case-insensitive strncasecmp against current g_room usernames
4. first hit wins; the search ignores the local user when the prefix
   is empty (so a lone "@<Tab>" defaults to the first *other* member,
   matching the typical "ping someone" intent)

If the buffer is too short to hold "@<match> ", the completion is a
no-op rather than silently truncating the match.

Standard chat-client behaviour — much less typing for @mentions.
2026-05-17 14:21:34 +08:00
585262fe4f commands: refresh :list output to match M7 aesthetic (UX-8)
:list used to look like an ASCII printout from the 90s:

    ========================================
         Online Users / 在线用户
    ========================================
    Total / 总数: 3
    ----------------------------------------
    * 1. alice (5m)
      2. bob (12s)
      3. carol (1h2m)
    ========================================
    * = you / 你

Now it matches the rest of the TUI:

    在线用户 · online  · 3
    ▎  alice  · 5m
       bob    · 12s
       carol  · 1h2m

- bold cyan title chip, dim grey total count
- 1-column ▎ gutter on your own row (same vocabulary as UX-1
  message gutter)
- dim grey "· duration" separator instead of parentheses + ASCII rule
- no trailing rule, no legend explaining "* = you" because the
  gutter speaks for itself
2026-05-17 14:17:02 +08:00
0e03c4d216 commands: highlight matched keyword in :search results (UX-7)
:search dumps the matching lines with username and content, but the
query word itself was just rendered as-is.  In a result set with 15
matches you had to eye-scan each line for the keyword.

Now each occurrence of the (case-insensitively matched) needle is
wrapped in a reverse-yellow ANSI chip both in the username column and
in the content column.  Original casing of the matched substring is
preserved.

Helper:
    append_highlighted(output, buf_size, &pos, text, needle)
emits text into the output buffer with every case-insensitive hit
wrapped in `\033[7;33m … \033[0m`.

strcasestr() needs _DEFAULT_SOURCE / _DARWIN_C_SOURCE feature macros;
the same dance message.c already does is now mirrored in commands.c.
2026-05-17 14:13:21 +08:00
0a013ed40f tui: title bar gracefully degrades on narrow terminals (UX-6)
When the terminal is too narrow to hold

    username · 在线 N · MODE  [静音]                ? 帮助

the chips and hint would visually collide.  Now the renderer measures
required width against render_width and drops optional segments in
reverse priority until what's left fits:

    1. drop the "? 帮助" hint
    2. drop the "静音" marker (if shown)
    3. drop the mode chip
    4. drop the online-count chip

The bold username is always shown.  A minimum 1-column gap is kept
between left and right halves so they never touch.

Mostly cosmetic on a regular terminal, but matters on phones /
tmux split panes / narrow side windows.
2026-05-17 13:51:25 +08:00
ae1bc2f166 input: vim-style paging keys in the help screen (UX-5)
The NORMAL chat mode has Ctrl+D/U (half page) and Ctrl+F/B (full page)
scrolling, which is what vim users reach for.  The help screen had
none of these — only j/k single-line and g/G top/bottom — so reaching
the bottom of a help dump meant mashing j.

Now the help screen accepts the same four shortcuts.  The page size
is computed from client->height (matching what NORMAL mode does), so
half/full page scroll size scales with the terminal.

Both the EN and ZH help text have been updated to advertise the new
shortcuts.
2026-05-17 13:49:00 +08:00
c66491d4f8 tui: byte-budget gauge in INSERT input line (UX-4)
The chat input is bounded at MAX_MESSAGE_LEN (1024 bytes).  Past that
limit the input loop silently drops further keystrokes — which is fine
mechanically but leaves the user typing into the void.

tui_render_input() now appends a right-aligned gauge to the input line
once the buffer crosses 80 % full:

    › some long message that goes on and on … 187 B    (dim grey)
    › this one is almost at the limit       … 12 B    (bold yellow > 95 %)

Below 80 % the prompt is unchanged.  The gauge eats display width from
the available content area so the existing horizontal scroll-truncate
logic keeps working — long input still scrolls cleanly past the gauge.

(Paste handling, which is the other half of the long-input UX gap,
lands separately as UX-13.)
2026-05-17 13:47:00 +08:00
b1353d904b commands: reject :nick collisions with active clients (UX-3)
:nick used to swap client->username unconditionally, so two clients
could both end up named "alice" — and the subsequent :msg / :w
disambiguation would just hit whichever came first in g_room->clients.

Now the rename happens under wrlock with an explicit scan: if any
*other* client already owns that username, the change is refused with

    Nickname 'alice' is already taken

If the requested name equals the current one, return a short
"Nickname unchanged" instead of broadcasting a no-op system message.

The room-lock-held scan is O(n) over current clients (n ≤ 64 by
default) and folds naturally into the existing wrlock, so there's no
new lock acquisition.
2026-05-17 13:44:25 +08:00
f3217de36b input: Up/Down in INSERT mode walks sent-message history (UX-2)
ESC in INSERT mode used to switch straight to NORMAL.  Now it follows
the same arrow-key probe COMMAND mode already does: if the next two
bytes are "[A" or "[B", treat as Up / Down and walk through the last
16 messages this client has sent.  A plain ESC still falls through to
NORMAL — the 50 ms probe timeout keeps that path responsive.

Storage: new client_t fields
  char insert_history[16][MAX_MESSAGE_LEN];
  int  insert_history_count;
  int  insert_history_pos;

Recording: every Enter that broadcasts a message pushes the input
buffer onto the ring (with FIFO eviction at 16) and resets pos.
Down at the bottom of the ring returns to an empty input.

This is the standard chat-client recall that vim users (and anyone
who's ever used a shell) expect.
2026-05-17 13:43:15 +08:00
94f3d28562 tui: cyan ▎ gutter on the user's own messages (UX-1)
Self-messages were rendered identically to anyone else's, so scrolling
back to find "what did I just say?" meant scanning every author name.

Now every rendered chat line carries a 1-column left gutter:
- ▎ (U+258E, cyan) when the message is from the local user
- single space otherwise — keeps the rest of the line aligned

Detection handles both regular messages (username == my_username) and
/me action messages (which use "*" as the author and prefix the actor
into the content).  System messages ("系统") always render with the
space gutter regardless.

Gutter width is folded into the existing truncation budget so long
messages still fit the terminal.
2026-05-17 13:35:57 +08:00
d3ebe25973 tui: dedicated MOTD renderer (M7-5)
Some checks failed
CI / build-and-test (macos-latest) (push) Has been cancelled
CI / build-and-test (ubuntu-latest) (push) Has been cancelled
Deploy / test (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
The MOTD used to ride on tui_render_command_output's coattails — text
got stuffed into client->command_output prefixed by "=== 公告 / MOTD ==="
and rendered under a reverse-video " COMMAND OUTPUT " title bar.  Two
different things (a transient command result vs. a one-shot service
notice) wearing the same chrome.

Now MOTD has its own renderer with its own aesthetic:

    ╭─ 公告 / MOTD ────────────────────────────────╮

      欢迎来到 TNT 公共聊天室。
      请互相尊重,不要刷屏。
      管理员:m1ng

    ╰─ 按任意键继续 / press any key ────────────────╯

- title chip embedded in the top border (bright cyan)
- footer hint embedded in the bottom border (dim grey)
- 2-column left padding on body lines, blank top/bottom pad rows
- dim cyan borders, no full-line reverse anywhere

Wiring:
- new tui_render_motd() declared in tui.h
- new client_t.show_motd flag selects the renderer; command_output
  remains the text storage (no extra buffer needed)
- input.c MOTD path sets show_motd = true and calls tui_render_motd()
- handle_key's dismiss path clears show_motd alongside command_output
- main-loop redraw dispatch checks show_motd before command_output[0]
2026-05-17 13:16:37 +08:00
2610bba76d tui: dim date divider between messages from different days (M7-4)
When the rendered message snapshot crosses a date boundary, insert a
single dim grey divider line before the first message of each new day:

    ── 2026-05-14 ──────────────────────────────────────────
    16:00  alice: 早!
    16:01  bob: 早呀
    ── 2026-05-15 ──────────────────────────────────────────
    04:30  alice: 晚上一起吃饭?
    ...

The divider also fires for the *first* visible date, so the user always
knows what day the top of their viewport belongs to.

Dividers and messages share the fixed msg_height budget — dividers
don't push other content off the bottom.  In the worst case a viewport
with one message per day shows half the rows as dividers, which is the
intended trade for readability.

Real win for a long-lived public chat where messages span many days
and timestamps alone only show HH:MM.
2026-05-17 13:10:24 +08:00
fdef16ae8e tui: refined status / prompt line (M7-3)
NORMAL mode status line:
  before: "-- NORMAL -- (3/100)  ↓ 5 new"
  after : "[reverse-yellow NORMAL]  3 / 100   ▼ 5 new"
- the mode is now a small reverse-video yellow chip so it visually
  echoes the mode colour in the title bar
- position is dim grey with em-spaced slash, "▼" replaces "↓" so the
  unseen-message marker matches the same downward-triangle vocabulary
  the help screen uses

INSERT mode prompt:
  before: "> "
  after : "› " (U+203A, dim grey)
- single-glyph chevron is quieter than the ASCII ">", aligns with the
  thinner aesthetic of the new title bar
- applied both in tui_render_screen() and tui_render_input() so
  per-keystroke redraws match the initial paint

COMMAND mode prompt:
  before: ":foo"
  after : "[magenta :]foo"
- the leading colon picks up the COMMAND mode colour so it reads as
  "you are in command mode now" rather than as part of the typed text
2026-05-17 12:56:21 +08:00
e2b16cf477 tui: lighter, segmented title bar (M7-2)
Replace the full-width reverse-video title

    ▍ tester | 在线: 3 | 模式: INSERT | ? 帮助 ▎  (reversed)

with a single line of small chips on a normal background:

     tester · 在线 3 · INSERT                  ? 帮助

- username is bold white, online count is plain
- mode name carries its own colour, so glance + colour disambiguates
  it (INSERT cyan, NORMAL yellow, COMMAND magenta, HELP blue) without
  having to read the word
- separators are dim grey middle-dots so the eye groups segments
- mute marker shrinks from "[静音]" to a dim "静音" suffix when active
- hint "? 帮助" is right-aligned in dim grey, far from content

The horizontal rule below the chat already provides the visual divider
that the reverse-video bar used to provide, so the bar can drop the
heaviness without losing structure.
2026-05-17 12:54:27 +08:00
159073ec49 tui: framed welcome banner before username prompt (M7-1)
Replace the four lines of `==` rules + bilingual title with a centred
rounded box so the eye lands on a single visual element instead of two.

The new tui_render_welcome():
- centres horizontally and vertically (about a third of the way down)
- shows TNT + version, then 中文 subtitle, then English caption with
  decreasing visual weight (bold cyan / default / dim grey)
- falls back to a plain "TNT $version — anonymous chat over SSH" line
  when the terminal is too narrow for the frame
- declared in tui.h, called from input.c read_username()

read_username()'s prompt becomes a single short bilingual hint
("请输入用户名 (留空 anonymous): ") that lives below the box.
2026-05-17 10:48:27 +08:00
7897d8820e refactor: extract client module (PR2-M6)
Move client_t I/O and lifecycle out of ssh_server.c into a dedicated
module.  ssh_server.c is now down to the listening socket, host key
setup, ssh_server_init / ssh_server_start, and the accept loop.

Migrated to client.{c,h}:
- client_send, client_printf
- client_addref, client_release (and the ssh_session / ssh_channel /
  channel_cb teardown that fires on the final release)
- client_install_channel_callbacks
- client_channel_window_change / _eof / _close (the post-bootstrap
  callbacks that target the client_t)

The client_t struct definition stays in ssh_server.h so we don't have
to revisit every existing #include chain.  bootstrap, commands, exec,
input, and tui pick up the new client.h alongside their existing
ssh_server.h include.

ssh_server.c shrinks from 402 to 249 lines (-153).

Final per-module size after PR2:
  ssh_server.c   249  accept loop + ssh_server_init/start + host key
  bootstrap.c    493  per-connection SSH handshake / auth / channel
  client.c       162  client_t I/O + lifecycle + channel callbacks
  input.c        637  username read + handle_key + main loop +
                      notify_mentions + idle timeout
  commands.c     269  vim ':' command dispatcher
  exec.c         453  SSH exec subcommand dispatcher
  ratelimit.c    197  IP rate limit + connection counters
  tui.c          614  screen rendering
  chat_room.c    151  room + client list
  message.c      350  message log load/save/search
  utf8.c         250  UTF-8 width / validation
  common.c       133  buffer_*, env_int, is_valid_username,
                      sanitize_terminal_size, tnt_state_*
  main.c         109  process entry

Total: ~4 000 lines of C across 13 files, no file over 650 lines, every
file is single-purpose.  Behaviour is preserved: the migrated code is
byte-for-byte identical.
2026-05-17 10:16:27 +08:00
3b8a1d18d8 refactor: extract input module (PR2-M5)
Move the interactive session — username read, vim-style key dispatcher,
main poll/redraw/keepalive/idle-timeout loop, the @mention bell, and
the tear-down path — out of ssh_server.c into a dedicated module.

New API (include/input.h):
- input_init()                 -- read TNT_IDLE_TIMEOUT once at startup
- input_run_session(client_t*) -- run the interactive session for an
                                  already-bootstrapped client_t,
                                  including exec_dispatch() short-circuit
                                  when client->exec_command is set
- notify_mentions(content, sender) -- moved from ssh_server.h since
                                  the INSERT-mode send path lives here
                                  too

Renamed: client_handle_session(void *arg) -> input_run_session(client_t *).
The old void* signature was a fossil from when this was a pthread entry;
bootstrap.c calls it synchronously inside its own detached thread.
bootstrap_run() now ends with `input_run_session(client); return NULL;`.

g_idle_timeout is now private to input.c; ssh_server_init() calls
input_init() instead of reading the env directly.

ssh_server.c shrinks from 1026 to 402 lines (-624) -- now down to just
the accept loop, ssh_server_init / ssh_server_start, host-key setup,
client_t I/O API (send/printf/addref/release), and the post-bootstrap
client_channel_* callbacks.  M6 will give those a proper home.

Behaviour is preserved: all moved code is byte-for-byte identical.
2026-05-17 10:01:48 +08:00
b5f9a17290 refactor: extract bootstrap module (PR2-M4)
Move the per-connection SSH bootstrap pipeline -- key exchange, auth,
channel open + PTY/shell-or-exec request, and the hand-off into a
client_t -- out of ssh_server.c into a dedicated module.

Migrated to bootstrap.{c,h}:
- session_context_t (now private to bootstrap.c)
- accepted_session_t (declared in bootstrap.h, the IPC envelope from
  the accept loop into the bootstrap thread)
- TNT_ACCESS_TOKEN handling: g_access_token + bootstrap_init()
- constant_time_strcmp (auth-only utility)
- bootstrap_peer_ip (peer IP read from libssh fd)
- auth_password / auth_none / auth_pubkey
- destroy_session_context, cleanup_failed_session
- channel_open_request_session, channel_pty_request,
  channel_pty_window_change, channel_shell_request, channel_exec_request
- setup_session_channel_callbacks
- bootstrap_run (formerly bootstrap_client_session, the pthread entry)

Stayed in ssh_server.c:
- accept loop in ssh_server_start (now calls bootstrap_peer_ip and
  pthread_create(bootstrap_run))
- ssh_server_init (now calls ratelimit_init() + bootstrap_init() +
  reads only g_idle_timeout / TNT_BIND_ADDR / TNT_SSH_LOG_LEVEL)
- client_send/printf/addref/release, notify_mentions
- client_channel_window_change/eof/close (post-bootstrap, target client_t)
- client_install_channel_callbacks (renamed from
  install_client_channel_callbacks, now non-static and exposed via
  ssh_server.h so bootstrap.c can install them on the new client_t)
- read_username, handle_key, client_handle_session (will move to
  input.c in PR2-M5)
- setup_host_key, ssh_server_start_time

Two helpers also lifted: sanitize_terminal_size moved to common.c (used
by the bootstrap PTY callback and the post-bootstrap window-change
callback), and is_valid_username already lived there from M2.

ssh_server.c shrinks from 1513 to 1026 lines (-487).
Behaviour is preserved: implementations are byte-for-byte the same.
2026-05-17 09:47:28 +08:00
8aa34c75b8 refactor: extract commands module (PR2-M3)
Move the vim-mode `:` command dispatcher (`:list`, `:nick`, `:msg`,
`:last`, `:search`, `:mute-joins`, `:help`, `:clear`, `:q`, …) out of
ssh_server.c into a dedicated module.

New API (include/commands.h):
- commands_dispatch(client_t *)  -- single entry point invoked from
  handle_key when Enter is pressed in MODE_COMMAND.

ssh_server.c shrinks from 1769 to 1513 lines (-256).
Behaviour preserved: implementation is byte-for-byte the same.
2026-05-17 09:26:48 +08:00
7f9babf4f4 refactor: extract exec module (PR2-M2)
Move the SSH exec subcommand interface (help, health, users, stats,
tail, post) and the dispatcher out of ssh_server.c into a dedicated
module.

New API (include/exec.h):
- exec_dispatch(client_t *)  -- single entry point invoked from the
  bootstrap path when client->exec_command[0] != '\0'.

Helpers that travel with the exec subcommands:
- format_timestamp_utc, trim_ascii_whitespace, json_append_string,
  resolve_exec_username, parse_tail_count

Two cross-module bridges:
- is_valid_username() lifted into common.c/h since exec, the input
  read path, and the :nick command all need it.
- ssh_server_start_time() added to ssh_server.h as a read-only
  accessor; exec_command_stats no longer reaches into the global.
- notify_mentions stays in ssh_server.c for now and is exposed via
  ssh_server.h.  Will move to a dedicated client.c during PR2-M6.

ssh_server.c shrinks from 2200 to 1769 lines (-431).
Behaviour is preserved: implementations are byte-for-byte the same.
2026-05-17 08:49:58 +08:00
562ee5296d refactor: extract ratelimit module (PR2-M1)
Move IP rate-limiting, auth-failure tracking, and global connection
counting out of ssh_server.c into a dedicated module.

New API (include/ratelimit.h):
- ratelimit_init()
- ratelimit_check_ip() / ratelimit_release_ip()
- ratelimit_record_auth_failure()
- ratelimit_check_and_increment_total() / ratelimit_decrement_total()
- ratelimit_get_active_total()  (replaces the direct g_total_connections
  read that exec_command_stats was doing under g_conn_count_lock)

env_int() also moves up to common.{c,h} since multiple modules need it.

ssh_server.c drops from 2469 to 2200 lines.  Behaviour is preserved:
the new functions are byte-for-byte the same implementations, only the
file boundary moved.

g_idle_timeout and g_access_token reads stay inline in ssh_server_init()
for now; they will follow the auth.c and input.c extractions later.
2026-05-16 23:06:56 +08:00
d9382882d1 chore: bug fixes and code cleanup
Fixes:
- message_load() now holds g_message_file_lock for the read, so :last [N]
  can no longer observe a half-written line while message_save() is
  flushing.
- constant_time_strcmp() accumulates the length difference in size_t.
  The old code truncated to unsigned char, which collapsed pairs whose
  lengths differed by a multiple of 256 down to 0 and lost the signal.

Refactor:
- buffer_appendf() / buffer_append_bytes() moved to common.c; the two
  identical copies in ssh_server.c and tui.c have been removed.

Docs / cleanup:
- README clarifies that exec 'post' uses the SSH login name as the
  author and that anonymous mode performs no identity check.
- Removed TODO.md (both items completed) and docs/README.old.
- Trimmed the auto-generated 2025 entry block from docs/CHANGELOG.md
  and added a 2026-05-16 entry summarising this change.
2026-05-16 22:44:41 +08:00
ed5fc43cbd feat: add :last, :search, :mute-joins commands and MOTD support
- :last [N]: show last N messages from log (max 50, default 10)
- :search <keyword>: case-insensitive full-text search across log history
- :mute-joins: per-client toggle to silence join/leave system messages,
  indicated by [静音] in the title bar
- MOTD: display motd.txt from state directory on connect before entering chat
- Add message_search() to message.c/h for log file scanning
- Update :help and tui help screen (EN/ZH) with new commands
2026-04-23 12:03:27 +08:00
bb77c77b8f feat: add @mention notifications, idle timeout, and online duration
- @mention: typing @username in a message sends bell char to that user
  and highlights the message content in bold yellow for them
- Idle timeout: disconnect inactive clients after TNT_IDLE_TIMEOUT
  seconds (default 1800 = 30min, 0 to disable)
- :list now shows connection duration per user (e.g. "alice (12m)")
- Document all three features in help text, manpage, and README

Closes #46
2026-04-19 23:12:45 +08:00
ee3f89f10a feat: improve TUI visual experience with colors and indicators
- Color-code usernames using hash-based ANSI color assignment (6 colors)
- Style system messages (join/leave/rename) in gray with --> prefix
- Style /me action messages in italic cyan
- Show "↓ N new" indicator in yellow when scrolled up in NORMAL mode
- Compact timestamps from full date to HH:MM for more message space

Closes #44
2026-04-19 22:56:38 +08:00
b07348c6e1 docs: update all user-facing help text and documentation
- Add /me action command to all help surfaces (EN/ZH help screen,
  :help output, exec help, manpage, README)
- Add Ctrl+D/U/F/B page scrolling keys to help text (were only in manpage)
- Add :q/:quit/:exit disconnect command to help text
- Update README COMMAND mode section with all current commands
  (:nick, :msg, :w were missing)
- Remove redundant COMMAND MODE KEYS section from help text
  (merged into AVAILABLE COMMANDS for clarity)
- Compact help screen layout (j/k on one line, g/G on one line)
2026-04-19 22:08:59 +08:00
6dfd66ed66 fix: suppress GCC format-truncation warning in exec /me handler 2026-04-19 19:05:57 +08:00
14789cd1c8 fix: guard terminal width/height in all TUI renderers and harden edge cases
- Replace all direct client->width/height reads with local variables
  clamped to minimums (width>=10, height>=4) across tui_render_screen,
  tui_render_input, tui_render_command_output, and tui_render_help
- Fix tui_render_input underflow when width < 3 (avail = max(rw-3, 1))
- Show username in title bar instead of generic label
- Add /me action message support in exec_command_post for scripting parity
- Reject empty usernames when loading messages from log file
2026-04-19 18:58:51 +08:00
9060259558 feat: add --port, --version long options and improved --help output
- Add --port as alias for -p
- Add -V/--version flag
- Improve --help with environment variable documentation
- Update manpage with long option forms
2026-04-19 18:37:38 +08:00
e2990000e6 feat: add :nick command and /me action messages
- :nick/:name <name>: change username in-session with full validation,
  thread-safe update under write lock, and system broadcast
- /me <action>: IRC-style action messages displayed as "* user action"
- Updated help text (EN/ZH) and manpage with new commands

Closes #38
2026-04-19 18:34:02 +08:00
450f1828fd
Merge pull request #37 from m1ngsama/fix/deadlock-uaf-logrotate-tail
fix: deadlock, use-after-free, log rotation, and tail parsing
2026-04-19 18:30:45 +08:00
b1c1e5a894 fix: deadlock in whisper, use-after-free in callbacks, log rotation, tail parsing
- Whisper: copy target client ref out of room lock before calling
  client_send, preventing lock-ordering inversion deadlock
- Channel callbacks: call ssh_remove_channel_callbacks before releasing
  refs to prevent use-after-free if a callback fires during cleanup
- Log rotation: rotate messages.log to messages.log.1 when it exceeds
  10 MiB, preventing unbounded growth on public servers
- tail -nN: accept both "tail -n5" and "tail -n 5" forms, matching
  standard Unix tail behavior

Closes #36
2026-04-19 18:27:54 +08:00
629812a2d8 fix: correct pubkey auth response, strncpy warning, and NUL byte validation
- auth_pubkey: return SSH_AUTH_SUCCESS for key offers instead of
  SSH_AUTH_PARTIAL, which incorrectly signals partial authentication
- command history: replace strncpy with snprintf to eliminate
  -Wstringop-truncation warning on GCC
- utf8_is_valid_sequence: reject NUL byte (0x00) in single-byte
  validation to prevent C string truncation attacks

Closes #34
2026-04-19 18:27:50 +08:00
e10b43074c feat: consolidated improvements, manpage, and deployment prep
Bug fixes:
- Fix data race on client->width/height (now _Atomic int)
- Persist join/leave system messages via message_save()
- Make room_add_message static to enforce lock contract
- Fix execute_command mutating command_input directly
- Increase help_copy buffer from 4096 to 8192 for CJK safety

New features:
- Add :msg/:w whisper command for private messaging
- Add command history with UP/DOWN arrows in command mode
- Add Ctrl+D/U/F/B page scrolling in normal mode
- Add :q/:quit/:exit Vim-style disconnect

Unix community:
- Add tnt.1 manpage (roff format) with full documentation
- Add manpage install/uninstall to Makefile
2026-04-19 17:49:06 +08:00
200e5a2f28
Merge pull request #22 from m1ngsama/feat/expand-unit-tests
Add chat_room unit tests and integrate into CI
2026-04-19 17:39:25 +08:00
65cb5d79d7
Merge pull request #24 from m1ngsama/fix/input-handling-and-auth-hardening
Fix CJK input handling and reduce auth timeout
2026-04-19 17:39:00 +08:00
83e964028a
Merge pull request #20 from m1ngsama/fix/edge-cases-and-robustness
Fix edge cases in message loading and network I/O
2026-04-19 17:38:51 +08:00
9607d8c2f2 fix: CJK backspace display, UTF-8 in command mode, auth timeout
- Fix backspace in read_username to erase correct display width for
  CJK/wide characters (was erasing only 1 column for 2-column chars)
- Add UTF-8 multi-byte input support in COMMAND mode (was silently
  dropping non-ASCII bytes, breaking CJK command arguments)
- Reduce SSH auth timeout from 30s to 10s to limit connection-slot
  exhaustion from slow/malicious handshakes
2026-04-19 16:19:43 +08:00
ecc45f285c test: add chat_room unit tests and integrate into build
- Add 11 unit tests for chat_room.c covering: create/destroy, message
  add/overflow, broadcast sequence, get_message bounds, client
  add/remove/capacity, and null argument handling
- Add unit-test target to root Makefile so `make test` runs unit tests
  before integration tests
- Add common.c to unit test link dependencies (needed for tnt_state_path)
- Guard _DARWIN_C_SOURCE define to prevent -Wmacro-redefined warning
2026-04-19 15:22:01 +08:00
8be6476367 fix: harden edge cases in message loading and network I/O
- Check ftell() return for errors (-1) in message_load to prevent
  corrupted backward scan on I/O failures
- Cap ssh_channel_write chunks to 32KB to prevent size_t-to-uint32_t
  narrowing on large buffers
- Log evicted active connection count in rate-limit table overflow
  warning for better diagnostics
2026-04-19 15:18:09 +08:00
9bbd5acd15 fix: resolve memory safety bugs and timing side-channel
- Fix use-after-free/double-free on install_client_channel_callbacks
  failure: nullify session/channel ownership before releasing refs so
  cleanup_failed_session does not double-free resources
- Fix constant_time_strcmp to always iterate over the full secret length,
  preventing timing leak of token length
- Fix data race on client->width/height by protecting window-change
  callback writes with io_lock
- Fix potential UTF-8 mid-sequence truncation in tui_render_input by
  sizing display buffer to MAX_MESSAGE_LEN
2026-04-19 14:08:31 +08:00
0de13a6314 fix: add _DARWIN_C_SOURCE for timegm() on macOS CI
Some checks failed
CI / build-and-test (macos-latest) (push) Has been cancelled
CI / build-and-test (ubuntu-latest) (push) Has been cancelled
Deploy / test (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
2026-04-15 10:15:32 +08:00
d745a8e1fe fix: address security vulnerabilities and design flaws from comprehensive audit
Critical fixes:
- C-1: Use atomic_bool for client->connected and redraw_pending to prevent
  data races between callback and main threads
- C-2: Add reference counting for channel callbacks to prevent use-after-free
  when callbacks fire during client cleanup
- C-3/M-7: Use ssh_channel_read_timeout (5s) for UTF-8 continuation bytes
  to prevent thread blocking and stream desynchronization

High-severity fixes:
- H-1: Replace non-thread-safe setenv/tzset with timegm() in parse_rfc3339_utc
- H-2: Change room_get_message to return by value copy instead of interior pointer
- H-3: Log warning when rate-limit table evicts active IP entry
- H-4: Replace strcmp with constant-time comparison for access token validation
- H-5: Check signature_state in auth_pubkey to reject unsigned key offers

Medium/low fixes:
- M-1: Replace all atoi() with strtol() for proper error detection
- M-3: Move calloc outside rwlock in tui_render_screen to avoid blocking writers
- M-8: Fix off-by-one in rate limit threshold (> to >=)
- M-9: Trim partial UTF-8 sequences after snprintf truncation in message_format
- L-1: Validate continuation byte mask (0xC0==0x80) in utf8_decode
- D-3: Remove vestigial client_t.fd field
- L-3: Remove unreachable pthread_attr_destroy after infinite loop
2026-04-15 10:13:17 +08:00
6c6c500134 fix: reject unknown command-line arguments instead of silently ignoring them 2026-04-15 09:51:49 +08:00
cb106de31b fix: separate per-ip concurrency from connection rate 2026-03-10 19:08:28 +08:00
e473b26e0d refactor: stabilize SSH runtime and add exec interface 2026-03-10 18:52:20 +08:00
da81e17187 fix: resolve crash and hang causes found in production audit
Fixes #10.

Five bugs that caused the server to crash or become unresponsive:

1. Signal handler deadlock (main.c)
   signal_handler called room_destroy (pthread_rwlock + free) and printf —
   neither is async-signal-safe. If SIGTERM arrived while any thread held
   g_room->lock, the process deadlocked permanently.
   Fix: handler now only writes a message via write(2) and calls _exit(0).
   Also remove close(g_listen_fd) which was closing stdin (fd 0), since
   ssh_server_init returns 0 on success, not a real file descriptor.

2. NULL dereference in room_broadcast when room is empty (chat_room.c)
   calloc(0, n) may return NULL per POSIX; memcpy on NULL is undefined.
   Also: no NULL check after calloc for the OOM case.
   Fix: early return if count == 0; check calloc return value.

3. Stack buffer overflow in tui_render_screen (tui.c)
   char buffer[8192] overflows with tall terminals: 197 visible lines *
   ~1031 bytes/message ≈ 203 KiB. Title padding loop also lacked a
   bounds check (buffer[pos++] = ' ' with no guard).
   Fix: switch to malloc(65536) with buf_size used consistently.
   Add bounds check to the title padding loop.

4. sleep() inside libssh auth callback (ssh_server.c)
   auth_password is called from ssh_event_dopoll in the main thread.
   sleep(2) there blocks the entire accept loop — one attacker with
   repeated wrong passwords stalls all incoming connections.
   IP blocking via record_auth_failure already handles brute force.
   Fix: remove sleep(2) from auth_password.

5. Spurious sleep() calls in the main accept loop (ssh_server.c)
   sleep(1/2) after rejecting rate-limited or over-limit connections
   delays accepting the next legitimate connection for no benefit.
   Fix: remove all sleep() from the accept loop error paths.
2026-03-06 01:58:56 +08:00
25a277ab27 feat: add SSH keepalive and CI/CD auto-deploy
Some checks failed
CI / build-and-test (macos-latest) (push) Has been cancelled
CI / build-and-test (ubuntu-latest) (push) Has been cancelled
Deploy / test (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Send keepalive every 30s to prevent NAT/firewall from silently
dropping idle SSH connections. Add deploy workflow that auto-deploys
to production server after CI passes on main.
2026-02-08 11:54:27 +08:00
07fd7b1513
refactor: optimize rendering, log loading, and migrate to libssh callback API (#9)
This PR addresses critical performance bottlenecks, improves UX, and eliminates technical debt.

### Key Changes

**1. Performance Optimization:**
- **Startup**: Rewrote `message_load` to scan `messages.log` backwards from the end
  - Complexity reduced from O(FileSize) to O(MaxMessages)
  - Large log file startup: seconds → milliseconds
- **Rendering**: Optimized TUI rendering to use line clearing (`\033[K`) instead of full-screen clearing (`\033[2J`)
  - Eliminated visual flicker

**2. libssh API Migration:**
- Replaced deprecated message-based API with callback-based server implementation
- Removed `#pragma GCC diagnostic ignored "-Wdeprecated-declarations"`
- Ensures future libssh compatibility

**3. User Experience (Vim Mode):**
- Added `Ctrl+W` (Delete Word) and `Ctrl+U` (Delete Line) in Insert/Command modes
- Modified `Ctrl+C` behavior to safely switch modes instead of terminating connection
- Added support for `\n` as Enter key (fixing piped input issues)

**4. Project Structure:**
- Moved all test scripts to `tests/` directory
- Added `make test` target
- Updated CI/CD to run comprehensive test suite

### Verification
-  All tests passing (17/17)
-  CI passing on Ubuntu and macOS
-  AddressSanitizer clean
-  Valgrind clean (no memory leaks)
-  Zero compilation warnings

### Code Quality
**Rating:** 🟢 Good Taste
- Algorithm-driven optimization (not hacks)
- Simplified architecture (callback-based API)
- Zero breaking changes (all tests pass)
2026-02-07 23:17:55 +08:00
5f8b8fd843 feat: enhance anonymous access and long-term stability
Improvements for low-barrier anonymous access:
- Enhanced welcome message to clarify anonymous access
- Added EASY_SETUP.md guide in Chinese and English
- Updated README with anonymous access notes

Long-term stability enhancements:
- Improved systemd service with auto-restart and resource limits
- Added log rotation script (scripts/logrotate.sh)
- Added health check script (scripts/healthcheck.sh)
- Added cron setup script for automated maintenance
- Added anonymous access test suite

Testing:
- All security features verified (10/10 passed)
- Anonymous access tests passed (2/2)
- Health check verified

This ensures:
- Zero-barrier SSH access (any username, any password)
- Stable long-term operation with auto-restart
- Automated log management
- Continuous health monitoring
2026-01-22 15:06:54 +08:00
bc08269743 Merge branch 'fix/concurrency-safety' into feat/security-audit-fixes 2026-01-22 14:08:45 +08:00
93c29ca2e9 Merge branch 'fix/auth-protection' into feat/security-audit-fixes
# Conflicts:
#	src/ssh_server.c
2026-01-22 14:08:40 +08:00
4a34a776c2 Merge branch 'fix/resource-management' into feat/security-audit-fixes 2026-01-22 14:07:10 +08:00
c1d0723392 Merge branch 'fix/input-validation' into feat/security-audit-fixes 2026-01-22 14:07:06 +08:00
c8e3743e9f Merge branch 'fix/ssh-hardening' into feat/security-audit-fixes 2026-01-22 14:07:00 +08:00
a5a62f057e fix(security): implement concurrency safety improvements
- Enhance room_broadcast() reference counting:
  * Check client state (connected, show_help, command_output) before rendering
  * Perform state check while holding client ref_lock
  * Prevents rendering to disconnected/invalid clients
  * Ensures safe cleanup when ref count reaches zero

- Fix tui_render_screen() message array TOCTOU:
  * Acquire all data (online count, message count, messages) in single lock
  * Create snapshot of messages to display
  * Calculate message range while holding lock
  * Render from snapshot without holding lock
  * Prevents inconsistencies from concurrent message additions
  * Eliminates race between two separate lock acquisitions

- Fix handle_key() scroll position TOCTOU:
  * Get message count atomically when calculating scroll bounds
  * Calculate max_scroll properly accounting for message height
  * Apply consistent bounds checking for 'j' (down) and 'G' (bottom)
  * Prevents out-of-bounds access from concurrent message changes

These changes address:
- Race condition in broadcast rendering to disconnecting clients
- TOCTOU between message count read and message access
- Scroll position bounds check race conditions

Prevents:
- Use-after-free in client cleanup
- Array out-of-bounds access
- Inconsistent UI rendering
- Crashes from concurrent message list modifications

Improves thread safety without introducing deadlocks by:
- Using snapshot approach to avoid long lock holds
- Acquiring data in consistent lock order
- Minimizing critical sections
2026-01-22 14:06:15 +08:00
a50f8c9c56 fix(security): implement comprehensive authentication protection
- Add IP-based rate limiting system:
  * Track up to 256 IPs with connection counts and auth failures
  * Rate limit: max 10 connections per IP per 60-second window
  * Block for 5 minutes after 5 auth failures
  * Auto-unblock when duration expires
- Add global connection limit (default: 64, configurable)
- Add per-IP connection limit (default: 5, configurable)
- Implement optional access token authentication:
  * If TNT_ACCESS_TOKEN set, require password matching token
  * If not set, maintain open access (backward compatible)
  * Rate limit auth attempts (max 3 per session)
  * Add 2-second delay after failed auth to slow brute force
- Add client IP tracking and logging
- Implement connection count management with proper cleanup

Environment variables:
- TNT_ACCESS_TOKEN: Access token for password authentication (optional)
- TNT_MAX_CONNECTIONS: Maximum concurrent connections (default: 64)
- TNT_MAX_CONN_PER_IP: Maximum connections per IP (default: 5)
- TNT_RATE_LIMIT: Enable/disable rate limiting (default: 1)

These changes address:
- Weak authentication allowing unrestricted access
- No protection against brute force attacks
- No rate limiting or connection throttling
- No IP-based access controls

Prevents:
- Brute force password attacks
- Connection flooding DoS
- Resource exhaustion
- Unauthorized access when token is configured

Design maintains backward compatibility: without TNT_ACCESS_TOKEN,
server remains fully open as before. With token, it's protected.
2026-01-22 14:04:15 +08:00
f65e8add64 fix(security): enhance resource management
- Convert message_load() file position array from fixed 1000 to dynamic:
  * Start with capacity of 1000, grow by 2x when needed
  * Use malloc/realloc for flexible memory management
  * Proper cleanup with free() after use
  * Graceful handling of memory allocation failures
- Enhance setup_host_key() error handling:
  * Validate key file size (reject 0 bytes and >10MB)
  * Automatically regenerate if key file is empty
  * Verify and fix insecure permissions (must be 0600)
  * Better error messages with file size reporting
- Improve client thread resource cleanup:
  * Use pthread_attr for explicit detached thread creation
  * Add pthread_mutex_destroy on thread creation failure
  * Proper cleanup order: mutex -> channel -> session -> memory
  * Add error logging with strerror() for thread failures

These changes address:
- Fixed 1000-line limit causing message truncation
- Corrupted/empty key file handling
- Permission race conditions
- Resource leaks on thread creation failure

Prevents:
- DoS via large log files
- Service startup failures from bad key files
- Memory/handle leaks under error conditions
2026-01-22 14:02:05 +08:00
4f3a07c5e2 fix(security): implement comprehensive input validation
- Add is_valid_username() function to prevent injection attacks
  * Reject shell metacharacters: |;&$`<>(){}[]'"\
  * Reject control characters (except tab)
  * Reject usernames starting with space, dot, or dash
- Apply username validation in read_username() with fallback to "anonymous"
- Add rate limiting via sleep(1) on validation failure
- Sanitize message content in message_save():
  * Replace pipe, newline, carriage return to prevent log injection
  * Ensure null termination of sanitized strings
- Enhance message_load() validation:
  * Check for oversized lines
  * Validate field lengths before copying
  * Validate timestamp reasonableness (not >1 day future, <10 years past)
  * Ensure null termination of all loaded strings

These changes address:
- Username injection vulnerabilities
- Message content injection in log files
- Log file format corruption attacks
- Malformed timestamp handling

Prevents:
- Command injection via usernames
- Log poisoning attacks
- DoS via oversized messages
2026-01-22 13:59:58 +08:00
325e524cee fix(security): implement SSH hardening improvements
- Upgrade RSA key size from 2048 to 4096 bits for stronger encryption
- Fix key file permission time window with atomic generation:
  * Use umask(0077) before file creation
  * Generate key to temporary file first
  * Atomically rename to final location
- Add configurable bind address via TNT_BIND_ADDR environment variable
- Add configurable SSH log level via TNT_SSH_LOG_LEVEL (0-4)

These changes address:
- Weak 2048-bit RSA keys
- Permission race condition during key generation
- Hardcoded bind address limiting deployment flexibility
- Inflexible logging configuration

Environment variables:
- TNT_BIND_ADDR: Bind address (default: 0.0.0.0)
- TNT_SSH_LOG_LEVEL: SSH logging verbosity 0-4 (default: 1)
2026-01-22 13:57:32 +08:00
36464007e8 fix(security): implement buffer security enhancements
- Replace all strcpy() calls with strncpy() to prevent buffer overflows
- Add buffer overflow checking in client_printf() vsnprintf result
- Implement UTF-8 sequence validation to prevent malformed input
- Add utf8_is_valid_sequence() function with complete validation
- Enhance read_username() with UTF-8 boundary checks
- Add UTF-8 validation for message input handling

These changes address:
- Buffer overflow vulnerabilities (lines 178, 423, 510)
- Insufficient vsnprintf() error checking (line 106)
- Missing UTF-8 sequence validation (lines 156-171)

Fixes prevent:
- Buffer overflow attacks
- Overlong UTF-8 encoding exploits
- Invalid UTF-8 surrogates injection
2026-01-22 13:54:15 +08:00
bf1fb99d11
Merge pull request #4 from m1ngsama/optimize/message-loading
[Optimize] Message history loading efficiency
2025-12-02 12:39:34 +08:00
cf95bcecaf
Merge pull request #2 from m1ngsama/fix/memory-race-conditions
[Fix] Critical memory and race condition bugs
2025-12-02 12:39:12 +08:00
1913a00f27 Optimize message history loading
Previous implementation:
- Allocated MAX_MESSAGES * 10 (1000 messages) temporarily
- Wasted ~100KB per server startup
- Could fail if log file grows very large

New implementation:
- Track file positions of last 1000 lines
- Seek to appropriate position before reading
- Only allocate MAX_MESSAGES (100 messages)
- Memory usage reduced by 90%

Benefits:
- Faster startup with large log files
- Lower memory footprint
- No risk of allocation failure
- Same functionality maintained

Uses fseek/ftell for efficient log file handling.
2025-12-01 16:30:00 +08:00
298995aa53 Fix critical memory and concurrency bugs
Fixes three critical bugs that caused crashes after long-running:

1. Use-after-free race condition in room_broadcast()
   - Added reference counting to client_t structure
   - Increment ref_count before using client outside lock
   - Decrement and free only when ref_count reaches 0
   - Prevents accessing freed client memory during broadcast

2. strtok() data corruption in tui_render_command_output()
   - strtok() modifies original string by replacing delimiters
   - Now use a local copy before calling strtok()
   - Prevents corruption of client->command_output

3. Improved handle_key() consistency
   - Return bool to indicate if key was consumed
   - Fixes issue where mode-switch keys were processed twice

Thread safety changes:
- Added client->ref_count and client->ref_lock
- Added client_release() for safe cleanup
- room_broadcast() now properly increments/decrements refs

This fixes the primary cause of crashes during extended operation.
2025-11-30 09:00:00 +08:00
03c89beeb4 Fix vim command mode double colon bug
When pressing ':' in NORMAL mode, the key was being processed twice:
1. handle_key() detected it and switched to COMMAND mode
2. The same ':' character was then added to command_input

This resulted in '::' appearing instead of ':'.

Solution:
- Changed handle_key() to return bool indicating if key was consumed
- Only add character to input if handle_key() returns false
- All mode-switching keys now return true to prevent reprocessing

Fixes the most annoying UX bug reported by users.
2025-11-29 10:00:00 +08:00
161fc904f3 Fix passwordless login and display alignment issues
- Allow SSH_AUTH_METHOD_NONE for passwordless authentication
- Replace all \n with \r\n in TUI rendering for proper line breaks
- Fixes messages appearing misaligned on terminal
2025-11-24 17:01:08 +08:00
a4d67be103 Replace telnet with SSH and fix full-screen display
- Implement SSH server using libssh for secure connections
- Replace insecure telnet with encrypted SSH protocol
- Add automatic terminal size detection via PTY requests
- Support dynamic window resize (SIGWINCH handling)
- Fix UI display bug by using SSH channel instead of fd
- Update tui_clear_screen to work with SSH connections
- Add RSA host key auto-generation on first run
- Update README with SSH instructions and security notes
- Add libssh dependency to Makefile with auto-detection
- Remove all telnet-related code

Security improvements:
- All traffic now encrypted
- Host key authentication
- No more plaintext transmission
2025-11-24 16:48:14 +08:00
63274b92ba Initial commit 2025-07-01 09:00:00 +08:00