mirror of
https://github.com/m1ngsama/TNT.git
synced 2026-03-26 06:43:50 +00:00
Compare commits
No commits in common. "49674b75e833a585c64224c6e7da28f205b5b2cc" and "e3e148618716be3151d2958c4fc32b891f9c5f06" have entirely different histories.
49674b75e8
...
e3e1486187
25 changed files with 394 additions and 1931 deletions
46
README.md
46
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
# 输入任意密码或直接回车
|
# 输入任意密码或直接回车
|
||||||
# 输入显示名称或留空
|
# 输入显示名称或留空
|
||||||
# 开始聊天!
|
# 开始聊天!
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
---------------------
|
---------------------
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
86
src/common.c
86
src/common.c
|
|
@ -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;
|
|
||||||
}
|
|
||||||
28
src/main.c
28
src/main.c
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
1284
src/ssh_server.c
1284
src/ssh_server.c
File diff suppressed because it is too large
Load diff
113
src/tui.c
113
src/tui.c
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
src/utf8.c
40
src/utf8.c
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue