Commit graph

22 commits

Author SHA1 Message Date
33e2dc4f13 Build public release readiness foundation 2026-05-26 09:42:14 +08:00
06a10e2df8 i18n: rename help language state to ui language 2026-05-24 12:11:54 +08:00
1f1c2398b6 tui: make command output scrollable 2026-05-24 11:55:26 +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
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
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
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
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
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
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
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
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
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
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
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