Compare commits

..

No commits in common. "49674b75e833a585c64224c6e7da28f205b5b2cc" and "e3e148618716be3151d2958c4fc32b891f9c5f06" have entirely different histories.

25 changed files with 394 additions and 1931 deletions

View file

@ -38,14 +38,13 @@ https://github.com/m1ngsama/TNT/releases
```sh ```sh
tnt # default port 2222 tnt # default port 2222
tnt -p 3333 # custom port tnt -p 3333 # custom port
tnt -d /var/lib/tnt
PORT=3333 tnt # via env var PORT=3333 tnt # via env var
``` ```
### Connecting ### Connecting
```sh ```sh
ssh -p 2222 chat.m1ng.space ssh -p 2222 localhost
``` ```
**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.
@ -93,12 +92,6 @@ TNT_BIND_ADDR=127.0.0.1 tnt
# Bind to specific IP # Bind to specific IP
TNT_BIND_ADDR=192.168.1.100 tnt TNT_BIND_ADDR=192.168.1.100 tnt
# Store host key and logs in an explicit state directory
TNT_STATE_DIR=/var/lib/tnt tnt
# Show the public SSH endpoint in startup logs
TNT_PUBLIC_HOST=chat.m1ng.space tnt
``` ```
**Rate limiting:** **Rate limiting:**
@ -106,13 +99,10 @@ TNT_PUBLIC_HOST=chat.m1ng.space tnt
# Max total connections (default 64) # Max total connections (default 64)
TNT_MAX_CONNECTIONS=100 tnt TNT_MAX_CONNECTIONS=100 tnt
# Max concurrent sessions per IP (default 5) # Max connections per IP (default 5)
TNT_MAX_CONN_PER_IP=10 tnt TNT_MAX_CONN_PER_IP=10 tnt
# Max new connection attempts per IP in 60 seconds (default 10) # Disable rate limiting (testing only)
TNT_MAX_CONN_RATE_PER_IP=30 tnt
# Disable connection-rate and auth-failure blocking (testing only)
TNT_RATE_LIMIT=0 tnt TNT_RATE_LIMIT=0 tnt
``` ```
@ -127,24 +117,11 @@ TNT_SSH_LOG_LEVEL=3 tnt
TNT_ACCESS_TOKEN="strong-password-123" \ TNT_ACCESS_TOKEN="strong-password-123" \
TNT_BIND_ADDR=0.0.0.0 \ TNT_BIND_ADDR=0.0.0.0 \
TNT_MAX_CONNECTIONS=200 \ TNT_MAX_CONNECTIONS=200 \
TNT_MAX_CONN_PER_IP=30 \ TNT_MAX_CONN_PER_IP=3 \
TNT_MAX_CONN_RATE_PER_IP=60 \
TNT_SSH_LOG_LEVEL=1 \ TNT_SSH_LOG_LEVEL=1 \
tnt -p 2222 tnt -p 2222
``` ```
### SSH Exec Interface
TNT also exposes a small non-interactive SSH surface for scripts:
```sh
ssh -p 2222 chat.m1ng.space health
ssh -p 2222 chat.m1ng.space stats --json
ssh -p 2222 chat.m1ng.space users
ssh -p 2222 chat.m1ng.space "tail -n 20"
ssh -p 2222 operator@chat.m1ng.space post "service notice"
```
## Development ## Development
### Building ### Building
@ -167,7 +144,6 @@ cd tests
./test_basic.sh # basic functionality ./test_basic.sh # basic functionality
./test_security_features.sh # security features ./test_security_features.sh # security features
./test_anonymous_access.sh # anonymous access ./test_anonymous_access.sh # anonymous access
./test_connection_limits.sh # per-IP concurrency and rate limits
./test_stress.sh # stress test ./test_stress.sh # stress test
``` ```
@ -226,19 +202,6 @@ sudo cp tnt.service /etc/systemd/system/
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl enable tnt sudo systemctl enable tnt
sudo systemctl start tnt sudo systemctl start tnt
# Optional: override defaults without editing the unit
sudo tee /etc/default/tnt >/dev/null <<'EOF'
PORT=2222
TNT_BIND_ADDR=0.0.0.0
TNT_STATE_DIR=/var/lib/tnt
TNT_MAX_CONNECTIONS=200
TNT_MAX_CONN_PER_IP=30
TNT_MAX_CONN_RATE_PER_IP=60
TNT_RATE_LIMIT=1
TNT_SSH_LOG_LEVEL=0
TNT_PUBLIC_HOST=chat.m1ng.space
EOF
``` ```
### Docker ### Docker
@ -265,7 +228,6 @@ tnt.service - systemd service unit
- [Development Guide](https://github.com/m1ngsama/TNT/wiki/Development-Guide) - Complete development manual - [Development Guide](https://github.com/m1ngsama/TNT/wiki/Development-Guide) - Complete development manual
- [Quick Setup](docs/EASY_SETUP.md) - 5-minute deployment guide - [Quick Setup](docs/EASY_SETUP.md) - 5-minute deployment guide
- [Roadmap](docs/ROADMAP.md) - Long-term Unix/GNU direction and next stages
- [Security Reference](docs/SECURITY_QUICKREF.md) - Security config quick reference - [Security Reference](docs/SECURITY_QUICKREF.md) - Security config quick reference
- [Contributing](docs/CONTRIBUTING.md) - How to contribute - [Contributing](docs/CONTRIBUTING.md) - How to contribute
- [Changelog](docs/CHANGELOG.md) - Version history - [Changelog](docs/CHANGELOG.md) - Version history

View file

@ -47,7 +47,7 @@
**用户体验:** **用户体验:**
```bash ```bash
# 用户连接(零配置) # 用户连接(零配置)
ssh -p 2222 chat.m1ng.space ssh -p 2222 your.server.ip
# 输入任意内容或直接按回车 # 输入任意内容或直接按回车
# 开始聊天! # 开始聊天!
``` ```
@ -143,7 +143,7 @@ ssh -p 2222 chat.m1ng.space
tnt tnt
# 用户端(任何人) # 用户端(任何人)
ssh -p 2222 chat.m1ng.space ssh -p 2222 server.ip
# 输入任何内容作为密码或直接回车 # 输入任何内容作为密码或直接回车
# 选择显示名称(可留空) # 选择显示名称(可留空)
# 开始聊天! # 开始聊天!
@ -164,12 +164,9 @@ TNT_ACCESS_TOKEN="secret" tnt
# 限制连接数 # 限制连接数
TNT_MAX_CONNECTIONS=100 tnt TNT_MAX_CONNECTIONS=100 tnt
# Limit concurrent sessions per IP # 限制每IP连接数
TNT_MAX_CONN_PER_IP=10 tnt TNT_MAX_CONN_PER_IP=10 tnt
# Limit new connections per IP per 60 seconds
TNT_MAX_CONN_RATE_PER_IP=30 tnt
# 只允许本地访问 # 只允许本地访问
TNT_BIND_ADDR=127.0.0.1 tnt TNT_BIND_ADDR=127.0.0.1 tnt
``` ```

View file

@ -1,24 +1,5 @@
# Changelog # Changelog
## 2026-03-10 - SSH Runtime & Unix Interface Update
### Fixed
- moved SSH handshake/auth/channel setup out of the main accept loop
- replaced synchronous room-wide fan-out with room update sequencing and per-client refresh
- switched idle session handling to `ssh_channel_poll_timeout()` plus blocking reads so quiet sessions are not dropped incorrectly
- made `-d/--state-dir` create the runtime state directory automatically
### Added
- SSH exec commands: `help`, `health`, `users`, `stats --json`, `tail`, `post`
- PTY window-change handling for terminal resize
- `TNT_MAX_CONN_RATE_PER_IP` for per-IP connection-rate control
- `tests/test_exec_mode.sh`
- `tests/test_connection_limits.sh`
### Changed
- `TNT_MAX_CONN_PER_IP` now means concurrent sessions per IP
- stress tests now disable rate-based blocking so they exercise concurrency instead of self-throttling
## 2026-01-22 - Security Audit Fixes ## 2026-01-22 - Security Audit Fixes
Comprehensive security hardening addressing 23 identified vulnerabilities across 6 categories. Comprehensive security hardening addressing 23 identified vulnerabilities across 6 categories.

View file

@ -33,6 +33,8 @@ sudo mv tnt-darwin-arm64 /usr/local/bin/tnt
1. Create user and directory: 1. Create user and directory:
```bash ```bash
sudo useradd -r -s /bin/false tnt sudo useradd -r -s /bin/false tnt
sudo mkdir -p /var/lib/tnt
sudo chown tnt:tnt /var/lib/tnt
``` ```
2. Install service file: 2. Install service file:
@ -43,24 +45,7 @@ sudo systemctl enable tnt
sudo systemctl start tnt sudo systemctl start tnt
``` ```
3. Optional runtime overrides: 3. Check status:
```bash
sudo tee /etc/default/tnt >/dev/null <<'EOF'
PORT=2222
TNT_BIND_ADDR=0.0.0.0
TNT_STATE_DIR=/var/lib/tnt
TNT_MAX_CONNECTIONS=200
TNT_MAX_CONN_PER_IP=30
TNT_MAX_CONN_RATE_PER_IP=60
TNT_RATE_LIMIT=1
TNT_SSH_LOG_LEVEL=0
TNT_PUBLIC_HOST=chat.m1ng.space
EOF
sudo systemctl restart tnt
```
4. Check status:
```bash ```bash
sudo systemctl status tnt sudo systemctl status tnt
sudo journalctl -u tnt -f sudo journalctl -u tnt -f
@ -79,16 +64,6 @@ Environment="PORT=3333"
sudo systemctl restart tnt sudo systemctl restart tnt
``` ```
The service uses `StateDirectory=tnt`, so systemd creates `/var/lib/tnt` automatically.
Use `TNT_STATE_DIR` or `tnt -d DIR` when running outside systemd to avoid depending on the current working directory.
Recommended interpretation:
- `TNT_MAX_CONNECTIONS`: global connection ceiling
- `TNT_MAX_CONN_PER_IP`: concurrent sessions allowed from one IP
- `TNT_MAX_CONN_RATE_PER_IP`: new connection attempts allowed per IP per 60 seconds
- `TNT_RATE_LIMIT=0`: disables rate-based blocking and auth-failure IP blocking, but not the explicit capacity limits
## Firewall ## Firewall
```bash ```bash

View file

@ -25,7 +25,7 @@ tnt # 监听 2222 端口
用户只需要一个SSH客户端即可无需任何配置 用户只需要一个SSH客户端即可无需任何配置
```bash ```bash
ssh -p 2222 chat.m1ng.space ssh -p 2222 your.server.ip
``` ```
**重要提示** **重要提示**
@ -125,7 +125,7 @@ That's it! Your server is now running.
Users only need an SSH client, no configuration required: Users only need an SSH client, no configuration required:
```bash ```bash
ssh -p 2222 chat.m1ng.space ssh -p 2222 your.server.ip
``` ```
**Important**: **Important**:
@ -181,12 +181,9 @@ PORT=3333 tnt
# Limit max connections # Limit max connections
TNT_MAX_CONNECTIONS=100 tnt TNT_MAX_CONNECTIONS=100 tnt
# Limit concurrent sessions per IP # Limit connections per IP
TNT_MAX_CONN_PER_IP=10 tnt TNT_MAX_CONN_PER_IP=10 tnt
# Limit new connections per IP per 60 seconds
TNT_MAX_CONN_RATE_PER_IP=30 tnt
# Bind to localhost only # Bind to localhost only
TNT_BIND_ADDR=127.0.0.1 tnt TNT_BIND_ADDR=127.0.0.1 tnt
@ -216,7 +213,7 @@ TNT_ACCESS_TOKEN="your_secret_password" tnt
tnt tnt
# 用户连接(从任何机器) # 用户连接(从任何机器)
ssh -p 2222 chat.m1ng.space ssh -p 2222 chat.example.com
# 输入任意密码或直接回车 # 输入任意密码或直接回车
# 输入显示名称或留空 # 输入显示名称或留空
# 开始聊天! # 开始聊天!

View file

@ -59,10 +59,10 @@ Branch 4: fix/resource-management (Medium Priority)
Branch 5: fix/auth-protection (Critical Priority) Branch 5: fix/auth-protection (Critical Priority)
-------------------------------------------------- --------------------------------------------------
✅ Add optional access token (TNT_ACCESS_TOKEN) ✅ Add optional access token (TNT_ACCESS_TOKEN)
✅ IP-based connection-rate limiting (10 new conn/IP/60s) ✅ IP-based rate limiting (10 conn/IP/60s)
✅ Auth failure tracking (5 failures → 5 min block) ✅ Auth failure tracking (5 failures → 5 min block)
✅ Connection counting (total, per-IP active sessions, per-IP recent attempts) ✅ Connection counting (total and per-IP)
✅ Configurable limits (TNT_MAX_CONNECTIONS, TNT_MAX_CONN_PER_IP, TNT_MAX_CONN_RATE_PER_IP) ✅ Configurable limits (TNT_MAX_CONNECTIONS, TNT_MAX_CONN_PER_IP)
✅ Rate limit toggle (TNT_RATE_LIMIT) ✅ Rate limit toggle (TNT_RATE_LIMIT)
Branch 6: fix/concurrency-safety (High Priority) Branch 6: fix/concurrency-safety (High Priority)
@ -84,8 +84,7 @@ TNT_BIND_ADDR - Configurable bind address (default: 0.0.0.0)
TNT_SSH_LOG_LEVEL - SSH logging verbosity 0-4 (default: 1) TNT_SSH_LOG_LEVEL - SSH logging verbosity 0-4 (default: 1)
TNT_RATE_LIMIT - Enable/disable rate limiting (default: 1) TNT_RATE_LIMIT - Enable/disable rate limiting (default: 1)
TNT_MAX_CONNECTIONS - Global connection limit (default: 64) TNT_MAX_CONNECTIONS - Global connection limit (default: 64)
TNT_MAX_CONN_PER_IP - Concurrent sessions allowed per IP (default: 5) TNT_MAX_CONN_PER_IP - Per-IP connection limit (default: 5)
TNT_MAX_CONN_RATE_PER_IP - New connections allowed per IP per 60s (default: 10)
Security Enhancements: Security Enhancements:
--------------------- ---------------------

View file

@ -1,99 +0,0 @@
# Roadmap
TNT is moving toward a durable Unix-style utility: a small, predictable tool with a stable interface, explicit configuration, scriptable output, and operationally simple deployment.
This roadmap is intentionally strict. Each stage should leave the project easier to reason about, easier to automate, and safer to operate.
## Design Principles
- Keep the default path simple: install, run, connect.
- Treat non-interactive interfaces as first-class, not as an afterthought to the TUI.
- Prefer explicit flags, stable exit codes, and machine-readable output over implicit behavior.
- Keep daemon concerns separate from control-plane concerns.
- Make failure modes observable and testable.
- Preserve the Vim-style interactive experience without coupling it to core server semantics.
## Stage 1: Interface Contract
Goal: make TNT predictable for operators, scripts, and package maintainers.
- split the current surface into `tntd` (daemon) and `tntctl` (control client)
- keep SSH exec support, but treat it as a transport for stable commands rather than the primary API shape
- define stable subcommands and exit codes for:
- `health`
- `stats`
- `users`
- `tail`
- `post`
- support text and JSON output modes where machine use is likely
- normalize command parsing, help text, and error reporting
- add `--bind`, `--port`, `--state-dir`, `--public-host`, `--max-clients`, and related long options consistently
- add a man page for `tntd` and `tntctl`
## Stage 2: Runtime Model
Goal: make long-running operation boring and reliable.
- move client state to a clearer ownership model with one release path
- finish replacing ad hoc cross-thread UI mutation with per-client event delivery
- add bounded outbound queues so slow clients cannot stall other users
- separate accept, session bootstrap, interactive I/O, and persistence concerns more cleanly
- make room/client capacity fully runtime-configurable with no hidden compile-time ceiling
- document hard guarantees and soft limits
## Stage 3: Data and Persistence
Goal: make stored history durable, inspectable, and recoverable.
- formalize the message log format and version it
- keep timestamps in a timezone-safe format throughout write and replay
- validate persisted UTF-8 and record structure before replay
- add log rotation and compaction tooling
- provide an offline inspection/export command
- define recovery behavior for truncated or partially corrupted logs
## Stage 4: Interactive UX
Goal: keep the interface efficient for terminal users without sacrificing simplicity.
- keep the current modal editing model, but make its behavior precise and documented
- support resize, cursor movement, command history, and predictable paste behavior
- add useful chat commands with clear semantics:
- `/nick`
- `/me`
- `/last N`
- `/search`
- `/mute-joins`
- improve discoverability of NORMAL and COMMAND mode actions
- make status lines and help output concise enough for small terminals
## Stage 5: Operations and Security
Goal: make public deployment manageable.
- provide clear distinction between concurrent session limits and connection-rate limits
- add admin-only controls for read-only mode, mute, and ban
- expose a minimal health and stats surface suitable for monitoring
- support systemd-friendly readiness and watchdog behavior
- document recommended production defaults for public, private, and localhost-only deployments
- tighten CI around authentication, limits, and restart behavior
## Stage 6: Release Quality
Goal: make regressions harder to introduce.
- expand CI coverage across Linux and macOS for build and smoke tests
- add sanitizer jobs and targeted fuzzing for UTF-8, log parsing, and command parsing
- add soak tests for long-lived sessions and slow-client behavior
- keep deployment and test docs aligned with actual runtime behavior
- require every user-visible interface change to update docs and tests in the same change set
## Immediate Next Tasks
These are the next changes that should happen before new feature work expands the surface area.
1. Introduce `tntctl` and move stable command handling behind it.
2. Define exit codes and JSON schemas for `health`, `stats`, `users`, `tail`, and `post`.
3. Add per-client outbound queues and finish untangling client-state ownership.
4. Remove the remaining hidden runtime limits and make them explicit configuration.
5. Add a long-running soak test that exercises idle sessions, reconnects, and slow consumers.

View file

@ -26,8 +26,7 @@ Connect: `sshpass -p "YourSecretPassword" ssh -p 2222 localhost`
| `TNT_SSH_LOG_LEVEL` | `1` | SSH logging (0-4) | `TNT_SSH_LOG_LEVEL=3` | | `TNT_SSH_LOG_LEVEL` | `1` | SSH logging (0-4) | `TNT_SSH_LOG_LEVEL=3` |
| `TNT_RATE_LIMIT` | `1` | Rate limiting on/off | `TNT_RATE_LIMIT=0` | | `TNT_RATE_LIMIT` | `1` | Rate limiting on/off | `TNT_RATE_LIMIT=0` |
| `TNT_MAX_CONNECTIONS` | `64` | Total connection limit | `TNT_MAX_CONNECTIONS=100` | | `TNT_MAX_CONNECTIONS` | `64` | Total connection limit | `TNT_MAX_CONNECTIONS=100` |
| `TNT_MAX_CONN_PER_IP` | `5` | Concurrent sessions per IP | `TNT_MAX_CONN_PER_IP=3` | | `TNT_MAX_CONN_PER_IP` | `5` | Per-IP limit | `TNT_MAX_CONN_PER_IP=3` |
| `TNT_MAX_CONN_RATE_PER_IP` | `10` | New connections per IP per 60s | `TNT_MAX_CONN_RATE_PER_IP=20` |
--- ---
@ -76,8 +75,7 @@ TNT_MAX_CONN_PER_IP=2 \
## Rate Limiting ## Rate Limiting
### Defaults ### Defaults
- **Concurrent Sessions:** 5 per IP - **Connection Rate:** 10 connections per IP per 60 seconds
- **Connection Rate:** 10 new connections per IP per 60 seconds
- **Auth Failures:** 5 failures → 5 minute IP block - **Auth Failures:** 5 failures → 5 minute IP block
- **Window:** 60 second rolling window - **Window:** 60 second rolling window
@ -112,12 +110,6 @@ TNT_MAX_CONN_PER_IP=3 ./tnt
``` ```
Each IP can have max 3 concurrent connections. Each IP can have max 3 concurrent connections.
### Per-IP Rate Limit
```bash
TNT_MAX_CONN_RATE_PER_IP=20 ./tnt
```
Each IP can open at most 20 new connections per 60 seconds before being temporarily blocked.
### Combined Example ### Combined Example
```bash ```bash
TNT_MAX_CONNECTIONS=100 TNT_MAX_CONN_PER_IP=10 ./tnt TNT_MAX_CONNECTIONS=100 TNT_MAX_CONN_PER_IP=10 ./tnt

View file

@ -75,8 +75,8 @@
| **Crypto** | RSA Key Size | 4096-bit (upgraded from 2048) | ✅ | | **Crypto** | RSA Key Size | 4096-bit (upgraded from 2048) | ✅ |
| **Crypto** | Key Permissions | Atomic generation with 0600 perms | ✅ | | **Crypto** | Key Permissions | Atomic generation with 0600 perms | ✅ |
| **Auth** | Access Token | Optional password protection | ✅ | | **Auth** | Access Token | Optional password protection | ✅ |
| **Auth** | Rate Limiting | Per-IP connection-rate throttling | ✅ | | **Auth** | Rate Limiting | IP-based connection throttling | ✅ |
| **Auth** | Connection Limits | Global and per-IP concurrent session limits | ✅ | | **Auth** | Connection Limits | Global and per-IP limits | ✅ |
| **Input** | Username Validation | Shell metacharacter rejection | ✅ | | **Input** | Username Validation | Shell metacharacter rejection | ✅ |
| **Input** | Log Sanitization | Pipe/newline replacement | ✅ | | **Input** | Log Sanitization | Pipe/newline replacement | ✅ |
| **Input** | UTF-8 Validation | Overlong encoding prevention | ✅ | | **Input** | UTF-8 Validation | Overlong encoding prevention | ✅ |
@ -114,10 +114,9 @@ TNT_BIND_ADDR=127.0.0.1 ./tnt
### Strict Limits ### Strict Limits
```bash ```bash
TNT_MAX_CONNECTIONS=10 TNT_MAX_CONN_PER_IP=2 TNT_MAX_CONN_RATE_PER_IP=10 ./tnt TNT_MAX_CONNECTIONS=10 TNT_MAX_CONN_PER_IP=2 ./tnt
# Max 10 total connections # Max 10 total connections
# Max 2 concurrent sessions per IP address # Max 2 connections per IP address
# Max 10 new connections per IP per 60 seconds
``` ```
### Disabled Rate Limiting (Testing) ### Disabled Rate Limiting (Testing)
@ -156,7 +155,7 @@ gcc -fsanitize=thread -g -O1 -c src/chat_room.c
## Known Limitations ## Known Limitations
1. **Exec Surface Is Minimal:** The SSH exec interface is intentionally small and currently focused on operational commands 1. **Interactive Only:** Server requires PTY sessions (no command execution via SSH)
2. **libssh Deprecations:** Uses deprecated PTY width/height functions (4 warnings) 2. **libssh Deprecations:** Uses deprecated PTY width/height functions (4 warnings)
3. **UTF-8 Unit Test:** Skipped in automated tests (requires manual compilation) 3. **UTF-8 Unit Test:** Skipped in automated tests (requires manual compilation)
@ -166,7 +165,7 @@ gcc -fsanitize=thread -g -O1 -c src/chat_room.c
✅ **All 23 security vulnerabilities fixed and verified** ✅ **All 23 security vulnerabilities fixed and verified**
**100% security-suite pass rate** (12/12 tests) **100% test pass rate** (10/10 tests)
**Backward compatible** - server remains open by default **Backward compatible** - server remains open by default

View file

@ -15,7 +15,6 @@ typedef struct {
int client_capacity; int client_capacity;
message_t *messages; message_t *messages;
int message_count; int message_count;
uint64_t update_seq;
} chat_room_t; } chat_room_t;
/* Global chat room instance */ /* Global chat room instance */
@ -48,7 +47,4 @@ int room_get_message_count(chat_room_t *room);
/* Get online client count */ /* Get online client count */
int room_get_client_count(chat_room_t *room); int room_get_client_count(chat_room_t *room);
/* Get room update sequence */
uint64_t room_get_update_seq(chat_room_t *room);
#endif /* CHAT_ROOM_H */ #endif /* CHAT_ROOM_H */

