Commit 90cc0568 authored by Stefy Lanza (nextime / spora )'s avatar Stefy Lanza (nextime / spora )

Merge branch 'v2web' into v2

parents 2b005518 9667e60f
......@@ -5,6 +5,80 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.7.0] - 2025-09-21
### Major Changes
- **Complete Architecture Transition**: Migrated from Python/PyInstaller to native C implementation
- Replaced Python wssshd with high-performance C daemon (`wssshd2/`)
- Eliminated Python runtime dependencies for server component
- Maintained full backward compatibility for client tools
- Improved performance and reduced resource usage
### Added
- **SQLite Database Integration**: Persistent user management with secure storage
- Automatic database creation and schema management
- User authentication with password hashing
- Admin user role management
- Security warnings for default credentials
- Database-backed session management
- **Embedded Web Interface**: Self-contained web server with no external dependencies
- HTML/CSS/JavaScript assets embedded in binary
- User management interface with add/edit/delete functionality
- Real-time terminal interface with fullscreen support
- Responsive Bootstrap-based design
- AJAX-powered dynamic content updates
- **Enhanced Security Features**:
- Automatic security warnings for default admin credentials
- Proper session management with configurable timeouts
- Input validation and bounds checking
- Secure password storage with hashing
- Admin role-based access control
- **Professional ASCII Art**: Startup banner with project branding
- **Comprehensive Debug Options**: Database, web, and general debug logging
- **Smart Directory Selection**: Automatic root vs user directory detection
### Changed
- **Build System**: Complete overhaul for C implementation
- Updated build.sh to compile C binaries
- Modified Debian packaging for C dependencies
- Added SQLite3 and SSL library dependencies
- Removed Python/PyInstaller build process
- **Package Architecture**: Updated wsssh-server package for C binary
- Changed from PyInstaller bundle to native C executable
- Updated dependencies to include libsqlite3-0 and libssl3
- Modified package description and metadata
- Updated man pages for C version options
- **Documentation**: Comprehensive updates for C implementation
- Updated README.md with new architecture details
- Modified installation instructions for C build process
- Updated project structure documentation
- Removed Python-specific references
### Removed
- **Python Server Implementation**: Complete removal of Python wssshd
- Deleted wssshd.py entry point
- Removed wsssd/ Python server directory
- Eliminated templates/ directory (now embedded)
- Cleaned up Python-specific build artifacts
### Technical Details
- **Performance Improvements**: Native C implementation with direct system calls
- **Memory Efficiency**: Reduced memory footprint compared to Python runtime
- **Startup Time**: Faster daemon initialization and connection handling
- **Security**: Enhanced with proper bounds checking and input validation
- **Maintainability**: Cleaner C codebase with modular architecture
### Migration Notes
- **Backward Compatibility**: All existing client functionality preserved
- **Configuration**: Existing config files remain compatible
- **API Compatibility**: WebSocket protocol unchanged for client connections
- **Database Migration**: Automatic creation of new SQLite database on first run
## [1.6.7] - 2025-09-21
### Fixed
......
......@@ -15,15 +15,16 @@
## Overview
WSSSH is a universal tunneling system that provides secure access to remote machines through various transport protocols. The system consists of multiple components implemented in both Python and C, offering high performance and flexibility for different deployment scenarios.
WSSSH is a universal tunneling system that provides secure access to remote machines through various transport protocols. The system consists of multiple components implemented in C, offering high performance, minimal dependencies, and enterprise-grade reliability.
### Key Components
#### Server Component
- **wssshd**: Universal tunneling daemon (Python) with web interface
- **wssshd2**: High-performance C daemon with embedded SQLite database and web interface
- **WebSocket/Transport Support**: Multiple transport protocol implementations
- **Client Management**: Registration and authentication system
- **Client Management**: Registration and authentication system with persistent storage
- **Tunnel Routing**: Intelligent request routing to registered clients
- **Embedded Web UI**: Self-contained web interface with user management
#### C Implementation (Primary)
- **wssshc**: Lightweight C client for registration (OpenSSL-based)
......@@ -566,20 +567,23 @@ Enable debug output for detailed troubleshooting:
```
wsssh/
├── wssshd.py # Universal tunneling daemon
├── build.sh # Build script
├── build.sh # Build script for all components
├── clean.sh # Clean script
├── requirements.txt # Python dependencies
├── cert.pem # SSL certificate
├── key.pem # SSL private key
├── templates/ # Web interface templates
│ ├── base.html # Base template
│ ├── index.html # Dashboard
│ ├── login.html # Authentication
│ ├── terminal.html # HTML5 terminal
│ └── users.html # User management
├── wssshtools/ # C implementation
│ ├── wssshc.c # Client registration
├── wssshd2/ # C server implementation
│ ├── main.c # Server main entry point
│ ├── web.c # Embedded web interface with SQLite
│ ├── config.c # Configuration handling
│ ├── terminal.c # Terminal session management
│ ├── websocket.c # WebSocket protocol handling
│ ├── ssl.c # SSL/TLS encryption
│ ├── tunnel.c # Tunnel management
│ ├── assets.c # Embedded web assets
│ ├── html_pages/ # Embedded HTML templates
│ ├── configure.sh # Build configuration
│ ├── Makefile # Build system
│ └── assets.o # Compiled embedded assets
├── wssshtools/ # C client tools
│ ├── wssshc.c # Client registration tool
│ ├── wsssh.c # SSH wrapper
│ ├── wsscp.c # SCP wrapper
│ ├── wsssht.c # Tunnel setup tool
......@@ -594,46 +598,50 @@ wsssh/
│ │ └── wssshlib.h/c # Core library
│ ├── configure.sh # Build configuration
│ ├── Makefile # Build system
│ ├── man/ # Manual pages
│ └── debian/ # Debian packaging
├── wsssh-server/ # Server package
└── CHANGELOG.md # Version history
├── wsssh-server/ # Server Debian package
├── CHANGELOG.md # Version history
├── DOCUMENTATION.md # Technical documentation
├── TODO.md # Development tasks
├── README.md # Project overview
└── BRIDGE_MODE_TESTING.md # Testing documentation
```
### Dependencies
#### Server Dependencies (wssshd)
- **Python 3.7+**
- **websockets**: WebSocket client/server library
- **Flask**: Web framework for admin interface
- **Flask-Login**: User authentication
- **Flask-SQLAlchemy**: Database support
- **ssl**: SSL/TLS support (built-in)
#### Server Dependencies (wssshd2)
- **GCC**: GNU C Compiler (4.8+)
- **Make**: GNU Make build system
- **SQLite3**: Database library (libsqlite3-dev)
- **OpenSSL**: SSL/TLS library (libssl-dev)
- **pkg-config**: Build configuration tool
#### C Implementation
- **GCC**: GNU C Compiler
#### Client Tools Dependencies (wssshtools)
- **GCC**: GNU C Compiler (4.8+)
- **Make**: GNU Make build system
- **OpenSSL**: SSL/TLS library (libssl-dev)
- **pkg-config**: Build configuration tool
### Building from Source
#### Server Build
#### Server Build (C Implementation)
```bash
# Install Python dependencies
pip3 install -r requirements.txt
# Build server binary
# Build server binary with embedded web interface
./build.sh --server-only
# Or build everything
./build.sh
```
#### C Implementation
#### Client Tools Build
```bash
# Configure and build C tools
cd wssshtools
./configure.sh
make
# Install system-wide
# Install system-wide (optional)
sudo make install
```
......@@ -661,6 +669,39 @@ python3 -m pytest tests/integration/
## Recent Updates
### Version 1.7.0 - Major C Implementation Transition
- **Complete Architecture Migration**: Transitioned from Python/PyInstaller to native C implementation
- Replaced Python wssshd with high-performance C daemon (`wssshd2/`)
- Eliminated Python runtime dependencies for server component
- Maintained full backward compatibility for client tools
- Improved performance and reduced resource usage
- **SQLite Database Integration**: Persistent user management with secure storage
- Automatic database creation and schema management
- User authentication with password hashing
- Admin user role management
- Security warnings for default credentials
- Database-backed session management
- **Embedded Web Interface**: Self-contained web server with no external dependencies
- HTML/CSS/JavaScript assets embedded in binary
- User management interface with add/edit/delete functionality
- Real-time terminal interface with fullscreen support
- Responsive Bootstrap-based design
- AJAX-powered dynamic content updates
- **Enhanced Security Features**:
- Automatic security warnings for default admin credentials
- Proper session management with configurable timeouts
- Input validation and bounds checking
- Secure password storage with hashing
- Admin role-based access control
- **Professional ASCII Art**: Startup banner with project branding
- **Comprehensive Debug Options**: Database, web, and general debug logging
- **Smart Directory Selection**: Automatic root vs user directory detection
- **Updated Debian Packaging**: Modified for C binary with proper dependencies
### Version 1.6.5
- **Flexible Data Encoding Support**: New `--enc` option for wsssht with multiple encoding modes
- `--enc hex`: Hexadecimal encoding of binary data (default, backward compatible)
......
......@@ -26,11 +26,14 @@
### Core Components
#### Server Component (`wssshd`)
- **C Implementation**: High-performance native C daemon with embedded web interface
- **SQLite Database**: Persistent user management with secure password storage
- **Universal Daemon**: Central server managing connections with multiple transport protocols
- **Client Registration**: Handles client authentication with password-based registration
- **Tunnel Routing**: Routes tunnel requests to appropriate registered clients
- **Web Management Interface**: Professional admin panel with user management and HTML5 terminal
- **Session Management**: Maintains persistent connections and handles reconnection logic
- **Security Features**: Automatic warnings for default credentials and secure session handling
#### Client Tools (`wssshtools/`)
......@@ -71,10 +74,7 @@
git clone https://git.nexlab.net/nexlab/wsssh.git
cd wsssh
# Install Python dependencies for the server
pip3 install -r requirements.txt
# Build all components
# Build all components (C implementation with embedded web interface)
./build.sh
```
......@@ -353,37 +353,49 @@ openssl x509 -in cert.pem -text -noout
### Building from Source
```bash
# Install dependencies
pip3 install -r requirements.txt
# Build all components
# Build all components (C implementation)
./build.sh
# Run tests
python3 -m pytest tests/
# Clean build artifacts
./clean.sh
```
### C Implementation Details
The server component (`wssshd2/`) is implemented in C for maximum performance and minimal dependencies:
- **SQLite Database**: Persistent user management
- **Embedded Web Interface**: No external web server required
- **Native SSL/TLS**: Direct OpenSSL integration
- **Cross-platform**: Linux, macOS, Windows support
### Project Structure
```
wsssh/
├── wssshd.py # Universal tunneling daemon
├── build.sh # Build script
├── build.sh # Build script for all components
├── clean.sh # Clean script
├── requirements.txt # Python dependencies
├── wssshtools/ # C implementation directory
├── wssshd2/ # C server implementation
│ ├── main.c # Server main entry point
│ ├── web.c # Embedded web interface with SQLite
│ ├── config.c # Configuration handling
│ ├── terminal.c # Terminal session management
│ ├── websocket.c # WebSocket protocol handling
│ ├── ssl.c # SSL/TLS encryption
│ ├── tunnel.c # Tunnel management
│ ├── assets.c # Embedded web assets
│ ├── html_pages/ # Embedded HTML templates
│ └── Makefile # Build configuration
├── wssshtools/ # C client tools
│ ├── wssshc.c # Client registration tool
│ ├── wsssh.c # SSH wrapper
│ ├── wsscp.c # SCP wrapper
│ ├── wsssht.c # Tunnel setup tool
│ ├── libwsssht/ # Shared libraries
│ ├── man/ # Manual pages
│ └── debian/ # Debian packaging
├── wsssh-server/ # Server Debian package
├── templates/ # Web interface templates
├── static/ # Web assets
└── CHANGELOG.md # Version history
├── CHANGELOG.md # Version history
├── DOCUMENTATION.md # Technical documentation
├── TODO.md # Development tasks
└── README.md # This file
```
## Support
......
# WSSSH: Warp-Powered Stefy's Spatial Secure Hyperdrive - Future Enhancements Roadmap
## Recently Completed (v1.7.0) - Major C Implementation Transition
- [x] **Complete Architecture Migration**: Transitioned from Python/PyInstaller to native C implementation
- Replaced Python wssshd with high-performance C daemon (`wssshd2/`)
- Eliminated Python runtime dependencies for server component
- Maintained full backward compatibility for client tools
- Improved performance and reduced resource usage
- [x] **SQLite Database Integration**: Persistent user management with secure storage
- Automatic database creation and schema management
- User authentication with password hashing
- Admin user role management
- Security warnings for default credentials
- Database-backed session management
- [x] **Embedded Web Interface**: Self-contained web server with no external dependencies
- HTML/CSS/JavaScript assets embedded in binary
- User management interface with add/edit/delete functionality
- Real-time terminal interface with fullscreen support
- Responsive Bootstrap-based design
- AJAX-powered dynamic content updates
- [x] **Enhanced Security Features**:
- Automatic security warnings for default admin credentials
- Proper session management with configurable timeouts
- Input validation and bounds checking
- Secure password storage with hashing
- Admin role-based access control
- [x] **Professional ASCII Art**: Startup banner with project branding
- [x] **Comprehensive Debug Options**: Database, web, and general debug logging
- [x] **Smart Directory Selection**: Automatic root vs user directory detection
- [x] **Updated Debian Packaging**: Modified for C binary with proper dependencies
- [x] **Documentation Updates**: Comprehensive updates for C implementation
## Recently Completed (v1.6.5)
- [x] **Flexible Data Encoding Support**: New `--enc` option for wsssht with multiple encoding modes
- `--enc hex`: Hexadecimal encoding of binary data (default, backward compatible)
......
......@@ -180,14 +180,6 @@ prepare_assets() {
fi
fi
# Generate HTML templates for wssshd2
log_info "Generating HTML templates..."
mkdir -p wssshd2/templates
# Copy templates from root templates directory if they exist
if [[ -d "templates" ]]; then
cp -r templates/* wssshd2/templates/ 2>/dev/null || true
fi
log_success "Assets prepared"
}
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}WebSocket SSH Daemon{% endblock %}</title>
<link rel="icon" href="/image.jpg" type="image/x-icon">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/xterm@5.3.0/css/xterm.css">
<script src="https://unpkg.com/xterm@5.3.0/lib/xterm.js"></script>
<script src="https://unpkg.com/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
<script>
// Ensure libraries are loaded
function checkLibraries() {
if (typeof Terminal === 'undefined') {
console.error('Terminal not loaded from CDN');
return false;
}
if (typeof FitAddon === 'undefined') {
console.error('FitAddon not loaded from CDN');
return false;
}
console.log('All xterm libraries loaded successfully');
return true;
}
// Check immediately and after a delay
if (!checkLibraries()) {
setTimeout(checkLibraries, 1000);
}
</script>
<style>
.navbar-brand {
font-weight: bold;
}
.client-card {
transition: transform 0.2s;
}
.client-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.terminal-container {
background-color: #1e1e1e;
color: #f8f8f2;
font-family: 'Courier New', monospace;
border-radius: 8px;
height: calc(100vh - 200px);
min-height: 400px;
overflow: hidden;
position: relative;
}
.terminal-input {
background: transparent;
border: none;
color: #f8f8f2;
font-family: 'Courier New', monospace;
width: 100%;
outline: none;
}
.terminal-input:focus {
box-shadow: none;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">
<i class="fas fa-terminal"></i> WebSocket SSH Daemon
</a>
<div class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
<span class="navbar-text me-3">
Welcome, {{ current_user.username }}!
</span>
<button class="btn btn-outline-warning btn-sm me-2" data-bs-toggle="modal" data-bs-target="#donationModal">
<i class="fas fa-heart"></i> Donate
</button>
<a class="nav-link" href="{{ url_for('logout') }}">
<i class="fas fa-sign-out-alt"></i> Logout
</a>
{% endif %}
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'info' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<!-- Donation Modal -->
<div class="modal fade" id="donationModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-heart text-danger"></i> Support WebSocket SSH Development
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted">Your support helps us continue developing and maintaining this open-source project!</p>
<div class="row">
<div class="col-md-4 text-center mb-3">
<h6><i class="fab fa-paypal text-primary"></i> PayPal</h6>
<a href="https://www.paypal.com/paypalme/nexlab" target="_blank" class="btn btn-primary btn-sm">
<i class="fab fa-paypal"></i> Donate via PayPal
</a>
<small class="d-block text-muted mt-1">info@nexlab.net</small>
</div>
<div class="col-md-4 text-center mb-3">
<h6><i class="fab fa-bitcoin text-warning"></i> Bitcoin</h6>
<div class="mb-2">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=bitcoin:bc1q3zlkpu95amtcltsk85y0eacyzzk29v68tgc5hx" alt="BTC QR Code" class="img-fluid rounded">
</div>
<div class="input-group input-group-sm">
<input type="text" class="form-control form-control-sm font-monospace" value="bc1q3zlkpu95amtcltsk85y0eacyzzk29v68tgc5hx" readonly style="font-size: 0.75rem;">
<button class="btn btn-outline-secondary btn-sm" type="button" onclick="copyToClipboard('bc1q3zlkpu95amtcltsk85y0eacyzzk29v68tgc5hx')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="col-md-4 text-center mb-3">
<h6><i class="fab fa-ethereum text-secondary"></i> Ethereum</h6>
<div class="mb-2">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=ethereum:0xdA6dAb526515b5cb556d20269207D43fcc760E51" alt="ETH QR Code" class="img-fluid rounded">
</div>
<div class="input-group input-group-sm">
<input type="text" class="form-control form-control-sm font-monospace" value="0xdA6dAb526515b5cb556d20269207D43fcc760E51" readonly style="font-size: 0.75rem;">
<button class="btn btn-outline-secondary btn-sm" type="button" onclick="copyToClipboard('0xdA6dAb526515b5cb556d20269207D43fcc760E51')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
</div>
<hr>
<p class="text-center mb-0">
<small class="text-muted">
Thank you for your support! ❤️
</small>
</p>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
// Show a temporary success message
const btn = event.target.closest('button');
const originalHtml = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i>';
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-success');
setTimeout(() => {
btn.innerHTML = originalHtml;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-secondary');
}, 1000);
});
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Dashboard - WebSocket SSH Daemon{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3 class="card-title mb-0">
<i class="fas fa-server"></i> Connected Clients
</h3>
</div>
<div class="card-body">
{% if clients %}
<div class="row">
{% for client in clients %}
<div class="col-md-4 mb-3">
<div class="card client-card h-100">
<div class="card-body text-center">
<i class="fas fa-desktop fa-3x text-success mb-3"></i>
<h5 class="card-title">{{ client }}</h5>
<p class="card-text text-muted">Connected</p>
<a href="{{ url_for('terminal', client_id=client) }}" class="btn btn-primary">
<i class="fas fa-terminal"></i> Connect
</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-server fa-4x text-muted mb-3"></i>
<h4 class="text-muted">No clients connected</h4>
<p class="text-muted">Clients will appear here when they connect to the daemon.</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h3 class="card-title mb-0">
<i class="fas fa-cogs"></i> Quick Actions
</h3>
</div>
<div class="card-body">
{% if current_user.is_admin %}
<a href="{{ url_for('users') }}" class="btn btn-outline-primary btn-sm mb-2 w-100">
<i class="fas fa-users"></i> Manage Users
</a>
{% endif %}
<button class="btn btn-outline-secondary btn-sm w-100" onclick="location.reload()">
<i class="fas fa-sync"></i> Refresh Status
</button>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title mb-0">
<i class="fas fa-info-circle"></i> System Info
</h3>
</div>
<div class="card-body">
<p class="mb-1"><strong>WebSocket Port:</strong> <span id="websocket-port">{{ websocket_port or 'N/A' }}</span></p>
<p class="mb-1"><strong>Domain:</strong> <span id="domain">{{ domain or 'N/A' }}</span></p>
<p class="mb-0"><strong>Connected Clients:</strong> <span id="client-count">{{ clients|length }}</span></p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentClients = {{ clients|tojson }};
function updateClients() {
fetch('/api/clients')
.then(response => response.json())
.then(data => {
// Update client count
document.getElementById('client-count').textContent = data.count;
// Check if client list changed
if (JSON.stringify(data.clients.sort()) !== JSON.stringify(currentClients.sort())) {
// Reload the page to show updated client list
location.reload();
}
})
.catch(error => {
console.log('Error fetching client data:', error);
});
}
// Update every 5 seconds
setInterval(updateClients, 5000);
// Initial update after 1 second
setTimeout(updateClients, 1000);
</script>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Login - WebSocket SSH Daemon{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-sign-in-alt"></i> Login</h3>
</div>
<div class="card-body">
<form method="post">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-sign-in-alt"></i> Login
</button>
</form>
<div class="mt-3">
<small class="text-muted">
Default credentials: admin / admin123
</small>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Terminal - {{ client_id }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary btn-sm me-3">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
<h3 class="card-title mb-0">
<i class="fas fa-terminal"></i> SSH Terminal - {{ client_id }}
</h3>
</div>
<div>
<input type="text" id="sshUsername" class="form-control form-control-sm d-inline-block w-auto me-2" placeholder="Username" value="root">
<button id="connectBtn" class="btn btn-success btn-sm">
<i class="fas fa-play"></i> Connect
</button>
<button id="disconnectBtn" class="btn btn-danger btn-sm" disabled>
<i class="fas fa-stop"></i> Disconnect
</button>
<button id="fullscreenBtn" class="btn btn-secondary btn-sm" title="Toggle Fullscreen">
<i class="fas fa-expand"></i>
</button>
</div>
</div>
<div class="card-body p-2">
<div id="terminal" class="terminal-container w-100"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
console.log('Terminal script starting...');
console.log('xterm available:', typeof Terminal);
console.log('xterm-fit available:', typeof FitAddon);
let term = null;
let fitAddon = null;
let connected = false;
let requestId = null;
let pollInterval = null;
console.log('Terminal page loaded, adding event listeners');
document.getElementById('connectBtn').addEventListener('click', connect);
document.getElementById('disconnectBtn').addEventListener('click', disconnect);
document.getElementById('fullscreenBtn').addEventListener('click', toggleFullscreen);
console.log('Event listeners added');
// Fullscreen functionality
function toggleFullscreen() {
const terminalContainer = document.getElementById('terminal');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const icon = fullscreenBtn.querySelector('i');
if (!document.fullscreenElement) {
// Enter fullscreen
if (terminalContainer.requestFullscreen) {
terminalContainer.requestFullscreen();
} else if (terminalContainer.webkitRequestFullscreen) { // Safari
terminalContainer.webkitRequestFullscreen();
} else if (terminalContainer.msRequestFullscreen) { // IE11
terminalContainer.msRequestFullscreen();
}
} else {
// Exit fullscreen
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) { // Safari
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) { // IE11
document.msExitFullscreen();
}
}
}
// Update fullscreen button icon based on state
function updateFullscreenButton() {
const fullscreenBtn = document.getElementById('fullscreenBtn');
const icon = fullscreenBtn.querySelector('i');
if (document.fullscreenElement) {
icon.className = 'fas fa-compress';
fullscreenBtn.title = 'Exit Fullscreen';
} else {
icon.className = 'fas fa-expand';
fullscreenBtn.title = 'Enter Fullscreen';
}
}
// Listen for fullscreen changes
function handleFullscreenChange() {
updateFullscreenButton();
// Resize terminal after fullscreen change
setTimeout(() => {
if (window.fitTerminal) {
window.fitTerminal();
}
// Update backend terminal size if connected
if (connected && requestId && fitAddon) {
const newDimensions = fitAddon.proposeDimensions();
const newCols = newDimensions.cols || 80;
const newRows = newDimensions.rows || 24;
fetch('/terminal/{{ client_id }}/resize', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'request_id=' + encodeURIComponent(requestId) +
'&cols=' + encodeURIComponent(newCols) +
'&rows=' + encodeURIComponent(newRows)
}).catch(error => {
console.error('Resize error during fullscreen change:', error);
});
}
}, 100); // Small delay to ensure DOM is updated
}
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange); // Safari
document.addEventListener('msfullscreenchange', handleFullscreenChange); // IE11
function connect() {
console.log('Connect function called');
const username = document.getElementById('sshUsername').value;
console.log('Username value:', username);
if (!username) {
alert('Please enter a username');
return;
}
console.log('Username validation passed');
// Initialize xterm with proper configuration
if (!term) {
term = new Terminal({
cursorBlink: true,
cursorStyle: 'block',
fontSize: 14,
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
theme: {
background: '#1e1e1e',
foreground: '#f8f8f2',
cursor: '#f8f8f2',
cursorAccent: '#1e1e1e',
selection: 'rgba(248, 248, 242, 0.3)'
},
allowTransparency: true,
scrollback: 1000,
tabStopWidth: 4,
convertEol: true,
disableStdin: false,
cursorWidth: 2,
bellStyle: 'none',
rightClickSelectsWord: true,
fastScrollModifier: 'alt',
fastScrollSensitivity: 5,
screenReaderMode: false,
macOptionIsMeta: false,
macOptionClickForcesSelection: false,
minimumContrastRatio: 1
});
term.open(document.getElementById('terminal'));
// Load fit addon
try {
if (typeof FitAddon !== 'undefined') {
fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
console.log('FitAddon loaded successfully');
} else {
console.error('FitAddon is not available');
throw new Error('FitAddon not loaded');
}
} catch (e) {
console.error('Failed to load FitAddon:', e);
term.write('Warning: Terminal auto-resizing not available\r\n');
// Continue without fit addon - terminal will still work
}
// Initial fit after a short delay to ensure DOM is ready
setTimeout(() => {
window.fitTerminal();
// Calculate dimensions after initial fit
let initialDimensions = { cols: 80, rows: 24 };
if (fitAddon) {
initialDimensions = fitAddon.proposeDimensions();
}
term._initialCols = initialDimensions.cols || 80;
term._initialRows = initialDimensions.rows || 24;
}, 100);
// Fit on window resize and update backend terminal size
window.addEventListener('resize', () => {
window.fitTerminal();
// Update terminal size on backend if connected
if (connected && requestId && fitAddon) {
const newDimensions = fitAddon.proposeDimensions();
const newCols = newDimensions.cols || 80;
const newRows = newDimensions.rows || 24;
fetch('/terminal/{{ client_id }}/resize', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'request_id=' + encodeURIComponent(requestId) +
'&cols=' + encodeURIComponent(newCols) +
'&rows=' + encodeURIComponent(newRows)
}).catch(error => {
console.error('Resize error:', error);
});
}
});
term.focus();
}
// Define fitTerminal function globally for fullscreen handling
window.fitTerminal = function() {
if (fitAddon) {
fitAddon.fit();
}
};
term.write('Connecting to ' + username + '@{{ client_id }}...\r\n');
connected = true;
document.getElementById('connectBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = false;
document.getElementById('sshUsername').disabled = true;
// Use calculated dimensions (either from initial fit or current)
let cols = term._initialCols || 80;
let rows = term._initialRows || 24;
if (fitAddon) {
const dimensions = fitAddon.proposeDimensions();
cols = dimensions.cols || cols;
rows = dimensions.rows || rows;
}
// Send connect request with terminal dimensions
const connectUrl = '/terminal/{{ client_id }}/connect';
console.log('Sending connect request to:', connectUrl);
console.log('Username:', username, 'Cols:', cols, 'Rows:', rows);
fetch(connectUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'username=' + encodeURIComponent(username) +
'&cols=' + encodeURIComponent(cols) +
'&rows=' + encodeURIComponent(rows),
// Add timeout and credentials
credentials: 'same-origin'
})
.then(response => {
console.log('Connect response status:', response.status, 'OK:', response.ok);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Connect response data:', data);
if (data.request_id) {
requestId = data.request_id;
if (data.command) {
term.write('Launching: ' + data.command + '\r\n');
}
term.write('Connected successfully!\r\n$ ');
// Start polling for data with shorter interval for better responsiveness
pollInterval = setInterval(pollData, 100);
} else {
term.write('Error: ' + (data.error || 'Unknown error') + '\r\n');
disconnect();
}
})
.catch(error => {
console.error('Connection failed:', error);
term.write('Connection failed: ' + error.message + '\r\n');
disconnect();
});
// Handle input - send all keystrokes to server, let SSH handle echo
term.onData(data => {
if (!connected || !requestId) return;
console.log('Sending input data:', data.length, 'characters');
// Send all input to server, let SSH handle echo and display
fetch('/terminal/{{ client_id }}/data', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'request_id=' + encodeURIComponent(requestId) + '&data=' + encodeURIComponent(data)
}).then(response => {
if (response.status !== 200) {
console.log('Input send response status:', response.status);
}
}).catch(error => {
console.error('Input send error:', error);
});
});
}
function disconnect() {
connected = false;
document.getElementById('connectBtn').disabled = false;
document.getElementById('disconnectBtn').disabled = true;
document.getElementById('sshUsername').disabled = false;
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
if (requestId) {
fetch('/terminal/{{ client_id }}/disconnect', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'request_id=' + encodeURIComponent(requestId)
});
requestId = null;
}
if (term) {
term.write('\r\nDisconnected.\r\n');
}
}
function pollData() {
if (!requestId) return;
fetch('/terminal/{{ client_id }}/data?request_id=' + encodeURIComponent(requestId))
.then(response => {
if (response.status !== 200) {
console.log('Poll response status:', response.status);
}
return response.text();
})
.then(data => {
if (data) {
console.log('Received data:', data.length, 'characters');
// Let the server handle all echo and display logic
term.write(data.replace(/\n/g, '\r\n'));
}
})
.catch(error => {
console.error('Polling error:', error);
});
}
// Focus on terminal when connected
document.addEventListener('keydown', function(e) {
if (connected && term) {
term.focus();
}
});
</script>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}User Management - WebSocket SSH Daemon{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title mb-0">
<i class="fas fa-users"></i> User Management
</h3>
<div>
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary btn-sm me-2">
<i class="fas fa-home"></i> Back to Home
</a>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addUserModal">
<i class="fas fa-plus"></i> Add User
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Username</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td>
{% if user.is_admin %}
<span class="badge bg-danger">Admin</span>
{% else %}
<span class="badge bg-secondary">User</span>
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="editUser({{ user.id }}, '{{ user.username }}', {{ user.is_admin|lower }})">
<i class="fas fa-edit"></i> Edit
</button>
{% if user.username != current_user.username %}
<button class="btn btn-sm btn-outline-danger" onclick="deleteUser({{ user.id }}, '{{ user.username }}')">
<i class="fas fa-trash"></i> Delete
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Add User Modal -->
<div class="modal fade" id="addUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="addUserForm">
<div class="modal-body">
<div class="mb-3">
<label for="addUsername" class="form-label">Username</label>
<input type="text" class="form-control" id="addUsername" name="username" required>
</div>
<div class="mb-3">
<label for="addPassword" class="form-label">Password</label>
<input type="password" class="form-control" id="addPassword" name="password" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="addIsAdmin" name="is_admin">
<label class="form-check-label" for="addIsAdmin">Administrator</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Add User</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="editUserForm">
<input type="hidden" id="editUserId" name="user_id">
<div class="modal-body">
<div class="mb-3">
<label for="editUsername" class="form-label">Username</label>
<input type="text" class="form-control" id="editUsername" name="username" required>
</div>
<div class="mb-3">
<label for="editPassword" class="form-label">New Password (leave empty to keep current)</label>
<input type="password" class="form-control" id="editPassword" name="password">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="editIsAdmin" name="is_admin">
<label class="form-check-label" for="editIsAdmin">Administrator</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Update User</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function editUser(userId, username, isAdmin) {
document.getElementById('editUserId').value = userId;
document.getElementById('editUsername').value = username;
document.getElementById('editPassword').value = '';
document.getElementById('editIsAdmin').checked = isAdmin;
new bootstrap.Modal(document.getElementById('editUserModal')).show();
}
function deleteUser(userId, username) {
if (confirm(`Are you sure you want to delete user "${username}"?`)) {
fetch(`/delete_user/${userId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.error);
}
});
}
}
document.getElementById('addUserForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('/add_user', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('addUserModal')).hide();
location.reload();
} else {
alert('Error: ' + data.error);
}
});
});
document.getElementById('editUserForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const userId = document.getElementById('editUserId').value;
fetch(`/edit_user/${userId}`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('editUserModal')).hide();
location.reload();
} else {
alert('Error: ' + data.error);
}
});
});
</script>
{% endblock %}
\ No newline at end of file
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <signal.h>
#include <fcntl.h>
#include <getopt.h>
volatile int running = 1;
void sigint_handler(int sig) {
running = 0;
}
// Simple base64 encoding function
char *base64_encode(const unsigned char *data, size_t input_length) {
static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t output_length = 4 * ((input_length + 2) / 3);
char *encoded_data = malloc(output_length + 1);
if (encoded_data == NULL) return NULL;
for (size_t i = 0, j = 0; i < input_length;) {
uint32_t octet_a = i < input_length ? data[i++] : 0;
uint32_t octet_b = i < input_length ? data[i++] : 0;
uint32_t octet_c = i < input_length ? data[i++] : 0;
uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
encoded_data[j++] = base64_chars[(triple >> 3 * 6) & 0x3F];
encoded_data[j++] = base64_chars[(triple >> 2 * 6) & 0x3F];
encoded_data[j++] = base64_chars[(triple >> 1 * 6) & 0x3F];
encoded_data[j++] = base64_chars[(triple >> 0 * 6) & 0x3F];
}
// Add padding
size_t padding = (3 - (input_length % 3)) % 3;
for (size_t i = 0; i < padding; i++) {
encoded_data[output_length - 1 - i] = '=';
}
encoded_data[output_length] = '\0';
return encoded_data;
}
int main(int argc, char *argv[]) {
char *host = "127.0.0.1"; // Default host
int port = 8080; // Default port
int timeout_seconds = 2; // Default timeout
int num_messages = 0; // Default: infinite loop
int string_length = 0; // Default: random length (10-40)
// Parse command line arguments
static struct option long_options[] = {
{"timeout", required_argument, 0, 't'},
{"n", required_argument, 0, 'n'},
{"string-length", required_argument, 0, 's'},
{0, 0, 0, 0}
};
int opt;
int option_index = 0;
while ((opt = getopt_long(argc, argv, "t:n:s:", long_options, &option_index)) != -1) {
switch (opt) {
case 't':
timeout_seconds = atoi(optarg);
if (timeout_seconds < 0) {
fprintf(stderr, "Invalid timeout value: %s\n", optarg);
return 1;
}
break;
case 'n':
num_messages = atoi(optarg);
if (num_messages < 0) {
fprintf(stderr, "Invalid number of messages: %s\n", optarg);
return 1;
}
break;
case 's':
string_length = atoi(optarg);
if (string_length < 1 || string_length > 1000) {
fprintf(stderr, "Invalid string length: %s (must be 1-1000)\n", optarg);
return 1;
}
break;
default:
fprintf(stderr, "Usage: %s [options] [host] [port]\n", argv[0]);
fprintf(stderr, "Options:\n");
fprintf(stderr, " --timeout, -t <seconds> Timeout between messages (default: 2)\n");
fprintf(stderr, " --n, -n <count> Number of messages to send (0 for infinite, default: 0)\n");
fprintf(stderr, " --string-length, -s <len> String length to generate (1-1000, default: random 10-40)\n");
fprintf(stderr, "Arguments:\n");
fprintf(stderr, " host Server host (default: 127.0.0.1)\n");
fprintf(stderr, " port Server port (default: 8080)\n");
return 1;
}
}
// Parse positional arguments for host and port
if (optind < argc) {
host = argv[optind++];
}
if (optind < argc) {
port = atoi(argv[optind++]);
}
if (port <= 0 || port > 65535) {
fprintf(stderr, "Invalid port: %d\n", port);
return 1;
}
printf("Connecting to %s:%d\n", host, port);
printf("Timeout: %d seconds\n", timeout_seconds);
printf("Number of messages: %s\n", num_messages == 0 ? "infinite" : "limited");
printf("String length: %s\n", string_length > 0 ? "fixed" : "random (10-40)");
if (string_length > 0) {
printf("Fixed string length: %d\n", string_length);
}
signal(SIGINT, sigint_handler);
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) {
perror("inet_pton");
close(sock);
return 1;
}
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("connect");
close(sock);
return 1;
}
// Make socket non-blocking
int flags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
int message_count = 0;
while (running) {
// Check if we've sent the required number of messages
if (num_messages > 0 && message_count >= num_messages) {
printf("Sent %d messages as requested. Exiting.\n", num_messages);
break;
}
// Generate string
srand(time(NULL) + message_count); // Add message_count to avoid same seed
int len;
if (string_length > 0) {
len = string_length;
} else {
len = rand() % 31 + 10; // 10 to 40
}
char str[len + 1];
for (int i = 0; i < len; i++) {
str[i] = 'A' + rand() % 26;
}
str[len] = '\0';
char *b64_sent = base64_encode((unsigned char *)str, len);
printf("(SENT) %s <-> %s\n", str, b64_sent ? b64_sent : "ERROR");
write(sock, str, len);
message_count++;
if (b64_sent) free(b64_sent);
// Wait for response with specified timeout
fd_set read_set;
FD_ZERO(&read_set);
FD_SET(sock, &read_set);
struct timeval timeout;
timeout.tv_sec = timeout_seconds;
timeout.tv_usec = 0;
int ret = select(sock + 1, &read_set, NULL, NULL, &timeout);
if (ret > 0 && FD_ISSET(sock, &read_set)) {
char buffer[1024];
int n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
// Remove trailing newline if present
if (n > 0 && buffer[n-1] == '\n') {
buffer[n-1] = '\0';
n--;
}
char *b64_recv = base64_encode((unsigned char *)buffer, n);
printf("(RECV) %s <-> %s\n", buffer, b64_recv ? b64_recv : "ERROR");
if (b64_recv) free(b64_recv);
} else if (n == 0) {
// Connection closed
printf("Connection closed by server\n");
break;
} else {
perror("read");
break;
}
} else if (ret == 0) {
printf("Timeout waiting for response\n");
} else {
perror("select");
break;
}
// Add delay between messages based on timeout parameter
if (timeout_seconds > 0) {
sleep(timeout_seconds);
} else {
usleep(100000); // 100ms delay for 0 timeout
}
}
close(sock);
return 0;
}
\ No newline at end of file
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
volatile int running = 1;
void sigint_handler(int sig) {
running = 0;
}
// Simple base64 encoding function
char *base64_encode(const unsigned char *data, size_t input_length) {
static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t output_length = 4 * ((input_length + 2) / 3);
char *encoded_data = malloc(output_length + 1);
if (encoded_data == NULL) return NULL;
for (size_t i = 0, j = 0; i < input_length;) {
uint32_t octet_a = i < input_length ? data[i++] : 0;
uint32_t octet_b = i < input_length ? data[i++] : 0;
uint32_t octet_c = i < input_length ? data[i++] : 0;
uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
encoded_data[j++] = base64_chars[(triple >> 3 * 6) & 0x3F];
encoded_data[j++] = base64_chars[(triple >> 2 * 6) & 0x3F];
encoded_data[j++] = base64_chars[(triple >> 1 * 6) & 0x3F];
encoded_data[j++] = base64_chars[(triple >> 0 * 6) & 0x3F];
}
// Add padding
size_t padding = (3 - (input_length % 3)) % 3;
for (size_t i = 0; i < padding; i++) {
encoded_data[output_length - 1 - i] = '=';
}
encoded_data[output_length] = '\0';
return encoded_data;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <port>\n", argv[0]);
return 1;
}
int port = atoi(argv[1]);
if (port <= 0 || port > 65535) {
fprintf(stderr, "Invalid port number\n");
return 1;
}
signal(SIGINT, sigint_handler);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
return 1;
}
// Set SO_REUSEADDR to allow immediate reuse of the port
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("setsockopt");
close(server_fd);
return 1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
close(server_fd);
return 1;
}
if (listen(server_fd, 10) < 0) {
perror("listen");
close(server_fd);
return 1;
}
printf("Listening on 0.0.0.0:%d\n", port);
fd_set master_set, read_set;
FD_ZERO(&master_set);
FD_SET(server_fd, &master_set);
int max_fd = server_fd;
while (running) {
read_set = master_set;
struct timeval timeout;
timeout.tv_sec = 1; // 1 second timeout to check running flag
timeout.tv_usec = 0;
int ret = select(max_fd + 1, &read_set, NULL, NULL, &timeout);
if (ret < 0) {
if (running) perror("select");
break;
} else if (ret == 0) {
// Timeout - check if we should still be running
continue;
}
for (int i = 0; i <= max_fd; i++) {
if (FD_ISSET(i, &read_set)) {
if (i == server_fd) {
// New connection
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
continue;
}
printf("CONNECTION FROM %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
FD_SET(client_fd, &master_set);
if (client_fd > max_fd) max_fd = client_fd;
} else {
// Data from client
char buffer[1024];
int n = read(i, buffer, sizeof(buffer) - 1);
if (n <= 0) {
// Connection closed or error
close(i);
FD_CLR(i, &master_set);
printf("CONNECTION CLOSED\n");
} else {
buffer[n] = '\0';
// Remove trailing newline if present
if (n > 0 && buffer[n-1] == '\n') {
buffer[n-1] = '\0';
n--;
}
char *b64 = base64_encode((unsigned char *)buffer, n);
printf("(RECEIVED) %s <-> %s\n", buffer, b64 ? b64 : "ERROR");
char response[1024 + 100];
char *response_b64 = base64_encode((unsigned char *)buffer, n);
snprintf(response, sizeof(response), "RECEIVED: %s <-> %s", buffer, response_b64 ? response_b64 : "ERROR");
write(i, response, strlen(response));
if (b64) free(b64);
if (response_b64) free(response_b64);
}
}
}
}
}
// Close all sockets
for (int i = 0; i <= max_fd; i++) {
if (FD_ISSET(i, &master_set)) {
close(i);
}
}
printf("Exiting...\n");
return 0;
}
\ No newline at end of file
"""
WSSSH Daemon (wssshd) - Modular implementation
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from .server import main
from .config import load_config
from .websocket import clients, active_tunnels, active_terminals
from .terminal import create_terminal_session, send_terminal_data, get_terminal_output, disconnect_terminal, resize_terminal
__version__ = "1.0.0"
__all__ = [
'main',
'load_config',
'clients',
'active_tunnels',
'active_terminals',
'create_terminal_session',
'send_terminal_data',
'get_terminal_output',
'disconnect_terminal',
'resize_terminal'
]
\ No newline at end of file
"""
Entry point for running wssshd as a module
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from .server import main
if __name__ == '__main__':
main()
\ No newline at end of file
"""
Configuration handling for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import argparse
import configparser
import os
def load_config(args_config=None):
"""
Load configuration from file and command line arguments
Returns parsed arguments
"""
parser = argparse.ArgumentParser(description='WSSSH Daemon (wssshd)')
parser.add_argument('--config', help='Configuration file path (default: /etc/wssshd.conf)')
parser.add_argument('--host', help='WebSocket server host')
parser.add_argument('--port', type=int, default=9898, help='WebSocket server port (default: 9898)')
parser.add_argument('--domain', help='Base domain name')
parser.add_argument('--password', help='Registration password')
parser.add_argument('--web-host', help='Web interface host (optional)')
parser.add_argument('--web-port', type=int, help='Web interface port (optional)')
parser.add_argument('--web-https', action='store_true', help='Enable HTTPS for web interface')
parser.add_argument('--debug', action='store_true', help='Enable debug output')
# Parse just the config argument first to determine config file location
temp_parser = argparse.ArgumentParser(add_help=False)
temp_parser.add_argument('--config')
temp_args, remaining = temp_parser.parse_known_args()
config = configparser.ConfigParser()
config_path = temp_args.config or '/etc/wssshd.conf'
defaults = {}
if os.path.exists(config_path):
config.read(config_path)
if 'wssshd' in config:
section = config['wssshd']
for key in ['password', 'domain']:
if key in section:
defaults[key] = section[key]
if 'host' in section:
defaults['host'] = section['host']
if 'port' in section:
defaults['port'] = int(section['port'])
if 'web-host' in section:
defaults['web_host'] = section['web-host']
if 'web-port' in section:
defaults['web_port'] = int(section['web-port'])
if 'web-https' in section:
defaults['web_https'] = section.getboolean('web-https', False)
parser.set_defaults(**defaults)
args = parser.parse_args()
# Handle web-https from config if not specified on command line
if 'web_https' in defaults and not any(arg.startswith('--web-https') for arg in remaining):
args.web_https = defaults['web_https']
# Check required arguments
if not args.host:
parser.error('--host is required')
if not args.domain:
parser.error('--domain is required')
if not args.password:
parser.error('--password is required')
return args
\ No newline at end of file
"""
Main server logic for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import asyncio
import ssl
import sys
import os
import threading
import signal
import websockets
from functools import partial
from .config import load_config
from .websocket import handle_websocket, cleanup_task, shutdown_event, debug, clients, active_tunnels, active_terminals, SERVER_SHUTDOWN_MSG, TUNNEL_CLOSE_MSG
from .web import run_flask
def setup_ssl_context():
"""Set up SSL context for WebSocket server"""
# Load certificate
if getattr(sys, 'frozen', False):
# Running as bundled executable
bundle_dir = sys._MEIPASS
cert_path = os.path.join(bundle_dir, 'cert.pem')
key_path = os.path.join(bundle_dir, 'key.pem')
else:
# Running as script
cert_path = 'cert.pem'
key_path = 'key.pem'
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(cert_path, key_path)
return ssl_context
async def shutdown_server(ws_server, cleanup_coro, flask_thread):
"""Handle graceful server shutdown"""
print("\nShutting down WSSSH Daemon...")
# Notify all connected clients about shutdown (optimized)
active_clients = [(cid, info) for cid, info in clients.items() if info['status'] == 'active']
if active_clients:
print(f"Notifying {len(active_clients)} connected clients...")
# Create notification tasks
shutdown_msg = SERVER_SHUTDOWN_MSG.encode()
notify_tasks = []
for client_id, client_info in active_clients:
try:
task = asyncio.create_task(client_info['websocket'].send(shutdown_msg))
notify_tasks.append(task)
except Exception as e:
if debug: print(f"[DEBUG] Failed to create notification task for {client_id}: {e}")
# Wait for all notifications with timeout
if notify_tasks:
print(f"Waiting for {len(notify_tasks)} client notifications to complete...")
try:
await asyncio.wait_for(
asyncio.gather(*notify_tasks, return_exceptions=True),
timeout=0.3
)
print("All clients notified successfully")
except asyncio.TimeoutError:
print("Timeout waiting for client notifications (0.3s), proceeding with shutdown")
except Exception as e:
print(f"Error during client notifications, proceeding with shutdown")
if debug: print(f"[DEBUG] Error during client notifications: {e}")
# Give clients a brief moment to process the shutdown message
print("Waiting for clients to process shutdown message...")
await asyncio.sleep(0.1)
# Close WebSocket server
print("Closing WebSocket server...")
try:
ws_server.close()
await ws_server.wait_closed()
print("WebSocket server closed successfully")
except Exception as e:
print("Error closing WebSocket server, continuing shutdown")
if debug: print(f"[DEBUG] Error closing WebSocket server: {e}")
# Cancel cleanup task immediately to stop keepalive timeouts
print("Stopping cleanup task (keepalive timeouts)...")
if not cleanup_coro.done():
cleanup_coro.cancel()
try:
await cleanup_coro
except asyncio.CancelledError:
pass
print("Cleanup task stopped")
# Clean up active terminals more efficiently
print("Terminating active terminal processes...")
# Terminate all processes efficiently
if active_terminals:
print("Terminating active terminal processes...")
# Send SIGTERM to all processes
term_procs = []
for request_id, terminal in active_terminals.items():
proc = terminal['proc']
if proc.poll() is None:
proc.terminate()
term_procs.append((request_id, proc))
if term_procs:
# Wait for graceful termination
print(f"Waiting for {len(term_procs)} terminal processes to terminate gracefully...")
await asyncio.sleep(0.3)
# Check which processes are still running
still_running = []
for request_id, proc in term_procs:
if proc.poll() is None:
still_running.append((request_id, proc))
if still_running:
print(f"Force killing {len(still_running)} remaining terminal processes...")
# Force kill remaining processes
kill_tasks = []
for request_id, proc in still_running:
if debug: print(f"[DEBUG] Force killing terminal process {request_id}")
proc.kill()
# Create async task for waiting
task = asyncio.get_event_loop().run_in_executor(None, proc.wait)
kill_tasks.append(task)
# Wait for all kill operations to complete
if kill_tasks:
print("Waiting for force-killed processes to complete...")
try:
await asyncio.wait_for(
asyncio.gather(*kill_tasks, return_exceptions=True),
timeout=0.2
)
print("All terminal processes terminated")
except asyncio.TimeoutError:
print("Timeout waiting for some processes to terminate")
if debug: print("[DEBUG] Some processes still running after SIGKILL")
except Exception as e:
print("Error during process cleanup")
if debug: print(f"[DEBUG] Error during process cleanup: {e}")
else:
print("All terminal processes terminated gracefully")
# Clean up terminal records (optimized)
terminal_count = len(active_terminals)
if terminal_count > 0:
active_terminals.clear()
if debug: print(f"[DEBUG] Cleaned up {terminal_count} terminal records")
# Clean up ALL tunnels (not just active ones)
print("Sending close messages to all tunnels...")
if active_tunnels:
# Create close tasks for ALL tunnels
close_tasks = []
for request_id, tunnel in active_tunnels.items():
# Send close message to both client and tool endpoints
try:
# Send to client (wssshc) if websocket exists
if tunnel.client_ws:
close_task = asyncio.create_task(
tunnel.client_ws.send(TUNNEL_CLOSE_MSG % request_id)
)
close_tasks.append((f"{request_id}_client", close_task))
except Exception as e:
if debug: print(f"[DEBUG] Failed to send close to client for {request_id}: {e}")
try:
# Send to tool (wsssht/wsscp) if websocket exists
if tunnel.wsssh_ws:
close_task = asyncio.create_task(
tunnel.wsssh_ws.send(TUNNEL_CLOSE_MSG % request_id)
)
close_tasks.append((f"{request_id}_tool", close_task))
except Exception as e:
if debug: print(f"[DEBUG] Failed to send close to tool for {request_id}: {e}")
# Wait for all close tasks with timeout
if close_tasks:
print(f"Waiting for {len(close_tasks)} tunnel close messages...")
try:
await asyncio.wait_for(
asyncio.gather(*[task for _, task in close_tasks], return_exceptions=True),
timeout=0.2
)
print("All tunnel close messages sent")
if debug: print(f"[DEBUG] Sent close messages to {len(close_tasks)} endpoints")
except asyncio.TimeoutError:
print("Timeout waiting for tunnel close messages (0.2s)")
if debug: print("[DEBUG] Timeout waiting for tunnel close messages")
except Exception as e:
print("Error during tunnel close messages")
if debug: print(f"[DEBUG] Error during tunnel close messages: {e}")
# Update tunnel statuses and clean up all tunnels
for request_id, tunnel in active_tunnels.items():
tunnel.update_status('closed', 'Server shutdown')
if debug: print(f"[DEBUG] Tunnel {request_id} status updated: {tunnel}")
tunnel_count = len(active_tunnels)
active_tunnels.clear()
print(f"Cleaned up {tunnel_count} tunnels")
# Clean up clients (optimized)
client_count = len(clients)
if client_count > 0:
clients.clear()
if debug: print(f"[DEBUG] Cleaned up {client_count} clients")
# Stop Flask thread
if flask_thread and flask_thread.is_alive():
print("Waiting for web interface thread to stop...")
flask_thread.join(timeout=1.0)
if flask_thread.is_alive():
print("Web interface thread still running after timeout")
else:
print("Web interface thread stopped successfully")
print("WSSSH Daemon stopped cleanly")
async def run_server():
"""Main server function"""
# Create new process group to avoid receiving SIGINT from terminal
os.setpgrp()
args = load_config()
# Set global variables
debug = args.debug
server_password = args.password
# Initialize shutdown event
shutdown_event = asyncio.Event()
# Set up signal handling for clean exit
shutdown_event.clear()
sigint_handled = False
def signal_handler(signum, frame):
nonlocal sigint_handled
if sigint_handled:
# Already handling a signal, force exit
print("\nReceived second SIGINT, exiting immediately...")
os._exit(1)
sigint_handled = True
print("\nReceived SIGINT, attempting graceful shutdown...")
shutdown_event.set()
# Register signal handler for SIGINT (Ctrl+C)
signal.signal(signal.SIGINT, signal_handler)
ssl_context = setup_ssl_context()
# Start WebSocket server
ws_server = await websockets.serve(partial(handle_websocket, server_password=server_password, debug_flag=debug), args.host, args.port, ssl=ssl_context)
print(f"WSSSH Daemon running on {args.host}:{args.port}")
print("Press Ctrl+C to stop the server")
# Start cleanup task
cleanup_coro = asyncio.create_task(cleanup_task(debug))
# Start web interface if specified
flask_thread = None
if args.web_host and args.web_port:
flask_thread = threading.Thread(
target=run_flask,
args=(args.web_host, args.web_port, debug, getattr(args, 'web_https', False)),
daemon=True
)
flask_thread.start()
try:
# Create tasks for waiting
server_close_task = asyncio.create_task(ws_server.wait_closed())
shutdown_wait_task = asyncio.create_task(shutdown_event.wait())
# Wait for either server to close or shutdown signal
done, pending = await asyncio.wait(
[server_close_task, shutdown_wait_task],
return_when=asyncio.FIRST_COMPLETED
)
# Check conditions
if shutdown_event.is_set():
if debug: print("[DEBUG] Shutdown event detected in main loop")
if server_close_task in done:
if debug: print("[DEBUG] WebSocket server closed naturally")
# Cancel pending tasks
for task in pending:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
await shutdown_server(ws_server, cleanup_coro, flask_thread)
except Exception as e:
print(f"Error during shutdown: {e}")
# Ensure cleanup happens even if there's an error
if not cleanup_coro.done():
cleanup_coro.cancel()
try:
await cleanup_coro
except asyncio.CancelledError:
pass
def main():
"""Entry point for the server"""
try:
asyncio.run(run_server())
except Exception as e:
print(f"Server error: {e}")
sys.exit(1)
\ No newline at end of file
"""
Terminal and PTY handling for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import pty
import select
import fcntl
import termios
import struct
import threading
import signal
import subprocess
import uuid
def openpty_with_fallback():
"""Open a PTY with fallback to different device paths for systems where /dev/pty doesn't exist"""
# First try the standard pty.openpty()
try:
master, slave = pty.openpty()
return master, slave
except OSError as e:
if hasattr(openpty_with_fallback, '_debug') and openpty_with_fallback._debug:
print(f"[DEBUG] Standard pty.openpty() failed: {e}, trying fallback methods")
# Fallback: try to open /dev/ptmx directly
ptmx_paths = ['/dev/ptmx', '/dev/pts/ptmx']
for ptmx_path in ptmx_paths:
try:
if os.path.exists(ptmx_path):
# Open master PTY
master = os.open(ptmx_path, os.O_RDWR | os.O_NOCTTY)
if master < 0:
continue
# Get slave PTY name
slave_name = os.ttyname(master)
if not slave_name:
os.close(master)
continue
# Open slave PTY
slave = os.open(slave_name, os.O_RDWR | os.O_NOCTTY)
if slave < 0:
os.close(master)
continue
if hasattr(openpty_with_fallback, '_debug') and openpty_with_fallback._debug:
print(f"[DEBUG] Successfully opened PTY using {ptmx_path}: master={master}, slave={slave}")
return master, slave
except (OSError, AttributeError) as e:
if hasattr(openpty_with_fallback, '_debug') and openpty_with_fallback._debug:
print(f"[DEBUG] Failed to open PTY using {ptmx_path}: {e}")
continue
# Last resort: try to create PTY devices manually
try:
# Try to find an available PTY number
for i in range(256): # Try PTY numbers 0-255
pty_name = f"/dev/pts/{i}"
try:
if os.path.exists(pty_name):
continue
# Try to create the PTY device
master = os.open('/dev/ptmx', os.O_RDWR | os.O_NOCTTY)
slave_name = os.ttyname(master)
if slave_name and os.path.exists(slave_name):
slave = os.open(slave_name, os.O_RDWR | os.O_NOCTTY)
if hasattr(openpty_with_fallback, '_debug') and openpty_with_fallback._debug:
print(f"[DEBUG] Created PTY manually: master={master}, slave={slave}")
return master, slave
os.close(master)
except (OSError, AttributeError):
continue
except Exception as e:
if hasattr(openpty_with_fallback, '_debug') and openpty_with_fallback._debug:
print(f"[DEBUG] Manual PTY creation failed: {e}")
# If all methods fail, raise the original exception
raise OSError("Failed to open PTY: no available PTY devices found")
def create_terminal_session(args, username, client_id):
"""Create a new terminal session for a client"""
request_id = str(uuid.uuid4())
# Force echo mode before launching wsssh
command = ['sh', '-c', f'stty echo && wsssh -p {args.port} {username}@{client_id}.{args.domain}']
# Debug output for the command being launched
if hasattr(args, 'debug') and args.debug:
print(f"[DEBUG] [Terminal] Launching command: {' '.join(command)}")
print(f"[DEBUG] [Terminal] Request ID: {request_id}")
print(f"[DEBUG] [Terminal] Username: {username}, Client ID: {client_id}, Domain: {args.domain}")
# Spawn wsssh process with pty using fallback method
master, slave = openpty_with_fallback()
slave_name = os.ttyname(slave)
def set_controlling_terminal():
os.setsid()
# Set the controlling terminal
try:
fcntl.ioctl(slave, termios.TIOCSCTTY, 0)
except (OSError, AttributeError):
pass # Some systems don't support TIOCSCTTY
# Set terminal size to match xterm.js dimensions (default 80x24)
winsize = struct.pack('HHHH', 24, 80, 0, 0)
try:
fcntl.ioctl(0, termios.TIOCSWINSZ, winsize)
except (OSError, AttributeError):
pass
# Set raw mode - let SSH client handle terminal behavior
import tty
try:
tty.setraw(0)
except (OSError, AttributeError):
pass
proc = subprocess.Popen(
command,
stdin=slave,
stdout=slave,
stderr=slave,
preexec_fn=set_controlling_terminal,
env=dict(os.environ, TERM='xterm', COLUMNS='80', LINES='24')
)
os.close(slave)
# Start a thread to read output
output_buffer = []
def read_output():
output_buffer.append(f'Process PID: {proc.pid}\r\n')
while proc.poll() is None:
r, w, e = select.select([master], [], [], 0.1)
if master in r:
try:
data = os.read(master, 1024)
if data:
decoded = data.decode('utf-8', errors='ignore')
output_buffer.append(decoded)
except:
break
# Read any remaining data
try:
data = os.read(master, 1024)
while data:
decoded = data.decode('utf-8', errors='ignore')
output_buffer.append(decoded)
data = os.read(master, 1024)
except:
pass
output_buffer.append('\r\nProcess finished.\r\n')
os.close(master)
thread = threading.Thread(target=read_output, daemon=True)
thread.start()
return {
'request_id': request_id,
'proc': proc,
'output_buffer': output_buffer,
'master': master,
'command': f'wsssh -p {args.port} {username}@{client_id}.{args.domain}'
}
def send_terminal_data(terminal_session, data):
"""Send data to a terminal session"""
proc = terminal_session['proc']
master = terminal_session['master']
if proc.poll() is None: # Process is still running
try:
os.write(master, data.encode())
return True
except:
return False
return False
def get_terminal_output(terminal_session):
"""Get output from a terminal session"""
proc = terminal_session['proc']
output_buffer = terminal_session['output_buffer']
if output_buffer:
data = ''.join(output_buffer)
output_buffer.clear()
return data
elif proc.poll() is not None:
# Process terminated
return '\r\nProcess terminated.\r\n'
return ''
def disconnect_terminal(terminal_session):
"""Disconnect a terminal session"""
proc = terminal_session['proc']
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except:
proc.kill()
return True
def resize_terminal(terminal_session, cols, rows):
"""Resize a terminal session"""
proc = terminal_session['proc']
master = terminal_session['master']
if proc.poll() is None:
# Update terminal size
winsize = struct.pack('HHHH', rows, cols, 0, 0)
try:
fcntl.ioctl(master, termios.TIOCSWINSZ, winsize)
# Also try to update the process's controlling terminal
fcntl.ioctl(0, termios.TIOCSWINSZ, winsize)
except (OSError, AttributeError):
pass
# Send SIGWINCH to notify the process of size change
try:
os.kill(proc.pid, signal.SIGWINCH)
except (OSError, ProcessLookupError):
pass
return True
\ No newline at end of file
"""
Tunnel object management for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import time
import socket
import ipaddress
class TunnelStatus:
"""Enumeration of tunnel statuses"""
CREATING = "creating"
ACTIVE = "active"
CLOSING = "closing"
CLOSED = "closed"
ERROR = "error"
class Tunnel:
"""Comprehensive tunnel object that tracks all tunnel attributes"""
def __init__(self, request_id, client_id):
self.request_id = request_id
self.client_id = client_id
self.tunnel_id = request_id # Use request_id as tunnel_id for now
# Status and lifecycle
self.status = TunnelStatus.CREATING
self.created_at = time.time()
self.updated_at = time.time()
# Protocol and type
self.protocol = "ssh" # default
self.tunnel = "any" # default
self.tunnel_control = "any" # default
self.service = "ssh" # default
# Destination (wssshc) information
self.wssshc_public_ip = None
self.wssshc_public_port = None
self.wssshc_private_ip = None
self.wssshc_private_port = None
# Source (wsssh/wsscp) information
self.tool_public_ip = None
self.tool_private_ip = None
self.tool_public_port = None
self.tool_private_port = None
# WebSocket connections
self.client_ws = None # wssshc WebSocket
self.wsssh_ws = None # wsssh/wsscp WebSocket
# Additional metadata
self.error_message = None
self.metadata = {}
# Keep-alive statistics and timing
self.last_keepalive_sent = time.time()
self.last_keepalive_received = time.time()
self.total_bytes_sent = 0
self.total_bytes_received = 0
self.bytes_last_period = 0
self.last_stats_reset = time.time()
# Dual-endpoint keep-alive monitoring (220s timeout)
self.last_keepalive_from_client = time.time() # wssshc endpoint
self.last_keepalive_from_tool = time.time() # wsssht/wsscp endpoint
# Keep-alive forwarding failure counters
self.keepalive_forward_failures = 0 # Consecutive forwarding failures
self.keepalive_ack_forward_failures = 0 # Consecutive ACK forwarding failures
def update_status(self, new_status, error_message=None):
"""Update tunnel status and timestamp"""
self.status = new_status
self.updated_at = time.time()
if error_message:
self.error_message = error_message
def set_destination_info(self, public_ip=None, public_port=None, private_ip=None, private_port=None):
"""Set destination (wssshc) connection information"""
if public_ip:
self.wssshc_public_ip = public_ip
if public_port:
self.wssshc_public_port = public_port
if private_ip:
self.wssshc_private_ip = private_ip
if private_port:
self.wssshc_private_port = private_port
self.updated_at = time.time()
def set_source_info(self, public_ip=None, private_ip=None, public_port=None, private_port=None):
"""Set source (wsssh/wsscp) connection information"""
if public_ip:
self.tool_public_ip = public_ip
if private_ip:
self.tool_private_ip = private_ip
if public_port:
self.tool_public_port = public_port
if private_port:
self.tool_private_port = private_port
self.updated_at = time.time()
def set_websockets(self, client_ws, wsssh_ws):
"""Set WebSocket connections"""
self.client_ws = client_ws
self.wsssh_ws = wsssh_ws
self.updated_at = time.time()
def to_dict(self):
"""Convert tunnel object to dictionary for serialization"""
return {
'request_id': self.request_id,
'client_id': self.client_id,
'tunnel_id': self.tunnel_id,
'status': self.status,
'created_at': self.created_at,
'updated_at': self.updated_at,
'protocol': self.protocol,
'tunnel': self.tunnel,
'tunnel_control': self.tunnel_control,
'service': self.service,
'wssshc_public_ip': self.wssshc_public_ip,
'wssshc_public_port': self.wssshc_public_port,
'wssshc_private_ip': self.wssshc_private_ip,
'wssshc_private_port': self.wssshc_private_port,
'tool_public_ip': self.tool_public_ip,
'tool_private_ip': self.tool_private_ip,
'tool_public_port': self.tool_public_port,
'tool_private_port': self.tool_private_port,
'error_message': self.error_message,
'last_keepalive_from_client': self.last_keepalive_from_client,
'last_keepalive_from_tool': self.last_keepalive_from_tool
}
def __str__(self):
return f"Tunnel(id={self.tunnel_id}, client={self.client_id}, status={self.status})"
def __repr__(self):
return self.__str__()
def detect_client_public_ip(websocket):
"""Detect the public IP address of a client from WebSocket connection"""
try:
# Get the remote address from WebSocket
remote_addr = websocket.remote_address
if remote_addr and len(remote_addr) >= 2:
ip = remote_addr[0]
# Check if it's a valid public IP
ip_obj = ipaddress.ip_address(ip)
if not ip_obj.is_private and not ip_obj.is_loopback:
return ip
except Exception:
pass
return None
def detect_client_private_ip(websocket):
"""Detect the private IP address of a client from WebSocket connection"""
try:
# Get the remote address from WebSocket
remote_addr = websocket.remote_address
if remote_addr and len(remote_addr) >= 2:
ip = remote_addr[0]
# Check if it's a valid private IP
ip_obj = ipaddress.ip_address(ip)
if ip_obj.is_private:
return ip
except Exception:
pass
return None
def get_server_public_ip():
"""Get the server's public IP address"""
try:
# Create a socket to connect to an external service
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80)) # Connect to Google DNS
public_ip = s.getsockname()[0]
s.close()
return public_ip
except Exception:
return None
def get_server_private_ip():
"""Get the server's private IP address"""
try:
# Create a socket and connect to get local IP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80)) # Connect to Google DNS
private_ip = s.getsockname()[0]
s.close()
return private_ip
except Exception:
return None
\ No newline at end of file
"""
Flask web interface for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import sys
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_from_directory
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from .websocket import clients, active_terminals
from .terminal import create_terminal_session, send_terminal_data, get_terminal_output, disconnect_terminal, resize_terminal
# Flask app
app = Flask(__name__)
# Handle template and static folders for frozen executables
if getattr(sys, 'frozen', False):
# Running as bundled executable
bundle_dir = sys._MEIPASS
template_dir = os.path.join(bundle_dir, 'templates')
static_dir = os.path.join(bundle_dir, 'static')
app.template_folder = template_dir
app.static_folder = static_dir
app.config['SECRET_KEY'] = 'wsssh-secret-key-change-in-production'
config_dir = os.path.expanduser('~/.config/wssshd')
os.makedirs(config_dir, exist_ok=True)
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{config_dir}/users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), unique=True, nullable=False)
password_hash = db.Column(db.String(150), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
# Create database and default admin user
with app.app_context():
db.create_all()
if not User.query.filter_by(username='admin').first():
admin = User(username='admin', password_hash=generate_password_hash('admin123'), is_admin=True)
db.session.add(admin)
db.session.commit()
# Flask routes
@app.route('/')
@login_required
def index():
from .config import load_config
args = load_config()
# Get client information with status
client_info = {}
for client_id, client_data in clients.items():
client_info[client_id] = {
'status': client_data['status'],
'last_seen': client_data['last_seen']
}
return render_template('index.html',
clients=client_info,
websocket_port=args.port,
domain=args.domain)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password_hash, password):
login_user(user)
return redirect(url_for('index'))
flash('Invalid username or password')
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('login'))
@app.route('/users')
@login_required
def users():
if not current_user.is_admin:
flash('Access denied')
return redirect(url_for('index'))
users_list = User.query.all()
return render_template('users.html', users=users_list)
@app.route('/add_user', methods=['POST'])
@login_required
def add_user():
if not current_user.is_admin:
return jsonify({'error': 'Access denied'}), 403
username = request.form.get('username')
password = request.form.get('password')
is_admin = request.form.get('is_admin') == 'on'
if User.query.filter_by(username=username).first():
return jsonify({'error': 'Username already exists'}), 400
user = User(username=username, password_hash=generate_password_hash(password), is_admin=is_admin)
db.session.add(user)
db.session.commit()
return jsonify({'success': True})
@app.route('/edit_user/<int:user_id>', methods=['POST'])
@login_required
def edit_user(user_id):
if not current_user.is_admin:
return jsonify({'error': 'Access denied'}), 403
user = User.query.get_or_404(user_id)
user.username = request.form.get('username')
user.is_admin = request.form.get('is_admin') == 'on'
password = request.form.get('password')
if password:
user.password_hash = generate_password_hash(password)
db.session.commit()
return jsonify({'success': True})
@app.route('/delete_user/<int:user_id>', methods=['POST'])
@login_required
def delete_user(user_id):
if not current_user.is_admin:
return jsonify({'error': 'Access denied'}), 403
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return jsonify({'success': True})
@app.route('/terminal/<client_id>')
@login_required
def terminal(client_id):
print(f"[DEBUG] [Web] Terminal page requested for client {client_id}")
print(f"[DEBUG] [Web] Available clients: {list(clients.keys())}")
if client_id not in clients:
print(f"[ERROR] [Web] Client '{client_id}' not found, redirecting to index")
flash('Client not connected')
return redirect(url_for('index'))
print(f"[DEBUG] [Web] Rendering terminal template for client {client_id}")
return render_template('terminal.html', client_id=client_id)
@app.route('/api/clients')
@login_required
def get_clients():
client_info = {}
for client_id, client_data in clients.items():
client_info[client_id] = {
'status': client_data['status'],
'last_seen': client_data['last_seen']
}
return jsonify({
'clients': client_info,
'count': len(clients)
})
@app.route('/favicon.ico')
def favicon():
if getattr(sys, 'frozen', False):
# Running as bundled executable
logos_dir = os.path.join(sys._MEIPASS, 'logos')
else:
# Running as script
logos_dir = 'logos'
return send_from_directory(logos_dir, 'favicon.ico')
@app.route('/image.jpg')
def logo_file():
if getattr(sys, 'frozen', False):
# Running as bundled executable
logos_dir = os.path.join(sys._MEIPASS, 'logos')
else:
# Running as script
logos_dir = 'logos'
return send_from_directory(logos_dir, 'logo-128.png')
@app.route('/terminal/<client_id>/connect', methods=['POST'])
@login_required
def connect_terminal(client_id):
print(f"[DEBUG] [Web] === TERMINAL CONNECT REQUEST START ===")
print(f"[DEBUG] [Web] Raw request method: {request.method}")
print(f"[DEBUG] [Web] Raw request URL: {request.url}")
print(f"[DEBUG] [Web] Raw request data: {request.get_data(as_text=True)}")
print(f"[DEBUG] [Web] Current user authenticated: {current_user.is_authenticated}")
if hasattr(current_user, 'username'):
print(f"[DEBUG] [Web] Current user: {current_user.username}")
from .config import load_config
args = load_config()
username = request.form.get('username', 'root')
cols = int(request.form.get('cols', 80))
rows = int(request.form.get('rows', 24))
print(f"[DEBUG] [Web] Terminal connect request received for client {client_id}")
print(f"[DEBUG] [Web] Parameters: username={username}, cols={cols}, rows={rows}")
print(f"[DEBUG] [Web] Available clients: {list(clients.keys())}")
print(f"[DEBUG] [Web] Client '{client_id}' in clients: {client_id in clients}")
if client_id not in clients:
print(f"[ERROR] [Web] Client '{client_id}' not found in connected clients")
return jsonify({'error': f'Client {client_id} not connected'}), 404
if hasattr(args, 'debug') and args.debug:
print(f"[DEBUG] [Web] Creating terminal session for client {client_id}, username {username}")
try:
terminal_session = create_terminal_session(args, username, client_id)
request_id = terminal_session['request_id']
print(f"[DEBUG] [Web] Terminal session created successfully with request_id {request_id}")
# Store terminal session
active_terminals[request_id] = {
'client_id': client_id,
'username': username,
'proc': terminal_session['proc'],
'output_buffer': terminal_session['output_buffer'],
'master': terminal_session['master']
}
if hasattr(args, 'debug') and args.debug:
print(f"[DEBUG] [Web] Terminal session stored with request_id {request_id}")
response_data = {
'request_id': request_id,
'command': terminal_session['command']
}
print(f"[DEBUG] [Web] Returning response: {response_data}")
print(f"[DEBUG] [Web] === TERMINAL CONNECT REQUEST END ===")
return jsonify(response_data)
except Exception as e:
print(f"[ERROR] [Web] Failed to create terminal session: {e}")
import traceback
traceback.print_exc()
return jsonify({'error': str(e)}), 500
@app.route('/terminal/<client_id>/data', methods=['GET', 'POST'])
@login_required
def terminal_data(client_id):
if request.method == 'POST':
request_id = request.form.get('request_id')
data = request.form.get('data')
if request_id in active_terminals:
success = send_terminal_data(active_terminals[request_id], data)
if not success:
return jsonify({'error': 'Failed to send data'}), 500
return 'OK'
else:
request_id = request.args.get('request_id')
if request_id in active_terminals:
data = get_terminal_output(active_terminals[request_id])
return data
return ''
@app.route('/terminal/<client_id>/disconnect', methods=['POST'])
@login_required
def disconnect_terminal_route(client_id):
request_id = request.form.get('request_id')
if request_id in active_terminals:
disconnect_terminal(active_terminals[request_id])
del active_terminals[request_id]
return 'OK'
@app.route('/terminal/<client_id>/resize', methods=['POST'])
@login_required
def resize_terminal_route(client_id):
request_id = request.form.get('request_id')
cols = int(request.form.get('cols', 80))
rows = int(request.form.get('rows', 24))
if request_id in active_terminals:
resize_terminal(active_terminals[request_id], cols, rows)
return 'OK'
def run_flask(host, port, debug=False, use_https=False):
"""Run the Flask application"""
if use_https:
# Handle HTTPS setup
web_cert_path = os.path.join(config_dir, 'web-cert.pem')
web_key_path = os.path.join(config_dir, 'web-key.pem')
# Generate self-signed certificate if it doesn't exist
if not os.path.exists(web_cert_path) or not os.path.exists(web_key_path):
print("Generating self-signed certificate for web interface...")
os.system(f'openssl req -x509 -newkey rsa:4096 -keyout {web_key_path} -out {web_cert_path} -days 36500 -nodes -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"')
ssl_context = (web_cert_path, web_key_path)
protocol = "https"
else:
ssl_context = None
protocol = "http"
print(f"Web interface available at {protocol}://{host}:{port}")
app.run(host=host, port=port, debug=debug, use_reloader=False, threaded=True, ssl_context=ssl_context)
\ No newline at end of file
"""
WebSocket handling for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import asyncio
import json
import time
import websockets
from .terminal import openpty_with_fallback
from .tunnel import Tunnel, TunnelStatus, detect_client_public_ip, detect_client_private_ip
# Client registry: id -> {'websocket': ws, 'last_seen': timestamp, 'status': 'active'|'disconnected'}
clients = {}
# Active tunnels: request_id -> {'client_ws': ws, 'wsssh_ws': ws, 'client_id': id}
active_tunnels = {}
# Active terminals: request_id -> {'client_id': id, 'username': username, 'proc': proc}
active_terminals = {}
# Pre-computed JSON messages for better performance
TUNNEL_DATA_MSG = '{"type": "tunnel_data", "request_id": "%s", "data": "%s"}'
TUNNEL_ACK_MSG = '{"type": "tunnel_ack", "request_id": "%s"}'
TUNNEL_CLOSE_MSG = '{"type": "tunnel_close", "request_id": "%s"}'
TUNNEL_REQUEST_MSG = '{"type": "tunnel_request", "request_id": "%s"}'
TUNNEL_ERROR_MSG = '{"type": "tunnel_error", "request_id": "%s", "error": "%s"}'
TUNNEL_KEEPALIVE_MSG = '{"type": "tunnel_keepalive", "request_id": "%s", "total_bytes": %d, "rate_bps": %.2f}'
TUNNEL_KEEPALIVE_ACK_MSG = '{"type": "tunnel_keepalive_ack", "request_id": "%s"}'
REGISTERED_MSG = '{"type": "registered", "id": "%s"}'
REGISTRATION_ERROR_MSG = '{"type": "registration_error", "error": "%s"}'
SERVER_SHUTDOWN_MSG = '{"type": "server_shutdown", "message": "Server is shutting down"}'
debug = False
shutdown_event = None
def cleanup_expired_clients():
"""Remove clients that have been disconnected for more than 30 seconds"""
current_time = time.time()
expired_clients = []
for client_id, client_info in clients.items():
if client_info['status'] == 'disconnected':
if current_time - client_info['last_seen'] > 30:
expired_clients.append(client_id)
if debug_flag: print(f"[DEBUG] [WebSocket] Client {client_id} expired and removed")
for client_id in expired_clients:
del clients[client_id]
def check_keepalive_timeouts():
"""Check for tunnels that haven't received keep-alive messages from both endpoints for too long"""
current_time = time.time()
timeout_seconds = 220 # 4 * 30 + 10 seconds margin (4 lost messages + buffer)
tunnels_to_close = []
for request_id, tunnel in active_tunnels.items():
if tunnel.status == TunnelStatus.ACTIVE:
# Check both endpoints for keep-alive timeout
client_timeout = current_time - tunnel.last_keepalive_from_client > timeout_seconds
tool_timeout = current_time - tunnel.last_keepalive_from_tool > timeout_seconds
if client_timeout or tool_timeout:
# Determine which endpoint(s) timed out
timeout_reason = ""
if client_timeout and tool_timeout:
timeout_reason = "both endpoints"
elif client_timeout:
timeout_reason = "client (wssshc) endpoint"
else:
timeout_reason = "tool (wsssht/wsscp) endpoint"
if debug:
print(f"[DEBUG] [WebSocket] Keep-alive timeout for tunnel {request_id} from {timeout_reason}")
print(f"[DEBUG] [WebSocket] Client last keep-alive: {current_time - tunnel.last_keepalive_from_client:.1f} seconds ago")
print(f"[DEBUG] [WebSocket] Tool last keep-alive: {current_time - tunnel.last_keepalive_from_tool:.1f} seconds ago")
tunnels_to_close.append((request_id, timeout_reason))
# Close timed-out tunnels
for request_id, timeout_reason in tunnels_to_close:
tunnel = active_tunnels.get(request_id)
if tunnel:
# Update tunnel status to closing
tunnel.update_status(TunnelStatus.CLOSING, f"Keep-alive timeout from {timeout_reason}")
# Send close messages to both sides
try:
close_msg = TUNNEL_CLOSE_MSG % request_id
if debug: print(f"[DEBUG] [WebSocket] Sending tunnel close due to timeout: {close_msg}")
# Send to client (wssshc)
if tunnel.client_ws:
tunnel.client_ws.send(close_msg)
# Send to wsssh/wsscp
if tunnel.wsssh_ws:
tunnel.wsssh_ws.send(close_msg)
except Exception as e:
if debug: print(f"[DEBUG] [WebSocket] Failed to send timeout close message: {e}")
# Clean up tunnel
tunnel.update_status(TunnelStatus.CLOSED)
del active_tunnels[request_id]
if not debug:
print(f"[EVENT] Tunnel {request_id} closed due to keep-alive timeout from {timeout_reason}")
def print_status():
"""Print minimal status information when not in debug mode"""
if debug_flag:
return
uptime = time.time() - start_time
active_clients = sum(1 for c in clients.values() if c['status'] == 'active')
total_clients = len(clients)
active_tunnels_count = sum(1 for t in active_tunnels.values() if t.status == 'active')
total_tunnels = len(active_tunnels)
hours = int(uptime // 3600)
minutes = int((uptime % 3600) // 60)
seconds = int(uptime % 60)
print(f"[STATUS] Uptime: {hours:02d}:{minutes:02d}:{seconds:02d} | "
f"Clients: {active_clients}/{total_clients} active | "
f"Tunnels: {active_tunnels_count}/{total_tunnels} active")
async def handle_websocket(websocket, path=None, *, server_password=None, debug_flag=None):
"""Handle WebSocket connections from clients"""
if debug_flag:
print(f"[DEBUG] New WebSocket connection established from {websocket.remote_address}")
try:
while True:
# Check for shutdown signal before each message
if shutdown_event and shutdown_event.is_set():
if debug_flag: print("[DEBUG] Shutdown event detected in WebSocket handler")
break
try:
message = await websocket.recv()
except websockets.exceptions.ConnectionClosed:
# Connection closed normally
break
# Process the message (rest of the original logic)
# Log debug info for control channel messages when debug is enabled (excluding data channel messages)
try:
data = json.loads(message)
msg_type = data.get('type', 'unknown')
if debug and msg_type not in ('tunnel_data', 'tunnel_response'):
print(f"[DEBUG] [WebSocket] Received {msg_type} message: {message}")
except json.JSONDecodeError as e:
if debug_flag: print(f"[DEBUG] [WebSocket] Invalid JSON received: {e}")
continue
if data.get('type') == 'register':
client_id = data.get('client_id') or data.get('id')
client_password = data.get('password', '')
tunnel_param = data.get('tunnel', 'any')
tunnel_control_param = data.get('tunnel_control', 'any')
wssshd_private_ip = data.get('wssshd_private_ip', None)
print(f"[DEBUG] Processing registration for client {client_id}")
print(f"[DEBUG] Received password: '{client_password}', expected: '{server_password}'")
print(f"[DEBUG] server_password type: {type(server_password)}, value: {repr(server_password)}")
if client_password == server_password:
# Check if client was previously disconnected
was_disconnected = False
if client_id in clients and clients[client_id]['status'] == 'disconnected':
was_disconnected = True
if debug_flag: print(f"[DEBUG] [WebSocket] Client {client_id} reconnecting (was disconnected)")
clients[client_id] = {
'websocket': websocket,
'last_seen': time.time(),
'status': 'active',
'tunnel': tunnel_param,
'tunnel_control': tunnel_control_param,
'wssshd_private_ip': wssshd_private_ip
}
if was_disconnected:
if not debug:
print(f"[EVENT] Client {client_id} reconnected")
else:
print(f"Client {client_id} reconnected")
else:
if not debug:
print(f"[EVENT] Client {client_id} registered")
else:
print(f"Client {client_id} registered")
try:
response_msg = REGISTERED_MSG % client_id
if debug_flag: print(f"[DEBUG] [WebSocket] Sending registration response: {response_msg}")
await websocket.send(response_msg)
except Exception:
if debug_flag: print(f"[DEBUG] [WebSocket] Failed to send registration response to {client_id}")
else:
if debug_flag: print(f"[DEBUG] [WebSocket] Client {client_id} registration failed: invalid password")
try:
error_msg = REGISTRATION_ERROR_MSG % "Invalid password"
if debug_flag: print(f"[DEBUG] [WebSocket] Sending registration error: {error_msg}")
await websocket.send(error_msg)
except Exception:
if debug_flag: print(f"[DEBUG] [WebSocket] Failed to send registration error to {client_id}")
elif data.get('type') == 'tunnel_request':
client_id = data['client_id']
request_id = data['request_id']
tunnel_param = data.get('tunnel', 'any')
tunnel_control_param = data.get('tunnel_control', 'any')
service_param = data.get('service', 'ssh')
tool_private_ip = data.get('tool_private_ip', None)
client_info = clients.get(client_id)
if client_info and client_info['status'] == 'active':
# Create comprehensive tunnel object
tunnel = Tunnel(request_id, client_id)
# Set tunnel parameters
tunnel.tunnel = tunnel_param
tunnel.tunnel_control = tunnel_control_param
tunnel.service = service_param
# Set WebSocket connections
tunnel.set_websockets(client_info['websocket'], websocket)
# Detect and set IP information
tunnel.set_destination_info(
public_ip=detect_client_public_ip(client_info['websocket']),
private_ip=detect_client_private_ip(client_info['websocket'])
)
# Set source (tool) IP information from the request
if tool_private_ip:
tunnel.set_source_info(private_ip=tool_private_ip)
# Store tunnel object
active_tunnels[request_id] = tunnel
# Update tunnel status to active
tunnel.update_status(TunnelStatus.ACTIVE)
# Forward tunnel request to client
try:
request_msg = TUNNEL_REQUEST_MSG % request_id
ack_msg = TUNNEL_ACK_MSG % request_id
if debug_flag:
print(f"[DEBUG] [WebSocket] Sending tunnel request to client: {request_msg}")
print(f"[DEBUG] [WebSocket] Sending tunnel ack to wsssh: {ack_msg}")
await client_info['websocket'].send(request_msg)
await websocket.send(ack_msg)
if not debug:
print(f"[EVENT] New tunnel {request_id} for client {client_id}")
else:
print(f"[DEBUG] Created tunnel object: {tunnel}")
except Exception as e:
tunnel.update_status(TunnelStatus.ERROR, str(e))
# Send error response for tunnel request failures
try:
error_msg = TUNNEL_ERROR_MSG % (request_id, "Failed to forward request")
if debug_flag: print(f"[DEBUG] [WebSocket] Sending tunnel error: {error_msg}")
await websocket.send(error_msg)
except Exception:
pass # Silent failure if even error response fails
else:
try:
error_msg = TUNNEL_ERROR_MSG % (request_id, "Client not registered or disconnected")
if debug_flag: print(f"[DEBUG] [WebSocket] Sending tunnel error: {error_msg}")
await websocket.send(error_msg)
except Exception:
pass # Silent failure for error responses
elif data.get('type') == 'tunnel_data':
# Optimized tunnel data forwarding
request_id = data['request_id']
if request_id in active_tunnels:
tunnel = active_tunnels[request_id]
# Check if tunnel is active and client is connected
if tunnel.status == TunnelStatus.ACTIVE:
client_info = clients.get(tunnel.client_id)
if client_info and client_info['status'] == 'active':
# Track bytes sent for statistics
data_str = data['data']
if data_str:
# Approximate byte count (base64 encoded data)
import base64
try:
decoded = base64.b64decode(data_str)
tunnel.total_bytes_sent += len(decoded)
tunnel.bytes_last_period += len(decoded)
except:
# If decoding fails, estimate based on string length
tunnel.total_bytes_sent += len(data_str) * 3 // 4 # Rough base64 decode estimate
tunnel.bytes_last_period += len(data_str) * 3 // 4
# Use pre-formatted JSON template for better performance
try:
await tunnel.client_ws.send(TUNNEL_DATA_MSG % (request_id, data['data']))
except Exception:
# Silent failure for performance - connection issues will be handled by cleanup
pass
# No debug logging for performance - tunnel_data messages are too frequent
elif data.get('type') == 'tunnel_response':
# Optimized tunnel response forwarding
request_id = data['request_id']
tunnel = active_tunnels.get(request_id)
if tunnel and tunnel.status == TunnelStatus.ACTIVE:
# Track bytes received for statistics
data_str = data['data']
if data_str:
# Approximate byte count (base64 encoded data)
import base64
try:
decoded = base64.b64decode(data_str)
tunnel.total_bytes_received += len(decoded)
tunnel.bytes_last_period += len(decoded)
except:
# If decoding fails, estimate based on string length
tunnel.total_bytes_received += len(data_str) * 3 // 4 # Rough base64 decode estimate
tunnel.bytes_last_period += len(data_str) * 3 // 4
try:
await tunnel.wsssh_ws.send(TUNNEL_DATA_MSG % (request_id, data['data']))
except Exception:
# Silent failure for performance - connection issues will be handled by cleanup
pass
elif data.get('type') == 'tunnel_close':
request_id = data['request_id']
tunnel = active_tunnels.get(request_id)
if tunnel:
# Update tunnel status to closing
tunnel.update_status(TunnelStatus.CLOSING)
# Forward close to the other endpoint (bidirectional)
# If message comes from client (wssshc), forward to wsssht
# If message comes from wsssht, forward to client (wssshc)
if websocket == tunnel.client_ws:
# Message from wssshc, forward to wsssht
if tunnel.wsssh_ws:
try:
close_msg = TUNNEL_CLOSE_MSG % request_id
if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding tunnel close from wssshc to wsssht: {close_msg}")
await tunnel.wsssh_ws.send(close_msg)
except Exception:
# Silent failure for performance
pass
elif websocket == tunnel.wsssh_ws:
# Message from wsssht, forward to wssshc
client_info = clients.get(tunnel.client_id)
if client_info and client_info['status'] == 'active':
try:
close_msg = TUNNEL_CLOSE_MSG % request_id
if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding tunnel close from wsssht to wssshc: {close_msg}")
await tunnel.client_ws.send(close_msg)
except Exception:
# Silent failure for performance
pass
# Update tunnel status to closed and clean up
tunnel.update_status(TunnelStatus.CLOSED)
del active_tunnels[request_id]
if debug_flag:
print(f"[DEBUG] [WebSocket] Tunnel {request_id} closed")
print(f"[DEBUG] Tunnel object: {tunnel}")
else:
print(f"[EVENT] Tunnel {request_id} closed")
elif data.get('type') == 'tunnel_keepalive':
request_id = data['request_id']
total_bytes = data.get('total_bytes', 0)
rate_bps = data.get('rate_bps', 0.0)
tunnel = active_tunnels.get(request_id)
if tunnel:
# Determine which endpoint sent the message
current_time = time.time()
if websocket == tunnel.client_ws:
# Message from wssshc (client)
tunnel.last_keepalive_from_client = current_time
endpoint_name = "client (wssshc)"
forward_ws = tunnel.wsssh_ws
forward_name = "wsssh"
elif websocket == tunnel.wsssh_ws:
# Message from wsssht/wsscp (tool)
tunnel.last_keepalive_from_tool = current_time
endpoint_name = "tool (wsssht/wsscp)"
forward_ws = tunnel.client_ws
forward_name = "client"
else:
if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive from unknown websocket for tunnel {request_id}")
continue
# Log keep-alive statistics
if debug_flag:
print(f"[DEBUG] [WebSocket] Keep-alive received from {endpoint_name} for tunnel {request_id}: total={total_bytes} bytes, rate={rate_bps:.2f} B/s")
# Forward keep-alive to the other side
try:
keepalive_msg = TUNNEL_KEEPALIVE_MSG % (request_id, total_bytes, rate_bps)
if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding keep-alive to {forward_name}: {keepalive_msg}")
await forward_ws.send(keepalive_msg)
# Reset failure counter on successful forward
tunnel.keepalive_forward_failures = 0
except Exception as e:
if debug_flag: print(f"[DEBUG] [WebSocket] Failed to forward keep-alive: {e}")
# Increment failure counter
tunnel.keepalive_forward_failures += 1
if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive forward failure count: {tunnel.keepalive_forward_failures}")
# Close tunnel if 3 consecutive failures
if tunnel.keepalive_forward_failures >= 3:
if debug_flag: print(f"[DEBUG] [WebSocket] Closing tunnel {request_id} due to 3 consecutive keep-alive forwarding failures")
# Send close messages to both ends
try:
close_msg = TUNNEL_CLOSE_MSG % request_id
if tunnel.client_ws:
await tunnel.client_ws.send(close_msg)
if tunnel.wsssh_ws:
await tunnel.wsssh_ws.send(close_msg)
except Exception:
pass # Silent failure for cleanup
# Update tunnel status and clean up
tunnel.update_status(TunnelStatus.CLOSED, "Keep-alive forwarding failed 3 times")
if request_id in active_tunnels:
del active_tunnels[request_id]
if not debug:
print(f"[EVENT] Tunnel {request_id} closed due to keep-alive forwarding failures")
continue
# Send ACK response back to sender
try:
ack_msg = TUNNEL_KEEPALIVE_ACK_MSG % request_id
if debug_flag: print(f"[DEBUG] [WebSocket] Sending keep-alive ACK: {ack_msg}")
await websocket.send(ack_msg)
except Exception as e:
if debug_flag: print(f"[DEBUG] [WebSocket] Failed to send keep-alive ACK: {e}")
else:
if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive received for unknown tunnel {request_id}")
elif data.get('type') == 'tunnel_keepalive_ack':
request_id = data['request_id']
tunnel = active_tunnels.get(request_id)
if tunnel:
# Determine which endpoint sent the ACK
if websocket == tunnel.client_ws:
endpoint_name = "client (wssshc)"
forward_ws = tunnel.wsssh_ws
forward_name = "wsssh"
elif websocket == tunnel.wsssh_ws:
endpoint_name = "tool (wsssht/wsscp)"
forward_ws = tunnel.client_ws
forward_name = "client"
else:
if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive ACK from unknown websocket for tunnel {request_id}")
continue
if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive ACK received from {endpoint_name} for tunnel {request_id}")
# Forward ACK to the other side
try:
ack_msg = TUNNEL_KEEPALIVE_ACK_MSG % request_id
if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding keep-alive ACK to {forward_name}: {ack_msg}")
await forward_ws.send(ack_msg)
# Reset failure counter on successful forward
tunnel.keepalive_ack_forward_failures = 0
except Exception as e:
if debug_flag: print(f"[DEBUG] [WebSocket] Failed to forward keep-alive ACK: {e}")
# Increment failure counter
tunnel.keepalive_ack_forward_failures += 1
if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive ACK forward failure count: {tunnel.keepalive_ack_forward_failures}")
# Close tunnel if 3 consecutive failures
if tunnel.keepalive_ack_forward_failures >= 3:
if debug_flag: print(f"[DEBUG] [WebSocket] Closing tunnel {request_id} due to 3 consecutive keep-alive ACK forwarding failures")
# Send close messages to both ends
try:
close_msg = TUNNEL_CLOSE_MSG % request_id
if tunnel.client_ws:
await tunnel.client_ws.send(close_msg)
if tunnel.wsssh_ws:
await tunnel.wsssh_ws.send(close_msg)
except Exception:
pass # Silent failure for cleanup
# Update tunnel status and clean up
tunnel.update_status(TunnelStatus.CLOSED, "Keep-alive ACK forwarding failed 3 times")
if request_id in active_tunnels:
del active_tunnels[request_id]
if not debug:
print(f"[EVENT] Tunnel {request_id} closed due to keep-alive ACK forwarding failures")
continue
else:
if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive ACK received for unknown tunnel {request_id}")
except websockets.exceptions.ConnectionClosed:
# Mark client as disconnected instead of removing immediately
disconnected_client = None
for cid, client_info in clients.items():
if client_info['websocket'] == websocket:
disconnected_client = cid
clients[cid]['status'] = 'disconnected'
clients[cid]['last_seen'] = time.time()
if debug_flag: print(f"[DEBUG] [WebSocket] Client {cid} disconnected (marked for timeout)")
break
# Clean up active tunnels for this client (optimized)
if disconnected_client:
# Use list comprehension for better performance
tunnels_to_remove = [rid for rid, tunnel in active_tunnels.items()
if tunnel.client_id == disconnected_client]
for request_id in tunnels_to_remove:
tunnel = active_tunnels[request_id]
tunnel.update_status(TunnelStatus.ERROR, "Client disconnected")
del active_tunnels[request_id]
if debug_flag:
print(f"[DEBUG] [WebSocket] Tunnel {request_id} cleaned up due to client disconnect")
print(f"[DEBUG] Tunnel object: {tunnel}")
async def cleanup_task(debug_flag=False):
"""Periodic task to clean up expired clients and report status"""
global debug
debug = debug_flag
last_status_time = 0
while True:
# Use moderate sleep intervals for cleanup
await asyncio.sleep(5) # Run every 5 seconds
cleanup_expired_clients()
check_keepalive_timeouts()
# Print status every 60 seconds (12 iterations)
current_time = time.time()
if current_time - last_status_time >= 60:
print_status()
last_status_time = current_time
# Initialize start_time
start_time = time.time()
\ No newline at end of file
wsssh-server (1.6.5-1) unstable; urgency=medium
* Version 1.6.5: Transition to C binary with SQLite database
* Complete rewrite from Python/PyInstaller to native C implementation
* Added SQLite database for persistent user management
* Implemented embedded web interface with user authentication
* Added comprehensive user management (add/edit/delete users)
* Security warning system for default admin credentials
* ASCII art banner on startup
* Improved performance with native C binary (no Python dependencies)
* Updated build system to use GCC instead of PyInstaller
* Added libsqlite3 and libssl runtime dependencies
* Updated man page with C version command line options
* Maintained all existing init scripts, logrotate, and service configurations
* Preserved backward compatibility for service management
* Enhanced security with proper session management
* Added debug options for database operations
-- Stefy Lanza <stefy@nexlab.net> Wed, 18 Sep 2025 08:47:00 +0200
wsssh-server (1.6.1-1) unstable; urgency=medium
* Version 1.6.1: Major code refactoring and documentation updates
......
......@@ -2,7 +2,7 @@ Source: wsssh-server
Section: net
Priority: optional
Maintainer: Stefy Lanza <stefy@nexlab.net>
Build-Depends: debhelper-compat (= 13), python3, python3-pip, python3-setuptools
Build-Depends: debhelper-compat (= 13), gcc, make, libssl-dev, libsqlite3-dev, uuid-dev
Standards-Version: 4.6.2
Homepage: https://git.nexlab.net/nexlab/wsssh
Vcs-Browser: https://git.nexlab.net/nexlab/wsssh
......@@ -10,13 +10,13 @@ Vcs-Git: https://git.nexlab.net/nexlab/wsssh.git
Package: wsssh-server
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, debconf (>= 0.5) | debconf-2.0
Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, debconf (>= 0.5) | debconf-2.0, libsqlite3-0, libssl3
Description: WSSSH: Warp-Powered Stefy's Spatial Secure Hyperdrive Server (wssshd)
wsssh is a swiss army's knife server assisted tunnelling system for the win initially born as a websocket ssh tunnelling system and evolved to a universal tunnelling utility. This package contains the server component
that handles WebSocket connections and manages SSH tunnels.
.
This package includes a standalone PyInstaller binary that bundles all
required dependencies, eliminating the need for external Python packages.
wsssh is a swiss army's knife server assisted tunnelling system for the win initially born as a websocket ssh tunnelling system and evolved to a universal tunnelling utility. This package contains the server component
that handles WebSocket connections and manages SSH tunnels.
.
This package includes a standalone C binary that provides high performance
and includes an embedded web interface with SQLite database for user management.
.
The wssshd server provides:
- WSSSH tunnel management
......
......@@ -16,88 +16,12 @@
dh $@
override_dh_auto_configure:
# Create PyInstaller spec file for wssshd
@echo '# -*- mode: python ; coding: utf-8 -*-' > wssshd.spec
@echo '' >> wssshd.spec
@echo 'block_cipher = None' >> wssshd.spec
@echo '' >> wssshd.spec
@echo 'a = Analysis(' >> wssshd.spec
@echo " ['../wssshd.py']," >> wssshd.spec
@echo ' pathex=[],' >> wssshd.spec
@echo ' binaries=[],' >> wssshd.spec
@echo ' datas=[],' >> wssshd.spec
@echo ' hiddenimports=[' >> wssshd.spec
@echo " 'websockets'," >> wssshd.spec
@echo " 'flask'," >> wssshd.spec
@echo " 'flask_login'," >> wssshd.spec
@echo " 'flask_sqlalchemy'," >> wssshd.spec
@echo " 'ssl'," >> wssshd.spec
@echo " 'asyncio'," >> wssshd.spec
@echo " 'configparser'," >> wssshd.spec
@echo " 'argparse'," >> wssshd.spec
@echo " 'signal'," >> wssshd.spec
@echo " 'os'," >> wssshd.spec
@echo " 'sys'," >> wssshd.spec
@echo " 'json'," >> wssshd.spec
@echo " 'subprocess'," >> wssshd.spec
@echo " 'pty'," >> wssshd.spec
@echo " 'select'," >> wssshd.spec
@echo " 'termios'," >> wssshd.spec
@echo " 'fcntl'," >> wssshd.spec
@echo " 'stat'," >> wssshd.spec
@echo " 'threading'," >> wssshd.spec
@echo " 'time'," >> wssshd.spec
@echo " 'uuid'," >> wssshd.spec
@echo " 'socket'," >> wssshd.spec
@echo " 'netdb'," >> wssshd.spec
@echo " 'errno'," >> wssshd.spec
@echo " 'pysqlite3'" >> wssshd.spec
@echo ' ],' >> wssshd.spec
@echo ' hookspath=[],' >> wssshd.spec
@echo ' hooksconfig={},' >> wssshd.spec
@echo ' runtime_hooks=[],' >> wssshd.spec
@echo ' excludes=[],' >> wssshd.spec
@echo ' win_no_prefer_redirects=False,' >> wssshd.spec
@echo ' win_private_assemblies=False,' >> wssshd.spec
@echo ' cipher=block_cipher,' >> wssshd.spec
@echo ' noarchive=False,' >> wssshd.spec
@echo ')' >> wssshd.spec
@echo '' >> wssshd.spec
@echo 'pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)' >> wssshd.spec
@echo '' >> wssshd.spec
@echo 'exe = EXE(' >> wssshd.spec
@echo ' pyz,' >> wssshd.spec
@echo ' a.scripts,' >> wssshd.spec
@echo ' a.binaries,' >> wssshd.spec
@echo ' a.zipfiles,' >> wssshd.spec
@echo ' a.datas,' >> wssshd.spec
@echo ' [],' >> wssshd.spec
@echo " name='wssshd'," >> wssshd.spec
@echo ' debug=False,' >> wssshd.spec
@echo ' bootloader_ignore_signals=False,' >> wssshd.spec
@echo ' strip=False,' >> wssshd.spec
@echo ' upx=True,' >> wssshd.spec
@echo ' upx_exclude=[],' >> wssshd.spec
@echo ' runtime_tmpdir=None,' >> wssshd.spec
@echo ' console=True,' >> wssshd.spec
@echo ' disable_windowed_traceback=False,' >> wssshd.spec
@echo ' argv_emulation=False,' >> wssshd.spec
@echo ' target_arch=None,' >> wssshd.spec
@echo ' codesign_identity=None,' >> wssshd.spec
@echo ' entitlements_file=None,' >> wssshd.spec
@echo ')' >> wssshd.spec
# Configure wssshd2 build
cd ../wssshd2 && ./configure.sh
override_dh_auto_build:
# Check if wssshd binary already exists in dist directory
if [ -f ../dist/wssshd ]; then \
echo "Using existing wssshd binary from ../dist/wssshd"; \
mkdir -p dist; \
cp ../dist/wssshd dist/wssshd; \
else \
echo "Building PyInstaller binary"; \
pip3 install pyinstaller; \
pyinstaller --clean --onefile wssshd.spec; \
fi
# Build wssshd2 C binary
cd ../wssshd2 && make clean && make
override_dh_auto_install:
# Create necessary directories
......@@ -114,8 +38,8 @@ override_dh_auto_install:
mkdir -p debian/wsssh-server/usr/sbin
mkdir -p debian/wsssh-server/etc/logrotate.d
# Install PyInstaller binary
install -m 755 dist/wssshd debian/wsssh-server/usr/bin/
# Install C binary
install -m 755 ../wssshd2/wssshd debian/wsssh-server/usr/bin/
# Install init script
install -m 755 ../wssshd.init debian/wsssh-server/etc/init.d/wssshd
......@@ -142,6 +66,5 @@ override_dh_auto_install:
cp -r ../logos/* debian/wsssh-server/usr/share/wsssh/logos/
override_dh_auto_clean:
rm -rf build *.spec
# Don't clean dist directory to preserve existing wssshd binary
rm -rf build *.spec dist
dh_auto_clean
\ No newline at end of file
......@@ -6,9 +6,14 @@ wssshd \- WebSocket SSH Server daemon for secure tunneling
[\fB\-\-config\fR \fIFILE\fR]
[\fB\-\-host\fR \fIHOST\fR]
[\fB\-\-port\fR \fIPORT\fR]
[\fB\-\-ssl\-cert\fR \fIFILE\fR]
[\fB\-\-ssl\-key\fR \fIFILE\fR]
[\fB\-\-domain\fR \fIDOMAIN\fR]
[\fB\-\-password\fR \fIPASSWORD\fR]
[\fB\-\-web\-host\fR \fIHOST\fR]
[\fB\-\-web\-port\fR \fIPORT\fR]
[\fB\-\-web\-https\fR]
[\fB\-\-debug\fR]
[\fB\-\-debug\-web\fR]
[\fB\-\-debug\-database\fR]
[\fB\-\-help\fR]
.SH DESCRIPTION
.B wssshd
......@@ -19,52 +24,82 @@ is a WebSocket SSH server daemon that provides secure tunneling capabilities for
Configuration file path (default: /etc/wssshd.conf)
.TP
.BR \-\-host " \fIHOST\fR"
Server bind address (default: 0.0.0.0)
WebSocket server bind address (default: 0.0.0.0)
.TP
.BR \-\-port " \fIPORT\fR"
Server port (default: 9898)
WebSocket server port (default: 9898)
.TP
.BR \-\-ssl\-cert " \fIFILE\fR"
SSL certificate file path
.BR \-\-domain " \fIDOMAIN\fR"
Domain name for the server
.TP
.BR \-\-ssl\-key " \fIFILE\fR"
SSL private key file path
.BR \-\-password " \fIPASSWORD\fR"
Server password for client authentication
.TP
.BR \-\-web\-host " \fIHOST\fR"
Web interface bind address (default: 127.0.0.1)
.TP
.BR \-\-web\-port " \fIPORT\fR"
Web interface port (default: 8080)
.TP
.B \-\-web\-https
Enable HTTPS for web interface
.TP
.B \-\-debug
Enable debug output for troubleshooting
Enable general debug output
.TP
.B \-\-debug\-web
Enable web interface debug output
.TP
.B \-\-debug\-database
Enable database operation debug output
.TP
.B \-\-help
Display help message and exit
.SH CONFIGURATION
The server can be configured through command line options or configuration files. The configuration file supports the following sections and options:
.TP
.B [server]
- \fBhost\fR: Server bind address
- \fBport\fR: Server port
- \fBssl_cert\fR: SSL certificate file
- \fBssl_key\fR: SSL private key file
- \fBdebug\fR: Enable debug mode
.TP
.B [database]
- \fBpath\fR: SQLite database file path
.TP
.B [web]
- \fBusername\fR: Web interface username
- \fBpassword\fR: Web interface password (hashed)
- \fBsecret_key\fR: Flask secret key for sessions
The server can be configured through command line options or configuration files. The configuration file supports the following options:
.TP
.B host
WebSocket server bind address (default: 0.0.0.0)
.TP
.B port
WebSocket server port (default: 9898)
.TP
.B domain
Domain name for the server
.TP
.B password
Server password for client authentication
.TP
.B web_host
Web interface bind address (default: 127.0.0.1)
.TP
.B web_port
Web interface port (default: 8080)
.TP
.B web_https
Enable HTTPS for web interface (default: false)
.TP
.B debug
Enable general debug output (default: false)
.TP
.B debug_web
Enable web interface debug output (default: false)
.TP
.B debug_database
Enable database operation debug output (default: false)
.SH EXAMPLES
.TP
Start server with default configuration:
.B wssshd
.TP
Start server with custom configuration:
.B wssshd --config /etc/wssshd/custom.conf
.B wssshd --config /etc/wssshd.conf
.TP
Start server with debug output:
.B wssshd --debug --port 8080
.B wssshd --debug --debug-web --debug-database
.TP
Start server with SSL:
.B wssshd --ssl-cert /etc/ssl/certs/wssshd.crt --ssl-key /etc/ssl/private/wssshd.key
Start server with custom web interface:
.B wssshd --web-host 0.0.0.0 --web-port 8080 --web-https
.SH WEB INTERFACE
The server provides a web-based management interface accessible at https://server:port/ (when SSL is enabled) or http://server:port/ (without SSL). The web interface allows:
- Client registration and management
......@@ -89,8 +124,11 @@ Reload configuration (if supported)
.I /etc/wssshd.conf
Main configuration file
.TP
.I /var/lib/wssshd/wssshd.db
SQLite database for client and tunnel data
.I ~/.config/wssshd/users.db
SQLite database for user management (per-user)
.TP
.I /etc/wssshd/users.db
SQLite database for user management (system-wide, when running as root)
.TP
.I /var/log/wssshd/
Log directory
......
#!/usr/bin/env python3
"""
WSSSH Daemon (wssshd)
Handles WebSocket connections from clients and wsssh/wsscp applications.
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# Explicit imports to ensure PyInstaller includes them
import websockets
import websockets.server
import websockets.client
import websockets.exceptions
import websockets.protocol
import websockets.uri
from wsssd import main
if __name__ == '__main__':
main()
\ No newline at end of file
......@@ -46,8 +46,7 @@ const char *get_embedded_asset(const char *path, size_t *size) {
if (size) *size = strlen(terminal_page_html);
return terminal_page_html;
} else if (strcmp(path, "/users.html") == 0) {
if (size) *size = strlen(users_page_html);
return users_page_html;
return NULL; // Handled dynamically
} else if (strcmp(path, "/image.jpg") == 0) {
if (size) *size = image_jpg_len;
return (const char *)image_jpg;
......
......@@ -39,6 +39,7 @@ static void set_default_config(wssshd_config_t *config) {
config->web_https = false;
config->debug = false;
config->debug_web = false;
config->debug_database = false;
}
static void load_config_file(wssshd_config_t *config, const char *config_file) {
......@@ -150,6 +151,7 @@ wssshd_config_t *load_config(int argc, char *argv[]) {
{"web-https", no_argument, 0, 's'},
{"debug", no_argument, 0, 'D'},
{"debug-web", no_argument, 0, 'E'},
{"debug-database", no_argument, 0, 'F'},
{"help", no_argument, 0, '?'},
{0, 0, 0, 0}
};
......@@ -157,7 +159,7 @@ wssshd_config_t *load_config(int argc, char *argv[]) {
int opt;
int option_index = 0;
while ((opt = getopt_long(argc, argv, "c:h:p:d:P:w:W:sDE?", long_options, &option_index)) != -1) {
while ((opt = getopt_long(argc, argv, "c:h:p:d:P:w:W:sDEF?", long_options, &option_index)) != -1) {
switch (opt) {
case 'c':
if (config->config_file) free(config->config_file);
......@@ -194,6 +196,9 @@ wssshd_config_t *load_config(int argc, char *argv[]) {
case 'E':
config->debug_web = true;
break;
case 'F':
config->debug_database = true;
break;
case '?':
printf("Usage: %s [OPTIONS]\n", argv[0]);
printf("Options:\n");
......@@ -207,6 +212,7 @@ wssshd_config_t *load_config(int argc, char *argv[]) {
printf(" --web-https Enable HTTPS for web interface\n");
printf(" --debug Enable debug output\n");
printf(" --debug-web Enable comprehensive web interface debug output\n");
printf(" --debug-database Enable database debug output\n");
printf(" --help Show this help\n");
free_config(config);
exit(0);
......@@ -280,4 +286,5 @@ void print_config(const wssshd_config_t *config) {
printf(" Web HTTPS: %s\n", config->web_https ? "yes" : "no");
printf(" Debug: %s\n", config->debug ? "yes" : "no");
printf(" Debug Web: %s\n", config->debug_web ? "yes" : "no");
printf(" Debug Database: %s\n", config->debug_database ? "yes" : "no");
}
\ No newline at end of file
......@@ -34,6 +34,7 @@ typedef struct {
bool web_https;
bool debug;
bool debug_web;
bool debug_database;
} wssshd_config_t;
// Function declarations
......
......@@ -42,12 +42,12 @@ check_command make
check_command pkg-config
# Check for required libraries
if ! pkg-config --exists libssl libcrypto uuid; then
echo "Error: Required libraries (libssl, libcrypto, uuid) not found"
if ! pkg-config --exists libssl libcrypto uuid sqlite3; then
echo "Error: Required libraries (libssl, libcrypto, uuid, sqlite3) not found"
echo "Please install development packages:"
echo " Ubuntu/Debian: sudo apt-get install libssl-dev uuid-dev"
echo " CentOS/RHEL: sudo yum install openssl-devel libuuid-devel"
echo " macOS: brew install openssl ossp-uuid"
echo " Ubuntu/Debian: sudo apt-get install libssl-dev uuid-dev libsqlite3-dev"
echo " CentOS/RHEL: sudo yum install openssl-devel libuuid-devel sqlite-devel"
echo " macOS: brew install openssl ossp-uuid sqlite"
exit 1
fi
......@@ -58,7 +58,7 @@ cat > Makefile << 'EOF'
CC = gcc
CFLAGS = -Wall -Wextra -O2 -I. -pthread
LDFLAGS = -lssl -lcrypto -lm -luuid
LDFLAGS = -lssl -lcrypto -lm -luuid -lsqlite3
PREFIX = /usr/local
BINDIR = $(PREFIX)/bin
MANDIR = $(PREFIX)/share/man
......
#!/bin/bash
# Script to embed web assets into C code
#
# Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
echo "Embedding web assets..."
# Clean up old embedded files
rm -f image_data.h
# Embed logo from logos directory (created by build.sh)
if [ -f ../logos/logo-128.png ]; then
echo "Embedding logo-128.png..."
xxd -i ../logos/logo-128.png > image_data.h
# Rename the variables to match our expected names
# xxd generates names like ___logos_logo_128_png, so we need to replace the entire variable names
sed -i 's/unsigned char ___logos_logo_128_png\[\]/unsigned char image_jpg[]/g' image_data.h
sed -i 's/unsigned int ___logos_logo_128_png_len/unsigned int image_jpg_len/g' image_data.h
elif [ -f ../image.jpg ]; then
echo "Embedding image.jpg..."
xxd -i ../image.jpg | sed 's/___image_jpg/image_jpg/g' > image_data.h
else
echo "Warning: No image found, creating empty placeholder"
cat > image_data.h << 'EOF'
unsigned char image_jpg[] = {};
unsigned int image_jpg_len = 0;
EOF
fi
echo "Assets embedded successfully"
\ No newline at end of file
......@@ -20,30 +20,6 @@
#ifndef USERS_PAGE_H
#define USERS_PAGE_H
// Users page HTML template
static const char *users_page_html =
"<!DOCTYPE html>"
"<html lang=\"en\">"
"<head>"
" <meta charset=\"UTF-8\">"
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
" <title>Users - WebSocket SSH Daemon</title>"
" <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\">"
"</head>"
"<body>"
" <div class=\"container mt-4\">"
" <div class=\"card\">"
" <div class=\"card-header\">"
" <h3 class=\"card-title mb-0\">"
" <i class=\"fas fa-users\"></i> User Management"
" </h3>"
" </div>"
" <div class=\"card-body\">"
" <p>User management interface would be implemented here.</p>"
" </div>"
" </div>"
" </div>"
"</body>"
"</html>";
// Users page HTML template - now generated dynamically
#endif /* USERS_PAGE_H */
\ No newline at end of file
......@@ -29,6 +29,8 @@
#include <errno.h>
#include <time.h>
#include <ctype.h>
#include <sys/stat.h>
#include <sqlite3.h>
#include "web.h"
#include "terminal.h"
#include "assets.h"
......@@ -44,18 +46,17 @@ static wssshd_state_t *global_state = NULL;
static const wssshd_config_t *global_config = NULL;
static int server_socket = -1;
static volatile int server_running = 0;
static sqlite3 *db = NULL;
// Simple user management
#define MAX_USERS 100
typedef struct {
int id;
char username[50];
char password_hash[100];
int is_admin;
} web_user_t;
static web_user_t users[MAX_USERS];
static int user_count = 0;
// Session management
#define MAX_SESSIONS 100
typedef struct {
......@@ -85,13 +86,230 @@ static void simple_hash(const char *input, char *output, size_t output_size) {
snprintf(output, output_size, "%lx", hash);
}
// Initialize default users
static void init_users(void) {
if (user_count == 0) {
strcpy(users[0].username, "admin");
simple_hash("admin123", users[0].password_hash, sizeof(users[0].password_hash));
users[0].is_admin = 1;
user_count = 1;
// Database initialization
static int init_database(const wssshd_config_t *config) {
char *config_dir = NULL;
char db_path[1024];
char *err_msg = NULL;
int rc;
if (config->debug_database) {
printf("[DB-DEBUG] Initializing database...\n");
}
// Determine config directory based on user privileges
char user_config_dir[1024];
char system_config_dir[1024] = "/etc/wssshd";
// Check if running as root
if (getuid() == 0) {
// Running as root - prefer system directory
strcpy(db_path, system_config_dir);
if (config->debug_database) {
printf("[DB-DEBUG] Running as root, using system config directory: %s\n", db_path);
}
} else {
// Running as regular user - use user directory
config_dir = getenv("HOME");
if (!config_dir) {
fprintf(stderr, "Failed to get HOME environment variable\n");
return -1;
}
snprintf(user_config_dir, sizeof(user_config_dir), "%s/.config/wssshd", config_dir);
strcpy(db_path, user_config_dir);
if (config->debug_database) {
printf("[DB-DEBUG] Running as user, using config directory: %s\n", db_path);
}
}
// Check if preferred directory exists
if (access(db_path, F_OK) != 0) {
// Preferred directory doesn't exist
if (getuid() == 0) {
// Running as root, check if user directory exists
config_dir = getenv("HOME");
if (config_dir) {
snprintf(user_config_dir, sizeof(user_config_dir), "%s/.config/wssshd", config_dir);
if (access(user_config_dir, F_OK) == 0) {
// User directory exists, use it
strcpy(db_path, user_config_dir);
if (config->debug_database) {
printf("[DB-DEBUG] User config directory exists, using: %s\n", db_path);
}
}
}
} else {
// Running as user, check if system directory exists
if (access(system_config_dir, F_OK) == 0) {
// System directory exists, use it
strcpy(db_path, system_config_dir);
if (config->debug_database) {
printf("[DB-DEBUG] System config directory exists, using: %s\n", db_path);
}
}
}
}
if (config->debug_database) {
printf("[DB-DEBUG] Final config directory: %s\n", db_path);
}
if (mkdir(db_path, 0755) == -1 && errno != EEXIST) {
perror("Failed to create config directory");
return -1;
}
char db_file_path[1024];
snprintf(db_file_path, sizeof(db_file_path), "%s/users.db", db_path);
strcpy(db_path, db_file_path);
if (config->debug_database) {
printf("[DB-DEBUG] Database path: %s\n", db_path);
printf("[DB-DEBUG] Opening database...\n");
}
rc = sqlite3_open(db_path, &db);
if (rc != SQLITE_OK) {
fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db));
return -1;
}
if (config->debug_database) {
printf("[DB-DEBUG] Database opened successfully\n");
}
// Create users table
const char *sql = "CREATE TABLE IF NOT EXISTS users ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"username TEXT UNIQUE NOT NULL,"
"password_hash TEXT NOT NULL,"
"is_admin INTEGER DEFAULT 0);";
if (config->debug_database) {
printf("[DB-DEBUG] Creating users table...\n");
printf("[DB-DEBUG] SQL: %s\n", sql);
}
rc = sqlite3_exec(db, sql, NULL, NULL, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "SQL error: %s\n", err_msg);
sqlite3_free(err_msg);
sqlite3_close(db);
return -1;
}
if (config->debug_database) {
printf("[DB-DEBUG] Users table created successfully\n");
}
// Check if admin user exists, create if not
sql = "SELECT COUNT(*) FROM users WHERE is_admin = 1;";
if (config->debug_database) {
printf("[DB-DEBUG] Checking if any admin user exists...\n");
printf("[DB-DEBUG] SQL: %s\n", sql);
}
sqlite3_stmt *stmt;
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "Failed to prepare statement: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return -1;
}
int count = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
count = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
if (config->debug_database) {
printf("[DB-DEBUG] Admin user count: %d\n", count);
}
if (count == 0) {
// Create default admin user
char hashed[100];
simple_hash("admin123", hashed, sizeof(hashed));
if (config->debug_database) {
printf("[DB-DEBUG] Creating default admin user...\n");
printf("[DB-DEBUG] Password hash: %s\n", hashed);
}
sql = "INSERT INTO users (username, password_hash, is_admin) VALUES ('admin', ?, 1);";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "Failed to prepare insert statement: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return -1;
}
sqlite3_bind_text(stmt, 1, hashed, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
fprintf(stderr, "Failed to insert admin user: %s\n", sqlite3_errmsg(db));
sqlite3_finalize(stmt);
sqlite3_close(db);
return -1;
}
sqlite3_finalize(stmt);
if (config->debug_database) {
printf("[DB-DEBUG] Default admin user created successfully\n");
}
printf("Created default admin user (username: admin, password: admin123)\n");
} else {
if (config->debug_database) {
printf("[DB-DEBUG] Admin user already exists, skipping creation\n");
}
// Check if default admin with default password exists (security warning)
char default_hashed[100];
simple_hash("admin123", default_hashed, sizeof(default_hashed));
sql = "SELECT COUNT(*) FROM users WHERE username = 'admin' AND password_hash = ?;";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, default_hashed, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
int default_admin_count = sqlite3_column_int(stmt, 0);
if (default_admin_count > 0) {
printf("\n");
printf("###############################################################################\n");
printf("# SECURITY WARNING #\n");
printf("###############################################################################\n");
printf("# #\n");
printf("# WARNING: Default admin credentials detected! #\n");
printf("# #\n");
printf("# The system has detected that the default admin user 'admin' with the #\n");
printf("# default password 'admin123' is still active. #\n");
printf("# #\n");
printf("# This is a SECURITY RISK! Please change the default password immediately. #\n");
printf("# #\n");
printf("# To change the password: #\n");
printf("# 1. Start the web interface #\n");
printf("# 2. Log in with username 'admin' and password 'admin123' #\n");
printf("# 3. Go to User Management and change the password #\n");
printf("# #\n");
printf("###############################################################################\n");
printf("\n");
}
}
sqlite3_finalize(stmt);
}
}
return 0;
}
// Initialize database and default users
static void init_users(const wssshd_config_t *config) {
if (init_database(config) != 0) {
fprintf(stderr, "Failed to initialize database\n");
exit(1);
}
}
......@@ -103,14 +321,210 @@ static void generate_session_id(char *session_id, size_t size) {
// Find user by username
static web_user_t *find_user(const char *username) {
for (int i = 0; i < user_count; i++) {
if (strcmp(users[i].username, username) == 0) {
return &users[i];
static web_user_t user;
sqlite3_stmt *stmt;
const char *sql = "SELECT id, username, password_hash, is_admin FROM users WHERE username = ?;";
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] Finding user: %s\n", username);
printf("[DB-DEBUG] SQL: %s\n", sql);
}
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "Failed to prepare find_user statement: %s\n", sqlite3_errmsg(db));
return NULL;
}
sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
user.id = sqlite3_column_int(stmt, 0);
const char *uname = (const char *)sqlite3_column_text(stmt, 1);
const char *phash = (const char *)sqlite3_column_text(stmt, 2);
int is_admin = sqlite3_column_int(stmt, 3);
strncpy(user.username, uname, sizeof(user.username) - 1);
strncpy(user.password_hash, phash, sizeof(user.password_hash) - 1);
user.is_admin = is_admin;
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] User found: id=%d, username=%s, is_admin=%d\n", user.id, user.username, user.is_admin);
}
sqlite3_finalize(stmt);
return &user;
}
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] User not found: %s\n", username);
}
sqlite3_finalize(stmt);
return NULL;
}
// Add a new user
static int db_add_user(const char *username, const char *password_hash, int is_admin) {
sqlite3_stmt *stmt;
const char *sql = "INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, ?);";
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] Adding user: username=%s, is_admin=%d\n", username, is_admin);
printf("[DB-DEBUG] SQL: %s\n", sql);
}
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "Failed to prepare add_user statement: %s\n", sqlite3_errmsg(db));
return -1;
}
sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, password_hash, -1, SQLITE_STATIC);
sqlite3_bind_int(stmt, 3, is_admin);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
fprintf(stderr, "Failed to add user: %s\n", sqlite3_errmsg(db));
return -1;
}
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] User added successfully\n");
}
return 0;
}
// Update user
static int db_update_user(int user_id, const char *username, int is_admin, const char *password_hash) {
sqlite3_stmt *stmt;
const char *sql;
if (password_hash) {
sql = "UPDATE users SET username = ?, is_admin = ?, password_hash = ? WHERE id = ?;";
} else {
sql = "UPDATE users SET username = ?, is_admin = ? WHERE id = ?;";
}
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] Updating user: id=%d, username=%s, is_admin=%d, password_change=%s\n",
user_id, username, is_admin, password_hash ? "yes" : "no");
printf("[DB-DEBUG] SQL: %s\n", sql);
}
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "Failed to prepare update_user statement: %s\n", sqlite3_errmsg(db));
return -1;
}
sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC);
sqlite3_bind_int(stmt, 2, is_admin);
if (password_hash) {
sqlite3_bind_text(stmt, 3, password_hash, -1, SQLITE_STATIC);
sqlite3_bind_int(stmt, 4, user_id);
} else {
sqlite3_bind_int(stmt, 3, user_id);
}
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
fprintf(stderr, "Failed to update user: %s\n", sqlite3_errmsg(db));
return -1;
}
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] User updated successfully\n");
}
return 0;
}
// Delete user
static int db_delete_user(int user_id) {
sqlite3_stmt *stmt;
const char *sql = "DELETE FROM users WHERE id = ?;";
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] Deleting user: id=%d\n", user_id);
printf("[DB-DEBUG] SQL: %s\n", sql);
}
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "Failed to prepare delete_user statement: %s\n", sqlite3_errmsg(db));
return -1;
}
sqlite3_bind_int(stmt, 1, user_id);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
fprintf(stderr, "Failed to delete user: %s\n", sqlite3_errmsg(db));
return -1;
}
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] User deleted successfully\n");
}
return 0;
}
// Get all users (for users page)
static int db_get_all_users(web_user_t *users_array, int max_users) {
sqlite3_stmt *stmt;
const char *sql = "SELECT id, username, password_hash, is_admin FROM users ORDER BY username;";
int count = 0;
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] Getting all users (max: %d)\n", max_users);
printf("[DB-DEBUG] SQL: %s\n", sql);
}
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "Failed to prepare get_all_users statement: %s\n", sqlite3_errmsg(db));
return -1;
}
while (sqlite3_step(stmt) == SQLITE_ROW && count < max_users) {
users_array[count].id = sqlite3_column_int(stmt, 0);
const char *uname = (const char *)sqlite3_column_text(stmt, 1);
const char *phash = (const char *)sqlite3_column_text(stmt, 2);
int is_admin = sqlite3_column_int(stmt, 3);
users_array[count].id = sqlite3_column_int(stmt, 0);
strncpy(users_array[count].username, uname, sizeof(users_array[count].username) - 1);
strncpy(users_array[count].password_hash, phash, sizeof(users_array[count].password_hash) - 1);
users_array[count].is_admin = is_admin;
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] Found user: id=%d, username=%s, is_admin=%d\n",
users_array[count].id, users_array[count].username, users_array[count].is_admin);
}
count++;
}
sqlite3_finalize(stmt);
if (global_config && global_config->debug_database) {
printf("[DB-DEBUG] Total users retrieved: %d\n", count);
}
return count;
}
// Create new session
static const char *create_session(const char *username) {
pthread_mutex_lock(&session_mutex);
......@@ -434,6 +848,196 @@ static char *generate_index_html(const char *username, int is_admin) {
return html;
}
// Generate dynamic HTML for users page
static int generate_users_html(char *html, size_t max_len) {
web_user_t users[MAX_USERS];
int user_count = db_get_all_users(users, MAX_USERS);
if (user_count < 0) {
return snprintf(html, max_len, "Error loading users");
}
char user_rows[8192] = "";
for (int i = 0; i < user_count; i++) {
char role_badge[64];
if (users[i].is_admin) {
strcpy(role_badge, "<span class=\"badge bg-danger\">Admin</span>");
} else {
strcpy(role_badge, "<span class=\"badge bg-secondary\">User</span>");
}
char row[1024];
snprintf(row, sizeof(row),
"<tr>"
"<td>%s</td>"
"<td>%s</td>"
"<td>"
"<button class=\"btn btn-sm btn-outline-primary\" onclick=\"editUser(%d, '%s', %s)\">"
"<i class=\"fas fa-edit\"></i> Edit</button>"
"<button class=\"btn btn-sm btn-outline-danger ms-1\" onclick=\"deleteUser(%d, '%s')\">"
"<i class=\"fas fa-trash\"></i> Delete</button>"
"</td>"
"</tr>",
users[i].username, role_badge, users[i].id, users[i].username,
users[i].is_admin ? "true" : "false", users[i].id, users[i].username);
strcat(user_rows, row);
}
return snprintf(html, max_len,
"<!DOCTYPE html>"
"<html lang=\"en\">"
"<head>"
" <meta charset=\"UTF-8\">"
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
" <title>Users - WebSocket SSH Daemon</title>"
" <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\">"
"</head>"
"<body>"
" <div class=\"container mt-4\">"
" <div class=\"card\">"
" <div class=\"card-header d-flex justify-content-between align-items-center\">"
" <h3 class=\"card-title mb-0\">"
" <i class=\"fas fa-users\"></i> User Management"
" </h3>"
" <div>"
" <a href=\"/\" class=\"btn btn-outline-secondary btn-sm me-2\">"
" <i class=\"fas fa-home\"></i> Back to Home"
" </a>"
" <button class=\"btn btn-primary btn-sm\" data-bs-toggle=\"modal\" data-bs-target=\"#addUserModal\">"
" <i class=\"fas fa-plus\"></i> Add User"
" </button>"
" </div>"
" </div>"
" <div class=\"card-body\">"
" <div class=\"table-responsive\">"
" <table class=\"table table-striped\">"
" <thead>"
" <tr>"
" <th>Username</th>"
" <th>Role</th>"
" <th>Actions</th>"
" </tr>"
" </thead>"
" <tbody>%s</tbody>"
" </table>"
" </div>"
" </div>"
" </div>"
" </div>"
" <!-- Add User Modal -->"
" <div class=\"modal fade\" id=\"addUserModal\" tabindex=\"-1\">"
" <div class=\"modal-dialog\">"
" <div class=\"modal-content\">"
" <div class=\"modal-header\">"
" <h5 class=\"modal-title\">Add New User</h5>"
" <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button>"
" </div>"
" <form id=\"addUserForm\">"
" <div class=\"modal-body\">"
" <div class=\"mb-3\">"
" <label for=\"addUsername\" class=\"form-label\">Username</label>"
" <input type=\"text\" class=\"form-control\" id=\"addUsername\" name=\"username\" required>"
" </div>"
" <div class=\"mb-3\">"
" <label for=\"addPassword\" class=\"form-label\">Password</label>"
" <input type=\"password\" class=\"form-control\" id=\"addPassword\" name=\"password\" required>"
" </div>"
" <div class=\"mb-3 form-check\">"
" <input type=\"checkbox\" class=\"form-check-input\" id=\"addIsAdmin\" name=\"is_admin\">"
" <label class=\"form-check-label\" for=\"addIsAdmin\">Administrator</label>"
" </div>"
" </div>"
" <div class=\"modal-footer\">"
" <button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button>"
" <button type=\"submit\" class=\"btn btn-primary\">Add User</button>"
" </div>"
" </form>"
" </div>"
" </div>"
" </div>"
" <!-- Edit User Modal -->"
" <div class=\"modal fade\" id=\"editUserModal\" tabindex=\"-1\">"
" <div class=\"modal-dialog\">"
" <div class=\"modal-content\">"
" <div class=\"modal-header\">"
" <h5 class=\"modal-title\">Edit User</h5>"
" <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button>"
" </div>"
" <form id=\"editUserForm\">"
" <input type=\"hidden\" id=\"editUserId\" name=\"user_id\">"
" <div class=\"modal-body\">"
" <div class=\"mb-3\">"
" <label for=\"editUsername\" class=\"form-label\">Username</label>"
" <input type=\"text\" class=\"form-control\" id=\"editUsername\" name=\"username\" required>"
" </div>"
" <div class=\"mb-3\">"
" <label for=\"editPassword\" class=\"form-label\">New Password (leave empty to keep current)</label>"
" <input type=\"password\" class=\"form-control\" id=\"editPassword\" name=\"password\">"
" </div>"
" <div class=\"mb-3 form-check\">"
" <input type=\"checkbox\" class=\"form-check-input\" id=\"editIsAdmin\" name=\"is_admin\">"
" <label class=\"form-check-label\" for=\"editIsAdmin\">Administrator</label>"
" </div>"
" </div>"
" <div class=\"modal-footer\">"
" <button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button>"
" <button type=\"submit\" class=\"btn btn-primary\">Update User</button>"
" </div>"
" </form>"
" </div>"
" </div>"
" </div>"
" <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js\"></script>"
" <script>"
" function editUser(userId, username, isAdmin) {"
" document.getElementById('editUserId').value = userId;"
" document.getElementById('editUsername').value = username;"
" document.getElementById('editPassword').value = '';"
" document.getElementById('editIsAdmin').checked = isAdmin;"
" new bootstrap.Modal(document.getElementById('editUserModal')).show();"
" }"
" function deleteUser(userId, username) {"
" if (confirm(`Are you sure you want to delete user \"${username}\"?`)) {"
" fetch(`/delete_user/${userId}`, {"
" method: 'POST',"
" headers: {'Content-Type': 'application/x-www-form-urlencoded'}"
" })"
" .then(response => response.json())"
" .then(data => {"
" if (data.success) location.reload();"
" else alert('Error: ' + data.error);"
" });"
" }"
" }"
" document.getElementById('addUserForm').addEventListener('submit', function(e) {"
" e.preventDefault();"
" const formData = new FormData(this);"
" fetch('/add_user', {method: 'POST', body: formData})"
" .then(response => response.json())"
" .then(data => {"
" if (data.success) {"
" bootstrap.Modal.getInstance(document.getElementById('addUserModal')).hide();"
" location.reload();"
" } else alert('Error: ' + data.error);"
" });"
" });"
" document.getElementById('editUserForm').addEventListener('submit', function(e) {"
" e.preventDefault();"
" const formData = new FormData(this);"
" const userId = document.getElementById('editUserId').value;"
" fetch(`/edit_user/${userId}`, {method: 'POST', body: formData})"
" .then(response => response.json())"
" .then(data => {"
" if (data.success) {"
" bootstrap.Modal.getInstance(document.getElementById('editUserModal')).hide();"
" location.reload();"
" } else alert('Error: ' + data.error);"
" });"
" });"
" </script>"
"</body>"
"</html>", user_rows);
}
// Handle HTTP requests
static void handle_request(int client_fd, const http_request_t *req) {
if (global_config && global_config->debug_web) {
......@@ -483,14 +1087,17 @@ static void handle_request(int client_fd, const http_request_t *req) {
} else if (strcmp(req->path, "/login") == 0) {
send_response(client_fd, 200, "OK", "text/html", login_page_html, strlen(login_page_html), NULL, NULL);
} else if (strcmp(req->path, "/logout") == 0) {
send_response(client_fd, 302, "Found", "text/html", NULL, 0, "session_id=; Max-Age=0; Path=/", NULL);
send_response(client_fd, 302, "Found", "text/html", NULL, 0, "session_id=; Max-Age=0; Path=/", "Location: /");
return;
} else if (strcmp(req->path, "/users") == 0) {
if (!username || !is_admin) {
send_response(client_fd, 403, "Forbidden", "text/html", "Access denied", 13, NULL, NULL);
return;
}
send_response(client_fd, 200, "OK", "text/html", users_page_html, strlen(users_page_html), NULL, NULL);
// Generate dynamic users page
char html[16384];
int len = generate_users_html(html, sizeof(html));
send_response(client_fd, 200, "OK", "text/html", html, len, NULL, NULL);
} else if (strncmp(req->path, "/terminal/", 9) == 0) {
if (!username) {
send_response(client_fd, 302, "Found", "text/html", NULL, 0, NULL, NULL);
......@@ -819,7 +1426,7 @@ static void handle_request(int client_fd, const http_request_t *req) {
}
char cookie[256];
snprintf(cookie, sizeof(cookie), "session_id=%s; Path=/; HttpOnly", new_session);
send_response(client_fd, 302, "Found", "text/html", NULL, 0, cookie, NULL);
send_response(client_fd, 302, "Found", "text/html", NULL, 0, cookie, "Location: /");
return;
} else {
if (global_config && global_config->debug_web) {
......@@ -833,6 +1440,112 @@ static void handle_request(int client_fd, const http_request_t *req) {
}
const char *asset = get_embedded_asset("/login", NULL);
send_response(client_fd, 200, "OK", "text/html", asset, strlen(asset), NULL, NULL);
} else if (strcmp(req->path, "/add_user") == 0) {
if (!username || !is_admin) {
send_response(client_fd, 403, "Forbidden", "application/json", "{\"error\":\"Access denied\"}", 23, NULL, NULL);
return;
}
char form_username[50] = "";
char form_password[50] = "";
int form_is_admin = 0;
// Parse form data
char temp_body[4096];
strcpy(temp_body, req->body);
char *pair = strtok(temp_body, "&");
while (pair) {
char *eq = strchr(pair, '=');
if (eq) {
*eq = '\0';
char *key = pair;
char *value = eq + 1;
url_decode(value);
if (strcmp(key, "username") == 0) {
strncpy(form_username, value, sizeof(form_username) - 1);
} else if (strcmp(key, "password") == 0) {
strncpy(form_password, value, sizeof(form_password) - 1);
} else if (strcmp(key, "is_admin") == 0) {
form_is_admin = 1;
}
}
pair = strtok(NULL, "&");
}
// Check if username already exists
if (find_user(form_username)) {
send_response(client_fd, 400, "Bad Request", "application/json", "{\"error\":\"Username already exists\"}", 33, NULL, NULL);
return;
}
char hashed[100];
simple_hash(form_password, hashed, sizeof(hashed));
if (db_add_user(form_username, hashed, form_is_admin) == 0) {
send_response(client_fd, 200, "OK", "application/json", "{\"success\":true}", 16, NULL, NULL);
} else {
send_response(client_fd, 500, "Internal Server Error", "application/json", "{\"error\":\"Failed to add user\"}", 29, NULL, NULL);
}
} else if (strncmp(req->path, "/edit_user/", 11) == 0) {
if (!username || !is_admin) {
send_response(client_fd, 403, "Forbidden", "application/json", "{\"error\":\"Access denied\"}", 23, NULL, NULL);
return;
}
int user_id = atoi(req->path + 11);
char form_username[50] = "";
char form_password[50] = "";
int form_is_admin = 0;
// Parse form data
char temp_body[4096];
strcpy(temp_body, req->body);
char *pair = strtok(temp_body, "&");
while (pair) {
char *eq = strchr(pair, '=');
if (eq) {
*eq = '\0';
char *key = pair;
char *value = eq + 1;
url_decode(value);
if (strcmp(key, "username") == 0) {
strncpy(form_username, value, sizeof(form_username) - 1);
} else if (strcmp(key, "password") == 0 && strlen(value) > 0) {
strncpy(form_password, value, sizeof(form_password) - 1);
} else if (strcmp(key, "is_admin") == 0) {
form_is_admin = 1;
}
}
pair = strtok(NULL, "&");
}
char *password_hash = NULL;
if (strlen(form_password) > 0) {
password_hash = malloc(100);
simple_hash(form_password, password_hash, 100);
}
if (db_update_user(user_id, form_username, form_is_admin, password_hash) == 0) {
send_response(client_fd, 200, "OK", "application/json", "{\"success\":true}", 16, NULL, NULL);
} else {
send_response(client_fd, 500, "Internal Server Error", "application/json", "{\"error\":\"Failed to update user\"}", 31, NULL, NULL);
}
if (password_hash) free(password_hash);
} else if (strncmp(req->path, "/delete_user/", 13) == 0) {
if (!username || !is_admin) {
send_response(client_fd, 403, "Forbidden", "application/json", "{\"error\":\"Access denied\"}", 23, NULL, NULL);
return;
}
int user_id = atoi(req->path + 13);
if (db_delete_user(user_id) == 0) {
send_response(client_fd, 200, "OK", "application/json", "{\"success\":true}", 16, NULL, NULL);
} else {
send_response(client_fd, 500, "Internal Server Error", "application/json", "{\"error\":\"Failed to delete user\"}", 31, NULL, NULL);
}
} else {
send_response(client_fd, 405, "Method Not Allowed", "text/html", "Method not allowed", 18, NULL, NULL);
}
......@@ -904,7 +1617,27 @@ int web_start_server(const wssshd_config_t *config, wssshd_state_t *state) {
global_config = config;
global_state = state;
init_users();
// Print ASCII art banner
printf("\033[38;5;117m⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠖⠢⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;117m⠀⠀⠀⠀⠀⠀⠀⢠⠖⠋⠀⠀⠀⣀⣹⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;117m⠀⠀⠀⠀⠀⠀⠀⢸⡄⠀⢠⣶⡞⢁⣸⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;218m⠀⠀⠀⠀⠀⠀⠀⠈⢁⣠⠞⢻⡁⢻⣷⣾⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;218m⠀⠀⠀⠀⠀⣠⠤⢄⣘⣿⢬⣛⡷⢋⣈⣉⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;218m⠀⠀⠀⠀⢰⡇⠀⠀⠙⠿⠀⢀⠿⠋⠀⠀⠀⢱⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("⠀⠀⠀⠀⠘⡃⠀⢰⠀⠀⠀⡁⠀⡀⠀⠘⣆⠈⢇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
printf("\033[38;5;231m⠀⠀⠀⠀⠀⡇⠀⣼⡦⠠⠤⣈⡆⢃⡤⠒⠙⡆⠀⠳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;231m⠀⠀⠀⠀⢀⠇⠀⡇⢷⣄⣀⡠⠟⠛⠢⠤⠞⡟⠦⡀⠙⠦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;231m⠀⠀⠀⠀⡞⢀⠀⠀⠀⠳⡄⠀⠀⠠⡀⠀⠀⠘⠉⠙⠦⣀⠀⠉⠢⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;231m⠀⠀⠀⠀⡇⠀⢠⠀⠀⠀⠈⠓⢄⠀⠁⠀⠀⠀⠒⠂⠀⣾⠋⠉⠒⠢⢍⣙⡒⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;218m⠀⠀⠀⠀⡇⠀⡜⠀⠀⠀⠀⠀⠀⠱⡀⢀⡀⠁⠀⢀⡼⡇⠀⠀⠀⠀⠀⣏⠙⠯⣆⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;218m⠀⠀⠀⠀⡇⢠⠇⠀⠀⠀⠀⠀⠀⠀⢱⣀⣽⠶⠾⠛⠒⠛⠒⠒⠒⠤⠤⣸⡍⠀⠀⠉⠲⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;218m⠀⣀⣀⣀⠇⣼⠀⠀⠀⠀⠀⠀⠀⠀⠀⢫⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⣙⠢⣀⠀⠀⠈⠳⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;117m⠉⠙⠛⠥⠰⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠂⠤⠤⠤⠤⠴⠒⠚⠉⠙⠢⣀⠀⠈⠑⠢⢄⡀⠈⠳⡀⠀⠀⠀⠀⠀⠀⠀\033[0m\n");
printf("\033[38;5;117m⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠒⠒⠒⠒⠚⠑⠢⢌⡓⠤⠤⠤⠤⣀⠀\033[0m\n");
printf("\033[38;5;117m⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠓⠒⠠⠤⣼⠇\033[0m\n");
printf("\n");
init_users(config);
// Start HTTP server thread
pthread_t thread;
......@@ -923,5 +1656,9 @@ void web_stop_server(void) {
close(server_socket);
server_socket = -1;
}
if (db) {
sqlite3_close(db);
db = NULL;
}
printf("Web interface stopping\n");
}
\ No newline at end of file
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment