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
{% 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
"""
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
#!/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