Version 1.6.2: Tunnel Close Forwarding & Web Terminal Enhancements

- FIX: Tunnel close forwarding between wssshc, server, and wsssht
  * Server now properly forwards tunnel_close messages to wsssht
  * Prevents wsssht processes from hanging after client-initiated closure
  * Ensures proper cleanup of tunnel resources on all endpoints

- FIX: Web terminal JavaScript issues
  * Fixed FitAddon loading issues by switching to unpkg CDN
  * Resolved JavaScript variable scoping error (fitAddon undefined)
  * Added proper error handling for xterm.js library loading failures
  * Enhanced debug logging for library loading and terminal initialization

- FEATURE: Fullscreen terminal support
  * Added fullscreen toggle button (⛶/⛝) to terminal interface
  * Cross-browser fullscreen API support (Chrome, Firefox, Safari, IE11)
  * Automatic terminal resizing when entering/exiting fullscreen mode
  * Backend synchronization of terminal dimensions during fullscreen changes

- FIX: Logo serving path
  * Updated Flask routes to serve logo files from logos/ directory
  * Added proper PyInstaller support for bundled logo assets
  * Fixed favicon.ico and image.jpg serving for both development and frozen executables

- DOCS: Updated changelog, README, and documentation
  * Added version 1.6.2 changelog with comprehensive change details
  * Updated README with fullscreen feature mention
  * Enhanced documentation with recent updates section