View file

@ -7,7 +7,6 @@
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <time.h> #include <time.h>
#include <limits.h>
#include <pthread.h> #include <pthread.h>
/* Project Metadata */ /* Project Metadata */
@ -18,11 +17,9 @@
#define MAX_MESSAGES 100 #define MAX_MESSAGES 100
#define MAX_USERNAME_LEN 64 #define MAX_USERNAME_LEN 64
#define MAX_MESSAGE_LEN 1024 #define MAX_MESSAGE_LEN 1024
#define MAX_EXEC_COMMAND_LEN 1024
#define MAX_CLIENTS 64 #define MAX_CLIENTS 64
#define LOG_FILE "messages.log" #define LOG_FILE "messages.log"
#define HOST_KEY_FILE "host_key" #define HOST_KEY_FILE "host_key"
#define TNT_DEFAULT_STATE_DIR "."
/* ANSI color codes */ /* ANSI color codes */
#define ANSI_RESET "\033[0m" #define ANSI_RESET "\033[0m"
@ -46,9 +43,4 @@ typedef enum {
LANG_ZH LANG_ZH
} help_lang_t; } help_lang_t;
/* Runtime helpers */
const char* tnt_state_dir(void);
int tnt_ensure_state_dir(void);
int tnt_state_path(char *buffer, size_t buf_size, const char *filename);
#endif /* COMMON_H */ #endif /* COMMON_H */

