mirror of
https://github.com/m1ngsama/TNT.git
synced 2026-02-08 08:54:05 +00:00
Compare commits
No commits in common. "4b158ede1887f3939ce3b4e2b33a7b9b25240539" and "aa2b842d0341b6b68efdd2e35a3051996791165c" have entirely different histories.
4b158ede18
...
aa2b842d03
28 changed files with 318 additions and 1431 deletions
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
- name: Install dependencies (macOS)
|
- name: Install dependencies (macOS)
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
run: |
|
run: |
|
||||||
brew install libssh coreutils
|
brew install libssh
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make
|
run: make
|
||||||
|
|
@ -33,13 +33,11 @@ jobs:
|
||||||
- name: Build with AddressSanitizer
|
- name: Build with AddressSanitizer
|
||||||
run: make asan
|
run: make asan
|
||||||
|
|
||||||
- name: Run comprehensive tests
|
- name: Run basic tests
|
||||||
run: |
|
run: |
|
||||||
make test
|
timeout 10 ./tnt &
|
||||||
cd tests
|
sleep 2
|
||||||
./test_security_features.sh
|
pkill tnt || true
|
||||||
# Skipping anonymous access test in CI as it requires interactive pty handling which might be flaky
|
|
||||||
# ./test_anonymous_access.sh
|
|
||||||
|
|
||||||
- name: Check for memory leaks
|
- name: Check for memory leaks
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
|
|
|
||||||
6
Makefile
6
Makefile
|
|
@ -39,7 +39,6 @@ $(OBJ_DIR):
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(OBJ_DIR) $(TARGET)
|
rm -rf $(OBJ_DIR) $(TARGET)
|
||||||
rm -f tests/*.log tests/host_key* tests/messages.log
|
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
install: $(TARGET)
|
install: $(TARGET)
|
||||||
|
|
@ -70,11 +69,6 @@ check:
|
||||||
@command -v cppcheck >/dev/null 2>&1 && cppcheck --enable=warning,performance --quiet src/ || echo "cppcheck not installed"
|
@command -v cppcheck >/dev/null 2>&1 && cppcheck --enable=warning,performance --quiet src/ || echo "cppcheck not installed"
|
||||||
@command -v clang-tidy >/dev/null 2>&1 && clang-tidy src/*.c -- -Iinclude $(INCLUDES) || echo "clang-tidy not installed"
|
@command -v clang-tidy >/dev/null 2>&1 && clang-tidy src/*.c -- -Iinclude $(INCLUDES) || echo "clang-tidy not installed"
|
||||||
|
|
||||||
# Test
|
|
||||||
test: all
|
|
||||||
@echo "Running tests..."
|
|
||||||
@cd tests && ./test_basic.sh
|
|
||||||
|
|
||||||
# Show build info
|
# Show build info
|
||||||
info:
|
info:
|
||||||
@echo "Compiler: $(CC)"
|
@echo "Compiler: $(CC)"
|
||||||
|
|
|
||||||
295
README.md
295
README.md
|
|
@ -1,277 +1,114 @@
|
||||||
# TNT - Terminal Network Talk
|
# TNT
|
||||||
|
|
||||||
A minimalist terminal chat server with Vim-style interface over SSH.
|
Terminal chat server. Vim-style interface. SSH-based.
|
||||||
|
|
||||||
## Features
|
## Install
|
||||||
|
|
||||||
- **Zero config** - Download and run, auto-generates SSH keys
|
|
||||||
- **SSH-based** - Leverage mature SSH protocol for encryption and auth
|
|
||||||
- **Vim-style UI** - Modal editing (INSERT/NORMAL/COMMAND)
|
|
||||||
- **UTF-8 native** - Full Unicode support
|
|
||||||
- **High performance** - Pure C, multi-threaded, sub-100ms startup
|
|
||||||
- **Secure** - Rate limiting, auth failure protection, input validation
|
|
||||||
- **Persistent** - Auto-saves chat history
|
|
||||||
- **Elegant** - Flicker-free TUI rendering
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
**One-liner:**
|
|
||||||
```sh
|
```sh
|
||||||
curl -sSL https://raw.githubusercontent.com/m1ngsama/TNT/main/install.sh | sh
|
curl -sSL https://raw.githubusercontent.com/m1ngsama/TNT/main/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**From source:**
|
Or download from [releases](https://github.com/m1ngsama/TNT/releases).
|
||||||
```sh
|
|
||||||
git clone https://github.com/m1ngsama/TNT.git
|
|
||||||
cd TNT
|
|
||||||
make
|
|
||||||
sudo make install
|
|
||||||
```
|
|
||||||
|
|
||||||
**Binary releases:**
|
## Run
|
||||||
https://github.com/m1ngsama/TNT/releases
|
|
||||||
|
|
||||||
### Running
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
tnt # default port 2222
|
tnt # port 2222
|
||||||
tnt -p 3333 # custom port
|
tnt -p 3333 # custom port
|
||||||
PORT=3333 tnt # via env var
|
PORT=3333 tnt # env var
|
||||||
```
|
```
|
||||||
|
|
||||||
### Connecting
|
Connect: `ssh -p 2222 localhost`
|
||||||
|
|
||||||
|
**Anonymous Access**: By default, users can connect with ANY username and ANY password (or empty password). No SSH keys required. This makes TNT perfect for public chat servers.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Configure via environment variables.
|
||||||
|
|
||||||
|
### Access Control
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ssh -p 2222 localhost
|
TNT_ACCESS_TOKEN="secret" tnt # require password
|
||||||
|
TNT_BIND_ADDR=127.0.0.1 tnt # localhost only
|
||||||
```
|
```
|
||||||
|
|
||||||
**Anonymous access by default**: Users can connect with ANY username/password (or empty password). No SSH keys required. Perfect for public chat servers.
|
Without `TNT_ACCESS_TOKEN`, server is open (default).
|
||||||
|
|
||||||
## Usage
|
### Rate Limiting
|
||||||
|
|
||||||
### Keybindings
|
|
||||||
|
|
||||||
**INSERT mode (default)**
|
|
||||||
```
|
|
||||||
ESC - Enter NORMAL mode
|
|
||||||
Enter - Send message
|
|
||||||
Backspace - Delete character
|
|
||||||
Ctrl+W - Delete last word
|
|
||||||
Ctrl+U - Delete line
|
|
||||||
Ctrl+C - Enter NORMAL mode
|
|
||||||
```
|
|
||||||
|
|
||||||
**NORMAL mode**
|
|
||||||
```
|
|
||||||
i - Return to INSERT mode
|
|
||||||
: - Enter COMMAND mode
|
|
||||||
j/k - Scroll down/up
|
|
||||||
g/G - Scroll to top/bottom
|
|
||||||
? - Show help
|
|
||||||
```
|
|
||||||
|
|
||||||
**COMMAND mode**
|
|
||||||
```
|
|
||||||
:list, :users, :who - Show online users
|
|
||||||
:help, :commands - Show available commands
|
|
||||||
:clear, :cls - Clear command output
|
|
||||||
ESC - Return to NORMAL mode
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security Configuration
|
|
||||||
|
|
||||||
**Access control:**
|
|
||||||
```sh
|
|
||||||
# Require password
|
|
||||||
TNT_ACCESS_TOKEN="secret" tnt
|
|
||||||
|
|
||||||
# Bind to localhost only
|
|
||||||
TNT_BIND_ADDR=127.0.0.1 tnt
|
|
||||||
|
|
||||||
# Bind to specific IP
|
|
||||||
TNT_BIND_ADDR=192.168.1.100 tnt
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rate limiting:**
|
|
||||||
```sh
|
|
||||||
# Max total connections (default 64)
|
|
||||||
TNT_MAX_CONNECTIONS=100 tnt
|
|
||||||
|
|
||||||
# Max connections per IP (default 5)
|
|
||||||
TNT_MAX_CONN_PER_IP=10 tnt
|
|
||||||
|
|
||||||
# Disable rate limiting (testing only)
|
|
||||||
TNT_RATE_LIMIT=0 tnt
|
|
||||||
```
|
|
||||||
|
|
||||||
**SSH logging:**
|
|
||||||
```sh
|
|
||||||
# 0=none, 1=warning, 2=protocol, 3=packet, 4=functions (default 1)
|
|
||||||
TNT_SSH_LOG_LEVEL=3 tnt
|
|
||||||
```
|
|
||||||
|
|
||||||
**Production example:**
|
|
||||||
```sh
|
|
||||||
TNT_ACCESS_TOKEN="strong-password-123" \
|
|
||||||
TNT_BIND_ADDR=0.0.0.0 \
|
|
||||||
TNT_MAX_CONNECTIONS=200 \
|
|
||||||
TNT_MAX_CONN_PER_IP=3 \
|
|
||||||
TNT_SSH_LOG_LEVEL=1 \
|
|
||||||
tnt -p 2222
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make # standard build
|
TNT_MAX_CONNECTIONS=100 tnt # total limit
|
||||||
make debug # debug build (with symbols)
|
TNT_MAX_CONN_PER_IP=10 tnt # per-IP limit
|
||||||
make asan # AddressSanitizer build
|
TNT_RATE_LIMIT=0 tnt # disable (testing only)
|
||||||
make check # static analysis (cppcheck)
|
|
||||||
make clean # clean build artifacts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
Default: 64 total, 5 per IP, rate limiting enabled.
|
||||||
|
|
||||||
|
### SSH Options
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make test # run comprehensive test suite
|
TNT_SSH_LOG_LEVEL=3 tnt # verbose logging (0-4)
|
||||||
|
|
||||||
# Individual tests
|
|
||||||
cd tests
|
|
||||||
./test_basic.sh # basic functionality
|
|
||||||
./test_security_features.sh # security features
|
|
||||||
./test_anonymous_access.sh # anonymous access
|
|
||||||
./test_stress.sh # stress test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test coverage:**
|
## Keys
|
||||||
- Basic functionality: 3 tests
|
|
||||||
- Anonymous access: 2 tests
|
|
||||||
- Security features: 11 tests
|
|
||||||
- Stress test: concurrent connections
|
|
||||||
|
|
||||||
### Dependencies
|
**INSERT** (default)
|
||||||
|
- `ESC` → NORMAL
|
||||||
|
- `Enter` → send
|
||||||
|
- `Backspace` → delete
|
||||||
|
|
||||||
- **libssh** (>= 0.9.0) - SSH protocol library
|
**NORMAL**
|
||||||
- **pthread** - POSIX threads
|
- `i` → INSERT
|
||||||
- **gcc/clang** - C11 compiler
|
- `:` → COMMAND
|
||||||
|
- `j/k` → scroll
|
||||||
|
- `g/G` → top/bottom
|
||||||
|
- `?` → help
|
||||||
|
|
||||||
**Ubuntu/Debian:**
|
**COMMAND**
|
||||||
```sh
|
- `:list` → users
|
||||||
sudo apt-get install libssh-dev
|
- `:help` → commands
|
||||||
```
|
- `ESC` → back
|
||||||
|
|
||||||
**macOS:**
|
## Build
|
||||||
```sh
|
|
||||||
brew install libssh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fedora/RHEL:**
|
|
||||||
```sh
|
|
||||||
sudo dnf install libssh-devel
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
TNT/
|
|
||||||
├── src/ # source code
|
|
||||||
│ ├── main.c # entry point
|
|
||||||
│ ├── ssh_server.c # SSH server implementation
|
|
||||||
│ ├── chat_room.c # chat room logic
|
|
||||||
│ ├── message.c # message persistence
|
|
||||||
│ ├── tui.c # terminal UI rendering
|
|
||||||
│ └── utf8.c # UTF-8 character handling
|
|
||||||
├── include/ # header files
|
|
||||||
├── tests/ # test scripts
|
|
||||||
├── docs/ # documentation
|
|
||||||
├── scripts/ # operational scripts
|
|
||||||
├── Makefile # build configuration
|
|
||||||
└── README.md # this file
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### systemd Service
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo cp tnt.service /etc/systemd/system/
|
make # normal
|
||||||
sudo systemctl daemon-reload
|
make debug # with symbols
|
||||||
sudo systemctl enable tnt
|
make asan # sanitizer
|
||||||
sudo systemctl start tnt
|
make check # static analysis
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
Requires: `libssh`
|
||||||
|
|
||||||
```dockerfile
|
## Deploy
|
||||||
FROM alpine:latest
|
|
||||||
RUN apk add --no-cache libssh
|
|
||||||
COPY tnt /usr/local/bin/
|
|
||||||
EXPOSE 2222
|
|
||||||
CMD ["tnt"]
|
|
||||||
```
|
|
||||||
|
|
||||||
See [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) for details.
|
See [DEPLOYMENT.md](DEPLOYMENT.md) for systemd setup.
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
```
|
```
|
||||||
messages.log - Chat history (RFC3339 format)
|
messages.log chat history
|
||||||
host_key - SSH host key (auto-generated, 4096-bit RSA)
|
host_key SSH key (auto-gen)
|
||||||
tnt.service - systemd service unit
|
tnt.service systemd unit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Test
|
||||||
|
|
||||||
- [Development Guide](https://github.com/m1ngsama/TNT/wiki/Development-Guide) - Complete development manual
|
```sh
|
||||||
- [Quick Setup](docs/EASY_SETUP.md) - 5-minute deployment guide
|
./test_basic.sh # functional
|
||||||
- [Security Reference](docs/SECURITY_QUICKREF.md) - Security config quick reference
|
./test_stress.sh 50 # 50 clients
|
||||||
- [Contributing](docs/CONTRIBUTING.md) - How to contribute
|
```
|
||||||
- [Changelog](docs/CHANGELOG.md) - Version history
|
|
||||||
- [CI/CD](docs/CICD.md) - Continuous integration setup
|
|
||||||
- [Quick Reference](docs/QUICKREF.md) - Command cheat sheet
|
|
||||||
|
|
||||||
## Performance
|
## Docs
|
||||||
|
|
||||||
- **Startup**: < 100ms (even with 100k+ message history)
|
- `README` - man page style
|
||||||
- **Memory**: ~2MB (idle)
|
- `EASY_SETUP.md` - 🚀 **快速部署指南 / Quick Setup Guide**
|
||||||
- **Concurrency**: Supports 100+ concurrent connections
|
- `HACKING` - dev guide
|
||||||
- **Throughput**: 1000+ messages/second
|
- `DEPLOYMENT.md` - production
|
||||||
|
- `CICD.md` - automation
|
||||||
## Known Limitations
|
- `QUICKREF` - cheat sheet
|
||||||
|
|
||||||
- Single chat room (no multi-room support yet)
|
|
||||||
- Keeps only last 100 messages in memory
|
|
||||||
- Ctrl+W only recognizes ASCII space as word boundary
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions welcome! See [CONTRIBUTING.md](docs/CONTRIBUTING.md)
|
|
||||||
|
|
||||||
**Process:**
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create feature branch (`git checkout -b feature/AmazingFeature`)
|
|
||||||
3. Commit changes (`git commit -m 'Add some AmazingFeature'`)
|
|
||||||
4. Push to branch (`git push origin feature/AmazingFeature`)
|
|
||||||
5. Open Pull Request
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License - see [LICENSE](LICENSE)
|
MIT
|
||||||
|
|
||||||
## Acknowledgments
|
|
||||||
|
|
||||||
- [libssh](https://www.libssh.org/) - SSH protocol implementation
|
|
||||||
- Linux kernel community - Code style and engineering practices
|
|
||||||
|
|
||||||
## Contact
|
|
||||||
|
|
||||||
- Issues: https://github.com/m1ngsama/TNT/issues
|
|
||||||
- Pull Requests: https://github.com/m1ngsama/TNT/pulls
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**"Talk is cheap. Show me the code."** - Linus Torvalds
|
|
||||||
|
|
|
||||||
12
TODO.md
12
TODO.md
|
|
@ -1,12 +0,0 @@
|
||||||
# TODO
|
|
||||||
|
|
||||||
## Maintenance
|
|
||||||
- [x] Replace deprecated `libssh` functions in `src/ssh_server.c`:
|
|
||||||
- ~~`ssh_message_auth_password`~~ → `auth_password_function` callback (✓ completed)
|
|
||||||
- ~~`ssh_message_channel_request_pty_width/height`~~ → `channel_pty_request_function` callback (✓ completed)
|
|
||||||
- Migrated to callback-based server API as of libssh 0.9+
|
|
||||||
|
|
||||||
## Future Features
|
|
||||||
- [x] Implement robust command handling for non-interactive SSH exec requests.
|
|
||||||
- Basic exec support completed (handles `exit` command)
|
|
||||||
- All tests passing
|
|
||||||
|
|
@ -1,585 +0,0 @@
|
||||||
# TNT Development Guide
|
|
||||||
|
|
||||||
Complete guide for TNT developers and contributors.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
1. [Architecture Overview](#architecture-overview)
|
|
||||||
2. [Code Structure](#code-structure)
|
|
||||||
3. [Building and Testing](#building-and-testing)
|
|
||||||
4. [Core Components](#core-components)
|
|
||||||
5. [Adding Features](#adding-features)
|
|
||||||
6. [Debugging](#debugging)
|
|
||||||
7. [Performance Optimization](#performance-optimization)
|
|
||||||
8. [Contributing Guidelines](#contributing-guidelines)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
TNT uses a multi-threaded architecture with a main accept loop and per-client threads.
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Main Thread │
|
|
||||||
│ ┌──────────────────────────────────────────────────┐ │
|
|
||||||
│ │ ssh_server_start() │ │
|
|
||||||
│ │ └─> ssh_bind_accept() │ │
|
|
||||||
│ │ └─> Event loop (auth + channel setup) │ │
|
|
||||||
│ │ └─> pthread_create(client_thread) │ │
|
|
||||||
│ └──────────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌─────────────────┴─────────────────┐
|
|
||||||
│ │
|
|
||||||
┌───────▼────────┐ ┌────────▼───────┐
|
|
||||||
│ Client Thread 1│ │ Client Thread N│
|
|
||||||
│ ┌──────────┐ │ │ ┌──────────┐ │
|
|
||||||
│ │ Session │ │ ... │ │ Session │ │
|
|
||||||
│ │ Handler │ │ │ │ Handler │ │
|
|
||||||
│ └──────────┘ │ │ └──────────┘ │
|
|
||||||
└────────────────┘ └────────────────┘
|
|
||||||
│ │
|
|
||||||
└───────────┬───────────────────────┘
|
|
||||||
│
|
|
||||||
┌──────────▼──────────┐
|
|
||||||
│ Chat Room │
|
|
||||||
│ ┌──────────────┐ │
|
|
||||||
│ │ RW Lock │ │
|
|
||||||
│ │ Clients[] │ │
|
|
||||||
│ │ Messages[] │ │
|
|
||||||
│ └──────────────┘ │
|
|
||||||
└─────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Design Principles
|
|
||||||
|
|
||||||
1. **Fixed-size buffers** - No dynamic allocation in hot paths
|
|
||||||
2. **Reader-writer locks** - Multiple readers, single writer
|
|
||||||
3. **Reference counting** - Prevent use-after-free
|
|
||||||
4. **Ring buffer** - Fixed-size message history (last 100 messages)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Structure
|
|
||||||
|
|
||||||
### Source Files
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── main.c - Entry point, signal handling
|
|
||||||
├── ssh_server.c - SSH server, client threads, authentication
|
|
||||||
├── chat_room.c - Chat room logic, message broadcasting
|
|
||||||
├── message.c - Message persistence (RFC3339 format)
|
|
||||||
├── tui.c - Terminal UI rendering (ANSI escape codes)
|
|
||||||
└── utf8.c - UTF-8 character handling
|
|
||||||
```
|
|
||||||
|
|
||||||
### Header Files
|
|
||||||
|
|
||||||
```
|
|
||||||
include/
|
|
||||||
├── common.h - Common definitions, constants
|
|
||||||
├── ssh_server.h - SSH server interface
|
|
||||||
├── chat_room.h - Chat room interface
|
|
||||||
├── message.h - Message structure and persistence
|
|
||||||
├── tui.h - TUI rendering functions
|
|
||||||
└── utf8.h - UTF-8 utilities
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Data Structures
|
|
||||||
|
|
||||||
#### `client_t` (ssh_server.h)
|
|
||||||
```c
|
|
||||||
typedef struct client {
|
|
||||||
ssh_session session;
|
|
||||||
ssh_channel channel;
|
|
||||||
char username[MAX_USERNAME_LEN];
|
|
||||||
int width, height; // Terminal dimensions
|
|
||||||
client_mode_t mode; // INSERT/NORMAL/COMMAND
|
|
||||||
int scroll_pos;
|
|
||||||
bool connected;
|
|
||||||
int ref_count; // Reference counting
|
|
||||||
pthread_mutex_t ref_lock;
|
|
||||||
} client_t;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `chat_room_t` (chat_room.h)
|
|
||||||
```c
|
|
||||||
typedef struct {
|
|
||||||
pthread_rwlock_t lock; // Reader-writer lock
|
|
||||||
struct client **clients; // Dynamic array
|
|
||||||
int client_count;
|
|
||||||
message_t *messages; // Ring buffer
|
|
||||||
int message_count;
|
|
||||||
} chat_room_t;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `message_t` (message.h)
|
|
||||||
```c
|
|
||||||
typedef struct {
|
|
||||||
time_t timestamp;
|
|
||||||
char username[MAX_USERNAME_LEN];
|
|
||||||
char content[MAX_MESSAGE_LEN];
|
|
||||||
} message_t;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Building and Testing
|
|
||||||
|
|
||||||
### Build Targets
|
|
||||||
|
|
||||||
```sh
|
|
||||||
make # Standard release build
|
|
||||||
make debug # Debug build with symbols (-g)
|
|
||||||
make asan # AddressSanitizer build
|
|
||||||
make check # Static analysis (cppcheck)
|
|
||||||
make clean # Clean build artifacts
|
|
||||||
make install # Install to /usr/local/bin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiler Flags
|
|
||||||
|
|
||||||
**Release build:**
|
|
||||||
```
|
|
||||||
-Wall -Wextra -O2 -std=c11 -D_XOPEN_SOURCE=700
|
|
||||||
```
|
|
||||||
|
|
||||||
**Debug build:**
|
|
||||||
```
|
|
||||||
-Wall -Wextra -g -O0 -std=c11 -D_XOPEN_SOURCE=700
|
|
||||||
```
|
|
||||||
|
|
||||||
**ASAN build:**
|
|
||||||
```
|
|
||||||
-Wall -Wextra -g -O0 -fsanitize=address -fno-omit-frame-pointer
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
```sh
|
|
||||||
make test # Run all tests
|
|
||||||
|
|
||||||
# Individual tests
|
|
||||||
cd tests
|
|
||||||
./test_basic.sh # Basic functionality
|
|
||||||
./test_security_features.sh # Security checks
|
|
||||||
./test_anonymous_access.sh # Anonymous access
|
|
||||||
./test_stress.sh # Concurrent connections
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Coverage
|
|
||||||
|
|
||||||
- **Basic**: Server startup, SSH connection, message logging
|
|
||||||
- **Security**: RSA keys, env vars, UTF-8 validation, buffer overflow protection
|
|
||||||
- **Anonymous**: Passwordless access, any username
|
|
||||||
- **Stress**: 10 concurrent clients for 30 seconds
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
### 1. SSH Server (ssh_server.c)
|
|
||||||
|
|
||||||
**Callback-based API** (libssh 0.9+):
|
|
||||||
|
|
||||||
```c
|
|
||||||
/* Authentication callbacks */
|
|
||||||
static int auth_password(ssh_session session, const char *user,
|
|
||||||
const char *password, void *userdata);
|
|
||||||
static int auth_none(ssh_session session, const char *user, void *userdata);
|
|
||||||
|
|
||||||
/* Channel callbacks */
|
|
||||||
static ssh_channel channel_open_request_session(ssh_session session, void *userdata);
|
|
||||||
static int channel_pty_request(ssh_session session, ssh_channel channel,
|
|
||||||
const char *term, int width, int height,
|
|
||||||
int pxwidth, int pxheight, void *userdata);
|
|
||||||
static int channel_shell_request(ssh_session session, ssh_channel channel,
|
|
||||||
void *userdata);
|
|
||||||
static int channel_exec_request(ssh_session session, ssh_channel channel,
|
|
||||||
const char *command, void *userdata);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Event loop:**
|
|
||||||
```c
|
|
||||||
ssh_event event = ssh_event_new();
|
|
||||||
ssh_event_add_session(event, session);
|
|
||||||
|
|
||||||
/* Wait for: auth_success, channel != NULL, channel_ready */
|
|
||||||
while ((!ctx->auth_success || ctx->channel == NULL || !ctx->channel_ready) && !timed_out) {
|
|
||||||
ssh_event_dopoll(event, 1000);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Chat Room (chat_room.c)
|
|
||||||
|
|
||||||
**Thread-safe broadcasting:**
|
|
||||||
```c
|
|
||||||
void room_broadcast(chat_room_t *room, const message_t *msg) {
|
|
||||||
pthread_rwlock_wrlock(&room->lock);
|
|
||||||
|
|
||||||
/* Copy client list with ref counting */
|
|
||||||
client_t **clients_copy = calloc(...);
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
clients_copy[i]->ref_count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
pthread_rwlock_unlock(&room->lock); // Release lock early
|
|
||||||
|
|
||||||
/* Render outside lock (avoid deadlock) */
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
tui_render_screen(clients_copy[i]);
|
|
||||||
client_release(clients_copy[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this works:**
|
|
||||||
- Copy client list while holding write lock
|
|
||||||
- Increment reference counts
|
|
||||||
- Release lock BEFORE rendering
|
|
||||||
- Render to all clients outside lock
|
|
||||||
- Decrement reference counts (may free clients)
|
|
||||||
|
|
||||||
### 3. Message Persistence (message.c)
|
|
||||||
|
|
||||||
**Log format:**
|
|
||||||
```
|
|
||||||
2024-01-13T10:30:45Z|username|message content
|
|
||||||
```
|
|
||||||
|
|
||||||
**Optimized loading** (backward scan):
|
|
||||||
```c
|
|
||||||
/* Scan backwards from file end */
|
|
||||||
fseek(fp, 0, SEEK_END);
|
|
||||||
long file_size = ftell(fp);
|
|
||||||
long pos = file_size - 1;
|
|
||||||
|
|
||||||
/* Read 4KB chunks backwards */
|
|
||||||
#define CHUNK_SIZE 4096
|
|
||||||
while (pos >= 0 && newlines_found < max_messages) {
|
|
||||||
/* Read chunk */
|
|
||||||
/* Count newlines backwards */
|
|
||||||
/* Stop when max_messages found */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Complexity:** O(last N messages) instead of O(file size)
|
|
||||||
|
|
||||||
### 4. TUI Rendering (tui.c)
|
|
||||||
|
|
||||||
**Flicker-free rendering:**
|
|
||||||
```c
|
|
||||||
/* Move to top (no clear screen!) */
|
|
||||||
pos += snprintf(buffer + pos, size - pos, ANSI_HOME);
|
|
||||||
|
|
||||||
/* Render each line with line clear */
|
|
||||||
for (each line) {
|
|
||||||
pos += snprintf(buffer + pos, size - pos, "%s\033[K\r\n", line);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**ANSI codes:**
|
|
||||||
- `\033[H` - HOME (move cursor to 0,0)
|
|
||||||
- `\033[K` - EL (erase to end of line)
|
|
||||||
- `\033[2J` - ED (erase display) - **DON'T USE** (causes flicker)
|
|
||||||
|
|
||||||
### 5. UTF-8 Handling (utf8.c)
|
|
||||||
|
|
||||||
**Character width:**
|
|
||||||
```c
|
|
||||||
int utf8_char_width(const char *str) {
|
|
||||||
unsigned char c = (unsigned char)str[0];
|
|
||||||
|
|
||||||
/* ASCII */
|
|
||||||
if (c < 0x80) return 1;
|
|
||||||
|
|
||||||
/* CJK ranges (width = 2) */
|
|
||||||
uint32_t codepoint = /* decode UTF-8 */;
|
|
||||||
if (codepoint >= 0x4E00 && codepoint <= 0x9FFF) return 2; // CJK Unified
|
|
||||||
if (codepoint >= 0x3040 && codepoint <= 0x30FF) return 2; // Hiragana/Katakana
|
|
||||||
if (codepoint >= 0xAC00 && codepoint <= 0xD7AF) return 2; // Hangul
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Word deletion (Ctrl+W):**
|
|
||||||
```c
|
|
||||||
void utf8_remove_last_word(char *str) {
|
|
||||||
int i = strlen(str);
|
|
||||||
|
|
||||||
/* Skip trailing spaces */
|
|
||||||
while (i > 0 && str[i-1] == ' ') i--;
|
|
||||||
|
|
||||||
/* Skip non-spaces (the word) */
|
|
||||||
while (i > 0 && str[i-1] != ' ') i--;
|
|
||||||
|
|
||||||
str[i] = '\0';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** Only recognizes ASCII space as word boundary (not ideal for CJK).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding Features
|
|
||||||
|
|
||||||
### Adding a New Command
|
|
||||||
|
|
||||||
1. **Add to `execute_command()` in ssh_server.c:**
|
|
||||||
```c
|
|
||||||
if (strcmp(cmd, "newcmd") == 0) {
|
|
||||||
pos += snprintf(output + pos, sizeof(output) - pos,
|
|
||||||
"New command output\n");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Update help text in tui.c:**
|
|
||||||
```c
|
|
||||||
"AVAILABLE COMMANDS:\n"
|
|
||||||
" newcmd - Description of new command\n"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Add test in tests/test_basic.sh:**
|
|
||||||
```sh
|
|
||||||
echo ":newcmd" | timeout 5 ssh -p $PORT localhost
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding a New Keybinding
|
|
||||||
|
|
||||||
1. **Add to `handle_key()` in ssh_server.c:**
|
|
||||||
```c
|
|
||||||
case MODE_INSERT:
|
|
||||||
if (key == 26) { /* Ctrl+Z */
|
|
||||||
/* Handle Ctrl+Z */
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Update help text in tui.c**
|
|
||||||
|
|
||||||
3. **Document in README.md**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Debugging
|
|
||||||
|
|
||||||
### Enable Verbose SSH Logging
|
|
||||||
|
|
||||||
```sh
|
|
||||||
TNT_SSH_LOG_LEVEL=4 ./tnt
|
|
||||||
```
|
|
||||||
|
|
||||||
Levels:
|
|
||||||
- 0 = No logs
|
|
||||||
- 1 = Warnings (default)
|
|
||||||
- 2 = Protocol
|
|
||||||
- 3 = Packet
|
|
||||||
- 4 = Functions
|
|
||||||
|
|
||||||
### Use AddressSanitizer
|
|
||||||
|
|
||||||
```sh
|
|
||||||
make asan
|
|
||||||
ASAN_OPTIONS=detect_leaks=1 ./tnt
|
|
||||||
```
|
|
||||||
|
|
||||||
Detects:
|
|
||||||
- Buffer overflows
|
|
||||||
- Use-after-free
|
|
||||||
- Memory leaks
|
|
||||||
- Stack corruption
|
|
||||||
|
|
||||||
### Use Valgrind
|
|
||||||
|
|
||||||
```sh
|
|
||||||
make
|
|
||||||
valgrind --leak-check=full --show-leak-kinds=all ./tnt
|
|
||||||
```
|
|
||||||
|
|
||||||
### GDB Debugging
|
|
||||||
|
|
||||||
```sh
|
|
||||||
make debug
|
|
||||||
gdb ./tnt
|
|
||||||
|
|
||||||
(gdb) run
|
|
||||||
(gdb) bt # backtrace on crash
|
|
||||||
(gdb) p var # print variable
|
|
||||||
```
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
**Problem:** Client disconnects immediately
|
|
||||||
**Solution:** Check `handle_pty_request()` - must wait for shell/exec request
|
|
||||||
|
|
||||||
**Problem:** Rendering flickers
|
|
||||||
**Solution:** Use `\033[K` (line clear) instead of `\033[2J` (screen clear)
|
|
||||||
|
|
||||||
**Problem:** Race condition / deadlock
|
|
||||||
**Solution:** Check lock order, use reader-writer locks correctly
|
|
||||||
|
|
||||||
**Problem:** Memory leak
|
|
||||||
**Solution:** Run with ASAN, check reference counting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Optimization
|
|
||||||
|
|
||||||
### Hot Paths
|
|
||||||
|
|
||||||
1. **Message broadcasting** (`room_broadcast`)
|
|
||||||
- Minimize lock holding time
|
|
||||||
- Render outside lock
|
|
||||||
- Use fixed-size buffers
|
|
||||||
|
|
||||||
2. **TUI rendering** (`tui_render_screen`)
|
|
||||||
- Build buffer completely, then single write
|
|
||||||
- No incremental writes
|
|
||||||
- Use `snprintf` (not `strcat`)
|
|
||||||
|
|
||||||
3. **Message loading** (`message_load`)
|
|
||||||
- Backward scan from file end
|
|
||||||
- 4KB chunked reads
|
|
||||||
- Early termination
|
|
||||||
|
|
||||||
### Profiling
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Compile with profiling
|
|
||||||
make CFLAGS="-pg"
|
|
||||||
|
|
||||||
# Run
|
|
||||||
./tnt
|
|
||||||
|
|
||||||
# Generate report
|
|
||||||
gprof tnt gmon.out > profile.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benchmarking
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Message throughput
|
|
||||||
time (for i in {1..1000}; do echo "msg $i"; done | ssh -p 2222 localhost)
|
|
||||||
|
|
||||||
# Concurrent connections
|
|
||||||
./tests/test_stress.sh
|
|
||||||
|
|
||||||
# Startup time with large log
|
|
||||||
dd if=/dev/zero of=messages.log bs=1M count=10
|
|
||||||
time ./tnt
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contributing Guidelines
|
|
||||||
|
|
||||||
### Code Style
|
|
||||||
|
|
||||||
Follow **Linux kernel coding style**:
|
|
||||||
|
|
||||||
```c
|
|
||||||
/* Function brace on new line */
|
|
||||||
int function(int arg)
|
|
||||||
{
|
|
||||||
/* Use tabs (width 8) */
|
|
||||||
if (condition) {
|
|
||||||
/* Do something */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Single exit point preferred */
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rules:**
|
|
||||||
- Tabs for indentation (8 spaces)
|
|
||||||
- Max 80 columns
|
|
||||||
- No trailing whitespace
|
|
||||||
- `/* C-style comments */` (not `//`)
|
|
||||||
- Functions < 100 lines
|
|
||||||
- Max 3 levels of indentation
|
|
||||||
|
|
||||||
### Commit Messages
|
|
||||||
|
|
||||||
```
|
|
||||||
subsystem: short description (max 50 chars)
|
|
||||||
|
|
||||||
Longer explanation if needed (wrap at 72 chars).
|
|
||||||
Explain WHAT and WHY, not HOW.
|
|
||||||
|
|
||||||
- Bullet points OK
|
|
||||||
- Reference issues: Fixes #123
|
|
||||||
|
|
||||||
Signed-off-by: Your Name <email@example.com>
|
|
||||||
```
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
```
|
|
||||||
ssh: migrate to callback-based API
|
|
||||||
|
|
||||||
Replace deprecated message-based API with modern callback-based
|
|
||||||
server implementation. Eliminates message loop complexity.
|
|
||||||
|
|
||||||
tui: optimize rendering to eliminate flicker
|
|
||||||
|
|
||||||
Use ANSI HOME + line clear instead of full screen clear.
|
|
||||||
Reduces rendering time from 50ms to <1ms.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pull Request Process
|
|
||||||
|
|
||||||
1. **Fork and branch**
|
|
||||||
```sh
|
|
||||||
git checkout -b feature/amazing-feature
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Make changes**
|
|
||||||
- Write code
|
|
||||||
- Add tests
|
|
||||||
- Update docs
|
|
||||||
|
|
||||||
3. **Test**
|
|
||||||
```sh
|
|
||||||
make clean
|
|
||||||
make
|
|
||||||
make test
|
|
||||||
make asan # must pass!
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Commit**
|
|
||||||
```sh
|
|
||||||
git commit -s -m "subsystem: description"
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Push and PR**
|
|
||||||
```sh
|
|
||||||
git push origin feature/amazing-feature
|
|
||||||
# Open PR on GitHub
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Review Checklist
|
|
||||||
|
|
||||||
- [ ] Follows Linux kernel code style
|
|
||||||
- [ ] All tests pass (`make test`)
|
|
||||||
- [ ] ASAN clean (`make asan`)
|
|
||||||
- [ ] No compiler warnings
|
|
||||||
- [ ] Documentation updated
|
|
||||||
- [ ] Commit messages follow convention
|
|
||||||
- [ ] No unnecessary complexity
|
|
||||||
- [ ] Thread-safe if touching shared data
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Additional Resources
|
|
||||||
|
|
||||||
- [libssh Documentation](https://api.libssh.org/stable/)
|
|
||||||
- [Linux Kernel Coding Style](https://www.kernel.org/doc/html/latest/process/coding-style.html)
|
|
||||||
- [ANSI Escape Codes](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
|
||||||
- [UTF-8 Specification](https://en.wikipedia.org/wiki/UTF-8)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Questions?** Open an issue or discussion on GitHub.
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
# GitHub Wiki Setup Instructions
|
|
||||||
|
|
||||||
The development documentation has been prepared and is ready to be added to GitHub Wiki.
|
|
||||||
|
|
||||||
## Quick Setup (Recommended)
|
|
||||||
|
|
||||||
1. **Enable Wiki** (already enabled for this repo)
|
|
||||||
|
|
||||||
2. **Create Wiki pages via GitHub Web UI:**
|
|
||||||
- Go to: https://github.com/m1ngsama/TNT/wiki
|
|
||||||
- Click "Create the first page"
|
|
||||||
- Title: `Home`
|
|
||||||
- Content: Copy from `README.md` (project overview)
|
|
||||||
- Click "Save Page"
|
|
||||||
|
|
||||||
3. **Add Development Guide:**
|
|
||||||
- Click "New Page"
|
|
||||||
- Title: `Development-Guide`
|
|
||||||
- Content: Copy from `docs/Development-Guide.md`
|
|
||||||
- Click "Save Page"
|
|
||||||
|
|
||||||
4. **Add other pages as needed:**
|
|
||||||
- Contributing Guide (from `docs/CONTRIBUTING.md`)
|
|
||||||
- Deployment Guide (from `docs/DEPLOYMENT.md`)
|
|
||||||
- Security Reference (from `docs/SECURITY_QUICKREF.md`)
|
|
||||||
- Quick Reference (from `docs/QUICKREF.md`)
|
|
||||||
|
|
||||||
## Alternative: Clone and Push
|
|
||||||
|
|
||||||
Once the Wiki has at least one page, you can manage it via git:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Clone wiki repository
|
|
||||||
git clone https://github.com/m1ngsama/TNT.wiki.git
|
|
||||||
cd TNT.wiki
|
|
||||||
|
|
||||||
# Copy documentation
|
|
||||||
cp ../docs/Development-Guide.md ./Development-Guide.md
|
|
||||||
cp ../docs/CONTRIBUTING.md ./Contributing.md
|
|
||||||
cp ../docs/DEPLOYMENT.md ./Deployment.md
|
|
||||||
|
|
||||||
# Commit and push
|
|
||||||
git add .
|
|
||||||
git commit -m "docs: add development documentation"
|
|
||||||
git push origin master
|
|
||||||
```
|
|
||||||
|
|
||||||
## Wiki Page Structure
|
|
||||||
|
|
||||||
Recommended structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
Home - Project overview (from README.md)
|
|
||||||
├── Development-Guide - Complete dev manual
|
|
||||||
├── Contributing - How to contribute
|
|
||||||
├── Deployment - Production deployment
|
|
||||||
├── Security-Reference - Security configuration
|
|
||||||
└── Quick-Reference - Command cheat sheet
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- GitHub Wiki uses its own git repository (separate from main repo)
|
|
||||||
- Wiki pages are written in Markdown
|
|
||||||
- Page titles become URLs (spaces → hyphens)
|
|
||||||
- First page must be created via Web UI
|
|
||||||
- After that, you can use git to manage content
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**All documentation is already in `docs/` directory and can be viewed directly in the repository.**
|
|
||||||
|
|
@ -9,9 +9,6 @@
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
|
||||||
/* Project Metadata */
|
|
||||||
#define TNT_VERSION "1.0.0"
|
|
||||||
|
|
||||||
/* Configuration constants */
|
/* Configuration constants */
|
||||||
#define DEFAULT_PORT 2222
|
#define DEFAULT_PORT 2222
|
||||||
#define MAX_MESSAGES 100
|
#define MAX_MESSAGES 100
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ 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[256];
|
|
||||||
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 */
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,6 @@ int utf8_strlen(const char *str);
|
||||||
/* Remove last UTF-8 character from string */
|
/* Remove last UTF-8 character from string */
|
||||||
void utf8_remove_last_char(char *str);
|
void utf8_remove_last_char(char *str);
|
||||||
|
|
||||||
/* Remove last word from string (mimic Ctrl+W) */
|
|
||||||
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);
|
||||||
|
|
||||||
|
|
|
||||||
109
src/message.c
109
src/message.c
|
|
@ -8,7 +8,7 @@ void message_init(void) {
|
||||||
/* Nothing to initialize for now */
|
/* Nothing to initialize for now */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Load messages from log file - Optimized for large files */
|
/* Load messages from log file */
|
||||||
int message_load(message_t **messages, int max_messages) {
|
int message_load(message_t **messages, int max_messages) {
|
||||||
/* 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));
|
||||||
|
|
@ -23,75 +23,56 @@ int message_load(message_t **messages, int max_messages) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Seek to end */
|
|
||||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
|
||||||
fclose(fp);
|
|
||||||
*messages = msg_array;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
long file_size = ftell(fp);
|
|
||||||
if (file_size == 0) {
|
|
||||||
fclose(fp);
|
|
||||||
*messages = msg_array;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scan backwards to find the start position */
|
|
||||||
int newlines_found = 0;
|
|
||||||
long pos = file_size - 1;
|
|
||||||
/* Skip the very last byte if it's a newline */
|
|
||||||
if (pos >= 0) {
|
|
||||||
/* Read last char */
|
|
||||||
fseek(fp, pos, SEEK_SET);
|
|
||||||
if (fgetc(fp) == '\n') {
|
|
||||||
pos--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Read backwards in chunks for performance */
|
|
||||||
#define CHUNK_SIZE 4096
|
|
||||||
char chunk[CHUNK_SIZE];
|
|
||||||
|
|
||||||
while (pos >= 0 && newlines_found < max_messages) {
|
|
||||||
long read_size = (pos >= CHUNK_SIZE) ? CHUNK_SIZE : (pos + 1);
|
|
||||||
long read_pos = pos - read_size + 1;
|
|
||||||
|
|
||||||
fseek(fp, read_pos, SEEK_SET);
|
|
||||||
if (fread(chunk, 1, read_size, fp) != (size_t)read_size) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scan chunk backwards */
|
|
||||||
for (int i = read_size - 1; i >= 0; i--) {
|
|
||||||
if (chunk[i] == '\n') {
|
|
||||||
newlines_found++;
|
|
||||||
if (newlines_found >= max_messages) {
|
|
||||||
/* Found our start point: one char after this newline */
|
|
||||||
fseek(fp, read_pos + i + 1, SEEK_SET);
|
|
||||||
goto read_messages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pos -= read_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* If we got here, we reached start of file or didn't find enough newlines */
|
|
||||||
fseek(fp, 0, SEEK_SET);
|
|
||||||
|
|
||||||
read_messages:;
|
|
||||||
char line[2048];
|
char line[2048];
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
|
||||||
/* Now read forward */
|
/* Use a ring buffer approach - keep only last max_messages */
|
||||||
|
/* First, count total lines and seek to appropriate position */
|
||||||
|
/* Use dynamic allocation to handle large log files */
|
||||||
|
long *file_pos = NULL;
|
||||||
|
int pos_capacity = 1000;
|
||||||
|
int line_count = 0;
|
||||||
|
int start_index = 0;
|
||||||
|
|
||||||
|
/* Allocate initial position array */
|
||||||
|
file_pos = malloc(pos_capacity * sizeof(long));
|
||||||
|
if (!file_pos) {
|
||||||
|
fclose(fp);
|
||||||
|
*messages = msg_array;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Record file positions */
|
||||||
|
while (fgets(line, sizeof(line), fp)) {
|
||||||
|
/* Expand array if needed */
|
||||||
|
if (line_count >= pos_capacity) {
|
||||||
|
int new_capacity = pos_capacity * 2;
|
||||||
|
long *new_pos = realloc(file_pos, new_capacity * sizeof(long));
|
||||||
|
if (!new_pos) {
|
||||||
|
/* Out of memory, stop scanning */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
file_pos = new_pos;
|
||||||
|
pos_capacity = new_capacity;
|
||||||
|
}
|
||||||
|
file_pos[line_count++] = ftell(fp) - strlen(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Determine where to start reading */
|
||||||
|
if (line_count > max_messages) {
|
||||||
|
start_index = line_count - max_messages;
|
||||||
|
fseek(fp, file_pos[start_index], SEEK_SET);
|
||||||
|
} else {
|
||||||
|
fseek(fp, 0, SEEK_SET);
|
||||||
|
start_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Now read the messages */
|
||||||
while (fgets(line, sizeof(line), fp) && count < max_messages) {
|
while (fgets(line, sizeof(line), fp) && count < max_messages) {
|
||||||
/* Check for oversized lines */
|
/* Check for oversized lines */
|
||||||
size_t line_len = strlen(line);
|
size_t line_len = strlen(line);
|
||||||
if (line_len >= sizeof(line) - 1) {
|
if (line_len >= sizeof(line) - 1) {
|
||||||
/* Skip remainder of line */
|
fprintf(stderr, "Warning: Skipping oversized line in messages.log\n");
|
||||||
int c;
|
|
||||||
while ((c = fgetc(fp)) != '\n' && c != EOF);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,6 +109,7 @@ read_messages:;
|
||||||
time_t msg_time = mktime(&tm);
|
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) {
|
||||||
|
/* Skip messages more than 1 day in future or 10 years in past */
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,6 +121,7 @@ read_messages:;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
free(file_pos);
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
*messages = msg_array;
|
*messages = msg_array;
|
||||||
return count;
|
return count;
|
||||||
|
|
|
||||||
397
src/ssh_server.c
397
src/ssh_server.c
|
|
@ -17,19 +17,6 @@
|
||||||
/* Global SSH bind instance */
|
/* Global SSH bind instance */
|
||||||
static ssh_bind g_sshbind = NULL;
|
static ssh_bind g_sshbind = NULL;
|
||||||
|
|
||||||
/* Session context for callback-based API */
|
|
||||||
typedef struct {
|
|
||||||
char client_ip[INET6_ADDRSTRLEN];
|
|
||||||
int pty_width;
|
|
||||||
int pty_height;
|
|
||||||
char exec_command[256];
|
|
||||||
bool auth_success;
|
|
||||||
int auth_attempts;
|
|
||||||
bool channel_ready; /* Set when shell/exec request received */
|
|
||||||
ssh_channel channel; /* Channel created in callback */
|
|
||||||
struct ssh_channel_callbacks_struct *channel_cb; /* Channel callbacks */
|
|
||||||
} session_context_t;
|
|
||||||
|
|
||||||
/* Rate limiting and connection tracking */
|
/* Rate limiting and connection tracking */
|
||||||
#define MAX_TRACKED_IPS 256
|
#define MAX_TRACKED_IPS 256
|
||||||
#define RATE_LIMIT_WINDOW 60 /* seconds */
|
#define RATE_LIMIT_WINDOW 60 /* seconds */
|
||||||
|
|
@ -438,7 +425,7 @@ static int read_username(client_t *client) {
|
||||||
if (pos < MAX_USERNAME_LEN - 1) {
|
if (pos < MAX_USERNAME_LEN - 1) {
|
||||||
username[pos++] = b;
|
username[pos++] = b;
|
||||||
username[pos] = '\0';
|
username[pos] = '\0';
|
||||||
client_send(client, (char *)&b, 1);
|
client_send(client, &b, 1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* UTF-8 multi-byte */
|
/* UTF-8 multi-byte */
|
||||||
|
|
@ -571,20 +558,6 @@ static void execute_command(client_t *client) {
|
||||||
|
|
||||||
/* Handle client key press - returns true if key was consumed */
|
/* Handle client key press - returns true if key was consumed */
|
||||||
static bool handle_key(client_t *client, unsigned char key, char *input) {
|
static bool handle_key(client_t *client, unsigned char key, char *input) {
|
||||||
/* Handle Ctrl+C (Exit or switch to NORMAL) */
|
|
||||||
if (key == 3) {
|
|
||||||
if (client->mode != MODE_NORMAL) {
|
|
||||||
client->mode = MODE_NORMAL;
|
|
||||||
client->command_input[0] = '\0';
|
|
||||||
client->show_help = false;
|
|
||||||
tui_render_screen(client);
|
|
||||||
} else {
|
|
||||||
/* In NORMAL mode, Ctrl+C exits */
|
|
||||||
client->connected = false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle help screen */
|
/* Handle help screen */
|
||||||
if (client->show_help) {
|
if (client->show_help) {
|
||||||
if (key == 'q' || key == 27) {
|
if (key == 'q' || key == 27) {
|
||||||
|
|
@ -630,7 +603,7 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
||||||
client->scroll_pos = 0;
|
client->scroll_pos = 0;
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
return true; /* Key consumed */
|
return true; /* Key consumed */
|
||||||
} else if (key == '\r' || key == '\n') { /* Enter */
|
} else if (key == '\r') { /* Enter */
|
||||||
if (input[0] != '\0') {
|
if (input[0] != '\0') {
|
||||||
message_t msg = {
|
message_t msg = {
|
||||||
.timestamp = time(NULL),
|
.timestamp = time(NULL),
|
||||||
|
|
@ -649,18 +622,6 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
||||||
tui_render_input(client, input);
|
tui_render_input(client, input);
|
||||||
}
|
}
|
||||||
return true; /* Key consumed */
|
return true; /* Key consumed */
|
||||||
} else if (key == 23) { /* Ctrl+W (Delete Word) */
|
|
||||||
if (input[0] != '\0') {
|
|
||||||
utf8_remove_last_word(input);
|
|
||||||
tui_render_input(client, input);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else if (key == 21) { /* Ctrl+U (Delete Line) */
|
|
||||||
if (input[0] != '\0') {
|
|
||||||
input[0] = '\0';
|
|
||||||
tui_render_input(client, input);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -729,18 +690,6 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
}
|
}
|
||||||
return true; /* Key consumed */
|
return true; /* Key consumed */
|
||||||
} else if (key == 23) { /* Ctrl+W (Delete Word) */
|
|
||||||
if (client->command_input[0] != '\0') {
|
|
||||||
utf8_remove_last_word(client->command_input);
|
|
||||||
tui_render_screen(client);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else if (key == 21) { /* Ctrl+U (Delete Line) */
|
|
||||||
if (client->command_input[0] != '\0') {
|
|
||||||
client->command_input[0] = '\0';
|
|
||||||
tui_render_screen(client);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -762,20 +711,6 @@ void* client_handle_session(void *arg) {
|
||||||
client->help_lang = LANG_ZH;
|
client->help_lang = LANG_ZH;
|
||||||
client->connected = true;
|
client->connected = true;
|
||||||
|
|
||||||
/* Check for exec command */
|
|
||||||
if (client->exec_command[0] != '\0') {
|
|
||||||
if (strcmp(client->exec_command, "exit") == 0) {
|
|
||||||
/* Just exit */
|
|
||||||
ssh_channel_request_send_exit_status(client->channel, 0);
|
|
||||||
goto cleanup;
|
|
||||||
} else {
|
|
||||||
/* Unknown command */
|
|
||||||
client_printf(client, "Command not supported: %s\r\nOnly 'exit' is supported in non-interactive mode.\r\n", client->exec_command);
|
|
||||||
ssh_channel_request_send_exit_status(client->channel, 1);
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Read username */
|
/* Read username */
|
||||||
if (read_username(client) < 0) {
|
if (read_username(client) < 0) {
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
|
|
@ -824,6 +759,9 @@ void* client_handle_session(void *arg) {
|
||||||
|
|
||||||
unsigned char b = buf[0];
|
unsigned char b = buf[0];
|
||||||
|
|
||||||
|
/* Ctrl+C */
|
||||||
|
if (b == 3) break;
|
||||||
|
|
||||||
/* Handle special keys - returns true if key was consumed */
|
/* Handle special keys - returns true if key was consumed */
|
||||||
bool key_consumed = handle_key(client, b, input);
|
bool key_consumed = handle_key(client, b, input);
|
||||||
|
|
||||||
|
|
@ -903,157 +841,150 @@ cleanup:
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Authentication callbacks for callback-based API */
|
/* Handle SSH authentication with optional token */
|
||||||
|
static int handle_auth(ssh_session session, const char *client_ip) {
|
||||||
|
ssh_message message;
|
||||||
|
int auth_attempts = 0;
|
||||||
|
|
||||||
/* Password authentication callback */
|
do {
|
||||||
static int auth_password(ssh_session session, const char *user,
|
message = ssh_message_get(session);
|
||||||
const char *password, void *userdata) {
|
if (!message) break;
|
||||||
(void)user; /* Unused - we don't validate usernames */
|
|
||||||
session_context_t *ctx = (session_context_t *)userdata;
|
|
||||||
|
|
||||||
ctx->auth_attempts++;
|
if (ssh_message_type(message) == SSH_REQUEST_AUTH) {
|
||||||
|
auth_attempts++;
|
||||||
|
|
||||||
/* Limit auth attempts */
|
/* Limit auth attempts */
|
||||||
if (ctx->auth_attempts > 3) {
|
if (auth_attempts > 3) {
|
||||||
record_auth_failure(ctx->client_ip);
|
record_auth_failure(client_ip);
|
||||||
fprintf(stderr, "Too many auth attempts from %s\n", ctx->client_ip);
|
ssh_message_free(message);
|
||||||
ssh_disconnect(session);
|
fprintf(stderr, "Too many auth attempts from %s\n", client_ip);
|
||||||
return SSH_AUTH_DENIED;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ssh_message_subtype(message) == SSH_AUTH_METHOD_PASSWORD) {
|
||||||
|
const char *password = ssh_message_auth_password(message);
|
||||||
|
|
||||||
/* If access token is configured, require it */
|
/* If access token is configured, require it */
|
||||||
if (g_access_token[0] != '\0') {
|
if (g_access_token[0] != '\0') {
|
||||||
if (password && strcmp(password, g_access_token) == 0) {
|
if (password && strcmp(password, g_access_token) == 0) {
|
||||||
/* Token matches */
|
/* Token matches */
|
||||||
ctx->auth_success = true;
|
ssh_message_auth_reply_success(message, 0);
|
||||||
return SSH_AUTH_SUCCESS;
|
ssh_message_free(message);
|
||||||
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
/* Wrong token */
|
/* Wrong token */
|
||||||
record_auth_failure(ctx->client_ip);
|
record_auth_failure(client_ip);
|
||||||
|
ssh_message_reply_default(message);
|
||||||
|
ssh_message_free(message);
|
||||||
sleep(2); /* Slow down brute force */
|
sleep(2); /* Slow down brute force */
|
||||||
return SSH_AUTH_DENIED;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* No token configured, accept any password */
|
/* No token configured, accept any password */
|
||||||
ctx->auth_success = true;
|
ssh_message_auth_reply_success(message, 0);
|
||||||
return SSH_AUTH_SUCCESS;
|
ssh_message_free(message);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
} else if (ssh_message_subtype(message) == SSH_AUTH_METHOD_NONE) {
|
||||||
|
|
||||||
/* Passwordless (none) authentication callback */
|
|
||||||
static int auth_none(ssh_session session, const char *user, void *userdata) {
|
|
||||||
(void)session; /* Unused */
|
|
||||||
(void)user; /* Unused */
|
|
||||||
session_context_t *ctx = (session_context_t *)userdata;
|
|
||||||
|
|
||||||
/* If access token is configured, reject passwordless */
|
/* If access token is configured, reject passwordless */
|
||||||
if (g_access_token[0] != '\0') {
|
if (g_access_token[0] != '\0') {
|
||||||
return SSH_AUTH_DENIED;
|
ssh_message_reply_default(message);
|
||||||
|
ssh_message_free(message);
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
/* No token configured, allow passwordless */
|
/* No token configured, allow passwordless */
|
||||||
ctx->auth_success = true;
|
ssh_message_auth_reply_success(message, 0);
|
||||||
return SSH_AUTH_SUCCESS;
|
ssh_message_free(message);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ssh_message_reply_default(message);
|
||||||
|
ssh_message_free(message);
|
||||||
|
} while (1);
|
||||||
|
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Forward declaration of channel callbacks setup */
|
/* Handle SSH channel requests */
|
||||||
static void setup_channel_callbacks(ssh_channel channel, session_context_t *ctx);
|
static ssh_channel handle_channel_open(ssh_session session) {
|
||||||
|
ssh_message message;
|
||||||
|
ssh_channel channel = NULL;
|
||||||
|
|
||||||
/* Channel open callback */
|
do {
|
||||||
static ssh_channel channel_open_request_session(ssh_session session, void *userdata) {
|
message = ssh_message_get(session);
|
||||||
session_context_t *ctx = (session_context_t *)userdata;
|
if (!message) break;
|
||||||
ssh_channel channel;
|
|
||||||
|
|
||||||
channel = ssh_channel_new(session);
|
|
||||||
if (channel == NULL) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Store channel in context for main loop */
|
|
||||||
ctx->channel = channel;
|
|
||||||
|
|
||||||
/* Set up channel-specific callbacks (PTY, shell, exec) */
|
|
||||||
setup_channel_callbacks(channel, ctx);
|
|
||||||
|
|
||||||
|
if (ssh_message_type(message) == SSH_REQUEST_CHANNEL_OPEN &&
|
||||||
|
ssh_message_subtype(message) == SSH_CHANNEL_SESSION) {
|
||||||
|
channel = ssh_message_channel_request_open_reply_accept(message);
|
||||||
|
ssh_message_free(message);
|
||||||
return channel;
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssh_message_reply_default(message);
|
||||||
|
ssh_message_free(message);
|
||||||
|
} while (1);
|
||||||
|
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Channel callback functions */
|
/* Handle PTY request and get terminal size */
|
||||||
|
static int handle_pty_request(ssh_channel channel, client_t *client) {
|
||||||
|
ssh_message message;
|
||||||
|
int pty_received = 0;
|
||||||
|
int shell_received = 0;
|
||||||
|
|
||||||
/* PTY request callback */
|
do {
|
||||||
static int channel_pty_request(ssh_session session, ssh_channel channel,
|
message = ssh_message_get(ssh_channel_get_session(channel));
|
||||||
const char *term, int width, int height,
|
if (!message) break;
|
||||||
int pxwidth, int pxheight, void *userdata) {
|
|
||||||
(void)session; /* Unused */
|
|
||||||
(void)channel; /* Unused */
|
|
||||||
(void)term; /* Unused */
|
|
||||||
(void)pxwidth; /* Unused */
|
|
||||||
(void)pxheight; /* Unused */
|
|
||||||
|
|
||||||
session_context_t *ctx = (session_context_t *)userdata;
|
if (ssh_message_type(message) == SSH_REQUEST_CHANNEL) {
|
||||||
|
if (ssh_message_subtype(message) == SSH_CHANNEL_REQUEST_PTY) {
|
||||||
/* Store terminal dimensions */
|
/* Get terminal dimensions from PTY request */
|
||||||
ctx->pty_width = width;
|
client->width = ssh_message_channel_request_pty_width(message);
|
||||||
ctx->pty_height = height;
|
client->height = ssh_message_channel_request_pty_height(message);
|
||||||
|
|
||||||
/* Default to 80x24 if invalid */
|
/* Default to 80x24 if invalid */
|
||||||
if (ctx->pty_width <= 0 || ctx->pty_width > 500) ctx->pty_width = 80;
|
if (client->width <= 0 || client->width > 500) client->width = 80;
|
||||||
if (ctx->pty_height <= 0 || ctx->pty_height > 200) ctx->pty_height = 24;
|
if (client->height <= 0 || client->height > 200) client->height = 24;
|
||||||
|
|
||||||
return SSH_OK;
|
ssh_message_channel_request_reply_success(message);
|
||||||
}
|
ssh_message_free(message);
|
||||||
|
pty_received = 1;
|
||||||
|
|
||||||
/* Shell request callback */
|
/* Don't return yet, wait for shell request */
|
||||||
static int channel_shell_request(ssh_session session, ssh_channel channel,
|
if (shell_received) {
|
||||||
void *userdata) {
|
return 0;
|
||||||
(void)session; /* Unused */
|
}
|
||||||
(void)channel; /* Unused */
|
continue;
|
||||||
|
|
||||||
session_context_t *ctx = (session_context_t *)userdata;
|
} else if (ssh_message_subtype(message) == SSH_CHANNEL_REQUEST_SHELL) {
|
||||||
|
ssh_message_channel_request_reply_success(message);
|
||||||
|
ssh_message_free(message);
|
||||||
|
shell_received = 1;
|
||||||
|
|
||||||
/* Mark channel as ready */
|
/* If we got PTY, we're done */
|
||||||
ctx->channel_ready = true;
|
if (pty_received) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
|
||||||
/* Accept shell request */
|
} else if (ssh_message_subtype(message) == SSH_CHANNEL_REQUEST_WINDOW_CHANGE) {
|
||||||
return SSH_OK;
|
/* Handle terminal resize - this should be handled during session, not here */
|
||||||
}
|
/* For now, just acknowledge and ignore during init */
|
||||||
|
ssh_message_channel_request_reply_success(message);
|
||||||
/* Exec request callback */
|
ssh_message_free(message);
|
||||||
static int channel_exec_request(ssh_session session, ssh_channel channel,
|
continue;
|
||||||
const char *command, void *userdata) {
|
}
|
||||||
(void)session; /* Unused */
|
|
||||||
(void)channel; /* Unused */
|
|
||||||
|
|
||||||
session_context_t *ctx = (session_context_t *)userdata;
|
|
||||||
|
|
||||||
/* Store exec command */
|
|
||||||
if (command) {
|
|
||||||
strncpy(ctx->exec_command, command, sizeof(ctx->exec_command) - 1);
|
|
||||||
ctx->exec_command[sizeof(ctx->exec_command) - 1] = '\0';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mark channel as ready */
|
ssh_message_reply_default(message);
|
||||||
ctx->channel_ready = true;
|
ssh_message_free(message);
|
||||||
|
} while (!pty_received || !shell_received);
|
||||||
|
|
||||||
return SSH_OK;
|
return (pty_received && shell_received) ? 0 : -1;
|
||||||
}
|
|
||||||
|
|
||||||
/* Set up channel callbacks */
|
|
||||||
static void setup_channel_callbacks(ssh_channel channel, session_context_t *ctx) {
|
|
||||||
/* Allocate channel callbacks on heap to persist */
|
|
||||||
ctx->channel_cb = calloc(1, sizeof(struct ssh_channel_callbacks_struct));
|
|
||||||
if (!ctx->channel_cb) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ssh_callbacks_init(ctx->channel_cb);
|
|
||||||
|
|
||||||
ctx->channel_cb->userdata = ctx;
|
|
||||||
ctx->channel_cb->channel_pty_request_function = channel_pty_request;
|
|
||||||
ctx->channel_cb->channel_shell_request_function = channel_shell_request;
|
|
||||||
ctx->channel_cb->channel_exec_request_function = channel_exec_request;
|
|
||||||
|
|
||||||
ssh_set_channel_callbacks(channel, ctx->channel_cb);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Initialize SSH server */
|
/* Initialize SSH server */
|
||||||
|
|
@ -1124,124 +1055,53 @@ int ssh_server_start(int unused) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create session context for callbacks */
|
/* Get client IP address */
|
||||||
session_context_t *ctx = calloc(1, sizeof(session_context_t));
|
char client_ip[INET6_ADDRSTRLEN];
|
||||||
if (!ctx) {
|
get_client_ip(session, client_ip, sizeof(client_ip));
|
||||||
ssh_disconnect(session);
|
|
||||||
ssh_free(session);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Initialize context */
|
|
||||||
get_client_ip(session, ctx->client_ip, sizeof(ctx->client_ip));
|
|
||||||
ctx->pty_width = 80; /* Default */
|
|
||||||
ctx->pty_height = 24; /* Default */
|
|
||||||
ctx->exec_command[0] = '\0';
|
|
||||||
ctx->auth_success = false;
|
|
||||||
ctx->auth_attempts = 0;
|
|
||||||
ctx->channel_ready = false;
|
|
||||||
ctx->channel = NULL;
|
|
||||||
ctx->channel_cb = NULL;
|
|
||||||
|
|
||||||
/* Check rate limit */
|
/* Check rate limit */
|
||||||
if (!check_rate_limit(ctx->client_ip)) {
|
if (!check_rate_limit(client_ip)) {
|
||||||
ssh_disconnect(session);
|
ssh_disconnect(session);
|
||||||
ssh_free(session);
|
ssh_free(session);
|
||||||
free(ctx);
|
|
||||||
sleep(1); /* Slow down blocked clients */
|
sleep(1); /* Slow down blocked clients */
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check total connection limit */
|
/* Check total connection limit */
|
||||||
if (!check_and_increment_connections()) {
|
if (!check_and_increment_connections()) {
|
||||||
fprintf(stderr, "Max connections reached, rejecting %s\n", ctx->client_ip);
|
fprintf(stderr, "Max connections reached, rejecting %s\n", client_ip);
|
||||||
ssh_disconnect(session);
|
ssh_disconnect(session);
|
||||||
ssh_free(session);
|
ssh_free(session);
|
||||||
free(ctx);
|
|
||||||
sleep(1);
|
sleep(1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Set up server callbacks (auth and channel) */
|
|
||||||
struct ssh_server_callbacks_struct server_cb;
|
|
||||||
memset(&server_cb, 0, sizeof(server_cb));
|
|
||||||
ssh_callbacks_init(&server_cb);
|
|
||||||
|
|
||||||
server_cb.userdata = ctx;
|
|
||||||
server_cb.auth_password_function = auth_password;
|
|
||||||
server_cb.auth_none_function = auth_none;
|
|
||||||
server_cb.channel_open_request_session_function = channel_open_request_session;
|
|
||||||
|
|
||||||
ssh_set_server_callbacks(session, &server_cb);
|
|
||||||
|
|
||||||
/* Perform key exchange */
|
/* Perform key exchange */
|
||||||
if (ssh_handle_key_exchange(session) != SSH_OK) {
|
if (ssh_handle_key_exchange(session) != SSH_OK) {
|
||||||
fprintf(stderr, "Key exchange failed: %s\n", ssh_get_error(session));
|
fprintf(stderr, "Key exchange failed: %s\n", ssh_get_error(session));
|
||||||
decrement_connections();
|
decrement_connections();
|
||||||
ssh_disconnect(session);
|
ssh_disconnect(session);
|
||||||
ssh_free(session);
|
ssh_free(session);
|
||||||
free(ctx);
|
|
||||||
sleep(1);
|
sleep(1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Event loop to handle authentication and channel setup */
|
/* Handle authentication */
|
||||||
ssh_event event = ssh_event_new();
|
if (handle_auth(session, client_ip) < 0) {
|
||||||
if (event == NULL) {
|
fprintf(stderr, "Authentication failed from %s\n", client_ip);
|
||||||
fprintf(stderr, "Failed to create event\n");
|
|
||||||
decrement_connections();
|
decrement_connections();
|
||||||
ssh_disconnect(session);
|
ssh_disconnect(session);
|
||||||
ssh_free(session);
|
ssh_free(session);
|
||||||
free(ctx);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ssh_event_add_session(event, session);
|
|
||||||
|
|
||||||
/* Wait for: auth success, channel open, AND channel ready (PTY/shell/exec) */
|
|
||||||
int timeout_sec = 30;
|
|
||||||
time_t start_time = time(NULL);
|
|
||||||
bool timed_out = false;
|
|
||||||
ssh_channel channel = NULL;
|
|
||||||
|
|
||||||
while ((!ctx->auth_success || ctx->channel == NULL || !ctx->channel_ready) && !timed_out) {
|
|
||||||
/* Poll with 1 second timeout per iteration */
|
|
||||||
int rc = ssh_event_dopoll(event, 1000);
|
|
||||||
|
|
||||||
if (rc == SSH_ERROR) {
|
|
||||||
fprintf(stderr, "Event poll error: %s\n", ssh_get_error(session));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Check timeout */
|
|
||||||
if (time(NULL) - start_time > timeout_sec) {
|
|
||||||
timed_out = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ssh_event_free(event);
|
|
||||||
|
|
||||||
/* Check if authentication succeeded */
|
|
||||||
if (!ctx->auth_success) {
|
|
||||||
fprintf(stderr, "Authentication failed or timed out from %s\n", ctx->client_ip);
|
|
||||||
decrement_connections();
|
|
||||||
ssh_disconnect(session);
|
|
||||||
ssh_free(session);
|
|
||||||
if (ctx->channel_cb) free(ctx->channel_cb);
|
|
||||||
free(ctx);
|
|
||||||
sleep(2); /* Longer delay for auth failures */
|
sleep(2); /* Longer delay for auth failures */
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check if channel opened and is ready */
|
/* Open channel */
|
||||||
channel = ctx->channel;
|
ssh_channel channel = handle_channel_open(session);
|
||||||
if (!channel || !ctx->channel_ready || timed_out) {
|
if (!channel) {
|
||||||
fprintf(stderr, "Failed to open/setup channel from %s\n", ctx->client_ip);
|
fprintf(stderr, "Failed to open channel\n");
|
||||||
decrement_connections();
|
|
||||||
ssh_disconnect(session);
|
ssh_disconnect(session);
|
||||||
ssh_free(session);
|
ssh_free(session);
|
||||||
if (ctx->channel_cb) free(ctx->channel_cb);
|
|
||||||
free(ctx);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1252,29 +1112,22 @@ int ssh_server_start(int unused) {
|
||||||
ssh_channel_free(channel);
|
ssh_channel_free(channel);
|
||||||
ssh_disconnect(session);
|
ssh_disconnect(session);
|
||||||
ssh_free(session);
|
ssh_free(session);
|
||||||
free(ctx);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Initialize client from context */
|
|
||||||
client->session = session;
|
client->session = session;
|
||||||
client->channel = channel;
|
client->channel = channel;
|
||||||
client->fd = -1; /* Not used with SSH */
|
client->fd = -1; /* Not used with SSH */
|
||||||
client->width = ctx->pty_width;
|
|
||||||
client->height = ctx->pty_height;
|
|
||||||
client->ref_count = 1; /* Initial reference */
|
client->ref_count = 1; /* Initial reference */
|
||||||
pthread_mutex_init(&client->ref_lock, NULL);
|
pthread_mutex_init(&client->ref_lock, NULL);
|
||||||
|
|
||||||
/* Copy exec command if any */
|
/* Handle PTY request and get terminal size */
|
||||||
if (ctx->exec_command[0] != '\0') {
|
if (handle_pty_request(channel, client) < 0) {
|
||||||
strncpy(client->exec_command, ctx->exec_command, sizeof(client->exec_command) - 1);
|
/* Set defaults if PTY request fails */
|
||||||
client->exec_command[sizeof(client->exec_command) - 1] = '\0';
|
client->width = 80;
|
||||||
|
client->height = 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Free context and channel callbacks - no longer needed */
|
|
||||||
if (ctx->channel_cb) free(ctx->channel_cb);
|
|
||||||
free(ctx);
|
|
||||||
|
|
||||||
/* Create thread for client */
|
/* Create thread for client */
|
||||||
pthread_t thread;
|
pthread_t thread;
|
||||||
pthread_attr_t attr;
|
pthread_attr_t attr;
|
||||||
|
|
|
||||||
34
src/tui.c
34
src/tui.c
|
|
@ -61,8 +61,8 @@ 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 */
|
/* Clear and move to top */
|
||||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_HOME);
|
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_CLEAR ANSI_HOME);
|
||||||
|
|
||||||
/* Title bar */
|
/* Title bar */
|
||||||
const char *mode_str = (client->mode == MODE_INSERT) ? "INSERT" :
|
const char *mode_str = (client->mode == MODE_INSERT) ? "INSERT" :
|
||||||
|
|
@ -82,21 +82,22 @@ void tui_render_screen(client_t *client) {
|
||||||
for (int i = 0; i < padding; i++) {
|
for (int i = 0; i < padding; i++) {
|
||||||
buffer[pos++] = ' ';
|
buffer[pos++] = ' ';
|
||||||
}
|
}
|
||||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_RESET "\033[K\r\n");
|
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_RESET "\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);
|
||||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "%s\033[K\r\n", msg_line);
|
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "%s\r\n", msg_line);
|
||||||
}
|
}
|
||||||
free(msg_snapshot);
|
free(msg_snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fill empty lines and clear them */
|
/* Fill empty lines */
|
||||||
for (int i = snapshot_count; i < msg_height; i++) {
|
for (int i = snapshot_count; i < msg_height; i++) {
|
||||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "\033[K\r\n");
|
buffer[pos++] = '\r';
|
||||||
|
buffer[pos++] = '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Separator - use box drawing character */
|
/* Separator - use box drawing character */
|
||||||
|
|
@ -106,20 +107,21 @@ void tui_render_screen(client_t *client) {
|
||||||
memcpy(buffer + pos, line_char, len);
|
memcpy(buffer + pos, line_char, len);
|
||||||
pos += len;
|
pos += len;
|
||||||
}
|
}
|
||||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "\033[K\r\n");
|
buffer[pos++] = '\r';
|
||||||
|
buffer[pos++] = '\n';
|
||||||
|
|
||||||
/* Status/Input line */
|
/* Status/Input line */
|
||||||
if (client->mode == MODE_INSERT) {
|
if (client->mode == MODE_INSERT) {
|
||||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "> \033[K");
|
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "> ");
|
||||||
} 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;
|
||||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos,
|
pos += snprintf(buffer + pos, sizeof(buffer) - pos,
|
||||||
"-- NORMAL -- (%d/%d)\033[K", scroll_pos, total);
|
"-- NORMAL -- (%d/%d)", scroll_pos, total);
|
||||||
} else if (client->mode == MODE_COMMAND) {
|
} else if (client->mode == MODE_COMMAND) {
|
||||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos,
|
pos += snprintf(buffer + pos, sizeof(buffer) - pos,
|
||||||
":%s\033[K", client->command_input);
|
":%s", client->command_input);
|
||||||
}
|
}
|
||||||
|
|
||||||
client_send(client, buffer, pos);
|
client_send(client, buffer, pos);
|
||||||
|
|
@ -222,9 +224,7 @@ const char* tui_get_help_text(help_lang_t lang) {
|
||||||
" ESC - Enter NORMAL mode\n"
|
" ESC - Enter NORMAL mode\n"
|
||||||
" Enter - Send message\n"
|
" Enter - Send message\n"
|
||||||
" Backspace - Delete character\n"
|
" Backspace - Delete character\n"
|
||||||
" Ctrl+W - Delete last word\n"
|
" Ctrl+C - Exit chat\n"
|
||||||
" Ctrl+U - Delete line\n"
|
|
||||||
" Ctrl+C - Enter NORMAL mode\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"NORMAL MODE KEYS:\n"
|
"NORMAL MODE KEYS:\n"
|
||||||
" i - Return to INSERT mode\n"
|
" i - Return to INSERT mode\n"
|
||||||
|
|
@ -240,8 +240,6 @@ const char* tui_get_help_text(help_lang_t lang) {
|
||||||
" Enter - Execute command\n"
|
" Enter - Execute command\n"
|
||||||
" ESC - Cancel, return to NORMAL\n"
|
" ESC - Cancel, return to NORMAL\n"
|
||||||
" Backspace - Delete character\n"
|
" Backspace - Delete character\n"
|
||||||
" Ctrl+W - Delete last word\n"
|
|
||||||
" Ctrl+U - Delete line\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"AVAILABLE COMMANDS:\n"
|
"AVAILABLE COMMANDS:\n"
|
||||||
" list, users, who - Show online users\n"
|
" list, users, who - Show online users\n"
|
||||||
|
|
@ -268,9 +266,7 @@ const char* tui_get_help_text(help_lang_t lang) {
|
||||||
" ESC - 进入 NORMAL 模式\n"
|
" ESC - 进入 NORMAL 模式\n"
|
||||||
" Enter - 发送消息\n"
|
" Enter - 发送消息\n"
|
||||||
" Backspace - 删除字符\n"
|
" Backspace - 删除字符\n"
|
||||||
" Ctrl+W - 删除上个单词\n"
|
" Ctrl+C - 退出聊天\n"
|
||||||
" Ctrl+U - 删除整行\n"
|
|
||||||
" Ctrl+C - 进入 NORMAL 模式\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"NORMAL 模式按键:\n"
|
"NORMAL 模式按键:\n"
|
||||||
" i - 返回 INSERT 模式\n"
|
" i - 返回 INSERT 模式\n"
|
||||||
|
|
@ -286,8 +282,6 @@ const char* tui_get_help_text(help_lang_t lang) {
|
||||||
" Enter - 执行命令\n"
|
" Enter - 执行命令\n"
|
||||||
" ESC - 取消,返回 NORMAL 模式\n"
|
" ESC - 取消,返回 NORMAL 模式\n"
|
||||||
" Backspace - 删除字符\n"
|
" Backspace - 删除字符\n"
|
||||||
" Ctrl+W - 删除上个单词\n"
|
|
||||||
" Ctrl+U - 删除整行\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"可用命令:\n"
|
"可用命令:\n"
|
||||||
" list, users, who - 显示在线用户\n"
|
" list, users, who - 显示在线用户\n"
|
||||||
|
|
|
||||||
20
src/utf8.c
20
src/utf8.c
|
|
@ -136,26 +136,6 @@ void utf8_remove_last_char(char *str) {
|
||||||
str[i] = '\0';
|
str[i] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove last word from string (mimic Ctrl+W) */
|
|
||||||
void utf8_remove_last_word(char *str) {
|
|
||||||
int len = strlen(str);
|
|
||||||
if (len == 0) return;
|
|
||||||
|
|
||||||
int i = len;
|
|
||||||
|
|
||||||
/* Skip trailing spaces */
|
|
||||||
while (i > 0 && str[i - 1] == ' ') {
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Skip non-spaces (the word) */
|
|
||||||
while (i > 0 && str[i - 1] != ' ') {
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
|
|
||||||
str[i] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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) {
|
||||||
if (len <= 0 || len > 4 || !bytes) {
|
if (len <= 0 || len > 4 || !bytes) {
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,20 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Test anonymous SSH access
|
# Test anonymous SSH access
|
||||||
|
|
||||||
BIN="../tnt"
|
|
||||||
PORT=${PORT:-2222}
|
|
||||||
|
|
||||||
if [ ! -f "$BIN" ]; then
|
|
||||||
echo "Error: Binary $BIN not found."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Starting TNT server on port $PORT..."
|
|
||||||
$BIN -p $PORT > /dev/null 2>&1 &
|
|
||||||
SERVER_PID=$!
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
kill $SERVER_PID 2>/dev/null
|
|
||||||
wait 2>/dev/null
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
# Detect timeout command
|
|
||||||
TIMEOUT_CMD="timeout"
|
|
||||||
if command -v gtimeout >/dev/null 2>&1; then
|
|
||||||
TIMEOUT_CMD="gtimeout"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Testing anonymous SSH access to TNT server..."
|
echo "Testing anonymous SSH access to TNT server..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Test 1: Connection with any username and password
|
# Test 1: Connection with any username and password
|
||||||
echo "Test 1: Connection with any username (should succeed)"
|
echo "Test 1: Connection with any username (should succeed)"
|
||||||
$TIMEOUT_CMD 10 expect -c "
|
timeout 5 expect -c '
|
||||||
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $PORT testuser@localhost
|
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2223 testuser@localhost
|
||||||
expect {
|
expect {
|
||||||
\"password:\" {
|
"password:" {
|
||||||
send \"anypassword\r\"
|
send "anypassword\r"
|
||||||
expect {
|
expect {
|
||||||
\"请输入用户名\" {
|
"请输入用户名:" {
|
||||||
send \"TestUser\r\"
|
send "TestUser\r"
|
||||||
send \"\003\"
|
send "\003"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
timeout { exit 1 }
|
timeout { exit 1 }
|
||||||
|
|
@ -47,28 +22,27 @@ expect {
|
||||||
}
|
}
|
||||||
timeout { exit 1 }
|
timeout { exit 1 }
|
||||||
}
|
}
|
||||||
" 2>&1 | grep -q "请输入用户名"
|
' 2>&1 | grep -q "请输入用户名"
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "✓ Test 1 PASSED: Can connect with any password"
|
echo "✓ Test 1 PASSED: Can connect with any password"
|
||||||
else
|
else
|
||||||
echo "✗ Test 1 FAILED: Cannot connect with any password"
|
echo "✗ Test 1 FAILED: Cannot connect with any password"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Test 2: Connection should work with empty password
|
# Test 2: Connection should work without special SSH options
|
||||||
echo "Test 2: Simple connection (standard SSH command)"
|
echo "Test 2: Simple connection (standard SSH command)"
|
||||||
$TIMEOUT_CMD 10 expect -c "
|
timeout 5 expect -c '
|
||||||
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $PORT anonymous@localhost
|
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2223 anonymous@localhost
|
||||||
expect {
|
expect {
|
||||||
\"password:\" {
|
"password:" {
|
||||||
send \"\r\"
|
send "\r"
|
||||||
expect {
|
expect {
|
||||||
\"请输入用户名\" {
|
"请输入用户名:" {
|
||||||
send \"\r\"
|
send "\r"
|
||||||
send \"\003\"
|
send "\003"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
timeout { exit 1 }
|
timeout { exit 1 }
|
||||||
|
|
@ -76,15 +50,13 @@ expect {
|
||||||
}
|
}
|
||||||
timeout { exit 1 }
|
timeout { exit 1 }
|
||||||
}
|
}
|
||||||
" 2>&1 | grep -q "请输入用户名"
|
' 2>&1 | grep -q "请输入用户名"
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "✓ Test 2 PASSED: Can connect with empty password"
|
echo "✓ Test 2 PASSED: Can connect with empty password"
|
||||||
else
|
else
|
||||||
echo "✗ Test 2 FAILED: Cannot connect with empty password"
|
echo "✗ Test 2 FAILED: Cannot connect with empty password"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Anonymous access test completed."
|
echo "Anonymous access test completed."
|
||||||
exit 0
|
|
||||||
|
|
@ -13,26 +13,12 @@ cleanup() {
|
||||||
|
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
# Detect timeout command
|
|
||||||
TIMEOUT_CMD="timeout"
|
|
||||||
if command -v gtimeout >/dev/null 2>&1; then
|
|
||||||
TIMEOUT_CMD="gtimeout"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "=== TNT Basic Tests ==="
|
echo "=== TNT Basic Tests ==="
|
||||||
|
|
||||||
# Path to binary
|
|
||||||
BIN="../tnt"
|
|
||||||
|
|
||||||
if [ ! -f "$BIN" ]; then
|
|
||||||
echo "Error: Binary $BIN not found. Run make first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Start server
|
# Start server
|
||||||
$BIN -p $PORT >test.log 2>&1 &
|
./tnt -p $PORT >test.log 2>&1 &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
sleep 5
|
sleep 2
|
||||||
|
|
||||||
# Test 1: Server started
|
# Test 1: Server started
|
||||||
if kill -0 $SERVER_PID 2>/dev/null; then
|
if kill -0 $SERVER_PID 2>/dev/null; then
|
||||||
|
|
@ -45,7 +31,7 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 2: SSH connection
|
# Test 2: SSH connection
|
||||||
if $TIMEOUT_CMD 5 ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
if timeout 5 ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||||
-o BatchMode=yes -p $PORT localhost exit 2>/dev/null; then
|
-o BatchMode=yes -p $PORT localhost exit 2>/dev/null; then
|
||||||
echo "✓ SSH connection works"
|
echo "✓ SSH connection works"
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
|
|
@ -55,7 +41,7 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 3: Message logging
|
# Test 3: Message logging
|
||||||
(echo "testuser"; echo "test message"; sleep 1) | $TIMEOUT_CMD 5 ssh -o StrictHostKeyChecking=no \
|
echo "test message" | timeout 5 ssh -o StrictHostKeyChecking=no \
|
||||||
-o UserKnownHostsFile=/dev/null -p $PORT localhost >/dev/null 2>&1 &
|
-o UserKnownHostsFile=/dev/null -p $PORT localhost >/dev/null 2>&1 &
|
||||||
sleep 3
|
sleep 3
|
||||||
if [ -f messages.log ]; then
|
if [ -f messages.log ]; then
|
||||||
|
|
@ -27,7 +27,7 @@ fail() {
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
pkill -f "^\.\./tnt" 2>/dev/null || true
|
pkill -f "^\./tnt" 2>/dev/null || true
|
||||||
sleep 1
|
sleep 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,22 +37,10 @@ echo -e "${YELLOW}========================================${NC}"
|
||||||
echo -e "${YELLOW}TNT Security Features Test Suite${NC}"
|
echo -e "${YELLOW}TNT Security Features Test Suite${NC}"
|
||||||
echo -e "${YELLOW}========================================${NC}"
|
echo -e "${YELLOW}========================================${NC}"
|
||||||
|
|
||||||
BIN="../tnt"
|
|
||||||
if [ ! -f "$BIN" ]; then
|
|
||||||
echo "Error: Binary $BIN not found."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Detect timeout command
|
|
||||||
TIMEOUT_CMD="timeout"
|
|
||||||
if command -v gtimeout >/dev/null 2>&1; then
|
|
||||||
TIMEOUT_CMD="gtimeout"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 1: 4096-bit RSA Key Generation
|
# Test 1: 4096-bit RSA Key Generation
|
||||||
print_test "1. RSA 4096-bit Key Generation"
|
print_test "1. RSA 4096-bit Key Generation"
|
||||||
rm -f host_key
|
rm -f host_key
|
||||||
$BIN &
|
./tnt &
|
||||||
PID=$!
|
PID=$!
|
||||||
sleep 8 # Wait for key generation
|
sleep 8 # Wait for key generation
|
||||||
kill $PID 2>/dev/null || true
|
kill $PID 2>/dev/null || true
|
||||||
|
|
@ -67,12 +55,7 @@ if [ -f host_key ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check permissions
|
# Check permissions
|
||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
||||||
PERMS=$(stat -f "%OLp" host_key)
|
PERMS=$(stat -f "%OLp" host_key)
|
||||||
else
|
|
||||||
PERMS=$(stat -c "%a" host_key)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$PERMS" = "600" ]; then
|
if [ "$PERMS" = "600" ]; then
|
||||||
pass "Host key has secure permissions (600)"
|
pass "Host key has secure permissions (600)"
|
||||||
else
|
else
|
||||||
|
|
@ -86,19 +69,19 @@ fi
|
||||||
print_test "2. Environment Variable Configuration"
|
print_test "2. Environment Variable Configuration"
|
||||||
|
|
||||||
# Test bind address
|
# Test bind address
|
||||||
TNT_BIND_ADDR=127.0.0.1 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \
|
TNT_BIND_ADDR=127.0.0.1 timeout 3 ./tnt 2>&1 | grep -q "TNT chat server" && \
|
||||||
pass "TNT_BIND_ADDR configuration works" || fail "TNT_BIND_ADDR not working"
|
pass "TNT_BIND_ADDR configuration works" || fail "TNT_BIND_ADDR not working"
|
||||||
|
|
||||||
# Test with access token set (just verify it starts)
|
# Test with access token set (just verify it starts)
|
||||||
TNT_ACCESS_TOKEN="test123" $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \
|
TNT_ACCESS_TOKEN="test123" timeout 3 ./tnt 2>&1 | grep -q "TNT chat server" && \
|
||||||
pass "TNT_ACCESS_TOKEN configuration accepted" || fail "TNT_ACCESS_TOKEN not working"
|
pass "TNT_ACCESS_TOKEN configuration accepted" || fail "TNT_ACCESS_TOKEN not working"
|
||||||
|
|
||||||
# Test max connections configuration
|
# Test max connections configuration
|
||||||
TNT_MAX_CONNECTIONS=10 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \
|
TNT_MAX_CONNECTIONS=10 timeout 3 ./tnt 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 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 3 ./tnt 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"
|
||||||
|
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
@ -118,7 +101,7 @@ newline
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start server and let it load messages
|
# Start server and let it load messages
|
||||||
$BIN &
|
./tnt &
|
||||||
PID=$!
|
PID=$!
|
||||||
sleep 3
|
sleep 3
|
||||||
kill $PID 2>/dev/null || true
|
kill $PID 2>/dev/null || true
|
||||||
|
|
@ -137,8 +120,8 @@ print_test "4. UTF-8 Input Validation"
|
||||||
cat > test_utf8.c <<'EOF'
|
cat > test_utf8.c <<'EOF'
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include "../include/utf8.h"
|
#include "include/utf8.h"
|
||||||
#include "../include/common.h"
|
#include "include/common.h"
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
// Valid UTF-8 sequences
|
// Valid UTF-8 sequences
|
||||||
|
|
@ -171,7 +154,7 @@ int main() {
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if gcc -I../include -o test_utf8 test_utf8.c ../src/utf8.c 2>/dev/null; then
|
if gcc -I. -o test_utf8 test_utf8.c src/utf8.c 2>/dev/null; then
|
||||||
if ./test_utf8; then
|
if ./test_utf8; then
|
||||||
pass "UTF-8 validation function works correctly"
|
pass "UTF-8 validation function works correctly"
|
||||||
else
|
else
|
||||||
|
|
@ -185,12 +168,12 @@ rm -f test_utf8.c
|
||||||
|
|
||||||
# Test 5: Buffer Safety with AddressSanitizer
|
# Test 5: Buffer Safety with AddressSanitizer
|
||||||
print_test "5. Buffer Overflow Protection (ASAN Build)"
|
print_test "5. Buffer Overflow Protection (ASAN Build)"
|
||||||
if make -C .. clean >/dev/null 2>&1 && make -C .. asan >/dev/null 2>&1; then
|
if make clean >/dev/null 2>&1 && make asan >/dev/null 2>&1; then
|
||||||
# Just verify it compiles - actual ASAN testing needs runtime
|
# Just verify it compiles - actual ASAN testing needs runtime
|
||||||
if [ -f ../tnt ]; then
|
if [ -f tnt ]; then
|
||||||
pass "AddressSanitizer build successful"
|
pass "AddressSanitizer build successful"
|
||||||
# Restore normal build
|
# Restore normal build
|
||||||
make -C .. clean >/dev/null 2>&1 && make -C .. >/dev/null 2>&1
|
make clean >/dev/null 2>&1 && make >/dev/null 2>&1
|
||||||
else
|
else
|
||||||
fail "AddressSanitizer build failed"
|
fail "AddressSanitizer build failed"
|
||||||
fi
|
fi
|
||||||
|
|
@ -201,8 +184,8 @@ fi
|
||||||
# Test 6: Concurrent Safety
|
# Test 6: Concurrent Safety
|
||||||
print_test "6. Concurrency Safety (Data Structure Integrity)"
|
print_test "6. Concurrency Safety (Data Structure Integrity)"
|
||||||
# This test verifies the code compiles with thread sanitizer flags
|
# This test verifies the code compiles with thread sanitizer flags
|
||||||
if gcc -fsanitize=thread -g -O1 -I../include -I/opt/homebrew/opt/libssh/include \
|
if gcc -fsanitize=thread -g -O1 -Iinclude -I/opt/homebrew/opt/libssh/include \
|
||||||
-c ../src/chat_room.c -o /tmp/test_tsan.o 2>/dev/null; then
|
-c src/chat_room.c -o /tmp/test_tsan.o 2>/dev/null; then
|
||||||
pass "Code compiles with ThreadSanitizer (concurrency checks enabled)"
|
pass "Code compiles with ThreadSanitizer (concurrency checks enabled)"
|
||||||
rm -f /tmp/test_tsan.o
|
rm -f /tmp/test_tsan.o
|
||||||
else
|
else
|
||||||
|
|
@ -217,7 +200,7 @@ for i in $(seq 1 2000); do
|
||||||
echo "2026-01-22T$(printf "%02d" $((i/100))):$(printf "%02d" $((i%60))):00Z|user$i|message $i" >> messages.log
|
echo "2026-01-22T$(printf "%02d" $((i/100))):$(printf "%02d" $((i%60))):00Z|user$i|message $i" >> messages.log
|
||||||
done
|
done
|
||||||
|
|
||||||
$BIN &
|
./tnt &
|
||||||
PID=$!
|
PID=$!
|
||||||
sleep 4
|
sleep 4
|
||||||
kill $PID 2>/dev/null || true
|
kill $PID 2>/dev/null || true
|
||||||
|
|
@ -5,21 +5,9 @@
|
||||||
PORT=${PORT:-2222}
|
PORT=${PORT:-2222}
|
||||||
CLIENTS=${1:-10}
|
CLIENTS=${1:-10}
|
||||||
DURATION=${2:-30}
|
DURATION=${2:-30}
|
||||||
BIN="../tnt"
|
|
||||||
|
|
||||||
if [ ! -f "$BIN" ]; then
|
|
||||||
echo "Error: Binary $BIN not found."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Detect timeout command
|
|
||||||
TIMEOUT_CMD="timeout"
|
|
||||||
if command -v gtimeout >/dev/null 2>&1; then
|
|
||||||
TIMEOUT_CMD="gtimeout"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Starting TNT server on port $PORT..."
|
echo "Starting TNT server on port $PORT..."
|
||||||
$BIN -p $PORT &
|
./tnt -p $PORT &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
|
|
@ -33,7 +21,7 @@ echo "Spawning $CLIENTS clients for ${DURATION}s..."
|
||||||
for i in $(seq 1 $CLIENTS); do
|
for i in $(seq 1 $CLIENTS); do
|
||||||
(
|
(
|
||||||
sleep $((i % 5))
|
sleep $((i % 5))
|
||||||
echo "test user $i" | $TIMEOUT_CMD $DURATION ssh -o StrictHostKeyChecking=no \
|
echo "test user $i" | timeout $DURATION ssh -o StrictHostKeyChecking=no \
|
||||||
-o UserKnownHostsFile=/dev/null -p $PORT localhost \
|
-o UserKnownHostsFile=/dev/null -p $PORT localhost \
|
||||||
>/dev/null 2>&1
|
>/dev/null 2>&1
|
||||||
) &
|
) &
|
||||||
|
|
@ -47,10 +35,4 @@ kill $SERVER_PID 2>/dev/null
|
||||||
wait
|
wait
|
||||||
|
|
||||||
echo "Stress test complete"
|
echo "Stress test complete"
|
||||||
if ps aux | grep tnt | grep -v grep > /dev/null; then
|
ps aux | grep tnt | grep -v grep && echo "WARNING: tnt process still running"
|
||||||
echo "WARNING: tnt process still running"
|
|
||||||
else
|
|
||||||
echo "Server shutdown confirmed."
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
Loading…
Reference in a new issue