Technical Details:
- Server-side tunnel close message routing in websocket.py
- JavaScript library management with robust error recovery
- Cross-browser fullscreen API implementation
- Asset management for both development and production environments
parent b3ef97b4
...@@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file. ...@@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.6.2] - 2025-09-19
### Fixed
- **Tunnel Close Forwarding**: Critical fix for tunnel closure synchronization between wssshc, server, and wsssht
- Server now properly forwards `tunnel_close` messages from wssshc to wsssht
- Prevents wsssht processes from hanging after client-initiated tunnel closure
- Ensures proper cleanup of tunnel resources on all endpoints
- Fixed server-side tunnel close message routing in `wsssd/websocket.py`
- **Web Terminal JavaScript Issues**: Comprehensive fixes for web interface terminal functionality
- Fixed `FitAddon` loading issues by switching from jsdelivr to unpkg CDN
- Resolved JavaScript variable scoping error (`fitAddon` undefined) in terminal initialization
- Added proper error handling for xterm.js library loading failures
- Enhanced debug logging for library loading and terminal initialization
- Improved cross-browser compatibility for xterm.js components
- **Logo Serving Path**: Fixed web interface logo loading from correct directory
- Updated Flask routes to serve logo files from `logos/` directory instead of root
- Added proper PyInstaller support for bundled logo assets
- Fixed favicon.ico and image.jpg serving for both development and frozen executables
### Added
- **Fullscreen Terminal Support**: Enhanced web terminal with fullscreen toggle functionality
- Added fullscreen toggle button (⛶/⛝) to terminal interface header
- Cross-browser fullscreen API support (Chrome, Firefox, Safari, IE11)
- Automatic terminal resizing when entering/exiting fullscreen mode
- Backend synchronization of terminal dimensions during fullscreen changes
- Proper event handling for fullscreen state changes
### Enhanced
- **Web Terminal User Experience**: Improved terminal interface with better controls and responsiveness
- Added fullscreen button with dynamic icon changes based on state
- Enhanced terminal resizing with proper backend dimension updates
- Improved error handling and user feedback for terminal operations
- Better visual feedback for terminal state changes
### Technical Details
- **Tunnel Close Protocol**: Server now correctly forwards tunnel close messages to both endpoints
- **JavaScript Library Management**: Robust xterm.js loading with fallback CDN and error recovery
- **Fullscreen API**: Comprehensive browser compatibility with proper event handling
- **Asset Management**: Proper static file serving for both development and production environments
## [1.6.1] - 2025-09-18 ## [1.6.1] - 2025-09-18
### Changed ### Changed
......
...@@ -658,6 +658,12 @@ python3 -m pytest tests/integration/ ...@@ -658,6 +658,12 @@ python3 -m pytest tests/integration/
## Recent Updates ## Recent Updates
### Version 1.6.2
- **Tunnel Close Forwarding**: Critical fix for tunnel closure synchronization between wssshc, server, and wsssht
- **Web Terminal JavaScript Fixes**: Comprehensive fixes for web interface terminal functionality
- **Fullscreen Terminal Support**: Enhanced web terminal with fullscreen toggle functionality
- **Logo Serving Path**: Fixed web interface logo loading from correct directory
### Version 1.6.1 ### Version 1.6.1
- **Major Code Refactoring**: Complete modularization of wsssht.c - **Major Code Refactoring**: Complete modularization of wsssht.c
- **Operating Modes**: Multiple operating modes implementation - **Operating Modes**: Multiple operating modes implementation
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
- **Drop-in SSH/SCP Replacement**: Use `wsssh` and `wsscp` as direct replacements for standard SSH/SCP commands - **Drop-in SSH/SCP Replacement**: Use `wsssh` and `wsscp` as direct replacements for standard SSH/SCP commands
- **SSL/TLS Encryption**: All communications are fully encrypted - **SSL/TLS Encryption**: All communications are fully encrypted
- **Multi-client Support**: Route connections to different registered clients - **Multi-client Support**: Route connections to different registered clients
- **Professional Web Interface**: Admin panel with user management and HTML5 terminal - **Professional Web Interface**: Admin panel with user management, HTML5 terminal, and fullscreen support
- **Cross-platform Compatibility**: Works on Linux, macOS, and Windows - **Cross-platform Compatibility**: Works on Linux, macOS, and Windows
- **Debian Packaging**: Easy installation with proper system integration - **Debian Packaging**: Easy installation with proper system integration
- **Service Management**: Complete init scripts and watchdog monitoring - **Service Management**: Complete init scripts and watchdog monitoring
......
...@@ -7,8 +7,29 @@ ...@@ -7,8 +7,29 @@
<link rel="icon" href="/image.jpg" type="image/x-icon"> <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://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 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css"> <link rel="stylesheet" href="https://unpkg.com/xterm@5.3.0/css/xterm.css">
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script> <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> <style>
.navbar-brand { .navbar-brand {
font-weight: bold; font-weight: bold;
......
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
<button id="disconnectBtn" class="btn btn-danger btn-sm" disabled> <button id="disconnectBtn" class="btn btn-danger btn-sm" disabled>
<i class="fas fa-stop"></i> Disconnect <i class="fas fa-stop"></i> Disconnect
</button> </button>
<button id="fullscreenBtn" class="btn btn-secondary btn-sm" title="Toggle Fullscreen">
<i class="fas fa-expand"></i>
</button>
</div> </div>
</div> </div>
<div class="card-body p-2"> <div class="card-body p-2">
...@@ -35,20 +38,105 @@ ...@@ -35,20 +38,105 @@
{% block scripts %} {% block scripts %}
<script> <script>
console.log('Terminal script starting...');
console.log('xterm available:', typeof Terminal);
console.log('xterm-fit available:', typeof FitAddon);
let term = null; let term = null;
let fitAddon = null;
let connected = false; let connected = false;
let requestId = null; let requestId = null;
let pollInterval = null; let pollInterval = null;
console.log('Terminal page loaded, adding event listeners');
document.getElementById('connectBtn').addEventListener('click', connect); document.getElementById('connectBtn').addEventListener('click', connect);
document.getElementById('disconnectBtn').addEventListener('click', disconnect); 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() { function connect() {
console.log('Connect function called');
const username = document.getElementById('sshUsername').value; const username = document.getElementById('sshUsername').value;
console.log('Username value:', username);
if (!username) { if (!username) {
alert('Please enter a username'); alert('Please enter a username');
return; return;
} }
console.log('Username validation passed');
// Initialize xterm with proper configuration // Initialize xterm with proper configuration
if (!term) { if (!term) {
...@@ -82,28 +170,38 @@ function connect() { ...@@ -82,28 +170,38 @@ function connect() {
term.open(document.getElementById('terminal')); term.open(document.getElementById('terminal'));
// Load fit addon // Load fit addon
const fitAddon = new FitAddon.FitAddon(); try {
if (typeof FitAddon !== 'undefined') {
fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon); term.loadAddon(fitAddon);
console.log('FitAddon loaded successfully');
// Fit terminal to container } else {
function fitTerminal() { console.error('FitAddon is not available');
fitAddon.fit(); 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 // Initial fit after a short delay to ensure DOM is ready
setTimeout(() => { setTimeout(() => {
fitTerminal(); window.fitTerminal();
// Calculate dimensions after initial fit // Calculate dimensions after initial fit
const initialDimensions = fitAddon.proposeDimensions(); let initialDimensions = { cols: 80, rows: 24 };
if (fitAddon) {
initialDimensions = fitAddon.proposeDimensions();
}
term._initialCols = initialDimensions.cols || 80; term._initialCols = initialDimensions.cols || 80;
term._initialRows = initialDimensions.rows || 24; term._initialRows = initialDimensions.rows || 24;
}, 100); }, 100);
// Fit on window resize and update backend terminal size // Fit on window resize and update backend terminal size
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
fitTerminal(); window.fitTerminal();
// Update terminal size on backend if connected // Update terminal size on backend if connected
if (connected && requestId) { if (connected && requestId && fitAddon) {
const newDimensions = fitAddon.proposeDimensions(); const newDimensions = fitAddon.proposeDimensions();
const newCols = newDimensions.cols || 80; const newCols = newDimensions.cols || 80;
const newRows = newDimensions.rows || 24; const newRows = newDimensions.rows || 24;
...@@ -125,6 +223,13 @@ function connect() { ...@@ -125,6 +223,13 @@ function connect() {
term.focus(); 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'); term.write('Connecting to ' + username + '@{{ client_id }}...\r\n');
connected = true; connected = true;
...@@ -133,21 +238,39 @@ function connect() { ...@@ -133,21 +238,39 @@ function connect() {
document.getElementById('sshUsername').disabled = true; document.getElementById('sshUsername').disabled = true;
// Use calculated dimensions (either from initial fit or current) // Use calculated dimensions (either from initial fit or current)
const cols = term._initialCols || fitAddon.proposeDimensions().cols || 80; let cols = term._initialCols || 80;
const rows = term._initialRows || fitAddon.proposeDimensions().rows || 24; 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 // Send connect request with terminal dimensions
fetch('/terminal/{{ client_id }}/connect', { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
body: 'username=' + encodeURIComponent(username) + body: 'username=' + encodeURIComponent(username) +
'&cols=' + encodeURIComponent(cols) + '&cols=' + encodeURIComponent(cols) +
'&rows=' + encodeURIComponent(rows) '&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(response => response.json())
.then(data => { .then(data => {
console.log('Connect response data:', data);
if (data.request_id) { if (data.request_id) {
requestId = data.request_id; requestId = data.request_id;
if (data.command) { if (data.command) {
...@@ -157,12 +280,13 @@ function connect() { ...@@ -157,12 +280,13 @@ function connect() {
// Start polling for data with shorter interval for better responsiveness // Start polling for data with shorter interval for better responsiveness
pollInterval = setInterval(pollData, 100); pollInterval = setInterval(pollData, 100);
} else { } else {
term.write('Error: ' + data.error + '\r\n'); term.write('Error: ' + (data.error || 'Unknown error') + '\r\n');
disconnect(); disconnect();
} }
}) })
.catch(error => { .catch(error => {
term.write('Connection failed: ' + error + '\r\n'); console.error('Connection failed:', error);
term.write('Connection failed: ' + error.message + '\r\n');
disconnect(); disconnect();
}); });
...@@ -170,6 +294,8 @@ function connect() { ...@@ -170,6 +294,8 @@ function connect() {
term.onData(data => { term.onData(data => {
if (!connected || !requestId) return; if (!connected || !requestId) return;
console.log('Sending input data:', data.length, 'characters');
// Send all input to server, let SSH handle echo and display // Send all input to server, let SSH handle echo and display
fetch('/terminal/{{ client_id }}/data', { fetch('/terminal/{{ client_id }}/data', {
method: 'POST', method: 'POST',
...@@ -177,6 +303,12 @@ function connect() { ...@@ -177,6 +303,12 @@ function connect() {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
body: 'request_id=' + encodeURIComponent(requestId) + '&data=' + encodeURIComponent(data) 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);
}); });
}); });
} }
...@@ -211,9 +343,15 @@ function disconnect() { ...@@ -211,9 +343,15 @@ function disconnect() {
function pollData() { function pollData() {
if (!requestId) return; if (!requestId) return;
fetch('/terminal/{{ client_id }}/data?request_id=' + encodeURIComponent(requestId)) fetch('/terminal/{{ client_id }}/data?request_id=' + encodeURIComponent(requestId))
.then(response => response.text()) .then(response => {
if (response.status !== 200) {
console.log('Poll response status:', response.status);
}
return response.text();
})
.then(data => { .then(data => {
if (data) { if (data) {
console.log('Received data:', data.length, 'characters');
// Let the server handle all echo and display logic // Let the server handle all echo and display logic
term.write(data.replace(/\n/g, '\r\n')); term.write(data.replace(/\n/g, '\r\n'));
} }
......
""" """
WSSSH Daemon (wssshd) - Modular implementation 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 .server import main
......
""" """
Entry point for running wssshd as a module 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 from .server import main
......
""" """
Configuration handling for wssshd 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 argparse
......
""" """
Main server logic for wssshd 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 asyncio
......
""" """
Terminal and PTY handling for wssshd 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 os
...@@ -86,6 +101,12 @@ def create_terminal_session(args, username, client_id): ...@@ -86,6 +101,12 @@ def create_terminal_session(args, username, client_id):
request_id = str(uuid.uuid4()) request_id = str(uuid.uuid4())
# Force echo mode before launching wsssh # Force echo mode before launching wsssh
command = ['sh', '-c', f'stty echo && wsssh -p {args.port} {username}@{client_id}.{args.domain}'] 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 # Spawn wsssh process with pty using fallback method
master, slave = openpty_with_fallback() master, slave = openpty_with_fallback()
slave_name = os.ttyname(slave) slave_name = os.ttyname(slave)
......
""" """
Tunnel object management for wssshd 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 time
...@@ -67,6 +82,10 @@ class Tunnel: ...@@ -67,6 +82,10 @@ class Tunnel:
self.last_keepalive_from_client = time.time() # wssshc endpoint self.last_keepalive_from_client = time.time() # wssshc endpoint
self.last_keepalive_from_tool = time.time() # wsssht/wsscp 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): def update_status(self, new_status, error_message=None):
"""Update tunnel status and timestamp""" """Update tunnel status and timestamp"""
self.status = new_status self.status = new_status
......
""" """
Flask web interface for wssshd 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 os
...@@ -158,9 +173,13 @@ def delete_user(user_id): ...@@ -158,9 +173,13 @@ def delete_user(user_id):
@app.route('/terminal/<client_id>') @app.route('/terminal/<client_id>')
@login_required @login_required
def terminal(client_id): 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: if client_id not in clients:
print(f"[ERROR] [Web] Client '{client_id}' not found, redirecting to index")
flash('Client not connected') flash('Client not connected')
return redirect(url_for('index')) 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) return render_template('terminal.html', client_id=client_id)
...@@ -179,23 +198,62 @@ def get_clients(): ...@@ -179,23 +198,62 @@ def get_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') @app.route('/image.jpg')
def logo_file(): def logo_file():
return send_from_directory('.', 'image.jpg') 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']) @app.route('/terminal/<client_id>/connect', methods=['POST'])
@login_required @login_required
def connect_terminal(client_id): 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 from .config import load_config
args = load_config() args = load_config()
username = request.form.get('username', 'root') username = request.form.get('username', 'root')
cols = int(request.form.get('cols', 80)) cols = int(request.form.get('cols', 80))
rows = int(request.form.get('rows', 24)) 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) terminal_session = create_terminal_session(args, username, client_id)
request_id = terminal_session['request_id'] request_id = terminal_session['request_id']
print(f"[DEBUG] [Web] Terminal session created successfully with request_id {request_id}")
# Store terminal session # Store terminal session
active_terminals[request_id] = { active_terminals[request_id] = {
'client_id': client_id, 'client_id': client_id,
...@@ -205,10 +263,22 @@ def connect_terminal(client_id): ...@@ -205,10 +263,22 @@ def connect_terminal(client_id):
'master': terminal_session['master'] 'master': terminal_session['master']
} }
return jsonify({ 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, 'request_id': request_id,
'command': terminal_session['command'] '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']) @app.route('/terminal/<client_id>/data', methods=['GET', 'POST'])
......
""" """
WebSocket handling for wssshd 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 asyncio
...@@ -22,7 +37,7 @@ TUNNEL_ACK_MSG = '{"type": "tunnel_ack", "request_id": "%s"}' ...@@ -22,7 +37,7 @@ TUNNEL_ACK_MSG = '{"type": "tunnel_ack", "request_id": "%s"}'
TUNNEL_CLOSE_MSG = '{"type": "tunnel_close", "request_id": "%s"}' TUNNEL_CLOSE_MSG = '{"type": "tunnel_close", "request_id": "%s"}'
TUNNEL_REQUEST_MSG = '{"type": "tunnel_request", "request_id": "%s"}' TUNNEL_REQUEST_MSG = '{"type": "tunnel_request", "request_id": "%s"}'
TUNNEL_ERROR_MSG = '{"type": "tunnel_error", "request_id": "%s", "error": "%s"}' TUNNEL_ERROR_MSG = '{"type": "tunnel_error", "request_id": "%s", "error": "%s"}'
TUNNEL_KEEPALIVE_MSG = '{"type": "tunnel_keepalive", "request_id": "%s", "total_bytes": %llu, "rate_bps": %.2f}' 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"}' TUNNEL_KEEPALIVE_ACK_MSG = '{"type": "tunnel_keepalive_ack", "request_id": "%s"}'
REGISTERED_MSG = '{"type": "registered", "id": "%s"}' REGISTERED_MSG = '{"type": "registered", "id": "%s"}'
REGISTRATION_ERROR_MSG = '{"type": "registration_error", "error": "%s"}' REGISTRATION_ERROR_MSG = '{"type": "registration_error", "error": "%s"}'
...@@ -332,12 +347,27 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_ ...@@ -332,12 +347,27 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_
# Update tunnel status to closing # Update tunnel status to closing
tunnel.update_status(TunnelStatus.CLOSING) tunnel.update_status(TunnelStatus.CLOSING)
# Forward close to client if still active # 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) client_info = clients.get(tunnel.client_id)
if client_info and client_info['status'] == 'active': if client_info and client_info['status'] == 'active':
try: try:
close_msg = TUNNEL_CLOSE_MSG % request_id close_msg = TUNNEL_CLOSE_MSG % request_id
if debug_flag: print(f"[DEBUG] [WebSocket] Sending tunnel close to client: {close_msg}") if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding tunnel close from wsssht to wssshc: {close_msg}")
await tunnel.client_ws.send(close_msg) await tunnel.client_ws.send(close_msg)
except Exception: except Exception:
# Silent failure for performance # Silent failure for performance
...@@ -386,8 +416,34 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_ ...@@ -386,8 +416,34 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_
keepalive_msg = TUNNEL_KEEPALIVE_MSG % (request_id, total_bytes, rate_bps) 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}") if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding keep-alive to {forward_name}: {keepalive_msg}")
await forward_ws.send(keepalive_msg) await forward_ws.send(keepalive_msg)
# Reset failure counter on successful forward
tunnel.keepalive_forward_failures = 0
except Exception as e: except Exception as e:
if debug_flag: print(f"[DEBUG] [WebSocket] Failed to forward keep-alive: {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 # Send ACK response back to sender
try: try:
...@@ -422,8 +478,34 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_ ...@@ -422,8 +478,34 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_
ack_msg = TUNNEL_KEEPALIVE_ACK_MSG % request_id ack_msg = TUNNEL_KEEPALIVE_ACK_MSG % request_id
if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding keep-alive ACK to {forward_name}: {ack_msg}") if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding keep-alive ACK to {forward_name}: {ack_msg}")
await forward_ws.send(ack_msg) await forward_ws.send(ack_msg)
# Reset failure counter on successful forward
tunnel.keepalive_ack_forward_failures = 0
except Exception as e: except Exception as e:
if debug_flag: print(f"[DEBUG] [WebSocket] Failed to forward keep-alive ACK: {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: else:
if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive ACK received for unknown tunnel {request_id}") if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive ACK received for unknown tunnel {request_id}")
except websockets.exceptions.ConnectionClosed: except websockets.exceptions.ConnectionClosed:
......
wsssh-server (1.6.1-1) unstable; urgency=medium
* Version 1.6.1: Major code refactoring and documentation updates
* Complete modularization of wsssht.c into libwsssht/ components
* Split monolithic 2769-line wsssht.c into modular structure:
- utils.h/c: Utility functions (print_usage, parse_connection_string, parse_args)
- modes.h/c: Operating mode implementations (bridge, script, daemon modes)
- threads.h/c: Thread-related functions and structures
- wsssht.h: Main header with includes and declarations
* Reduced main wsssht.c from 2769 to 674 lines (75% size reduction)
* Updated configure.sh and Makefile for new modular structure
* Maintained 100% backward compatibility and functionality
* Improved developer experience with logical code grouping
* Easier debugging, testing, and feature development
* Complete documentation rewrite and README update
* Updated project description from WebSocket-based to universal tunneling system
* Enhanced README.md with comprehensive features and installation guide
* Rewritten DOCUMENTATION.md with detailed technical specifications
* Updated TODO.md with current project status and future enhancements
* Ensured all command-line examples are accurate and exclude hidden options
* Maintained consistency across all documentation files
-- Stefy Lanza <stefy@nexlab.net> Wed, 18 Sep 2025 08:47:00 +0200
wsssh-server (1.4.4-1) unstable; urgency=medium wsssh-server (1.4.4-1) unstable; urgency=medium
* New upstream release 1.4.4 * New upstream release 1.4.4
......
/*
* WSSSH Library - Control Channel Messages 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/>.
*/
#include "control_messages.h"
#include "websocket.h"
#include "tunnel.h"
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#define BUFFER_SIZE 8192
int send_json_message(SSL *ssl, const char *type, const char *client_id, const char *request_id) {
char message[1024];
if (request_id) {
snprintf(message, sizeof(message),
"{\"type\":\"%s\",\"client_id\":\"%s\",\"request_id\":\"%s\"}",
type, client_id, request_id);
} else {
snprintf(message, sizeof(message),
"{\"type\":\"%s\",\"client_id\":\"%s\"}",
type, client_id);
}
// Send as WebSocket frame
return send_websocket_frame(ssl, message);
}
int send_registration_message(SSL *ssl, const char *client_id, const char *password, const char *tunnel, const char *tunnel_control, const char *wssshd_private_ip) {
char message[2048];
if (password && strlen(password) > 0) {
if (wssshd_private_ip && strlen(wssshd_private_ip) > 0) {
snprintf(message, sizeof(message),
"{\"type\":\"register\",\"client_id\":\"%s\",\"password\":\"%s\",\"tunnel\":\"%s\",\"tunnel_control\":\"%s\",\"wssshd_private_ip\":\"%s\",\"version\":\"%s\"}",
client_id, password, tunnel, tunnel_control, wssshd_private_ip, WSSSH_VERSION);
} else {
snprintf(message, sizeof(message),
"{\"type\":\"register\",\"client_id\":\"%s\",\"password\":\"%s\",\"tunnel\":\"%s\",\"tunnel_control\":\"%s\",\"version\":\"%s\"}",
client_id, password, tunnel, tunnel_control, WSSSH_VERSION);
}
} else {
if (wssshd_private_ip && strlen(wssshd_private_ip) > 0) {
snprintf(message, sizeof(message),
"{\"type\":\"register\",\"client_id\":\"%s\",\"tunnel\":\"%s\",\"tunnel_control\":\"%s\",\"wssshd_private_ip\":\"%s\",\"version\":\"%s\"}",
client_id, tunnel, tunnel_control, wssshd_private_ip, WSSSH_VERSION);
} else {
snprintf(message, sizeof(message),
"{\"type\":\"register\",\"client_id\":\"%s\",\"tunnel\":\"%s\",\"tunnel_control\":\"%s\",\"version\":\"%s\"}",
client_id, tunnel, tunnel_control, WSSSH_VERSION);
}
}
printf("[DEBUG] Sending registration message: %s\n", message);
fflush(stdout);
// Send as WebSocket frame
int result = send_websocket_frame(ssl, message);
if (result) {
printf("[DEBUG] Registration message sent successfully\n");
fflush(stdout);
} else {
printf("[DEBUG] Failed to send registration message\n");
fflush(stdout);
}
return result;
}
int send_tunnel_request_message(SSL *ssl, const char *client_id, const char *request_id, const char *tunnel, const char *tunnel_control, const char *service) {
char message[1024];
if (service) {
snprintf(message, sizeof(message),
"{\"type\":\"tunnel_request\",\"client_id\":\"%s\",\"request_id\":\"%s\",\"tunnel\":\"%s\",\"tunnel_control\":\"%s\",\"service\":\"%s\",\"version\":\"%s\"}",
client_id, request_id, tunnel, tunnel_control, service, WSSSH_VERSION);
} else {
snprintf(message, sizeof(message),
"{\"type\":\"tunnel_request\",\"client_id\":\"%s\",\"request_id\":\"%s\",\"tunnel\":\"%s\",\"tunnel_control\":\"%s\",\"version\":\"%s\"}",
client_id, request_id, tunnel, tunnel_control, WSSSH_VERSION);
}
// Send as WebSocket frame
return send_websocket_frame(ssl, message);
}
int send_tunnel_close_message(SSL *ssl, const char *request_id, int debug) {
char close_msg[256];
snprintf(close_msg, sizeof(close_msg), "{\"type\":\"tunnel_close\",\"request_id\":\"%s\"}", request_id);
if (debug) {
printf("[DEBUG - Tunnel] Sending tunnel_close: %s\n", close_msg);
fflush(stdout);
}
if (!send_websocket_frame(ssl, close_msg)) {
if (debug) {
printf("[DEBUG - Tunnel] Failed to send tunnel_close message\n");
fflush(stdout);
}
return 0;
}
return 1;
}
int send_tunnel_keepalive_message(SSL *ssl, tunnel_t *tunnel, int debug) {
if (!tunnel || !tunnel->active) return 0;
time_t current_time = time(NULL);
// Reset stats every 30 seconds
if (current_time - tunnel->last_stats_reset >= 30) {
tunnel->bytes_last_period = 0;
tunnel->last_stats_reset = current_time;
}
// Calculate rate (bytes per second over last 30 seconds)
double rate_bps = 0.0;
if (current_time > tunnel->last_stats_reset) {
rate_bps = (double)tunnel->bytes_last_period / (current_time - tunnel->last_stats_reset);
}
// Send keep-alive message
char keepalive_msg[512];
unsigned long long total_bytes = tunnel->total_bytes_sent + tunnel->total_bytes_received;
snprintf(keepalive_msg, sizeof(keepalive_msg),
"{\"type\":\"tunnel_keepalive\",\"request_id\":\"%s\",\"total_bytes\":%llu,\"rate_bps\":%.2f}",
tunnel->request_id, total_bytes, rate_bps);
if (debug) {
printf("[DEBUG - Tunnel] Sending keep-alive for tunnel %s: %s\n", tunnel->request_id, keepalive_msg);
fflush(stdout);
}
if (send_websocket_frame(ssl, keepalive_msg)) {
tunnel->last_keepalive_sent = current_time;
return 1;
} else {
if (debug) {
printf("[DEBUG - Tunnel] Failed to send keep-alive for tunnel %s\n", tunnel->request_id);
fflush(stdout);
}
return 0;
}
}
int send_tunnel_keepalive_ack_message(SSL *ssl, const char *request_id, int debug) {
char ack_msg[256];
snprintf(ack_msg, sizeof(ack_msg), "{\"type\":\"tunnel_keepalive_ack\",\"request_id\":\"%s\"}", request_id);
if (debug) {
printf("[DEBUG - WebSockets] Sending tunnel_keepalive_ack: %s\n", ack_msg);
fflush(stdout);
}
if (!send_websocket_frame(ssl, ack_msg)) {
if (debug) {
printf("[DEBUG - Tunnel] Failed to send keep-alive ACK for tunnel %s\n", request_id);
fflush(stdout);
}
return 0;
}
return 1;
}
int send_ping_frame(SSL *ssl, const char *ping_payload, int payload_len) {
// Lock SSL mutex to prevent concurrent SSL operations
pthread_mutex_lock(&ssl_mutex);
char frame[BUFFER_SIZE];
frame[0] = 0x89; // FIN + ping opcode
int header_len = 2;
if (payload_len <= 125) {
frame[1] = 0x80 | payload_len; // MASK + length
} else if (payload_len <= 65535) {
frame[1] = 0x80 | 126; // MASK + extended length
frame[2] = (payload_len >> 8) & 0xFF;
frame[3] = payload_len & 0xFF;
header_len = 4;
} else {
frame[1] = 0x80 | 127; // MASK + extended length
frame[2] = 0;
frame[3] = 0;
frame[4] = 0;
frame[5] = 0;
frame[6] = (payload_len >> 24) & 0xFF;
frame[7] = (payload_len >> 16) & 0xFF;
frame[8] = (payload_len >> 8) & 0xFF;
frame[9] = payload_len & 0xFF;
header_len = 10;
}
// Add mask key
char mask_key[4];
for (int i = 0; i < 4; i++) {
mask_key[i] = rand() % 256;
frame[header_len + i] = mask_key[i];
}
header_len += 4;
// Mask payload
for (int i = 0; i < payload_len; i++) {
frame[header_len + i] = ping_payload[i] ^ mask_key[i % 4];
}
int frame_len = header_len + payload_len;
// Handle partial writes for large frames
int total_written = 0;
int retry_count = 0;
const int max_retries = 3;
while (total_written < frame_len && retry_count < max_retries) {
int to_write = frame_len - total_written;
// Limit to BUFFER_SIZE to avoid issues with very large frames
if (to_write > BUFFER_SIZE) {
to_write = BUFFER_SIZE;
}
int written = SSL_write(ssl, frame + total_written, to_write);
if (written <= 0) {
int ssl_error = SSL_get_error(ssl, written);
// Handle transient SSL errors with retry
if ((ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) && retry_count < max_retries - 1) {
retry_count++;
usleep(10000); // Wait 10ms before retry
continue; // Retry the write operation
}
fprintf(stderr, "Ping frame SSL_write failed: %d (after %d retries)\n", ssl_error, retry_count);
char error_buf[256];
ERR_error_string_n(ssl_error, error_buf, sizeof(error_buf));
fprintf(stderr, "SSL write error details: %s\n", error_buf);
pthread_mutex_unlock(&ssl_mutex);
return 0; // Write failed
}
total_written += written;
retry_count = 0; // Reset retry count on successful write
}
if (total_written < frame_len) {
fprintf(stderr, "Ping frame write incomplete: %d/%d bytes written\n", total_written, frame_len);
pthread_mutex_unlock(&ssl_mutex);
return 0;
}
pthread_mutex_unlock(&ssl_mutex);
return 1;
}
int send_pong_frame(SSL *ssl, const char *ping_payload, int payload_len) {
// Lock SSL mutex to prevent concurrent SSL operations
pthread_mutex_lock(&ssl_mutex);
char frame[BUFFER_SIZE];
frame[0] = 0x8A; // FIN + pong opcode
int header_len = 2;
if (payload_len <= 125) {
frame[1] = 0x80 | payload_len; // MASK + length
} else if (payload_len <= 65535) {
frame[1] = 0x80 | 126; // MASK + extended length
frame[2] = (payload_len >> 8) & 0xFF;
frame[3] = payload_len & 0xFF;
header_len = 4;
} else {
frame[1] = 0x80 | 127; // MASK + extended length
frame[2] = 0;
frame[3] = 0;
frame[4] = 0;
frame[5] = 0;
frame[6] = (payload_len >> 24) & 0xFF;
frame[7] = (payload_len >> 16) & 0xFF;
frame[8] = (payload_len >> 8) & 0xFF;
frame[9] = payload_len & 0xFF;
header_len = 10;
}
// Add mask key
char mask_key[4];
for (int i = 0; i < 4; i++) {
mask_key[i] = rand() % 256;
frame[header_len + i] = mask_key[i];
}
header_len += 4;
// Mask payload
for (int i = 0; i < payload_len; i++) {
frame[header_len + i] = ping_payload[i] ^ mask_key[i % 4];
}
int frame_len = header_len + payload_len;
// Handle partial writes for large frames
int total_written = 0;
int retry_count = 0;
const int max_retries = 3;
while (total_written < frame_len && retry_count < max_retries) {
int to_write = frame_len - total_written;
// Limit to BUFFER_SIZE to avoid issues with very large frames
if (to_write > BUFFER_SIZE) {
to_write = BUFFER_SIZE;
}
int written = SSL_write(ssl, frame + total_written, to_write);
if (written <= 0) {
int ssl_error = SSL_get_error(ssl, written);
// Handle transient SSL errors with retry
if ((ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) && retry_count < max_retries - 1) {
retry_count++;
usleep(10000); // Wait 10ms before retry
continue; // Retry the write operation
}
fprintf(stderr, "Pong frame SSL_write failed: %d (after %d retries)\n", ssl_error, retry_count);
char error_buf[256];
ERR_error_string_n(ssl_error, error_buf, sizeof(error_buf));
fprintf(stderr, "SSL write error details: %s\n", error_buf);
pthread_mutex_unlock(&ssl_mutex);
return 0; // Write failed
}
total_written += written;
retry_count = 0; // Reset retry count on successful write
}
if (total_written < frame_len) {
fprintf(stderr, "Pong frame write incomplete: %d/%d bytes written\n", total_written, frame_len);
pthread_mutex_unlock(&ssl_mutex);
return 0;
}
pthread_mutex_unlock(&ssl_mutex);
return 1;
}
\ No newline at end of file
/*
* WSSSH Library - Control Channel Messages
*
* 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/>.
*/
#ifndef CONTROL_MESSAGES_H
#define CONTROL_MESSAGES_H
#include <openssl/ssl.h>
#include "tunnel.h"
// Function declarations for control channel messages
int send_json_message(SSL *ssl, const char *type, const char *client_id, const char *request_id);
int send_registration_message(SSL *ssl, const char *client_id, const char *password, const char *tunnel, const char *tunnel_control, const char *wssshd_private_ip);
int send_tunnel_request_message(SSL *ssl, const char *client_id, const char *request_id, const char *tunnel, const char *tunnel_control, const char *service);
int send_tunnel_close_message(SSL *ssl, const char *request_id, int debug);
int send_tunnel_keepalive_message(SSL *ssl, tunnel_t *tunnel, int debug);
int send_tunnel_keepalive_ack_message(SSL *ssl, const char *request_id, int debug);
int send_ping_frame(SSL *ssl, const char *ping_payload, int payload_len);
int send_pong_frame(SSL *ssl, const char *ping_payload, int payload_len);
#endif // CONTROL_MESSAGES_H
\ No newline at end of file
/*
* WSSSH Library - Data Channel Messages 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/>.
*/
#include "data_messages.h"
#include "websocket.h"
#include "tunnel.h"
#include <openssl/ssl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int send_tunnel_data_message(SSL *ssl, const char *request_id, const char *data_hex, int debug) {
// Send as tunnel_data
size_t msg_size = strlen("{\"type\":\"tunnel_data\",\"request_id\":\"\",\"data\":\"\"}") + strlen(request_id) + strlen(data_hex) + 1;
char *message = malloc(msg_size);
if (!message) {
if (debug) {
printf("[DEBUG] Failed to allocate memory for tunnel_data message (%zu bytes)\n", msg_size);
fflush(stdout);
}
return 0;
}
snprintf(message, msg_size,
"{\"type\":\"tunnel_data\",\"request_id\":\"%s\",\"data\":\"%s\"}",
request_id, data_hex);
if (!send_websocket_frame(ssl, message)) {
if (debug) {
printf("[DEBUG] Failed to send tunnel_data WebSocket frame\n");
fflush(stdout);
}
free(message);
return 0;
}
free(message);
return 1;
}
int send_tunnel_response_message(SSL *ssl, const char *request_id, const char *data_hex, int debug) {
// Send as tunnel_response (from target back to WebSocket)
size_t msg_size = strlen("{\"type\":\"tunnel_response\",\"request_id\":\"\",\"data\":\"\"}") + strlen(request_id) + strlen(data_hex) + 1;
char *message = malloc(msg_size);
if (!message) {
if (debug) {
printf("[DEBUG] Failed to allocate memory for tunnel_response message (%zu bytes)\n", msg_size);
fflush(stdout);
}
return 0;
}
snprintf(message, msg_size,
"{\"type\":\"tunnel_response\",\"request_id\":\"%s\",\"data\":\"%s\"}",
request_id, data_hex);
if (!send_websocket_frame(ssl, message)) {
if (debug) {
printf("[DEBUG] Failed to send tunnel_response WebSocket frame\n");
fflush(stdout);
}
free(message);
return 0;
}
free(message);
return 1;
}
\ No newline at end of file
/*
* WSSSH Library - Data Channel Messages
*
* 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/>.
*/
#ifndef DATA_MESSAGES_H
#define DATA_MESSAGES_H
#include <openssl/ssl.h>
// Function declarations for data channel messages
int send_tunnel_data_message(SSL *ssl, const char *request_id, const char *data_hex, int debug);
int send_tunnel_response_message(SSL *ssl, const char *request_id, const char *data_hex, int debug);
#endif // DATA_MESSAGES_H
\ No newline at end of file
...@@ -43,6 +43,9 @@ ...@@ -43,6 +43,9 @@
#include "control_messages.h" #include "control_messages.h"
#include "data_messages.h" #include "data_messages.h"
// External signal handling variables from wsssht.c
extern volatile sig_atomic_t graceful_shutdown;
int run_bridge_mode(wsssh_config_t *config, const char *client_id, const char *wssshd_host, int wssshd_port) { int run_bridge_mode(wsssh_config_t *config, const char *client_id, const char *wssshd_host, int wssshd_port) {
// Bridge mode: Pure transport layer - no tunnel setup, just WebSocket transport // Bridge mode: Pure transport layer - no tunnel setup, just WebSocket transport
if (config->debug) { if (config->debug) {
...@@ -813,6 +816,23 @@ int run_pipe_mode(wsssh_config_t *config, const char *client_id, const char *wss ...@@ -813,6 +816,23 @@ int run_pipe_mode(wsssh_config_t *config, const char *client_id, const char *wss
static int frame_buffer_used = 0; static int frame_buffer_used = 0;
while (1) { while (1) {
// Check for graceful shutdown signal
if (graceful_shutdown) {
fprintf(stderr, "Sending tunnel_close on graceful shutdown...\n");
if (config->debug) {
fprintf(stderr, "[DEBUG] Graceful shutdown requested, sending tunnel_close\n");
}
// Send tunnel_close before exiting
send_tunnel_close(ws_ssl, request_id, config->debug);
fprintf(stderr, "Tunnel close message sent\n");
// Small delay to allow parent process (scp) to finish cleanly
// This prevents "Broken pipe" errors from scp trying to write after we exit
usleep(200000); // 200ms delay
break;
}
// Check tunnel status // Check tunnel status
pthread_mutex_lock(&tunnel_mutex); pthread_mutex_lock(&tunnel_mutex);
int tunnel_active = pipe_tunnel && pipe_tunnel->active; int tunnel_active = pipe_tunnel && pipe_tunnel->active;
...@@ -842,11 +862,17 @@ int run_pipe_mode(wsssh_config_t *config, const char *client_id, const char *wss ...@@ -842,11 +862,17 @@ int run_pipe_mode(wsssh_config_t *config, const char *client_id, const char *wss
ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer)); ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer));
if (bytes_read <= 0) { if (bytes_read <= 0) {
// EOF or error on stdin // EOF or error on stdin
fprintf(stderr, "EOF detected on stdin, sending tunnel_close...\n");
if (config->debug) { if (config->debug) {
fprintf(stderr, "[DEBUG] EOF on stdin, sending tunnel_close\n"); fprintf(stderr, "[DEBUG] EOF on stdin, sending tunnel_close\n");
} }
// Send tunnel_close before exiting // Send tunnel_close before exiting
send_tunnel_close(ws_ssl, request_id, config->debug); send_tunnel_close(ws_ssl, request_id, config->debug);
fprintf(stderr, "Tunnel close message sent\n");
// Small delay to allow parent process (scp) to finish cleanly
usleep(500000); // 500ms delay
break; break;
} }
......
/* /*
* WebSocket SCP (wsscp) - C Implementation * WSSSH SCP (wsscp) - C Implementation
* SCP wrapper with WebSocket ProxyCommand support. * SCP Wrapper with wsssht ProxyCommand support.
* *
* Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me * Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
* *
......
...@@ -35,7 +35,7 @@ void print_wsssh_usage(const char *program_name) { ...@@ -35,7 +35,7 @@ void print_wsssh_usage(const char *program_name) {
fprintf(stderr, " --help Show this help\n"); fprintf(stderr, " --help Show this help\n");
fprintf(stderr, " --clientid ID Client ID of the registered wssshc endpoint\n"); fprintf(stderr, " --clientid ID Client ID of the registered wssshc endpoint\n");
fprintf(stderr, " --wssshd-host HOST wssshd relay host\n"); fprintf(stderr, " --wssshd-host HOST wssshd relay host\n");
fprintf(stderr, " --wssshd-port PORT wssshd relay websocket port (default: 9898)\n"); fprintf(stderr, " -p, --wssshd-port PORT wssshd relay websocket port (default: 9898)\n");
fprintf(stderr, " --debug Enable debug output\n"); fprintf(stderr, " --debug Enable debug output\n");
fprintf(stderr, " --tunnel TRANSPORT Select data channel transport (comma-separated or 'any')\n"); fprintf(stderr, " --tunnel TRANSPORT Select data channel transport (comma-separated or 'any')\n");
fprintf(stderr, " --tunnel-control TRANSPORT Select control channel transport (comma-separated or 'any')\n"); fprintf(stderr, " --tunnel-control TRANSPORT Select control channel transport (comma-separated or 'any')\n");
...@@ -69,7 +69,7 @@ int parse_wsssh_args(int argc, char *argv[], wsssh_config_t *config) { ...@@ -69,7 +69,7 @@ int parse_wsssh_args(int argc, char *argv[], wsssh_config_t *config) {
} else if (strcmp(argv[i], "--wssshd-host") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--wssshd-host") == 0 && i + 1 < argc) {
config->wssshd_host = strdup(argv[i + 1]); config->wssshd_host = strdup(argv[i + 1]);
i++; i++;
} else if (strcmp(argv[i], "--wssshd-port") == 0 && i + 1 < argc) { } else if ((strcmp(argv[i], "--wssshd-port") == 0 || strcmp(argv[i], "-p") == 0) && i + 1 < argc) {
config->wssshd_port = atoi(argv[i + 1]); config->wssshd_port = atoi(argv[i + 1]);
config->wssshd_port_explicit = 1; config->wssshd_port_explicit = 1;
i++; i++;
......
...@@ -45,20 +45,46 @@ ...@@ -45,20 +45,46 @@
volatile sig_atomic_t sigint_count = 0; volatile sig_atomic_t sigint_count = 0;
volatile sig_atomic_t graceful_shutdown = 0; volatile sig_atomic_t graceful_shutdown = 0;
volatile sig_atomic_t force_exit = 0;
volatile sig_atomic_t tunnel_close_sent = 0;
void sigint_handler(int sig __attribute__((unused))) { void signal_handler(int sig __attribute__((unused))) {
sigint_count++; sigint_count++;
if (sigint_count == 1) { if (sigint_count == 1) {
fprintf(stderr, "\nReceived SIGINT, attempting graceful shutdown...\n"); fprintf(stderr, "\nReceived signal %d, attempting graceful shutdown...\n", sig);
fflush(stderr); fflush(stderr);
graceful_shutdown = 1; graceful_shutdown = 1;
} else if (sigint_count >= 2) { } else if (sigint_count >= 2) {
fprintf(stderr, "\nReceived second SIGINT, exiting immediately...\n"); fprintf(stderr, "\nReceived second signal, exiting immediately...\n");
fflush(stderr); fflush(stderr);
force_exit = 1;
exit(1); exit(1);
} }
} }
void cleanup_on_exit(void) {
// Only attempt cleanup if we haven't been forced to exit and haven't already sent tunnel_close
if (!force_exit && !tunnel_close_sent) {
pthread_mutex_lock(&tunnel_mutex);
if (active_tunnel && active_tunnel->active && active_tunnel->ssl) {
fprintf(stderr, "Sending tunnel_close on exit...\n");
send_tunnel_close(active_tunnel->ssl, active_tunnel->request_id, 0); // debug=0 for cleaner output
tunnel_close_sent = 1;
// Wait up to 3 seconds for tunnel close to complete, but allow interruption
time_t shutdown_start = time(NULL);
while (time(NULL) - shutdown_start < 3 && !force_exit) {
if (!active_tunnel->active) {
fprintf(stderr, "Tunnel closed gracefully\n");
break;
}
usleep(100000); // Sleep 100ms before checking again
}
}
pthread_mutex_unlock(&tunnel_mutex);
}
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
// Read config from wsssht.conf // Read config from wsssht.conf
char *config_domain = read_config_value_from_file("wssshd-host", "wsssht"); char *config_domain = read_config_value_from_file("wssshd-host", "wsssht");
...@@ -165,8 +191,12 @@ int main(int argc, char *argv[]) { ...@@ -165,8 +191,12 @@ int main(int argc, char *argv[]) {
pthread_mutex_init(&tunnel_mutex, NULL); pthread_mutex_init(&tunnel_mutex, NULL);
pthread_mutex_init(&ssl_mutex, NULL); pthread_mutex_init(&ssl_mutex, NULL);
// Set up signal handler for SIGINT // Register cleanup function to run on exit
signal(SIGINT, sigint_handler); atexit(cleanup_on_exit);
// Set up signal handlers for SIGINT and SIGTERM
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// Parse wsssht arguments // Parse wsssht arguments
int remaining_argc; int remaining_argc;
...@@ -425,17 +455,25 @@ int main(int argc, char *argv[]) { ...@@ -425,17 +455,25 @@ int main(int argc, char *argv[]) {
fflush(stdout); fflush(stdout);
} }
send_tunnel_close(active_tunnel->ssl, active_tunnel->request_id, config.debug); send_tunnel_close(active_tunnel->ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
// Wait up to 3 seconds for tunnel close to complete // Wait up to 3 seconds for tunnel close to complete, but allow interruption by second signal
fprintf(stderr, "Sent close message for tunnel, waiting up to 3 seconds...\n"); fprintf(stderr, "Sent close message for tunnel, waiting up to 3 seconds...\n");
time_t shutdown_start = time(NULL); time_t shutdown_start = time(NULL);
while (time(NULL) - shutdown_start < 3) { while (time(NULL) - shutdown_start < 3 && !force_exit) {
if (!active_tunnel->active) { if (!active_tunnel->active) {
fprintf(stderr, "Tunnel closed gracefully\n"); fprintf(stderr, "Tunnel closed gracefully\n");
break; break;
} }
usleep(100000); // Sleep 100ms before checking again usleep(100000); // Sleep 100ms before checking again
} }
// If we were forced to exit, break immediately
if (force_exit) {
fprintf(stderr, "Forced exit during tunnel close wait\n");
pthread_mutex_unlock(&tunnel_mutex);
break;
}
} }
pthread_mutex_unlock(&tunnel_mutex); pthread_mutex_unlock(&tunnel_mutex);
break; break;
...@@ -475,6 +513,7 @@ int main(int argc, char *argv[]) { ...@@ -475,6 +513,7 @@ int main(int argc, char *argv[]) {
fflush(stdout); fflush(stdout);
} }
send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug); send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
pthread_mutex_unlock(&tunnel_mutex); pthread_mutex_unlock(&tunnel_mutex);
goto cleanup_and_exit; goto cleanup_and_exit;
} }
...@@ -491,6 +530,7 @@ int main(int argc, char *argv[]) { ...@@ -491,6 +530,7 @@ int main(int argc, char *argv[]) {
active_tunnel->broken = 1; active_tunnel->broken = 1;
// Send tunnel_close notification // Send tunnel_close notification
send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug); send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
pthread_mutex_unlock(&tunnel_mutex); pthread_mutex_unlock(&tunnel_mutex);
goto cleanup_and_exit; goto cleanup_and_exit;
} }
...@@ -516,6 +556,7 @@ int main(int argc, char *argv[]) { ...@@ -516,6 +556,7 @@ int main(int argc, char *argv[]) {
fflush(stdout); fflush(stdout);
} }
send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug); send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
goto cleanup_and_exit; goto cleanup_and_exit;
} else if (retval == 0) { } else if (retval == 0) {
// Timeout, check if tunnel became inactive // Timeout, check if tunnel became inactive
...@@ -681,6 +722,7 @@ int main(int argc, char *argv[]) { ...@@ -681,6 +722,7 @@ int main(int argc, char *argv[]) {
fflush(stdout); fflush(stdout);
} }
send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug); send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
goto cleanup_and_exit; goto cleanup_and_exit;
} }
...@@ -715,6 +757,7 @@ int main(int argc, char *argv[]) { ...@@ -715,6 +757,7 @@ int main(int argc, char *argv[]) {
fflush(stdout); fflush(stdout);
} }
send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug); send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
goto cleanup_and_exit; goto cleanup_and_exit;
} else if (frame_type == 0x89) { // Ping frame } else if (frame_type == 0x89) { // Ping frame
if (config.debug) { if (config.debug) {
......
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