View file

@ -3,7 +3,6 @@
#include "common.h" #include "common.h"
#include "chat_room.h" #include "chat_room.h"
#include <arpa/inet.h>
#include <libssh/libssh.h> #include <libssh/libssh.h>
#include <libssh/server.h> #include <libssh/server.h>
@ -13,7 +12,6 @@ typedef struct client {
ssh_session session; /* SSH session */ ssh_session session; /* SSH session */
ssh_channel channel; /* SSH channel */ ssh_channel channel; /* SSH channel */
char username[MAX_USERNAME_LEN]; char username[MAX_USERNAME_LEN];
char client_ip[INET6_ADDRSTRLEN];
int width; int width;
int height; int height;
client_mode_t mode; client_mode_t mode;
@ -23,15 +21,11 @@ typedef struct client {
bool show_help; bool show_help;
char command_input[256]; char command_input[256];
char command_output[2048]; char command_output[2048];
char exec_command[MAX_EXEC_COMMAND_LEN]; char exec_command[256];
char ssh_login[MAX_USERNAME_LEN];
bool redraw_pending;
pthread_t thread; pthread_t thread;
bool connected; bool connected;
int ref_count; /* Reference count for safe cleanup */ int ref_count; /* Reference count for safe cleanup */
pthread_mutex_t ref_lock; /* Lock for ref_count */ pthread_mutex_t ref_lock; /* Lock for ref_count */
pthread_mutex_t io_lock; /* Serialize SSH channel writes */
struct ssh_channel_callbacks_struct *channel_cb;
} client_t; } client_t;
/* Initialize SSH server */ /* Initialize SSH server */
@ -49,8 +43,4 @@ int client_send(client_t *client, const char *data, size_t len);
/* Send formatted string to client */ /* Send formatted string to client */
int client_printf(client_t *client, const char *fmt, ...); int client_printf(client_t *client, const char *fmt, ...);
/* Reference counting helpers */
void client_addref(client_t *client);
void client_release(client_t *client);
#endif /* SSH_SERVER_H */ #endif /* SSH_SERVER_H */

