Initial commit: Complete WebSocket SSH tunneling system

- WebSocket SSH Daemon (wssshd) with web management interface
- WebSocket SSH Client (wssshc) with password authentication
- SSH wrapper (wsssh) with intelligent hostname parsing
- SCP wrapper (wsscp) with tunneling support
- Professional web UI with user management and HTML5 terminal
- SQLite database for persistent user storage
- Role-based access control (admin/normal users)
- SSL certificate auto-generation during build
- Automated build system with venv management
- Comprehensive documentation and examples
parents
Pipeline #183 canceled with stages
# Changelog
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.0.0] - 2024-09-12
### Added
- **Initial release** of WebSocket SSH tunneling system
- **wssshd**: WebSocket SSH Daemon for managing client connections and tunnel routing
- **wssshc**: WebSocket SSH Client for registering machines with the daemon
- **wsssh**: SSH command wrapper with automatic hostname parsing and tunnel establishment
- **wsscp**: SCP command wrapper with WebSocket tunneling support
- **Intelligent hostname parsing**: Support for `<CLIENT_ID>.<HOST>[:<PORT>]` format
- **Automatic port detection**: Priority-based port resolution (hostname suffix > command options > defaults)
- **SSL/TLS encryption**: Secure WebSocket communications with self-signed certificates
- **Client registration system**: Unique client identification and persistent connections
- **Build system**: Automated binary creation using PyInstaller
- **Cross-platform support**: Linux, macOS, and Windows compatibility
- **Comprehensive documentation**: README, installation guides, and usage examples
### Features
- **Drop-in SSH/SCP replacement**: Use `wsssh` and `wsscp` as direct replacements for standard commands
- **Multi-client routing**: Route connections to different registered clients based on hostname
- **Automatic tunnel management**: Dynamic local port allocation and tunnel lifecycle management
- **Debug mode**: Detailed logging for troubleshooting connections and tunnels
- **Reconnection logic**: Automatic reconnection for client registration
### Technical Details
- **WebSocket-based architecture**: Real-time bidirectional communication
- **Async I/O**: High-performance asynchronous operations using asyncio
- **Binary distribution**: Standalone executables with embedded dependencies
- **Certificate management**: Automatic SSL certificate generation and embedding
- **Protocol parsing**: SSH protocol inspection for user authentication routing
### Security
- **Encrypted communications**: All WebSocket traffic protected by SSL/TLS
- **Client authentication**: Unique identifier-based client verification
- **Secure tunneling**: Isolated tunnel sessions per connection
## [0.1.0] - 2024-09-11
### Added
- **Proof of concept** implementation
- Basic WebSocket SSH tunneling functionality
- Initial client-server architecture
- Command-line interface foundation
- SSL certificate generation
- Build system setup
### Changed
- Initial architecture design
- Basic protocol implementation
### Fixed
- Initial connection handling
- Basic error management
---
## Types of changes
- `Added` for new features
- `Changed` for changes in existing functionality
- `Deprecated` for soon-to-be removed features
- `Removed` for now removed features
- `Fixed` for any bug fixes
- `Security` in case of vulnerabilities
## Versioning
This project uses [Semantic Versioning](https://semver.org/).
Given a version number MAJOR.MINOR.PATCH, increment the:
- **MAJOR** version when you make incompatible API changes
- **MINOR** version when you add functionality in a backwards compatible manner
- **PATCH** version when you make backwards compatible bug fixes
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
---
## Development Notes
### Upcoming Features (Roadmap)
- **Enhanced security**: Certificate pinning and mutual TLS authentication
- **Load balancing**: Multi-server daemon support with automatic failover
- **Performance optimization**: Connection pooling and multiplexing
- **GUI client**: Desktop application for easier management
- **REST API**: HTTP API for programmatic tunnel management
- **Container support**: Docker images and Kubernetes integration
- **Monitoring**: Metrics collection and health monitoring
- **Plugin system**: Extensible architecture for custom protocols
### Known Issues
- Self-signed certificate warnings in browsers/clients
- Limited Windows testing (primarily developed on Linux)
- No built-in rate limiting or DDoS protection
- Basic error handling (room for improvement)
### Contributing
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines and contribution procedures.
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
# WebSocket SSH (wsssh)
A modern SSH tunneling system that uses WebSocket connections to securely route SSH/SCP traffic through registered client machines. This allows you to access remote servers through intermediate "jump hosts" using WebSocket-based tunnels.
## Features
- **WebSocket-based Tunneling**: Secure SSH/SCP access through WebSocket connections
- **Client Registration**: Register client machines with the WebSocket daemon
- **Password Authentication**: Secure client registration with configurable passwords
- **Web Management Interface**: Professional admin panel with user management and HTML5 terminal
- **Intelligent Hostname Parsing**: `<CLIENT_ID>.<WSSSHD_HOST>` format with `-p`/`-P` port options
- **Drop-in SSH/SCP Replacement**: Use `wsssh` and `wsscp` as direct replacements for `ssh` and `scp`
- **SSL/TLS Encryption**: All WebSocket communications are encrypted
- **Multi-client Support**: Route connections to different registered clients
- **Cross-platform**: Works on Linux, macOS, and Windows
## Architecture
The system consists of four main components:
1. **`wssshd`** - WebSocket SSH Daemon (server)
- Manages WebSocket connections
- Handles client registrations with password authentication
- Routes tunnel requests to appropriate clients
- Optional web management interface with user administration
- HTML5 terminal interface for SSH connections
2. **`wssshc`** - WebSocket SSH Client (registration)
- Registers client machines with the daemon
- Maintains persistent WebSocket connection
3. **`wsssh`** - SSH wrapper with tunneling
- Parses SSH commands and hostnames
- Establishes WebSocket tunnels
- Launches SSH to local tunnel port
4. **`wsscp`** - SCP wrapper with tunneling
- Similar to wsssh but for SCP operations
- Handles file transfers through tunnels
## Installation
### From Source
```bash
# Clone the repository
git clone https://github.com/your-repo/wsssh.git
cd wsssh
# Install dependencies
pip3 install -r requirements.txt
# Build binaries (optional)
./build.sh
```
### Using Pre-built Binaries
Download the latest release binaries for your platform from the releases page.
## Quick Start
### 1. Start the WebSocket Daemon
```bash
./wssshd --host 0.0.0.0 --port 9898 --domain example.com
```
### 2. Register a Client
On the client machine you want to access through:
```bash
./wssshc --server-ip <daemon_ip> --port 9898 --id myclient
```
### 3. Connect via SSH
```bash
./wsssh ssh user@myclient.example.com
```
This automatically:
- Parses `myclient.example.com` to extract client ID `myclient`
- Connects to wssshd at `example.com:22` (default SSH port)
- Requests tunnel to client `myclient`
- Opens local port and launches `ssh user@localhost`
## Hostname Format
The system uses intelligent hostname parsing:
```
<CLIENT_ID>.<WSSSHD_HOST>
```
Port is specified using `-p` (SSH) or `-P` (SCP) options.
### Examples:
- `remote.example.com -p 9898` → Client: `remote`, Server: `example.com:9898`
- `server.datacenter.com -P 2222` → Client: `server`, Server: `datacenter.com:2222`
- `test.localhost -p 8080` → Client: `test`, Server: `localhost:8080`
## Port Detection Priority
1. **Hostname suffix**: `host:port` in the target hostname
2. **SSH/SCP port option**: `-p <port>` (SSH) or `-P <port>` (SCP)
3. **Default**: `22` (standard SSH port)
## Detailed Usage
### wssshd (WebSocket SSH Daemon)
```bash
./wssshd --host 0.0.0.0 --port 9898 --domain example.com --password mysecret [--web-host 0.0.0.0 --web-port 8080] [--debug]
```
**Options:**
- `--host`: Bind address (default: 0.0.0.0)
- `--port`: WebSocket port (required)
- `--domain`: Domain suffix for hostname parsing
- `--password`: Registration password (required)
- `--web-host`: Web interface bind address (optional)
- `--web-port`: Web interface port (optional)
- `--debug`: Enable debug output
**Web Interface:**
When `--web-host` and `--web-port` are specified, a web management interface is available with:
- User authentication (default: admin/admin123)
- Client connection overview
- User management (admin only)
- HTML5 terminal interface
### wssshc (WebSocket SSH Client)
```bash
./wssshc --server-ip <ip> --port 9898 --id client1 --password mysecret [--interval 30] [--debug]
```
**Options:**
- `--server-ip`: wssshd server IP address (required)
- `--port`: wssshd server port (required)
- `--id`: Unique client identifier (required)
- `--password`: Registration password (required)
- `--interval`: Reconnection interval in seconds (default: 30)
- `--debug`: Enable debug output
### wsssh (SSH Wrapper)
```bash
./wsssh [options] ssh user@client.domain [ssh_options...]
```
**Options:**
- `--local-port`: Local tunnel port (default: auto)
- `--debug`: Enable debug output
**Examples:**
```bash
# Basic SSH connection
./wsssh ssh -p 9898 user@myclient.example.com
# SSH with custom port
./wsssh ssh -p 2222 user@myclient.example.com
# SSH with options
./wsssh ssh -p 9898 -i ~/.ssh/key user@myclient.example.com ls -la
```
### wsscp (SCP Wrapper)
```bash
./wsscp [options] scp [scp_options...] source destination
```
**Options:**
- `--local-port`: Local tunnel port (default: auto)
- `--debug`: Enable debug output
**Examples:**
```bash
# Copy file to remote
./wsscp scp -P 9898 localfile user@myclient.example.com:/remote/path/
# Copy file from remote
./wsscp scp -P 9898 user@myclient.example.com:/remote/file ./localfile
# Copy with custom port
./wsscp scp -P 2222 localfile user@myclient.example.com:/remote/path/
```
## Configuration
### SSL Certificates
The system uses self-signed SSL certificates for WebSocket encryption. Certificates are automatically generated during the build process if they don't exist.
- `cert.pem`: SSL certificate
- `key.pem`: Private key
Certificates are created with:
- 4096-bit RSA key
- Valid for 365 days
- Subject: `/C=US/ST=State/L=City/O=Organization/CN=localhost`
### Client Registration
Each client machine must be registered with wssshd using a unique ID:
```bash
./wssshc --server-ip <daemon_ip> --port <port> --id <unique_id>
```
The client will maintain a persistent WebSocket connection to the daemon.
## Security Considerations
- **SSL/TLS**: All WebSocket communications are encrypted
- **Client Authentication**: Clients are identified by unique IDs
- **Network Security**: Ensure wssshd is only accessible from trusted networks
- **Certificate Validation**: Currently uses self-signed certificates
## Troubleshooting
### Connection Issues
1. **Check wssshd is running**: Verify the daemon is accessible on the specified port
2. **Client registration**: Ensure the client is registered and connected
3. **Firewall rules**: Check that required ports are open
4. **SSL certificates**: Verify certificate files exist and are valid
### Debug Mode
Enable debug output for detailed troubleshooting:
```bash
./wssshd --debug --host 0.0.0.0 --port 9898 --domain example.com
./wssshc --debug --server-ip <ip> --port 9898 --id client1
./wsssh --debug ssh user@client.domain
```
### Common Issues
- **"Client not registered"**: Register the client with wssshc first
- **"Connection refused"**: Check wssshd is running and ports are accessible
- **"SSL verification failed"**: The system uses self-signed certificates
## Development
### Building from Source
```bash
# Install dependencies
pip3 install -r requirements.txt
# Run tests
python3 -m pytest
# Build binaries
./build.sh
# Clean build artifacts
./clean.sh
```
### Project Structure
```
wsssh/
├── wssshd.py # WebSocket SSH Daemon with web interface
├── wssshc.py # WebSocket SSH Client
├── wsssh.py # SSH wrapper
├── wsscp.py # SCP wrapper
├── build.sh # Build script
├── clean.sh # Clean script
├── requirements.txt # Python dependencies
├── cert.pem # SSL certificate
├── key.pem # SSL private key
├── templates/ # Flask HTML templates
│ ├── base.html
│ ├── index.html
│ ├── login.html
│ ├── terminal.html
│ └── users.html
├── static/ # Static web assets
├── LICENSE # GPLv3 license
├── README.md # This file
└── docs/ # Documentation
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request
## License
This project is licensed under the GNU General Public License v3.0 (GPLv3).
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
See [LICENSE.md](LICENSE.md) for the full license text.
## Support
- **Issues**: Report bugs and request features on GitHub
- **Documentation**: See the [docs/](docs/) directory for detailed documentation
- **Community**: Join our community discussions
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history and changes.
\ No newline at end of file
#!/bin/bash
# Check if virtual environment exists, create if not
if [ ! -d "venv" ]; then
echo "Creating virtual environment..."
python3 -m venv venv
fi
# Activate virtual environment
. venv/bin/activate
# Install requirements
echo "Installing requirements..."
pip3 install -r requirements.txt
# Install pyinstaller if not already installed
pip3 install pyinstaller
# Create dist directory if not exists
mkdir -p dist
# Generate SSL certificates if they don't exist
if [ ! -f "cert.pem" ] || [ ! -f "key.pem" ]; then
echo "Generating SSL certificates..."
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
fi
# Build wssshd (server) binary with certificates and web assets
pyinstaller --onefile --distpath dist --add-data "cert.pem:." --add-data "key.pem:." --add-data "templates:templates" --add-data "static:static" --runtime-tmpdir /tmp --clean wssshd.py
# Build wssshc (client) binary
pyinstaller --onefile --distpath dist --runtime-tmpdir /tmp --clean wssshc.py
# Build wsssh binary
pyinstaller --onefile --distpath dist --runtime-tmpdir /tmp --clean wsssh.py
# Build wsscp binary
pyinstaller --onefile --distpath dist --runtime-tmpdir /tmp --clean wsscp.py
# Deactivate venv
deactivate
echo "Build complete. Binaries are in dist/ directory:"
echo "- dist/wssshd (server with web interface)"
echo "- dist/wssshc (client)"
echo "- dist/wsssh (SSH wrapper)"
echo "- dist/wsscp (SCP wrapper)"
\ No newline at end of file
-----BEGIN CERTIFICATE-----
MIIFjzCCA3egAwIBAgIUQFDaAf1+EIfkMETiI17s2FLbB7swDQYJKoZIhvcNAQEL
BQAwVzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0y
NTA5MTExNjM0MjdaFw0yNjA5MTExNjM0MjdaMFcxCzAJBgNVBAYTAlVTMQ4wDAYD
VQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UECgwMT3JnYW5pemF0aW9u
MRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQC4rfzwx9nCgRXyQCCdDTRjeBxOUezdR37zZfypVsoRbhdxA0k6dlnuWdhs
kCeZl9qvfbUcEvjylmQEKMDcf3GS1DpU25MDkAc52Tjx9jRITg8o4DpXAaLb9F+M
fYe9ANhFQx1KKoZn0JgyAHMqE818xanM3saKXp/NP1QZczb5dvbFGFK3knoAWHOU
skJgaAzRVBN7VKSq8M3lc+4cL6Dg3VbJjpq9qLEdNMlhsUfVM/nfFMQvLKIcLo6g
EcOdKsnGlEZnx+e1RfHiVpmEEM4MwVgr4uo6osAwTwMi94xXtPl6L9ulJrZOMWH5
H3CVadfvIAq7Mb44lDdIglapu4QsZAH53kJMU4HzZga7lfYTdJDyZLxEOt2DTcxd
5r8AU+WJw3DK5tN1AmIS2RdqA4NSLqcutrO3nPf6NFwEtVLpTMH8zP6hoBqB/91Q
cafYo2A3+uR+bWcDV9/fOPfGNcvRzWUSejYPgq0iD5AT8eN94LtGq7QYaQu/DJep
7Iz1EtV177Zoey1U+t91ePGhgeSuDhaNO23NXCjeSJUDmC2KBwELfRAbEbpU31cB
EOaljmuz2MP2pbCBWE/0YX0m0lPQqlPIXxG0ZxByBR1jZM8pgBNJNdwNKU7d0e+N
h/FAu9s6TKBGL4z9itt20mzgu1qshM7NL8rHeoe0p8Gj2LRGrQIDAQABo1MwUTAd
BgNVHQ4EFgQUDUnUTD1RyD1nmEnx8ANxCiKgGWowHwYDVR0jBBgwFoAUDUnUTD1R
yD1nmEnx8ANxCiKgGWowDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
AgEAQGOwX9vnJS9RwyN4PELOHClIZU4GmxWNTbdRm51H8uJ6g7UTS1sqOprujHfr
3mi8PpAvNt/leWmerQ8p7iQcS+gRTaxd+qk/xReJqywRexZQXMduU+7vjOCqfy7f
cJZx20Snxu2T8tu8JpWfxQrM0BeJXimllIOUwyt4KC0Qz4zUhS1CPpYZGR+iLeJB
A1dykzWcDDEYHbJIMVHD0mC/2qIKvPpZXQKQNvTdQK+T7jc1LG3QZ4873bLQ/p9L
o5tLFHmLiwrHJi9Ox1rQEopttwM7x3vGah8MRHcedw2sNjiPqUMXFWOtwtrdoQkv
rgzZh/IsnY22XOAHpP3UaJw8r4UvMjEz5daH4V0EBq0hij4NhrRc3l+Vgzc+Aoom
wDvdi/ptuxzygWPITIvLxWfm6JkiHQzVLXWqBGVNE3GPWNMPgyEEXkENjthtgEfx
B88PNIZdBy33FOdXBlfww2NvpnKe++Crx3pGaus7w35c9PbB/Gk/2qGY9tgTzXC1
qa8V1eqg3HBss4f6VbBfY5QGV28eDttdFHwuIrNromTe9l9dUzxdvM/FPfxEtZ+y
bYJ7EARFQNR0WjaeEb0njZC2u2QsGiX7vT0II+Wu/ZrHSP7yoDnsaNzSWO+C2tpc
7vXS5+GazhAIscBzp5KRoJs6X5DRX6Y6yKYaiVwDOzs4Q9k=
-----END CERTIFICATE-----
#!/bin/bash
# Remove PyInstaller build artifacts
rm -rf build/
rm -rf dist/
rm -f *.spec
# Optionally remove SSL certificates (uncomment if needed)
# rm -f cert.pem key.pem
echo "Clean complete. Build artifacts removed."
echo "Note: SSL certificates (cert.pem, key.pem) preserved. Uncomment lines in clean.sh to remove them."
\ No newline at end of file
-----BEGIN PRIVATE KEY-----
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
zHTicyzq+uuJYB+ORdwQGoOh9ktNAQ==
-----END PRIVATE KEY-----
websockets>=15.0
paramiko>=4.0
flask>=3.1
flask-login>=0.6
flask-sqlalchemy>=3.1
werkzeug>=3.1
\ No newline at end of file
<!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 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">
<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;
padding: 20px;
border-radius: 8px;
height: 600px;
overflow-y: auto;
}
.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="#">
<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>
<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>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></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> {{ config.WEBSOCKET_PORT or 'N/A' }}</p>
<p class="mb-1"><strong>Domain:</strong> {{ config.DOMAIN or 'N/A' }}</p>
<p class="mb-0"><strong>Connected Clients:</strong> {{ clients|length }}</p>
</div>
</div>
</div>
</div>
{% 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">
<h3 class="card-title mb-0">
<i class="fas fa-terminal"></i> SSH Terminal - {{ client_id }}
</h3>
<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>
</div>
</div>
<div class="card-body p-0">
<div id="terminal" class="terminal-container">
<div id="output"></div>
<div class="input-group">
<span class="text-success me-2">$</span>
<input type="text" id="commandInput" class="terminal-input" placeholder="Type your command..." disabled>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let ws = null;
let connected = false;
document.getElementById('connectBtn').addEventListener('click', connect);
document.getElementById('disconnectBtn').addEventListener('click', disconnect);
document.getElementById('commandInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendCommand();
}
});
function connect() {
const username = document.getElementById('sshUsername').value;
if (!username) {
alert('Please enter a username');
return;
}
// For demo purposes, we'll simulate the connection
// In a real implementation, this would use WebSocket to communicate with wsssh
connected = true;
document.getElementById('connectBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = false;
document.getElementById('commandInput').disabled = false;
document.getElementById('sshUsername').disabled = true;
appendOutput(`Connecting to ${username}@{{ client_id }}...`);
setTimeout(() => {
appendOutput(`Connected successfully!`);
appendOutput(`Welcome to {{ client_id }}`);
appendOutput(`$ `);
}, 1000);
}
function disconnect() {
connected = false;
document.getElementById('connectBtn').disabled = false;
document.getElementById('disconnectBtn').disabled = true;
document.getElementById('commandInput').disabled = true;
document.getElementById('sshUsername').disabled = false;
appendOutput('Disconnected.');
}
function sendCommand() {
if (!connected) return;
const input = document.getElementById('commandInput');
const command = input.value.trim();
if (!command) return;
appendOutput(`$ ${command}`);
// Simulate command execution
setTimeout(() => {
if (command === 'ls') {
appendOutput(`Desktop Documents Downloads Music Pictures Videos`);
} else if (command === 'pwd') {
appendOutput(`/home/${document.getElementById('sshUsername').value}`);
} else if (command === 'whoami') {
appendOutput(document.getElementById('sshUsername').value);
} else if (command === 'exit' || command === 'logout') {
disconnect();
return;
} else {
appendOutput(`Command not found: ${command}`);
}
appendOutput(`$ `);
}, 500);
input.value = '';
}
function appendOutput(text) {
const output = document.getElementById('output');
const line = document.createElement('div');
line.textContent = text;
output.appendChild(line);
output.scrollTop = output.scrollHeight;
}
// Focus on command input when connected
document.addEventListener('keydown', function(e) {
if (connected && e.target.tagName !== 'INPUT') {
document.getElementById('commandInput').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>
<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 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
#!/usr/bin/env python3
"""
WebSocket SCP (wsscp)
SCP wrapper that uses WebSocket tunnels through 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 asyncio
import ssl
import websockets
import json
import socket
import subprocess
import sys
import uuid
debug = False
async def handle_tunnel(ws, local_port, request_id):
"""Handle tunnel data forwarding"""
try:
# Open local TCP listener
server = await asyncio.start_server(
lambda r, w: handle_local_connection(r, w, ws, request_id),
'localhost', local_port
)
if debug: print(f"[DEBUG] Listening on localhost:{local_port}")
async with server:
await server.serve_forever()
except Exception as e:
if debug: print(f"[DEBUG] Tunnel handler error: {e}")
async def handle_local_connection(reader, writer, ws, request_id):
"""Handle connections from local SCP client"""
try:
# Forward data between local connection and WebSocket
async def forward_to_ws():
try:
while True:
data = await reader.read(1024)
if not data:
break
await ws.send(json.dumps({
"type": "tunnel_data",
"request_id": request_id,
"data": data.hex() # Use hex encoding for binary data
}))
except Exception as e:
if debug: print(f"[DEBUG] Local to WS error: {e}")
async def forward_from_ws():
try:
async for message in ws:
data = json.loads(message)
if data.get('type') == 'tunnel_data' and data.get('request_id') == request_id:
# Decode and forward data
tunnel_data = bytes.fromhex(data['data'])
writer.write(tunnel_data)
await writer.drain()
elif data.get('type') == 'tunnel_close' and data.get('request_id') == request_id:
break
except Exception as e:
if debug: print(f"[DEBUG] WS to local error: {e}")
await asyncio.gather(forward_to_ws(), forward_from_ws())
except Exception as e:
if debug: print(f"[DEBUG] Local connection error: {e}")
finally:
writer.close()
await writer.wait_closed()
async def run_scp(server_ip, server_port, client_id, local_port, scp_args):
"""Connect to wssshd and run SCP"""
uri = f"wss://{server_ip}:{server_port}"
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
request_id = str(uuid.uuid4())
try:
async with websockets.connect(uri, ssl=ssl_context) as websocket:
# Request tunnel
await websocket.send(json.dumps({
"type": "tunnel_request",
"client_id": client_id,
"request_id": request_id
}))
# Wait for acknowledgment
response = await websocket.recv()
data = json.loads(response)
if data.get('type') == 'tunnel_ack':
if debug: print(f"[DEBUG] Tunnel request acknowledged: {request_id}")
# Start tunnel handler
tunnel_task = asyncio.create_task(handle_tunnel(websocket, local_port, request_id))
# Launch SCP with modified arguments
scp_cmd = ['scp'] + scp_args + ['-P', str(local_port)]
if debug: print(f"[DEBUG] Launching: {' '.join(scp_cmd)}")
# Run SCP process
process = await asyncio.create_subprocess_exec(
*scp_cmd,
stdin=sys.stdin,
stdout=sys.stdout,
stderr=sys.stderr
)
# Wait for SCP to complete
await process.wait()
# Close tunnel
await websocket.send(json.dumps({
"type": "tunnel_close",
"request_id": request_id
}))
tunnel_task.cancel()
elif data.get('type') == 'tunnel_error':
print(f"Error: {data.get('error', 'Unknown error')}")
return 1
except Exception as e:
print(f"Connection failed: {e}")
return 1
return 0
def parse_hostname(hostname):
"""Parse hostname to extract CLIENT_ID and WSSSHD_HOST"""
# Split by dots to get client_id and wssshd_host
parts = hostname.split('.')
if len(parts) >= 2:
client_id = parts[0]
wssshd_host = '.'.join(parts[1:])
else:
# No domain, assume whole hostname is client_id
client_id = hostname
wssshd_host = 'localhost' # Default fallback
return client_id, wssshd_host
def main():
parser = argparse.ArgumentParser(description='WebSocket SCP (wsscp)', add_help=False)
parser.add_argument('--local-port', type=int, default=0, help='Local port for tunnel (0 = auto)')
parser.add_argument('--debug', action='store_true', help='Enable debug output')
# Parse our arguments first
args, remaining = parser.parse_known_args()
global debug
debug = args.debug
# Find host arguments and -P port option in remaining args (SCP can have multiple host:paths)
hosts = []
scp_port = None
for i, arg in enumerate(remaining):
if arg == '-P' and i + 1 < len(remaining):
try:
scp_port = int(remaining[i + 1])
except ValueError:
pass
elif not arg.startswith('-') and ':' in arg:
# Extract host from host:path format
host_part = arg.split(':', 1)[0]
if '@' in host_part:
host = host_part.split('@', 1)[1]
else:
host = host_part
hosts.append(host)
if not hosts:
print("Error: Could not determine target host(s)")
sys.exit(1)
if not scp_port:
print("Error: Could not determine wssshd port from -P option")
sys.exit(1)
# Use the first host for parsing
host = hosts[0]
# Parse hostname to extract client_id and wssshd_host
client_id, wssshd_host = parse_hostname(host)
if debug:
print(f"[DEBUG] Hosts: {hosts}")
print(f"[DEBUG] Client ID: {client_id}")
print(f"[DEBUG] WSSSHD Host: {wssshd_host}")
print(f"[DEBUG] WSSSHD Port: {scp_port}")
# Find available local port
if args.local_port == 0:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('localhost', 0))
local_port = s.getsockname()[1]
else:
local_port = args.local_port
if debug: print(f"[DEBUG] Using local port: {local_port}")
# Modify remaining args to use localhost:local_port
modified_args = []
for arg in remaining:
if ':' in arg:
host_part, path_part = arg.split(':', 1)
if '@' in host_part:
user, host_in_arg = host_part.split('@', 1)
if host_in_arg == host: # Only modify the first host
modified_args.append(f"{user}@localhost:{path_part}")
else:
modified_args.append(arg)
else:
if host_part == host: # Only modify the first host
modified_args.append(f"localhost:{path_part}")
else:
modified_args.append(arg)
else:
modified_args.append(arg)
# Add port argument for local tunnel
modified_args.extend(['-P', str(local_port)])
if debug: print(f"[DEBUG] Modified SCP args: {modified_args}")
# Run the async SCP wrapper
exit_code = asyncio.run(run_scp(
wssshd_host, # Use parsed wssshd_host as server_ip
scp_port, # Use -P port as wssshd_port
client_id,
local_port,
modified_args
))
sys.exit(exit_code)
if __name__ == '__main__':
main()
\ No newline at end of file
#!/usr/bin/env python3
"""
WebSocket SSH (wsssh)
SSH wrapper that uses WebSocket tunnels through 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 asyncio
import ssl
import websockets
import json
import socket
import subprocess
import sys
import uuid
debug = False
async def handle_tunnel(ws, local_port, request_id):
"""Handle tunnel data forwarding"""
try:
# Open local TCP listener
server = await asyncio.start_server(
lambda r, w: handle_local_connection(r, w, ws, request_id),
'localhost', local_port
)
if debug: print(f"[DEBUG] Listening on localhost:{local_port}")
async with server:
await server.serve_forever()
except Exception as e:
if debug: print(f"[DEBUG] Tunnel handler error: {e}")
async def handle_local_connection(reader, writer, ws, request_id):
"""Handle connections from local SSH client"""
try:
# Forward data between local connection and WebSocket
async def forward_to_ws():
try:
while True:
data = await reader.read(1024)
if not data:
break
await ws.send(json.dumps({
"type": "tunnel_data",
"request_id": request_id,
"data": data.hex() # Use hex encoding for binary data
}))
except Exception as e:
if debug: print(f"[DEBUG] Local to WS error: {e}")
async def forward_from_ws():
try:
async for message in ws:
data = json.loads(message)
if data.get('type') == 'tunnel_data' and data.get('request_id') == request_id:
# Decode and forward data
tunnel_data = bytes.fromhex(data['data'])
writer.write(tunnel_data)
await writer.drain()
elif data.get('type') == 'tunnel_close' and data.get('request_id') == request_id:
break
except Exception as e:
if debug: print(f"[DEBUG] WS to local error: {e}")
await asyncio.gather(forward_to_ws(), forward_from_ws())
except Exception as e:
if debug: print(f"[DEBUG] Local connection error: {e}")
finally:
writer.close()
await writer.wait_closed()
async def run_ssh(server_ip, server_port, client_id, local_port, ssh_args):
"""Connect to wssshd and run SSH"""
uri = f"wss://{server_ip}:{server_port}"
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
request_id = str(uuid.uuid4())
try:
async with websockets.connect(uri, ssl=ssl_context) as websocket:
# Request tunnel
await websocket.send(json.dumps({
"type": "tunnel_request",
"client_id": client_id,
"request_id": request_id
}))
# Wait for acknowledgment
response = await websocket.recv()
data = json.loads(response)
if data.get('type') == 'tunnel_ack':
if debug: print(f"[DEBUG] Tunnel request acknowledged: {request_id}")
# Start tunnel handler
tunnel_task = asyncio.create_task(handle_tunnel(websocket, local_port, request_id))
# Launch SSH with modified arguments
ssh_cmd = ['ssh'] + ssh_args + ['-p', str(local_port), 'localhost']
if debug: print(f"[DEBUG] Launching: {' '.join(ssh_cmd)}")
# Run SSH process
process = await asyncio.create_subprocess_exec(
*ssh_cmd,
stdin=sys.stdin,
stdout=sys.stdout,
stderr=sys.stderr
)
# Wait for SSH to complete
await process.wait()
# Close tunnel
await websocket.send(json.dumps({
"type": "tunnel_close",
"request_id": request_id
}))
tunnel_task.cancel()
elif data.get('type') == 'tunnel_error':
print(f"Error: {data.get('error', 'Unknown error')}")
return 1
except Exception as e:
print(f"Connection failed: {e}")
return 1
return 0
def parse_hostname(hostname):
"""Parse hostname to extract CLIENT_ID and WSSSHD_HOST"""
# Split by dots to get client_id and wssshd_host
parts = hostname.split('.')
if len(parts) >= 2:
client_id = parts[0]
wssshd_host = '.'.join(parts[1:])
else:
# No domain, assume whole hostname is client_id
client_id = hostname
wssshd_host = 'localhost' # Default fallback
return client_id, wssshd_host
def main():
parser = argparse.ArgumentParser(description='WebSocket SSH (wsssh)', add_help=False)
parser.add_argument('--local-port', type=int, default=0, help='Local port for tunnel (0 = auto)')
parser.add_argument('--debug', action='store_true', help='Enable debug output')
# Parse our arguments first
args, remaining = parser.parse_known_args()
global debug
debug = args.debug
# Find the host argument and -p port option in remaining args
host = None
ssh_port = None
for i, arg in enumerate(remaining):
if arg == '-p' and i + 1 < len(remaining):
try:
ssh_port = int(remaining[i + 1])
except ValueError:
pass
elif not arg.startswith('-') and i > 0 and remaining[i-1] in ['-h', '--host']:
host = arg
break
elif not arg.startswith('-') and '@' in arg:
# Handle user@host format
host = arg.split('@', 1)[1]
break
elif not arg.startswith('-') and i == 0:
# First non-option argument might be host
host = arg
break
if not host:
print("Error: Could not determine target host")
sys.exit(1)
if not ssh_port:
print("Error: Could not determine wssshd port from -p option")
sys.exit(1)
# Parse hostname to extract client_id and wssshd_host
client_id, wssshd_host = parse_hostname(host)
if debug:
print(f"[DEBUG] Host: {host}")
print(f"[DEBUG] Client ID: {client_id}")
print(f"[DEBUG] WSSSHD Host: {wssshd_host}")
print(f"[DEBUG] WSSSHD Port: {ssh_port}")
# Find available local port
if args.local_port == 0:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('localhost', 0))
local_port = s.getsockname()[1]
else:
local_port = args.local_port
if debug: print(f"[DEBUG] Using local port: {local_port}")
# Modify remaining args to use localhost:local_port
modified_args = []
skip_next = False
for i, arg in enumerate(remaining):
if skip_next:
skip_next = False
continue
if arg in ['-h', '--host', '-p', '--port']:
skip_next = True
continue
elif '@' in arg and arg.split('@', 1)[1] == host:
# Replace user@host with user@localhost
user = arg.split('@', 1)[0]
modified_args.append(f"{user}@localhost")
continue
elif arg == host:
modified_args.append('localhost')
continue
else:
modified_args.append(arg)
# Add port argument for local tunnel
modified_args.extend(['-p', str(local_port)])
if debug: print(f"[DEBUG] Modified SSH args: {modified_args}")
# Run the async SSH wrapper
exit_code = asyncio.run(run_ssh(
wssshd_host, # Use parsed wssshd_host as server_ip
ssh_port, # Use -p port as wssshd_port
client_id,
local_port,
modified_args
))
sys.exit(exit_code)
if __name__ == '__main__':
main()
\ No newline at end of file
#!/usr/bin/env python3
"""
WebSocket SSH Client (wssshc)
Connects to wssshd server and registers as a client.
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 asyncio
import ssl
import websockets
import json
debug = False
async def connect_to_server(server_ip, port, client_id, password, interval):
uri = f"wss://{server_ip}:{port}"
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
while True:
try:
async with websockets.connect(uri, ssl=ssl_context) as websocket:
# Register
await websocket.send(json.dumps({"type": "register", "id": client_id, "password": password}))
print(f"Connected and registered as {client_id}")
# Wait for registration acknowledgment
response = await websocket.recv()
data = json.loads(response)
if data.get('type') == 'registered':
print(f"Successfully registered as {client_id}")
elif data.get('type') == 'registration_error':
print(f"Registration failed: {data.get('error', 'Unknown error')}")
return # Exit and retry
async for message in websocket:
if debug: print(f"[DEBUG] WebSocket message: {message[:100]}...")
data = json.loads(message)
if data.get('type') == 'tunnel_request':
print(f"Tunnel request received: {data['request_id']}")
# Client would handle tunnel setup here
elif data.get('type') == 'tunnel_data':
print(f"Tunnel data received: {len(data.get('data', ''))} bytes")
# Client would forward data here
elif data.get('type') == 'tunnel_close':
print(f"Tunnel closed: {data['request_id']}")
# Client would close tunnel here
except Exception as e:
print(f"Connection failed: {e}, retrying in {interval} seconds")
await asyncio.sleep(interval)
def main():
parser = argparse.ArgumentParser(description='WebSocket SSH Client (wssshc)')
parser.add_argument('--server-ip', required=True, help='Server IP address')
parser.add_argument('--port', type=int, required=True, help='Server port')
parser.add_argument('--id', required=True, help='Client ID')
parser.add_argument('--password', required=True, help='Registration password')
parser.add_argument('--interval', type=int, default=30, help='Reconnect interval in seconds (default: 30)')
parser.add_argument('--debug', action='store_true', help='Enable debug output')
args = parser.parse_args()
global debug
debug = args.debug
asyncio.run(connect_to_server(args.server_ip, args.port, args.id, args.password, args.interval))
if __name__ == '__main__':
main()
\ No newline at end of file
#!/usr/bin/env python3
"""
WebSocket SSH 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/>.
"""
import argparse
import asyncio
import ssl
import websockets
import json
import sys
import os
import threading
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
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
# Client registry: id -> websocket
clients = {}
debug = False
server_password = None
# Flask app for web interface
app = Flask(__name__)
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 User.query.get(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():
return render_template('index.html', clients=list(clients.keys()))
@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 = User.query.all()
return render_template('users.html', users=users)
@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):
if client_id not in clients:
flash('Client not connected')
return redirect(url_for('index'))
return render_template('terminal.html', client_id=client_id)
async def handle_websocket(websocket, path=None):
try:
async for message in websocket:
if debug: print(f"[DEBUG] WebSocket message received: {message[:100]}...")
data = json.loads(message)
if data.get('type') == 'register':
client_id = data['id']
client_password = data.get('password', '')
if client_password == server_password:
clients[client_id] = websocket
print(f"Client {client_id} registered")
await websocket.send(json.dumps({"type": "registered", "id": client_id}))
else:
print(f"Client {client_id} registration failed: invalid password")
await websocket.send(json.dumps({"type": "registration_error", "error": "Invalid password"}))
elif data.get('type') == 'tunnel_request':
client_id = data['client_id']
if client_id in clients:
# Forward tunnel request to client
await clients[client_id].send(json.dumps({
"type": "tunnel_request",
"request_id": data['request_id']
}))
await websocket.send(json.dumps({
"type": "tunnel_ack",
"request_id": data['request_id']
}))
else:
await websocket.send(json.dumps({
"type": "tunnel_error",
"request_id": data['request_id'],
"error": "Client not registered"
}))
elif data.get('type') == 'tunnel_data':
# Forward tunnel data to appropriate client
client_id = data.get('client_id')
if client_id and client_id in clients:
await clients[client_id].send(json.dumps({
"type": "tunnel_data",
"request_id": data['request_id'],
"data": data['data']
}))
elif data.get('type') == 'tunnel_close':
client_id = data.get('client_id')
if client_id and client_id in clients:
await clients[client_id].send(json.dumps({
"type": "tunnel_close",
"request_id": data['request_id']
}))
except websockets.exceptions.ConnectionClosed:
# Remove from registry
for cid, ws in clients.items():
if ws == websocket:
del clients[cid]
print(f"Client {cid} disconnected")
break
async def main():
parser = argparse.ArgumentParser(description='WebSocket SSH Daemon (wssshd)')
parser.add_argument('--host', required=True, help='WebSocket server host')
parser.add_argument('--port', type=int, required=True, help='WebSocket server port')
parser.add_argument('--domain', required=True, help='Base domain name')
parser.add_argument('--password', required=True, 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('--debug', action='store_true', help='Enable debug output')
args = parser.parse_args()
global debug
debug = args.debug
global server_password
server_password = args.password
# 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)
# Start WebSocket server
ws_server = await websockets.serve(handle_websocket, args.host, args.port, ssl=ssl_context)
print(f"WebSocket SSH Daemon running on {args.host}:{args.port}")
# Start web interface if specified
if args.web_host and args.web_port:
def run_flask():
app.run(host=args.web_host, port=args.web_port, debug=False, use_reloader=False)
flask_thread = threading.Thread(target=run_flask, daemon=True)
flask_thread.start()
print(f"Web interface available at http://{args.web_host}:{args.web_port}")
await ws_server.wait_closed()
if __name__ == '__main__':
asyncio.run(main())
\ 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