Remove obsolete Python server implementation and templates

- Removed templates/ directory (no longer needed with embedded HTML)
- Removed wssshd.py (Python server entry point)
- Removed wsssd/ directory (entire Python server implementation)
- Project now uses only wssshd2/ C implementation
- Cleaner codebase with single server implementation
- Reduced maintenance overhead and confusion
parent 98978a74
<!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
This diff is collapsed.
{% 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
"""
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
This diff is collapsed.
"""
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
This diff is collapsed.
This diff is collapsed.
#!/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
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