View file

@ -30,7 +30,4 @@ void utf8_remove_last_word(char *str);
/* Validate a UTF-8 byte sequence */ /* Validate a UTF-8 byte sequence */
bool utf8_is_valid_sequence(const char *bytes, int len); bool utf8_is_valid_sequence(const char *bytes, int len);
/* Validate an entire NUL-terminated UTF-8 string */
bool utf8_is_valid_string(const char *str);
#endif /* UTF8_H */ #endif /* UTF8_H */

View file

@ -1,23 +1,11 @@
#include "chat_room.h" #include "chat_room.h"
#include "ssh_server.h"
#include "tui.h"
#include <unistd.h>
/* Global chat room instance */ /* Global chat room instance */
chat_room_t *g_room = NULL; chat_room_t *g_room = NULL;
static int room_capacity_from_env(void) {
const char *env = getenv("TNT_MAX_CONNECTIONS");
if (!env || env[0] == '\0') {
return MAX_CLIENTS;
}
int capacity = atoi(env);
if (capacity < 1 || capacity > 1024) {
return MAX_CLIENTS;
}
return capacity;
}
/* Initialize chat room */ /* Initialize chat room */
chat_room_t* room_create(void) { chat_room_t* room_create(void) {
chat_room_t *room = calloc(1, sizeof(chat_room_t)); chat_room_t *room = calloc(1, sizeof(chat_room_t));
@ -25,8 +13,8 @@ chat_room_t* room_create(void) {
pthread_rwlock_init(&room->lock, NULL); pthread_rwlock_init(&room->lock, NULL);
room->client_capacity = room_capacity_from_env(); room->client_capacity = MAX_CLIENTS;
room->clients = calloc(room->client_capacity, sizeof(struct client *)); room->clients = calloc(room->client_capacity, sizeof(client_t*));
if (!room->clients) { if (!room->clients) {
free(room); free(room);
return NULL; return NULL;
@ -54,7 +42,7 @@ void room_destroy(chat_room_t *room) {
} }
/* Add client to room */ /* Add client to room */
int room_add_client(chat_room_t *room, struct client *client) { int room_add_client(chat_room_t *room, client_t *client) {
pthread_rwlock_wrlock(&room->lock); pthread_rwlock_wrlock(&room->lock);
if (room->client_count >= room->client_capacity) { if (room->client_count >= room->client_capacity) {
@ -69,7 +57,7 @@ int room_add_client(chat_room_t *room, struct client *client) {
} }
/* Remove client from room */ /* Remove client from room */
void room_remove_client(chat_room_t *room, struct client *client) { void room_remove_client(chat_room_t *room, client_t *client) {
pthread_rwlock_wrlock(&room->lock); pthread_rwlock_wrlock(&room->lock);
for (int i = 0; i < room->client_count; i++) { for (int i = 0; i < room->client_count; i++) {
@ -92,9 +80,69 @@ void room_broadcast(chat_room_t *room, const message_t *msg) {
/* Add to history */ /* Add to history */
room_add_message(room, msg); room_add_message(room, msg);
room->update_seq++;
/* Get copy of client list and increment ref counts */
int count = room->client_count;
if (count == 0) {
pthread_rwlock_unlock(&room->lock);
return;
}
client_t **clients_copy = calloc(count, sizeof(client_t*));
if (!clients_copy) {
pthread_rwlock_unlock(&room->lock);
return;
}
memcpy(clients_copy, room->clients, count * sizeof(client_t*));
/* Increment reference count for each client */
for (int i = 0; i < count; i++) {
pthread_mutex_lock(&clients_copy[i]->ref_lock);
clients_copy[i]->ref_count++;
pthread_mutex_unlock(&clients_copy[i]->ref_lock);
}
pthread_rwlock_unlock(&room->lock); pthread_rwlock_unlock(&room->lock);
/* Render to each client (outside of lock) */
for (int i = 0; i < count; i++) {
client_t *client = clients_copy[i];
/* Check client state before rendering (while holding ref) */
bool should_render = false;
pthread_mutex_lock(&client->ref_lock);
if (client->ref_count > 0) {
should_render = client->connected &&
!client->show_help &&
client->command_output[0] == '\0';
}
pthread_mutex_unlock(&client->ref_lock);
if (should_render) {
tui_render_screen(client);
}
/* Decrement reference count and free if needed */
pthread_mutex_lock(&client->ref_lock);
client->ref_count--;
int ref = client->ref_count;
pthread_mutex_unlock(&client->ref_lock);
if (ref == 0) {
/* Safe to free now */
if (client->channel) {
ssh_channel_close(client->channel);
ssh_channel_free(client->channel);
}
if (client->session) {
ssh_disconnect(client->session);
ssh_free(client->session);
}
pthread_mutex_destroy(&client->ref_lock);
free(client);
}
}
free(clients_copy);
} }
/* Add message to room history */ /* Add message to room history */
@ -139,13 +187,3 @@ int room_get_client_count(chat_room_t *room) {
pthread_rwlock_unlock(&room->lock); pthread_rwlock_unlock(&room->lock);
return count; return count;
} }
uint64_t room_get_update_seq(chat_room_t *room) {
uint64_t seq;
pthread_rwlock_rdlock(&room->lock);
seq = room->update_seq;
pthread_rwlock_unlock(&room->lock);
return seq;
}

View file

@ -1,86 +0,0 @@
#include "common.h"
#include <errno.h>
#include <sys/stat.h>
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
const char* tnt_state_dir(void) {
const char *dir = getenv("TNT_STATE_DIR");
if (!dir || dir[0] == '\0') {
return TNT_DEFAULT_STATE_DIR;
}
return dir;
}
int tnt_ensure_state_dir(void) {
const char *dir = tnt_state_dir();
char path[PATH_MAX];
struct stat st;
size_t len;
if (!dir || dir[0] == '\0') {
return -1;
}
if (strcmp(dir, ".") == 0 || strcmp(dir, "/") == 0) {
return 0;
}
if (snprintf(path, sizeof(path), "%s", dir) >= (int)sizeof(path)) {
return -1;
}
len = strlen(path);
while (len > 1 && path[len - 1] == '/') {
path[len - 1] = '\0';
len--;
}
for (char *p = path + 1; *p; p++) {
if (*p != '/') {
continue;
}
*p = '\0';
if (mkdir(path, 0700) < 0 && errno != EEXIST) {
return -1;
}
*p = '/';
}
if (mkdir(path, 0700) < 0 && errno != EEXIST) {
return -1;
}
if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode)) {
return -1;
}
return 0;
}
int tnt_state_path(char *buffer, size_t buf_size, const char *filename) {
const char *dir;
int written;
if (!buffer || buf_size == 0 || !filename || filename[0] == '\0') {
return -1;
}
dir = tnt_state_dir();
if (strcmp(dir, "/") == 0) {
written = snprintf(buffer, buf_size, "/%s", filename);
} else {
written = snprintf(buffer, buf_size, "%s/%s", dir, filename);
}
if (written < 0 || (size_t)written >= buf_size) {
return -1;
}
return 0;
}

View file

@ -11,54 +11,40 @@
static void signal_handler(int sig) { static void signal_handler(int sig) {
(void)sig; (void)sig;
static const char msg[] = "\nShutting down...\n"; static const char msg[] = "\nShutting down...\n";
ssize_t ignored = write(STDERR_FILENO, msg, sizeof(msg) - 1); (void)write(STDERR_FILENO, msg, sizeof(msg) - 1);
(void)ignored;
_exit(0); _exit(0);
} }
int main(int argc, char **argv) { int main(int argc, char **argv) {
int port = DEFAULT_PORT; int port = DEFAULT_PORT;
/* Environment provides defaults; command-line flags override it. */
const char *port_env = getenv("PORT");
if (port_env) {
port = atoi(port_env);
}
/* 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 && i + 1 < argc) { if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
port = atoi(argv[i + 1]); port = atoi(argv[i + 1]);
i++; i++;
} else if ((strcmp(argv[i], "-d") == 0 ||
strcmp(argv[i], "--state-dir") == 0) && i + 1 < argc) {
if (setenv("TNT_STATE_DIR", argv[i + 1], 1) != 0) {
perror("setenv TNT_STATE_DIR");
return 1;
}
i++;
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
printf("TNT - Terminal Network Talk\n"); printf("TNT - Terminal Network Talk\n");
printf("Usage: %s [options]\n", argv[0]); printf("Usage: %s [options]\n", argv[0]);
printf("Options:\n"); printf("Options:\n");
printf(" -p PORT Listen on PORT (default: %d)\n", DEFAULT_PORT); printf(" -p PORT Listen on PORT (default: %d)\n", DEFAULT_PORT);
printf(" -d DIR Store host key and logs in DIR\n");
printf(" -h Show this help\n"); printf(" -h Show this help\n");
return 0; return 0;
} }
} }
/* Check environment variable for port */
const char *port_env = getenv("PORT");
if (port_env) {
port = atoi(port_env);
}
/* Setup signal handlers */ /* Setup signal handlers */
signal(SIGINT, signal_handler); signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler); signal(SIGTERM, signal_handler);
signal(SIGPIPE, SIG_IGN); signal(SIGPIPE, SIG_IGN);
/* Initialize subsystems */ /* Initialize subsystems */
if (tnt_ensure_state_dir() < 0) {
fprintf(stderr, "Failed to create state directory: %s\n", tnt_state_dir());
return 1;
}
message_init(); message_init();
/* Create chat room */ /* Create chat room */

View file

@ -3,50 +3,6 @@
#include <unistd.h> #include <unistd.h>
#include <fcntl.h> #include <fcntl.h>
static pthread_mutex_t g_message_file_lock = PTHREAD_MUTEX_INITIALIZER;
static time_t parse_rfc3339_utc(const char *timestamp_str) {
struct tm tm = {0};
char *result;
char *old_tz = NULL;
time_t parsed;
if (!timestamp_str) {
return (time_t)-1;
}
result = strptime(timestamp_str, "%Y-%m-%dT%H:%M:%SZ", &tm);
if (!result || *result != '\0') {
return (time_t)-1;
}
const char *tz = getenv("TZ");
if (tz) {
old_tz = strdup(tz);
if (!old_tz) {
return (time_t)-1;
}
}
if (setenv("TZ", "UTC0", 1) != 0) {
free(old_tz);
return (time_t)-1;
}
tzset();
parsed = mktime(&tm);
if (old_tz) {
setenv("TZ", old_tz, 1);
free(old_tz);
} else {
unsetenv("TZ");
}
tzset();
return parsed;
}
/* Initialize message subsystem */ /* Initialize message subsystem */
void message_init(void) { void message_init(void) {
/* Nothing to initialize for now */ /* Nothing to initialize for now */
@ -54,20 +10,13 @@ void message_init(void) {
/* Load messages from log file - Optimized for large files */ /* Load messages from log file - Optimized for large files */
int message_load(message_t **messages, int max_messages) { int message_load(message_t **messages, int max_messages) {
char log_path[PATH_MAX];
/* Always allocate the message array */ /* Always allocate the message array */
message_t *msg_array = calloc(max_messages, sizeof(message_t)); message_t *msg_array = calloc(max_messages, sizeof(message_t));
if (!msg_array) { if (!msg_array) {
return 0; return 0;
} }
if (tnt_state_path(log_path, sizeof(log_path), LOG_FILE) < 0) { FILE *fp = fopen(LOG_FILE, "r");
*messages = msg_array;
return 0;
}
FILE *fp = fopen(log_path, "r");
if (!fp) { if (!fp) {
/* File doesn't exist yet, no messages */ /* File doesn't exist yet, no messages */
*messages = msg_array; *messages = msg_array;
@ -168,17 +117,15 @@ read_messages:;
continue; continue;
} }
if (!utf8_is_valid_string(username) || !utf8_is_valid_string(content)) { /* Parse ISO 8601 timestamp */
continue; struct tm tm = {0};
} char *result = strptime(timestamp_str, "%Y-%m-%dT%H:%M:%S", &tm);
if (!result) {
/* Parse strict UTC RFC3339 timestamp */
time_t msg_time = parse_rfc3339_utc(timestamp_str);
if (msg_time == (time_t)-1) {
continue; continue;
} }
/* Validate timestamp is reasonable (not in far future or past) */ /* Validate timestamp is reasonable (not in far future or past) */
time_t msg_time = mktime(&tm);
time_t now = time(NULL); time_t now = time(NULL);
if (msg_time > now + 86400 || msg_time < now - 31536000 * 10) { if (msg_time > now + 86400 || msg_time < now - 31536000 * 10) {
continue; continue;
@ -199,18 +146,8 @@ read_messages:;
/* Save a message to log file */ /* Save a message to log file */
int message_save(const message_t *msg) { int message_save(const message_t *msg) {
char log_path[PATH_MAX]; FILE *fp = fopen(LOG_FILE, "a");
int rc = 0;
if (tnt_state_path(log_path, sizeof(log_path), LOG_FILE) < 0) {
return -1;
}
pthread_mutex_lock(&g_message_file_lock);
FILE *fp = fopen(log_path, "a");
if (!fp) { if (!fp) {
pthread_mutex_unlock(&g_message_file_lock);
return -1; return -1;
} }
@ -243,14 +180,10 @@ int message_save(const message_t *msg) {
} }
/* Write to file: timestamp|username|content */ /* Write to file: timestamp|username|content */
if (fprintf(fp, "%s|%s|%s\n", timestamp, safe_username, safe_content) < 0 || fprintf(fp, "%s|%s|%s\n", timestamp, safe_username, safe_content);
fflush(fp) != 0) {
rc = -1;
}
fclose(fp); fclose(fp);
pthread_mutex_unlock(&g_message_file_lock); return 0;
return rc;
} }
/* Format a message for display */ /* Format a message for display */

File diff suppressed because it is too large Load diff

113
src/tui.c
View file

@ -5,46 +5,6 @@
#include <stdarg.h> #include <stdarg.h>
#include <unistd.h> #include <unistd.h>
static void buffer_append_bytes(char *buffer, size_t buf_size, size_t *pos,
const char *data, size_t len) {
size_t available;
size_t to_copy;
if (!buffer || !pos || !data || len == 0 || buf_size == 0 || *pos >= buf_size - 1) {
return;
}
available = (buf_size - 1) - *pos;
to_copy = (len < available) ? len : available;
memcpy(buffer + *pos, data, to_copy);
*pos += to_copy;
buffer[*pos] = '\0';
}
static void buffer_appendf(char *buffer, size_t buf_size, size_t *pos,
const char *fmt, ...) {
va_list args;
int written;
if (!buffer || !pos || !fmt || buf_size == 0 || *pos >= buf_size - 1) {
return;
}
va_start(args, fmt);
written = vsnprintf(buffer + *pos, buf_size - *pos, fmt, args);
va_end(args);
if (written < 0) {
return;
}
if ((size_t)written >= buf_size - *pos) {
*pos = buf_size - 1;
} else {
*pos += (size_t)written;
}
}
/* Clear the screen */ /* Clear the screen */
void tui_clear_screen(client_t *client) { void tui_clear_screen(client_t *client) {
if (!client || !client->connected) return; if (!client || !client->connected) return;
@ -61,8 +21,7 @@ void tui_render_screen(client_t *client) {
const size_t buf_size = 65536; const size_t buf_size = 65536;
char *buffer = malloc(buf_size); char *buffer = malloc(buf_size);
if (!buffer) return; if (!buffer) return;
size_t pos = 0; int pos = 0;
buffer[0] = '\0';
/* Acquire all data in one lock to prevent TOCTOU */ /* Acquire all data in one lock to prevent TOCTOU */
pthread_rwlock_rdlock(&g_room->lock); pthread_rwlock_rdlock(&g_room->lock);
@ -107,7 +66,7 @@ void tui_render_screen(client_t *client) {
/* Now render using snapshot (no lock held) */ /* Now render using snapshot (no lock held) */
/* Move to top (Home) - Do NOT clear screen to prevent flicker */ /* Move to top (Home) - Do NOT clear screen to prevent flicker */
buffer_appendf(buffer, buf_size, &pos, ANSI_HOME); pos += snprintf(buffer + pos, buf_size - pos, ANSI_HOME);
/* Title bar */ /* Title bar */
const char *mode_str = (client->mode == MODE_INSERT) ? "INSERT" : const char *mode_str = (client->mode == MODE_INSERT) ? "INSERT" :
@ -123,44 +82,48 @@ void tui_render_screen(client_t *client) {
int padding = client->width - title_width; int padding = client->width - title_width;
if (padding < 0) padding = 0; if (padding < 0) padding = 0;
buffer_appendf(buffer, buf_size, &pos, ANSI_REVERSE "%s", title); pos += snprintf(buffer + pos, buf_size - pos, ANSI_REVERSE "%s", title);
for (int i = 0; i < padding; i++) { for (int i = 0; i < padding && pos < (int)buf_size - 4; i++) {
buffer_append_bytes(buffer, buf_size, &pos, " ", 1); buffer[pos++] = ' ';
} }
buffer_appendf(buffer, buf_size, &pos, ANSI_RESET "\033[K\r\n"); pos += snprintf(buffer + pos, buf_size - pos, ANSI_RESET "\033[K\r\n");
/* Render messages from snapshot */ /* Render messages from snapshot */
if (msg_snapshot) { if (msg_snapshot) {
for (int i = 0; i < snapshot_count; i++) { for (int i = 0; i < snapshot_count; i++) {
char msg_line[1024]; char msg_line[1024];
message_format(&msg_snapshot[i], msg_line, sizeof(msg_line), client->width); message_format(&msg_snapshot[i], msg_line, sizeof(msg_line), client->width);
buffer_appendf(buffer, buf_size, &pos, "%s\033[K\r\n", msg_line); pos += snprintf(buffer + pos, buf_size - pos, "%s\033[K\r\n", msg_line);
} }
free(msg_snapshot); free(msg_snapshot);
} }
/* Fill empty lines and clear them */ /* Fill empty lines and clear them */
for (int i = snapshot_count; i < msg_height; i++) { for (int i = snapshot_count; i < msg_height; i++) {
buffer_appendf(buffer, buf_size, &pos, "\033[K\r\n"); pos += snprintf(buffer + pos, buf_size - pos, "\033[K\r\n");
} }
/* Separator - use box drawing character */ /* Separator - use box drawing character */
for (int i = 0; i < client->width; i++) { for (int i = 0; i < client->width && pos < (int)buf_size - 10; i++) {
buffer_append_bytes(buffer, buf_size, &pos, "", strlen("")); const char *line_char = ""; /* U+2500 box drawing, 3 bytes */
int len = strlen(line_char);
memcpy(buffer + pos, line_char, len);
pos += len;
} }
buffer_appendf(buffer, buf_size, &pos, "\033[K\r\n"); pos += snprintf(buffer + pos, buf_size - pos, "\033[K\r\n");
/* Status/Input line */ /* Status/Input line */
if (client->mode == MODE_INSERT) { if (client->mode == MODE_INSERT) {
buffer_appendf(buffer, buf_size, &pos, "> \033[K"); pos += snprintf(buffer + pos, buf_size - pos, "> \033[K");
} else if (client->mode == MODE_NORMAL) { } else if (client->mode == MODE_NORMAL) {
int total = msg_count; int total = msg_count;
int scroll_pos = client->scroll_pos + 1; int scroll_pos = client->scroll_pos + 1;
if (total == 0) scroll_pos = 0; if (total == 0) scroll_pos = 0;
buffer_appendf(buffer, buf_size, &pos, pos += snprintf(buffer + pos, buf_size - pos,
"-- NORMAL -- (%d/%d)\033[K", scroll_pos, total); "-- NORMAL -- (%d/%d)\033[K", scroll_pos, total);
} else if (client->mode == MODE_COMMAND) { } else if (client->mode == MODE_COMMAND) {
buffer_appendf(buffer, buf_size, &pos, ":%s\033[K", client->command_input); pos += snprintf(buffer + pos, buf_size - pos,
":%s\033[K", client->command_input);
} }
client_send(client, buffer, pos); client_send(client, buffer, pos);
@ -207,11 +170,10 @@ void tui_render_command_output(client_t *client) {
if (!client || !client->connected) return; if (!client || !client->connected) return;
char buffer[4096]; char buffer[4096];
size_t pos = 0; int pos = 0;
buffer[0] = '\0';
/* Clear screen */ /* Clear screen */
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_CLEAR ANSI_HOME); pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_CLEAR ANSI_HOME);
/* Title */ /* Title */
const char *title = " COMMAND OUTPUT "; const char *title = " COMMAND OUTPUT ";
@ -219,11 +181,11 @@ void tui_render_command_output(client_t *client) {
int padding = client->width - title_width; int padding = client->width - title_width;
if (padding < 0) padding = 0; if (padding < 0) padding = 0;
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_REVERSE "%s", title); pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_REVERSE "%s", title);
for (int i = 0; i < padding; i++) { for (int i = 0; i < padding; i++) {
buffer_append_bytes(buffer, sizeof(buffer), &pos, " ", 1); buffer[pos++] = ' ';
} }
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_RESET "\r\n"); pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_RESET "\r\n");
/* Command output - use a copy to avoid strtok corruption */ /* Command output - use a copy to avoid strtok corruption */
char output_copy[2048]; char output_copy[2048];
@ -243,7 +205,7 @@ void tui_render_command_output(client_t *client) {
utf8_truncate(truncated, client->width); utf8_truncate(truncated, client->width);
} }
buffer_appendf(buffer, sizeof(buffer), &pos, "%s\r\n", truncated); pos += snprintf(buffer + pos, sizeof(buffer) - pos, "%s\r\n", truncated);
line = strtok(NULL, "\n"); line = strtok(NULL, "\n");
line_count++; line_count++;
} }
@ -353,11 +315,10 @@ void tui_render_help(client_t *client) {
if (!client || !client->connected) return; if (!client || !client->connected) return;
char buffer[8192]; char buffer[8192];
size_t pos = 0; int pos = 0;
buffer[0] = '\0';
/* Clear screen */ /* Clear screen */
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_CLEAR ANSI_HOME); pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_CLEAR ANSI_HOME);
/* Title */ /* Title */
const char *title = " HELP "; const char *title = " HELP ";
@ -365,11 +326,11 @@ void tui_render_help(client_t *client) {
int padding = client->width - title_width; int padding = client->width - title_width;
if (padding < 0) padding = 0; if (padding < 0) padding = 0;
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_REVERSE "%s", title); pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_REVERSE "%s", title);
for (int i = 0; i < padding; i++) { for (int i = 0; i < padding; i++) {
buffer_append_bytes(buffer, sizeof(buffer), &pos, " ", 1); buffer[pos++] = ' ';
} }
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_RESET "\r\n"); pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_RESET "\r\n");
/* Help content */ /* Help content */
const char *help_text = tui_get_help_text(client->help_lang); const char *help_text = tui_get_help_text(client->help_lang);
@ -387,27 +348,27 @@ void tui_render_help(client_t *client) {
} }
int content_height = client->height - 2; int content_height = client->height - 2;
if (content_height < 1) content_height = 1;
int max_scroll = line_count - content_height + 1;
if (max_scroll < 0) max_scroll = 0;
int start = client->help_scroll_pos; int start = client->help_scroll_pos;
if (start > max_scroll) start = max_scroll;
int end = start + content_height - 1; int end = start + content_height - 1;
if (end > line_count) end = line_count; if (end > line_count) end = line_count;
for (int i = start; i < end && (i - start) < content_height - 1; i++) { for (int i = start; i < end && (i - start) < content_height - 1; i++) {
buffer_appendf(buffer, sizeof(buffer), &pos, "%s\r\n", lines[i]); pos += snprintf(buffer + pos, sizeof(buffer) - pos, "%s\r\n", lines[i]);
} }
/* Fill remaining lines */ /* Fill remaining lines */
for (int i = end - start; i < content_height - 1; i++) { for (int i = end - start; i < content_height - 1; i++) {
buffer_append_bytes(buffer, sizeof(buffer), &pos, "\r\n", 2); buffer[pos++] = '\r';
buffer[pos++] = '\n';
} }
/* Status line */ /* Status line */
buffer_appendf(buffer, sizeof(buffer), &pos, int max_scroll = line_count - content_height + 1;
if (max_scroll < 0) max_scroll = 0;
pos += snprintf(buffer + pos, sizeof(buffer) - pos,
"-- HELP -- (%d/%d) j/k:scroll g/G:top/bottom e/z:lang q:close", "-- HELP -- (%d/%d) j/k:scroll g/G:top/bottom e/z:lang q:close",
start + 1, max_scroll + 1); client->help_scroll_pos + 1, max_scroll + 1);
client_send(client, buffer, pos); client_send(client, buffer, pos);
} }

View file

@ -15,17 +15,6 @@ uint32_t utf8_decode(const char *str, int *bytes_read) {
uint32_t codepoint = 0; uint32_t codepoint = 0;
int len = utf8_byte_length(s[0]); int len = utf8_byte_length(s[0]);
if (len < 1 || len > 4) {
len = 1;
}
for (int i = 1; i < len; i++) {
if (s[i] == '\0') {
*bytes_read = 1;
return s[0];
}
}
*bytes_read = len; *bytes_read = len;
switch (len) { switch (len) {
@ -218,32 +207,3 @@ bool utf8_is_valid_sequence(const char *bytes, int len) {
return true; return true;
} }
bool utf8_is_valid_string(const char *str) {
const unsigned char *p = (const unsigned char *)str;
if (!str) {
return false;
}
while (*p != '\0') {
int len = utf8_byte_length(*p);
if (len < 1 || len > 4) {
return false;
}
for (int i = 1; i < len; i++) {
if (p[i] == '\0') {
return false;
}
}
if (!utf8_is_valid_sequence((const char *)p, len)) {
return false;
}
p += len;
}
return true;
}

View file

@ -1,135 +0,0 @@
#!/bin/sh
# Connection limit regression tests for TNT
PORT=${PORT:-2222}
BIN="../tnt"
PASS=0
FAIL=0
STATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tnt-limit-test.XXXXXX")
SERVER_PID=""
WATCHER_PID=""
cleanup() {
if [ -n "$WATCHER_PID" ]; then
kill "$WATCHER_PID" 2>/dev/null || true
wait "$WATCHER_PID" 2>/dev/null || true
fi
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. Run make first."
exit 1
fi
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -p $PORT"
wait_for_health() {
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
if [ -n "$SERVER_PID" ] && ! kill -0 "$SERVER_PID" 2>/dev/null; then
return 1
fi
OUT=$(ssh $SSH_OPTS localhost health 2>/dev/null || true)
[ "$OUT" = "ok" ] && return 0
sleep 1
done
return 1
}
echo "=== TNT Connection Limit Tests ==="
TNT_RATE_LIMIT=0 TNT_MAX_CONN_PER_IP=1 "$BIN" -p "$PORT" -d "$STATE_DIR" \
>"$STATE_DIR/concurrent.log" 2>&1 &
SERVER_PID=$!
if wait_for_health; then
echo "✓ server started for concurrent limit test"
PASS=$((PASS + 1))
else
echo "✗ server failed to start for concurrent limit test"
exit 1
fi
WATCHER_READY="$STATE_DIR/watcher.ready"
cat >"$STATE_DIR/watcher.expect" <<EOF
set timeout 10
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $PORT watcher@localhost
expect "请输入用户名"
send "watcher\r"
exec touch "$WATCHER_READY"
sleep 8
send "\003"
expect eof
EOF
expect "$STATE_DIR/watcher.expect" >"$STATE_DIR/watcher.log" 2>&1 &
WATCHER_PID=$!
for _ in 1 2 3 4 5 6 7 8 9 10; do
[ -f "$WATCHER_READY" ] && break
sleep 1
done
if [ ! -f "$WATCHER_READY" ]; then
echo "✗ watcher session did not become ready"
sed -n '1,120p' "$STATE_DIR/watcher.log"
exit 1
fi
if ssh $SSH_OPTS localhost health >/dev/null 2>&1; then
echo "✗ concurrent per-IP limit was not enforced"
FAIL=$((FAIL + 1))
else
echo "✓ concurrent per-IP limit rejects a second session"
PASS=$((PASS + 1))
fi
wait "$WATCHER_PID" 2>/dev/null || true
WATCHER_PID=""
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
SERVER_PID=""
RATE_PORT=$((PORT + 1))
SSH_RATE_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -p $RATE_PORT"
TNT_MAX_CONN_PER_IP=10 TNT_MAX_CONN_RATE_PER_IP=2 "$BIN" -p "$RATE_PORT" -d "$STATE_DIR" \
>"$STATE_DIR/rate.log" 2>&1 &
SERVER_PID=$!
sleep 2
if kill -0 "$SERVER_PID" 2>/dev/null; then
echo "✓ server started for rate limit test"
PASS=$((PASS + 1))
else
echo "✗ server failed to start for rate limit test"
sed -n '1,120p' "$STATE_DIR/rate.log"
exit 1
fi
R1=$(ssh $SSH_RATE_OPTS localhost health 2>/dev/null || true)
R2=$(ssh $SSH_RATE_OPTS localhost health 2>/dev/null || true)
if ssh $SSH_RATE_OPTS localhost health >/dev/null 2>&1; then
echo "✗ per-IP connection-rate limit was not enforced"
FAIL=$((FAIL + 1))
else
if [ "$R1" = "ok" ] && [ "$R2" = "ok" ]; then
echo "✓ per-IP connection-rate limit blocks after the configured burst"
PASS=$((PASS + 1))
else
echo "✗ per-IP connection-rate limit setup failed unexpectedly"
FAIL=$((FAIL + 1))
fi
fi
echo ""
echo "PASSED: $PASS"
echo "FAILED: $FAIL"
[ "$FAIL" -eq 0 ] && echo "All tests passed" || echo "Some tests failed"
exit "$FAIL"

View file

@ -1,167 +0,0 @@
#!/bin/sh
# Exec-mode regression tests for TNT
PORT=${PORT:-2222}
PASS=0
FAIL=0
BIN="../tnt"
STATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tnt-exec-test.XXXXXX")
INTERACTIVE_PID=""
cleanup() {
if [ -n "${INTERACTIVE_PID}" ]; then
kill "${INTERACTIVE_PID}" 2>/dev/null || true
wait "${INTERACTIVE_PID}" 2>/dev/null || true
fi
kill "${SERVER_PID}" 2>/dev/null || true
wait "${SERVER_PID}" 2>/dev/null || true
rm -rf "${STATE_DIR}"
}
trap cleanup EXIT
if [ ! -f "$BIN" ]; then
echo "Error: Binary $BIN not found. Run make first."
exit 1
fi
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -p $PORT"
echo "=== TNT Exec Mode Tests ==="
TNT_RATE_LIMIT=0 $BIN -p "$PORT" -d "$STATE_DIR" >"${STATE_DIR}/server.log" 2>&1 &
SERVER_PID=$!
HEALTH_OUTPUT=""
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
echo "✗ Server failed to start"
exit 1
fi
HEALTH_OUTPUT=$(ssh $SSH_OPTS localhost health 2>/dev/null || true)
[ "$HEALTH_OUTPUT" = "ok" ] && break
sleep 1
done
if [ "$HEALTH_OUTPUT" = "ok" ]; then
echo "✓ health returns ok"
PASS=$((PASS + 1))
else
echo "✗ health failed: $HEALTH_OUTPUT"
FAIL=$((FAIL + 1))
fi
STATS_OUTPUT=$(ssh $SSH_OPTS localhost stats 2>/dev/null || true)
printf '%s\n' "$STATS_OUTPUT" | grep -q '^status ok$' &&
printf '%s\n' "$STATS_OUTPUT" | grep -q '^online_users 0$'
if [ $? -eq 0 ]; then
echo "✓ stats returns key/value output"
PASS=$((PASS + 1))
else
echo "✗ stats output unexpected"
printf '%s\n' "$STATS_OUTPUT"
FAIL=$((FAIL + 1))
fi
STATS_JSON=$(ssh $SSH_OPTS localhost stats --json 2>/dev/null || true)
printf '%s\n' "$STATS_JSON" | grep -q '"status":"ok"' &&
printf '%s\n' "$STATS_JSON" | grep -q '"online_users":0'
if [ $? -eq 0 ]; then
echo "✓ stats --json returns JSON"
PASS=$((PASS + 1))
else
echo "✗ stats --json output unexpected"
printf '%s\n' "$STATS_JSON"
FAIL=$((FAIL + 1))
fi
POST_OUTPUT=$(ssh $SSH_OPTS execposter@localhost post "hello from exec" 2>/dev/null || true)
if [ "$POST_OUTPUT" = "posted" ]; then
echo "✓ post publishes a message"
PASS=$((PASS + 1))
else
echo "✗ post failed: $POST_OUTPUT"
FAIL=$((FAIL + 1))
fi
TAIL_OUTPUT=$(ssh $SSH_OPTS localhost "tail -n 1" 2>/dev/null || true)
printf '%s\n' "$TAIL_OUTPUT" | grep -q 'execposter' &&
printf '%s\n' "$TAIL_OUTPUT" | grep -q 'hello from exec'
if [ $? -eq 0 ]; then
echo "✓ tail returns recent messages"
PASS=$((PASS + 1))
else
echo "✗ tail output unexpected"
printf '%s\n' "$TAIL_OUTPUT"
FAIL=$((FAIL + 1))
fi
EXPECT_SCRIPT="${STATE_DIR}/watcher.expect"
WATCHER_READY="${STATE_DIR}/watcher.ready"
cat >"$EXPECT_SCRIPT" <<EOF
set timeout 10
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $PORT watcher@localhost
expect "请输入用户名"
send "watcher\r"
exec touch "$WATCHER_READY"
sleep 8
send "\003"
expect eof
EOF
expect "$EXPECT_SCRIPT" >"${STATE_DIR}/expect.log" 2>&1 &
INTERACTIVE_PID=$!
for _ in 1 2 3 4 5 6 7 8 9 10; do
[ -f "$WATCHER_READY" ] && break
sleep 1
done
USERS_OUTPUT=""
for _ in 1 2 3 4 5; do
USERS_OUTPUT=$(ssh $SSH_OPTS localhost users 2>/dev/null || true)
printf '%s\n' "$USERS_OUTPUT" | grep -q '^watcher$' && break
sleep 1
done
printf '%s\n' "$USERS_OUTPUT" | grep -q '^watcher$'
if [ $? -eq 0 ]; then
echo "✓ users lists active interactive clients"
PASS=$((PASS + 1))
else
echo "✗ users output unexpected"
printf '%s\n' "$USERS_OUTPUT"
[ -f "$WATCHER_READY" ] || echo "watcher readiness marker was not created"
[ -f "${STATE_DIR}/expect.log" ] && sed -n '1,120p' "${STATE_DIR}/expect.log"
sed -n '1,120p' "${STATE_DIR}/server.log"
FAIL=$((FAIL + 1))
fi
USERS_JSON=""
for _ in 1 2 3 4 5; do
USERS_JSON=$(ssh $SSH_OPTS localhost users --json 2>/dev/null || true)
printf '%s\n' "$USERS_JSON" | grep -q '"watcher"' && break
sleep 1
done
printf '%s\n' "$USERS_JSON" | grep -q '"watcher"'
if [ $? -eq 0 ]; then
echo "✓ users --json returns JSON array"
PASS=$((PASS + 1))
else
echo "✗ users --json output unexpected"
printf '%s\n' "$USERS_JSON"
[ -f "$WATCHER_READY" ] || echo "watcher readiness marker was not created"
[ -f "${STATE_DIR}/expect.log" ] && sed -n '1,120p' "${STATE_DIR}/expect.log"
sed -n '1,120p' "${STATE_DIR}/server.log"
FAIL=$((FAIL + 1))
fi
wait "${INTERACTIVE_PID}" 2>/dev/null || true
INTERACTIVE_PID=""
echo ""
echo "PASSED: $PASS"
echo "FAILED: $FAIL"
[ "$FAIL" -eq 0 ] && echo "All tests passed" || echo "Some tests failed"
exit "$FAIL"

View file

@ -97,10 +97,6 @@ TNT_ACCESS_TOKEN="test123" $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server"
TNT_MAX_CONNECTIONS=10 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \ TNT_MAX_CONNECTIONS=10 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \
pass "TNT_MAX_CONNECTIONS configuration accepted" || fail "TNT_MAX_CONNECTIONS not working" pass "TNT_MAX_CONNECTIONS configuration accepted" || fail "TNT_MAX_CONNECTIONS not working"
# Test per-IP connection rate configuration
TNT_MAX_CONN_RATE_PER_IP=20 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \
pass "TNT_MAX_CONN_RATE_PER_IP configuration accepted" || fail "TNT_MAX_CONN_RATE_PER_IP not working"
# Test rate limit toggle # Test rate limit toggle
TNT_RATE_LIMIT=0 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \ TNT_RATE_LIMIT=0 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \
pass "TNT_RATE_LIMIT configuration accepted" || fail "TNT_RATE_LIMIT not working" pass "TNT_RATE_LIMIT configuration accepted" || fail "TNT_RATE_LIMIT not working"

View file

@ -19,7 +19,7 @@ if command -v gtimeout >/dev/null 2>&1; then
fi fi
echo "Starting TNT server on port $PORT..." echo "Starting TNT server on port $PORT..."
TNT_RATE_LIMIT=0 TNT_MAX_CONN_PER_IP=$CLIENTS $BIN -p $PORT & $BIN -p $PORT &
SERVER_PID=$! SERVER_PID=$!
sleep 2 sleep 2
@ -47,7 +47,7 @@ kill $SERVER_PID 2>/dev/null
wait wait
echo "Stress test complete" echo "Stress test complete"
if kill -0 $SERVER_PID 2>/dev/null; then if ps aux | grep tnt | grep -v grep > /dev/null; then
echo "WARNING: tnt process still running" echo "WARNING: tnt process still running"
else else
echo "Server shutdown confirmed." echo "Server shutdown confirmed."

View file

@ -7,9 +7,8 @@ After=network.target
Type=simple Type=simple
User=tnt User=tnt
Group=tnt Group=tnt
WorkingDirectory=/var/lib/tnt
ExecStart=/usr/local/bin/tnt ExecStart=/usr/local/bin/tnt
StateDirectory=tnt
StateDirectoryMode=0700
# Automatic restart on failure for long-term stability # Automatic restart on failure for long-term stability
Restart=always Restart=always
@ -40,8 +39,6 @@ TimeoutStopSec=30
# Environment (can be customized via systemctl edit) # Environment (can be customized via systemctl edit)
Environment="PORT=2222" Environment="PORT=2222"
Environment="TNT_STATE_DIR=/var/lib/tnt"
EnvironmentFile=-/etc/default/tnt
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target