Fix PyInstaller build issues and asyncio warnings

- Fix missing websockets import in wsssd/server.py causing 'name websockets is not defined' error
- Resolve asyncio runtime warnings by properly awaiting cancelled tasks in shutdown handling
- Fix global variable sharing issue in frozen application by passing server password as parameter to websocket handler
- Improve WebSocket handler signature compatibility with functools.partial for proper function binding
- Update CHANGELOG.md and TODO.md with version 1.4.9 changes
parent 6516a011
......@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4.9] - 2025-09-17
### Fixed
- **PyInstaller Build Issues**: Critical fixes for frozen application deployment
- Fixed missing `websockets` import in `wsssd/server.py` causing "name 'websockets' is not defined" error
- Resolved asyncio runtime warnings by properly awaiting cancelled tasks in shutdown handling
- Fixed global variable sharing issue in frozen application by passing server password as parameter to websocket handler
- Improved WebSocket handler signature compatibility with `functools.partial` for proper function binding
### Technical Details
- **Import Management**: Added explicit `websockets` import to server.py for PyInstaller compatibility
- **Async Task Cleanup**: Proper awaiting of cancelled asyncio tasks to prevent runtime warnings
- **Global Variable Isolation**: Resolved PyInstaller global variable sharing limitations by using parameter passing
- **Function Signature Compatibility**: Updated websocket handler to use keyword-only parameters for `functools.partial` binding
## [1.4.8] - 2025-09-17
### Fixed
......
......@@ -11,6 +11,7 @@
7. [Security](#security)
8. [Troubleshooting](#troubleshooting)
9. [Development](#development)
10. [Recent Updates](#recent-updates)
## Overview
......@@ -42,6 +43,9 @@ WebSocket SSH (wsssh) is a tunneling system that enables secure SSH/SCP access t
- **Real-time Resize Support**: Terminal resizes dynamically with browser window
- **Force Echo Mode**: SSH connections force echo mode for better visibility
- **Enhanced Logging**: Complete logrotate integration for daemon processes
- **SSL Connection Stability**: Advanced SSL error handling and connection resilience
- **Process Exit Handling**: Fixed critical process hanging issues
- **Code Architecture**: Modular design with shared libraries for better maintainability
- **Cross-platform**: Works on Linux, macOS, and Windows
- **Donation Support**: Community funding through PayPal and cryptocurrency
......@@ -339,11 +343,13 @@ id = client1
#### Command Line Options
```bash
wsssh [--local-port PORT] [--debug] user@client.domain [ssh_options...]
wsssh [--local-port PORT] [--debug] [--dev-tunnel] [--help] user@client.domain [ssh_options...]
```
- `--local-port PORT`: Local tunnel port (default: auto-assign)
- `--debug`: Enable debug output
- `--dev-tunnel`: Development mode: setup tunnel but don't launch SSH
- `--help`: Display help message and exit
#### Environment Variables
......@@ -354,11 +360,13 @@ wsssh [--local-port PORT] [--debug] user@client.domain [ssh_options...]
#### Command Line Options
```bash
wsscp [--local-port PORT] [--debug] [SCP_OPTIONS...] SOURCE... DESTINATION
wsscp [--local-port PORT] [--debug] [--dev-tunnel] [--help] [SCP_OPTIONS...] SOURCE... DESTINATION
```
- `--local-port PORT`: Local tunnel port (default: auto-assign)
- `--debug`: Enable debug output
- `--dev-tunnel`: Development mode: setup tunnel but don't launch SCP
- `--help`: Display help message and exit
### C Implementation Tools
......@@ -695,8 +703,14 @@ Enable debug output for detailed troubleshooting:
# wsssh debug
./wsssh --debug user@client.example.com -p 9898
# wsssh dev-tunnel mode (setup tunnel without launching SSH)
./wsssh --dev-tunnel user@client.example.com -p 9898
# wsscp debug
./wsscp --debug file user@client.example.com:/path/ -P 9898
# wsscp dev-tunnel mode (setup tunnel without launching SCP)
./wsscp --dev-tunnel file user@client.example.com:/path/ -P 9898
```
### Log Analysis
......@@ -723,9 +737,9 @@ wsssh/
├── templates/ # Flask web templates
├── static/ # Web assets
├── wssshtools/ # C implementation directory
│ ├── wssshc.c # C client (280 lines)
│ ├── wsssh.c # C SSH wrapper (378 lines)
│ ├── wsscp.c # C SCP wrapper (418 lines)
│ ├── wssshc.c # C client for registration
│ ├── wsssh.c # C SSH wrapper
│ ├── wsscp.c # C SCP wrapper
│ ├── wssshlib.h # Shared utilities library header
│ ├── wssshlib.c # Shared utilities library implementation
│ ├── websocket.h # WebSocket functions library header
......@@ -736,13 +750,14 @@ wsssh/
│ ├── tunnel.c # Tunnel management library implementation
│ ├── configure.sh # Build configuration
│ ├── Makefile # GNU Make build system
│ ├── common.c # Common utilities
│ └── debian/ # Debian packaging for wsssh-tools
│ ├── control # Package metadata
│ ├── rules # Build rules
│ ├── changelog # Package changelog
│ ├── copyright # Copyright info
│ └── compat # Debhelper compatibility
├── wsssh-server/ # wsssh-server Debian package (v1.4.0)
├── wsssh-server/ # wsssh-server Debian package
│ ├── debian/ # Debian packaging for wssshd daemon
│ │ ├── control # Package metadata
│ │ ├── rules # Build rules with PyInstaller
......@@ -754,6 +769,7 @@ wsssh/
│ │ ├── postrm # Post-removal script
│ │ └── wssshd.default # Service defaults
│ └── PyInstaller/ # PyInstaller spec files
├── wssshc.conf.example # Example wssshc configuration file
├── .gitignore # Git ignore rules
├── LICENSE.md # GPLv3 license
├── README.md # Main documentation
......@@ -909,6 +925,66 @@ python3 -m pytest tests/integration/
---
## Recent Updates
### Version 1.4.8 (Latest)
- **Critical SSL Connection Stability**: Comprehensive SSL error handling and connection resilience improvements
- Fixed WebSocket frame sending failures that caused connection drops
- Added detailed SSL error reporting with specific error codes and descriptions
- Implemented automatic retry logic for transient SSL errors (SSL_ERROR_WANT_READ/WRITE)
- Added 5-second timeout protection for SSL read operations to prevent indefinite hangs
- Enhanced connection state validation before SSL operations
- Improved WebSocket frame transmission with retry mechanisms and partial write handling
### Version 1.4.7
- **Critical Process Exit Bug Fix**: Fixed wsssh process hanging after SSH client disconnection
- Added `broken` flag to tunnel structure to distinguish between normal closure and broken connections
- Implemented proper tunnel state tracking for EBADF, EPIPE, and ECONNRESET errors
- Enhanced error handling in `handle_tunnel_data()` for SSH client disconnections
- Fixed main loop to immediately kill SSH child process and exit when tunnel breaks
- Added proper exit code handling: 0 for normal termination, 1 for error conditions
### Version 1.4.6
- **Code Architecture Overhaul**: Major refactoring to eliminate code duplication
- Created shared libraries: `wssshlib.h/.c`, `websocket.h/.c`, `wssh_ssl.h/.c`, `tunnel.h/.c`
- Extracted ~1500+ lines of duplicate code between wsssh.c and wssshc.c
- Improved maintainability and code organization with modular architecture
- Enhanced build system with proper library dependencies
- **SSH Tunneling Enhancements**: Enhanced SSH tunnel handling and error recovery
- Added specific handling for EBADF (Bad file descriptor) errors
- Improved error handling in handle_tunnel_data for SSH client disconnections
- Removed aggressive socket validation causing SSH client disconnections
- Fixed SSH client socket invalidation issue in handle_tunnel_data
- Added missing SSH client accept() logic in wsssh socket handling
- Fixed SSH tunneling timing issue with proper data buffering
- **Port Option Behavior Correction**: Fixed critical port option behavior
- `-p`/`-P` options now correctly specify wssshd server port (not SSH/SCP server port)
- Removed misleading "passed through to ssh/scp" documentation
- Updated help text to clearly indicate port options are consumed by wsssh/wsscp
- Fixed argument parsing to properly handle wssshd server port specification
### Version 1.4.5
- **Python Implementation Removal**: Removed Python implementations of wssshc, wsssh, and wsscp
- Deleted wssshc.py, wsssh.py, and wsscp.py files
- Removed pyinstaller commands from build.sh for these tools
- Updated Debian packaging to exclude Python script installations
- Maintained C implementations in wssshtools/ directory
### Version 1.4.4
- **Advanced Logging with Logrotate**: Comprehensive logging system for all daemons
- Added `/var/log/wssshd/wssshd.log` for main daemon logging with automatic rotation
- Added `/var/log/wssshc/wssshc.log` for client daemon logging with automatic rotation
- Configured logrotate with weekly rotation, 52-week retention, and automatic compression
- Proper file permissions and ownership for security
- **Dynamic Terminal Sizing**: Web terminal now properly calculates and uses actual browser window dimensions
- **Terminal Resize Support**: Real-time terminal resizing when browser window is resized
- **Force Echo Mode**: SSH connections now force echo mode for better visibility
---
## Donations
If you find WebSocket SSH useful, please consider supporting the project development:
......
......@@ -316,6 +316,8 @@ Command line options override configuration file values. Required parameters are
**Options:**
- `--local-port`: Local tunnel port (default: auto)
- `--debug`: Enable debug output
- `--dev-tunnel`: Development mode: setup tunnel but don't launch SSH
- `--help`: Display help message and exit
**Examples:**
```bash
......@@ -341,6 +343,8 @@ Command line options override configuration file values. Required parameters are
**Options:**
- `--local-port`: Local tunnel port (default: auto)
- `--debug`: Enable debug output
- `--dev-tunnel`: Development mode: setup tunnel but don't launch SCP
- `--help`: Display help message and exit
**Examples:**
```bash
......@@ -463,6 +467,9 @@ Enable debug output for detailed troubleshooting:
./wssshd --debug --host 0.0.0.0 --port 9898 --domain example.com --password mysecret
./wssshc --debug --server-ip <ip> --port 9898 --id client1 --password mysecret
./wsssh --debug -p 9898 user@client.domain
./wsssh --dev-tunnel -p 9898 user@client.domain # Setup tunnel without launching SSH
./wsscp --debug -P 9898 file user@client.domain:/path/
./wsscp --dev-tunnel -P 9898 file user@client.domain:/path/ # Setup tunnel without launching SCP
```
### Common Issues
......@@ -494,7 +501,7 @@ python3 -m pytest
```
wsssh/
├── wssshd.py # WebSocket SSH Daemon with web interface
├── build.sh # Build script (supports --debian flag)
├── build.sh # Build script (supports --debian, --server-only)
├── clean.sh # Clean script
├── requirements.txt # Python dependencies
├── cert.pem # SSL certificate (auto-generated)
......@@ -518,6 +525,7 @@ wsssh/
│ ├── wssh_ssl.c # SSL functions library implementation
│ ├── tunnel.h # Tunnel management library header
│ ├── tunnel.c # Tunnel management library implementation
│ ├── common.c # Common utilities
│ ├── configure.sh # C build configuration
│ ├── Makefile # C build system
│ └── debian/ # Debian packaging
......@@ -526,6 +534,19 @@ wsssh/
│ ├── changelog # Package changelog
│ ├── copyright # Copyright info
│ └── compat # Debhelper compatibility
├── wsssh-server/ # wsssh-server Debian package
│ ├── debian/ # Debian packaging for wssshd daemon
│ │ ├── control # Package metadata
│ │ ├── rules # Build rules with PyInstaller
│ │ ├── changelog # Package changelog
│ │ ├── copyright # Copyright info
│ │ ├── compat # Debhelper compatibility
│ │ ├── wssshd.1 # Man page for wssshd
│ │ ├── postinst # Post-installation script
│ │ ├── postrm # Post-removal script
│ │ └── wssshd.default # Service defaults
│ └── PyInstaller/ # PyInstaller spec files
├── wssshc.conf.example # Example wssshc configuration file
├── .gitignore # Git ignore rules
├── LICENSE.md # GPLv3 license
├── README.md # This file
......@@ -597,7 +618,17 @@ Your support helps us continue developing and maintaining this open-source proje
## Changelog
### Version 1.4.7 (Latest)
### Version 1.4.8 (Latest)
**Critical SSL Connection Stability:**
- Fixed WebSocket frame sending failures that caused connection drops
- Added detailed SSL error reporting with specific error codes and descriptions
- Implemented automatic retry logic for transient SSL errors (SSL_ERROR_WANT_READ/WRITE)
- Added 5-second timeout protection for SSL read operations to prevent indefinite hangs
- Enhanced connection state validation before SSL operations
- Improved WebSocket frame transmission with retry mechanisms and partial write handling
### Version 1.4.7
**Critical Bug Fix:**
- **Process Exit Issue Resolution**: Fixed wsssh process hanging after SSH client disconnection
......
# WebSocket SSH - Future Enhancements Roadmap
## Recently Completed (v1.4.9)
- [x] **PyInstaller Build Issues**: Critical fixes for frozen application deployment
- Fixed missing `websockets` import in `wsssd/server.py` causing "name 'websockets' is not defined" error
- Resolved asyncio runtime warnings by properly awaiting cancelled tasks in shutdown handling
- Fixed global variable sharing issue in frozen application by passing server password as parameter to websocket handler
- Improved WebSocket handler signature compatibility with `functools.partial` for proper function binding
## Recently Completed (v1.4.8)
- [x] **Critical SSL Connection Stability Issues**: Comprehensive SSL error handling and connection resilience improvements
- Fixed WebSocket frame sending failures that caused connection drops
......@@ -10,6 +17,14 @@
- Improved WebSocket frame transmission with retry mechanisms and partial write handling
- Applied improvements to all three C tools (wssshc, wsssh, wsscp)
- [x] **Documentation Updates**: Updated CHANGELOG.md, README.md, DOCUMENTATION.md, and TODO.md for version 1.4.8
- [x] **Comprehensive Documentation Review**: Carefully reviewed all documentation files and updated with current features
- Updated version references from 1.4.0 to 1.4.8 throughout documentation
- Added documentation for new --dev-tunnel option in wsssh and wsscp
- Added documentation for --help option in all tools
- Updated project structure to reflect current codebase
- Added missing configuration file references
- Updated debug mode examples with new options
- Enhanced feature descriptions with recent improvements
## Recently Completed (v1.4.7)
- [x] **Critical Process Exit Bug Fix**: Fixed wsssh process hanging after SSH client disconnection
......
......@@ -97,6 +97,8 @@ if [ "$BUILD_CLEAN" = true ]; then
rm -rf dist/
rm -f *.spec
rm -f wssshd # Remove PyInstaller binary
rm -f wsssd/__pycache__/*.pyc 2>/dev/null || true
rm -f wsssd/__pycache__ 2>/dev/null || true
# Remove virtual environment (unless --novenv is specified)
if [ "$BUILD_NO_VENV" = false ]; then
......@@ -226,7 +228,7 @@ if [ "$BUILD_DEBIAN_ONLY" = false ] && [ "$BUILD_WSSSHTOOLS_ONLY" = false ]; the
# Build wssshd (server) binary unless --no-server is specified
if [ "$BUILD_NO_SERVER" = false ]; then
pyinstaller --onefile --distpath dist --add-data "cert.pem:." --add-data "key.pem:." --add-data "templates:templates" --add-data "logos:logos" --runtime-tmpdir /tmp --clean wssshd.py
pyinstaller --onefile --distpath dist --add-data "cert.pem:." --add-data "key.pem:." --add-data "templates:templates" --add-data "logos:logos" --add-data "wsssd:wsssd" --hidden-import wsssd --hidden-import wsssd.config --hidden-import wsssd.websocket --hidden-import wsssd.terminal --hidden-import wsssd.web --hidden-import wsssd.server --hidden-import websockets --hidden-import websockets.server --hidden-import websockets.client --hidden-import websockets.exceptions --hidden-import websockets.protocol --hidden-import websockets.uri --runtime-tmpdir /tmp --clean wssshd.py
fi
# Build client binaries
......
#!/bin/bash
# WebSocket SSH Tools Clean Script
# Clean script for removing build artifacts from WebSocket SSH tools
#
# 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/>.
# Use build.sh --clean for consistent cleaning
./build.sh --clean
\ No newline at end of file
"""
WebSocket SSH Daemon (wssshd) - Modular implementation
"""
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
"""
from .server import main
if __name__ == '__main__':
main()
\ No newline at end of file
"""
Configuration handling for wssshd
"""
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='WebSocket SSH 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
"""
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
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 WebSocket SSH 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:
try:
await asyncio.wait_for(
asyncio.gather(*notify_tasks, return_exceptions=True),
timeout=0.3
)
print("All clients notified")
except asyncio.TimeoutError:
print("Timeout waiting for client notifications, proceeding with shutdown")
except Exception as e:
if debug: print(f"[DEBUG] Error during client notifications: {e}")
# Give clients a brief moment to process the shutdown message
await asyncio.sleep(0.1)
# Close WebSocket server
try:
ws_server.close()
await ws_server.wait_closed()
except Exception as e:
if debug: print(f"[DEBUG] Error closing WebSocket server: {e}")
# Cancel cleanup task
if not cleanup_coro.done():
cleanup_coro.cancel()
try:
await cleanup_coro
except asyncio.CancelledError:
pass
# 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
await asyncio.sleep(0.3)
# Force kill remaining processes
kill_tasks = []
for request_id, proc in term_procs:
if proc.poll() is None:
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:
try:
await asyncio.wait_for(
asyncio.gather(*kill_tasks, return_exceptions=True),
timeout=0.2
)
except asyncio.TimeoutError:
if debug: print("[DEBUG] Some processes still running after SIGKILL")
except Exception as e:
if debug: print(f"[DEBUG] Error during process cleanup: {e}")
# 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 active tunnels (optimized)
print("Cleaning up active tunnels...")
if active_tunnels:
# Create close tasks for all active tunnels
close_tasks = []
for request_id, tunnel in active_tunnels.items():
client_info = clients.get(tunnel['client_id'])
if client_info and client_info['status'] == 'active':
try:
close_task = asyncio.create_task(
tunnel['client_ws'].send(SERVER_SHUTDOWN_MSG)
)
close_tasks.append((request_id, close_task))
except Exception as e:
if debug: print(f"[DEBUG] Failed to create close task for {request_id}: {e}")
# Wait for all close tasks with timeout
if close_tasks:
try:
await asyncio.wait_for(
asyncio.gather(*[task for _, task in close_tasks], return_exceptions=True),
timeout=0.2
)
if debug: print(f"[DEBUG] Sent shutdown to {len(close_tasks)} clients")
except asyncio.TimeoutError:
if debug: print("[DEBUG] Timeout waiting for tunnel close notifications")
except Exception as e:
if debug: print(f"[DEBUG] Error during tunnel close notifications: {e}")
# Clean up all tunnels
active_tunnels.clear()
if debug: print(f"[DEBUG] Cleaned up {len(close_tasks)} 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():
flask_thread.join(timeout=1.0)
print("WebSocket SSH Daemon stopped cleanly")
async def run_server():
"""Main server function"""
args = load_config()
# Set global variables
global debug, server_password, shutdown_event
debug = args.debug
server_password = args.password
# Initialize shutdown event
shutdown_event = asyncio.Event()
# Set up signal handling for clean exit
shutdown_event.clear()
def signal_handler(signum, frame):
if debug: print(f"[DEBUG] Received signal {signum}, initiating 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), args.host, args.port, ssl=ssl_context)
print(f"WebSocket SSH 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())
# 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 KeyboardInterrupt:
print("\nServer interrupted by user")
except Exception as e:
print(f"Server error: {e}")
sys.exit(1)
\ No newline at end of file
"""
Terminal and PTY handling for wssshd
"""
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}']
# 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
"""
Flask web interface for wssshd
"""
import os
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__)
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):
if client_id not in clients:
flash('Client not connected')
return redirect(url_for('index'))
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('/logos/<path:filename>')
def logos_files(filename):
return send_from_directory('logos', filename)
@app.route('/terminal/<client_id>/connect', methods=['POST'])
@login_required
def connect_terminal(client_id):
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))
terminal_session = create_terminal_session(args, username, client_id)
request_id = terminal_session['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']
}
return jsonify({
'request_id': request_id,
'command': terminal_session['command']
})
@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
"""
import asyncio
import json
import time
import websockets
from .terminal import openpty_with_fallback
# 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"}'
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: print(f"[DEBUG] [WebSocket] Client {client_id} expired and removed")
for client_id in expired_clients:
del clients[client_id]
def print_status():
"""Print minimal status information when not in debug mode"""
if debug:
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 = 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} active")
async def handle_websocket(websocket, path=None, *, server_password=None):
"""Handle WebSocket connections from clients"""
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: 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)
# Only log debug info for non-data messages to reduce overhead
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] {msg_type} message received")
except json.JSONDecodeError as e:
if debug: 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', '')
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: print(f"[DEBUG] [WebSocket] Client {client_id} reconnecting (was disconnected)")
clients[client_id] = {
'websocket': websocket,
'last_seen': time.time(),
'status': 'active'
}
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:
await websocket.send(REGISTERED_MSG % client_id)
except Exception:
if debug: print(f"[DEBUG] [WebSocket] Failed to send registration response to {client_id}")
else:
if debug: print(f"[DEBUG] [WebSocket] Client {client_id} registration failed: invalid password")
try:
await websocket.send(REGISTRATION_ERROR_MSG % "Invalid password")
except Exception:
if debug: 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']
client_info = clients.get(client_id)
if client_info and client_info['status'] == 'active':
# Store tunnel mapping with optimized structure
active_tunnels[request_id] = {
'client_ws': client_info['websocket'],
'wsssh_ws': websocket,
'client_id': client_id
}
# Forward tunnel request to client
try:
await client_info['websocket'].send(TUNNEL_REQUEST_MSG % request_id)
await websocket.send(TUNNEL_ACK_MSG % request_id)
if not debug:
print(f"[EVENT] New tunnel {request_id} for client {client_id}")
except Exception:
# Send error response for tunnel request failures
try:
await websocket.send(TUNNEL_ERROR_MSG % (request_id, "Failed to forward request"))
except Exception:
pass # Silent failure if even error response fails
else:
try:
await websocket.send(TUNNEL_ERROR_MSG % (request_id, "Client not registered or disconnected"))
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 client status first (faster lookup)
client_info = clients.get(tunnel['client_id'])
if client_info and client_info['status'] == 'active':
# 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:
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:
# Forward close to client if still active
client_info = clients.get(tunnel['client_id'])
if client_info and client_info['status'] == 'active':
try:
await tunnel['client_ws'].send(TUNNEL_CLOSE_MSG % request_id)
except Exception:
# Silent failure for performance
pass
# Clean up tunnel
del active_tunnels[request_id]
if debug:
print(f"[DEBUG] [WebSocket] Tunnel {request_id} closed")
else:
print(f"[EVENT] Tunnel {request_id} closed")
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: 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:
del active_tunnels[request_id]
if debug: print(f"[DEBUG] [WebSocket] Tunnel {request_id} cleaned up due to client disconnect")
async def cleanup_task():
"""Periodic task to clean up expired clients and report status"""
last_status_time = 0
while True:
# Use moderate sleep intervals for cleanup
await asyncio.sleep(5) # Run every 5 seconds
cleanup_expired_clients()
# 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
......@@ -19,926 +19,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import argparse
import asyncio
import ssl
# Explicit imports to ensure PyInstaller includes them
import websockets
import json
import sys
import os
import threading
import uuid
import subprocess
import pty
import select
import fcntl
import termios
import stat
import configparser
import signal
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
import websockets.server
import websockets.client
import websockets.exceptions
import websockets.protocol
import websockets.uri
# 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"}'
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
server_password = None
args = None
shutdown_event = None
import time
start_time = time.time()
# Flask app for web interface
app = Flask(__name__)
app.config['SECRET_KEY'] = 'wsssh-secret-key-change-in-production'
config_dir = os.path.expanduser('~/.config/wssshd')
os.makedirs(config_dir, exist_ok=True)
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{config_dir}/users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), unique=True, nullable=False)
password_hash = db.Column(db.String(150), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
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: print(f"[DEBUG] [WebSocket] Client {client_id} expired and removed")
for client_id in expired_clients:
del clients[client_id]
def print_status():
"""Print minimal status information when not in debug mode"""
if debug:
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 = 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} active")
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 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 debug:
print(f"[DEBUG] Successfully opened PTY using {ptmx_path}: master={master}, slave={slave}")
return master, slave
except (OSError, AttributeError) as e:
if 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 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 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")
# 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():
global args
# 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 = User.query.all()
return render_template('users.html', users=users)
@app.route('/add_user', methods=['POST'])
@login_required
def add_user():
if not current_user.is_admin:
return jsonify({'error': 'Access denied'}), 403
username = request.form.get('username')
password = request.form.get('password')
is_admin = request.form.get('is_admin') == 'on'
if User.query.filter_by(username=username).first():
return jsonify({'error': 'Username already exists'}), 400
user = User(username=username, password_hash=generate_password_hash(password), is_admin=is_admin)
db.session.add(user)
db.session.commit()
return jsonify({'success': True})
@app.route('/edit_user/<int:user_id>', methods=['POST'])
@login_required
def edit_user(user_id):
if not current_user.is_admin:
return jsonify({'error': 'Access denied'}), 403
user = User.query.get_or_404(user_id)
user.username = request.form.get('username')
user.is_admin = request.form.get('is_admin') == 'on'
password = request.form.get('password')
if password:
user.password_hash = generate_password_hash(password)
db.session.commit()
return jsonify({'success': True})
@app.route('/delete_user/<int:user_id>', methods=['POST'])
@login_required
def delete_user(user_id):
if not current_user.is_admin:
return jsonify({'error': 'Access denied'}), 403
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return jsonify({'success': True})
@app.route('/terminal/<client_id>')
@login_required
def terminal(client_id):
if client_id not in clients:
flash('Client not connected')
return redirect(url_for('index'))
return render_template('terminal.html', client_id=client_id)
@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('/logos/<path:filename>')
def logos_files(filename):
return send_from_directory('logos', filename)
@app.route('/terminal/<client_id>/connect', methods=['POST'])
@login_required
def connect_terminal(client_id):
username = request.form.get('username', 'root')
cols = int(request.form.get('cols', 80))
rows = int(request.form.get('rows', 24))
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}']
# 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
import struct
winsize = struct.pack('HHHH', rows, cols, 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=str(cols), LINES=str(rows))
)
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()
# Process status will be checked in the reading thread
active_terminals[request_id] = {'client_id': client_id, 'username': username, 'proc': proc, 'output_buffer': output_buffer, 'master': master}
# Show the actual wsssh command being executed
actual_command = f'wsssh -p {args.port} {username}@{client_id}.{args.domain}'
return jsonify({'request_id': request_id, 'command': actual_command})
@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:
proc = active_terminals[request_id]['proc']
master = active_terminals[request_id]['master']
if proc.poll() is None: # Process is still running
try:
os.write(master, data.encode())
except:
pass
return 'OK'
else:
request_id = request.args.get('request_id')
if request_id in active_terminals:
proc = active_terminals[request_id]['proc']
output_buffer = active_terminals[request_id]['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 ''
@app.route('/terminal/<client_id>/disconnect', methods=['POST'])
@login_required
def disconnect_terminal(client_id):
request_id = request.form.get('request_id')
if request_id in active_terminals:
proc = active_terminals[request_id]['proc']
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except:
proc.kill()
del active_terminals[request_id]
return 'OK'
@app.route('/terminal/<client_id>/resize', methods=['POST'])
@login_required
def resize_terminal(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:
proc = active_terminals[request_id]['proc']
master = active_terminals[request_id]['master']
if proc.poll() is None:
# Update terminal size
import struct
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 'OK'
async def handle_websocket(websocket, path=None):
global shutdown_event
try:
while True:
# Check for shutdown signal before each message
if shutdown_event and shutdown_event.is_set():
if debug: print("[DEBUG] Shutdown event detected in WebSocket handler")
break
try:
# Use wait_for with timeout to allow shutdown checking
message = await asyncio.wait_for(websocket.recv(), timeout=0.05)
except asyncio.TimeoutError:
# Timeout occurred, check shutdown again and continue
continue
except websockets.exceptions.ConnectionClosed:
# Connection closed normally
break
# Process the message (rest of the original logic)
# Only log debug info for non-data messages to reduce overhead
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] {msg_type} message received")
except json.JSONDecodeError as e:
if debug: 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', '')
print(f"[DEBUG] [WebSocket] Processing registration for client {client_id}")
print(f"[DEBUG] [WebSocket] Received password: '{client_password}', expected: '{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
print(f"[DEBUG] [WebSocket] Client {client_id} reconnecting (was disconnected)")
clients[client_id] = {
'websocket': websocket,
'last_seen': time.time(),
'status': 'active'
}
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:
await websocket.send(REGISTERED_MSG % client_id)
except Exception:
if debug: print(f"[DEBUG] [WebSocket] Failed to send registration response to {client_id}")
else:
print(f"[DEBUG] [WebSocket] Client {client_id} registration failed: invalid password")
try:
await websocket.send(REGISTRATION_ERROR_MSG % "Invalid password")
except Exception:
if debug: 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']
client_info = clients.get(client_id)
if client_info and client_info['status'] == 'active':
# Store tunnel mapping with optimized structure
active_tunnels[request_id] = {
'client_ws': client_info['websocket'],
'wsssh_ws': websocket,
'client_id': client_id
}
# Forward tunnel request to client
try:
await client_info['websocket'].send(TUNNEL_REQUEST_MSG % request_id)
await websocket.send(TUNNEL_ACK_MSG % request_id)
if not debug:
print(f"[EVENT] New tunnel {request_id} for client {client_id}")
except Exception:
# Send error response for tunnel request failures
try:
await websocket.send(TUNNEL_ERROR_MSG % (request_id, "Failed to forward request"))
except Exception:
pass # Silent failure if even error response fails
else:
try:
await websocket.send(TUNNEL_ERROR_MSG % (request_id, "Client not registered or disconnected"))
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 client status first (faster lookup)
client_info = clients.get(tunnel['client_id'])
if client_info and client_info['status'] == 'active':
# 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:
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:
# Forward close to client if still active
client_info = clients.get(tunnel['client_id'])
if client_info and client_info['status'] == 'active':
try:
await tunnel['client_ws'].send(TUNNEL_CLOSE_MSG % request_id)
except Exception:
# Silent failure for performance
pass
# Clean up tunnel
del active_tunnels[request_id]
if debug:
print(f"[DEBUG] [WebSocket] Tunnel {request_id} closed")
else:
print(f"[EVENT] Tunnel {request_id} closed")
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()
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:
del active_tunnels[request_id]
if debug: print(f"[DEBUG] [WebSocket] Tunnel {request_id} cleaned up due to client disconnect")
async def cleanup_task():
"""Periodic task to clean up expired clients and report status"""
last_status_time = 0
while True:
# Use shorter sleep intervals for more responsive signal handling
await asyncio.sleep(1) # Run every 1 second instead of 10
cleanup_expired_clients()
# Print status every 60 seconds (60 iterations)
current_time = time.time()
if current_time - last_status_time >= 60:
print_status()
last_status_time = current_time
async def main():
parser = argparse.ArgumentParser(description='WebSocket SSH 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)
global args
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 sys.argv):
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')
global debug
debug = args.debug
global server_password
server_password = args.password
# Set up signal handling for clean exit
global shutdown_event
shutdown_event = asyncio.Event()
def signal_handler(signum, frame):
if debug: print(f"[DEBUG] Received signal {signum}, initiating shutdown")
print(f"[DEBUG] Signal handler called, setting shutdown event")
shutdown_event.set()
# Register signal handler for SIGINT (Ctrl+C)
signal.signal(signal.SIGINT, signal_handler)
# Keep signal handling simple and effective
# The existing signal handler is sufficient for our needs
# Load certificate
if getattr(sys, 'frozen', False):
# Running as bundled executable
bundle_dir = sys._MEIPASS
cert_path = os.path.join(bundle_dir, 'cert.pem')
key_path = os.path.join(bundle_dir, 'key.pem')
else:
# Running as script
cert_path = 'cert.pem'
key_path = 'key.pem'
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(cert_path, key_path)
# Start WebSocket server
ws_server = await websockets.serve(handle_websocket, args.host, args.port, ssl=ssl_context)
print(f"WebSocket SSH Daemon running on {args.host}:{args.port}")
print("Press Ctrl+C to stop the server")
# Start cleanup task
cleanup_coro = asyncio.create_task(cleanup_task())
# Start web interface if specified
flask_thread = None
if args.web_host and args.web_port:
# Handle HTTPS setup
ssl_context = None
protocol = "http"
if args.web_https:
protocol = "https"
# Use config directory for web certificates
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)
def run_flask():
app.run(host=args.web_host, port=args.web_port, debug=debug, use_reloader=False, threaded=True)
flask_thread = threading.Thread(target=run_flask, daemon=True)
flask_thread.start()
print(f"Web interface available at {protocol}://{args.web_host}:{args.web_port}")
try:
# Create tasks for waiting
server_wait_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 with periodic checks
while True:
done, pending = await asyncio.wait(
[server_wait_task, shutdown_wait_task],
return_when=asyncio.FIRST_COMPLETED,
timeout=0.1 # Check every 100ms for more responsive shutdown
)
# If shutdown event is set, break immediately
if shutdown_event.is_set():
if debug: print("[DEBUG] Shutdown event detected in main loop")
break
# If server closed naturally, break
if server_wait_task in done:
if debug: print("[DEBUG] WebSocket server closed naturally")
break
# If timeout occurred, continue checking
if not done:
continue
# Cancel pending tasks
for task in pending:
task.cancel()
print("\nShutting down WebSocket SSH 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:
try:
await asyncio.wait_for(
asyncio.gather(*notify_tasks, return_exceptions=True),
timeout=0.3
)
print("All clients notified")
except asyncio.TimeoutError:
print("Timeout waiting for client notifications, proceeding with shutdown")
except Exception as e:
if debug: print(f"[DEBUG] Error during client notifications: {e}")
# Give clients a brief moment to process the shutdown message
await asyncio.sleep(0.1)
# Close WebSocket server
try:
ws_server.close()
await ws_server.wait_closed()
except Exception as e:
if debug: print(f"[DEBUG] Error closing WebSocket server: {e}")
# Cancel cleanup task
if not cleanup_coro.done():
cleanup_coro.cancel()
try:
await cleanup_coro
except asyncio.CancelledError:
pass
# Signal handling is managed by the signal module, no asyncio task to cancel
# 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
await asyncio.sleep(0.3)
# Force kill remaining processes
kill_tasks = []
for request_id, proc in term_procs:
if proc.poll() is None:
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:
try:
await asyncio.wait_for(
asyncio.gather(*kill_tasks, return_exceptions=True),
timeout=0.2
)
except asyncio.TimeoutError:
if debug: print("[DEBUG] Some processes still running after SIGKILL")
except Exception as e:
if debug: print(f"[DEBUG] Error during process cleanup: {e}")
# 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 active tunnels (optimized)
print("Cleaning up active tunnels...")
if active_tunnels:
# Create close tasks for all active tunnels
close_tasks = []
for request_id, tunnel in active_tunnels.items():
client_info = clients.get(tunnel['client_id'])
if client_info and client_info['status'] == 'active':
try:
close_task = asyncio.create_task(
tunnel['client_ws'].send(TUNNEL_CLOSE_MSG % request_id)
)
close_tasks.append((request_id, close_task))
except Exception as e:
if debug: print(f"[DEBUG] Failed to create close task for {request_id}: {e}")
# Wait for all close tasks with timeout
if close_tasks:
try:
await asyncio.wait_for(
asyncio.gather(*[task for _, task in close_tasks], return_exceptions=True),
timeout=0.2
)
if debug: print(f"[DEBUG] Sent tunnel_close to {len(close_tasks)} clients")
except asyncio.TimeoutError:
if debug: print("[DEBUG] Timeout waiting for tunnel close notifications")
except Exception as e:
if debug: print(f"[DEBUG] Error during tunnel close notifications: {e}")
# Clean up all tunnels
active_tunnels.clear()
if debug: print(f"[DEBUG] Cleaned up {len(close_tasks)} 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")
print("WebSocket SSH Daemon stopped cleanly")
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()
from wsssd import main
if __name__ == '__main__':
asyncio.run(main())
\ No newline at end of file
main()
\ No newline at end of file
nextime@sissy:~/wsssh$ wssshtools/wsscp --debug /home/nextime/vr_gloryhole.mp4 root@zeiss:
[DEBUG] Found SCP destination: root@zeiss:
[DEBUG - Tunnel] SCP Destination: root@zeiss:
[DEBUG - Tunnel] Client ID: zeiss
[DEBUG - Tunnel] WSSSHD Host: mbetter.nexlab.net
[DEBUG - Tunnel] WSSSHD Port: 9898
[DEBUG - Tunnel] Using local port: 44081
[DEBUG - Tunnel] Modified SCP command: scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -P 44081 /home/nextime/vr_gloryhole.mp4 root@localhost:
[DEBUG - Tunnel] Parent process setting up tunnel...
[DEBUG] Establishing SSL connection...
[DEBUG] SSL connection established
[DEBUG] Performing WebSocket handshake to mbetter.nexlab.net:9898
[DEBUG] WebSocket handshake successful
[DEBUG] Tunnel request sent for client: zeiss, request_id: a5a9502a18dfdd97817f3f4488d691b365cc
[DEBUG] Read 78 bytes acknowledgment
[DEBUG] Raw acknowledgment: \x81L{"type": "tunnel_ack", "request_id": "a5a9502a18
[DEBUG] Received payload: '{"type": "tunnel_ack", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc"}' (length: 76)
[DEBUG] Looking for tunnel_ack in: {"type": "tunnel_ack", "request_id": "a5a9502a18df
[DEBUG] Tunnel established, local port: 44081
[DEBUG] Listening on localhost:44081
[DEBUG - Tunnel] About to fork SCP process...
[DEBUG - Tunnel] Waiting for connection on localhost:44081...
[DEBUG - Tunnel] Child process starting SCP with command: scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -P 44081 /home/nextime/vr_gloryhole.mp4 root@localhost:
[DEBUG] Local SCP connection accepted! Starting data forwarding...
[DEBUG - Tunnel] wsscp socket already connected, starting data forwarding
[DEBUG - TCPConnection] Forwarding 32 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 159 bytes, frame: 0xffffff81 0x7e 0x00 0xffffff9b
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "5353482d322e302d4f70656e5353485f31302e3070322044656269616e2d380d0a"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "5353482d322e302d4f70656e5353485f31302e3070322044656269616e2d380d0a"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 33 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 1568 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 2173 bytes, frame: 0xffffff81 0x7e 0x08 0x79
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "0000040c0914feb87f18b5cc680fcebfedb8b162019e000000df6d6c6b656d3736387832353531392d7368613235362c736e747275703736317832353531392d7368613531322c736e747275703736317832353531392d736861353132406f70656e7373682e636f6d2c637572766532353531392d7368613235362c637572766532353531392d736861323536406c69627373682e6f72672c656364682d736861322d6e697374703235362c656364682d736861322d6e697374703338342c656364682d736861322d6e697374703532312c6578742d696e666f2d732c6b65782d7374726963742d732d763030406f70656e7373682e636f6d000000397273612d736861322d3531322c7273612d736861322d3235362c65636473612d736861322d6e697374703235362c7373682d656432353531390000006c63686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d67636d406f70656e7373682e636f6d2c6165733235362d67636d406f70656e7373682e636f6d2c6165733132382d6374722c6165733139322d6374722c6165733235362d6374720000006c63686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d67636d406f70656e7373682e636f6d2c6165733235362d67636d406f70656e7373682e636f6d2c6165733132382d6374722c6165733139322d6374722c6165733235362d637472000000d5756d61632d36342d65746d406f70656e7373682e636f6d2c756d61632d3132382d65746d406f70656e7373682e636f6d2c686d61632d736861322d3235362d65746d406f70656e7373682e636f6d2c686d61632d736861322d3531322d65746d406f70656e7373682e636f6d2c686d61632d736861312d65746d406f70656e7373682e636f6d2c756d61632d3634406f70656e7373682e636f6d2c756d61632d313238406f70656e7373682e636f6d2c686d61632d736861322d3235362c686d61632d736861322d3531322c686d61632d73686131000000d5756d61632d36342d65746d406f70656e7373682e636f6d2c756d61632d3132382d65746d406f70656e7373682e636f6d2c686d61632d736861322d3235362d65746d406f70656e7373682e636f6d2c686d61632d736861322d3531322d65746d406f70656e7373682e636f6d2c686d61632d736861312d65746d406f70656e7373682e636f6d2c756d61632d3634406f70656e7373682e636f6d2c756d61632d313238406f70656e7373682e636f6d2c686d61632d736861322d3235362c686d61632d736861322d3531322c686d61632d73686131000000156e6f6e652c7a6c6962406f70656e7373682e636f6d000000156e6f6e652c7a6c6962406f70656e7373682e636f6d00000000000000000000000000000000000000000000"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "0000040c0914feb87f18b5cc680fcebfedb8b162019e000000df6d6c6b656d3736387832353531392d7368613235362c736e747275703736317832353531392d7368613531322c736e747275703736317832353531392d736861353132406f70656e7373682e636f6d2c637572766532353531392d7368613235362c637572766532353531392d736861323536406c69627373682e6f72672c656364682d736861322d6e697374703235362c656364682d736861322d6e697374703338342c656364682d736861322d6e697374703532312c6578742d696e666f2d732c6b65782d7374726963742d732d763030406f70656e7373682e636f6d000000397273612d736861322d3531322c7273612d736861322d3235362c65636473612d736861322d6e697374703235362c7373682d656432353531390000006c63686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d67636d406f70656e7373682e636f6d2c6165733235362d67636d406f70656e7373682e636f6d2c6165733132382d6374722c6165733139322d6374722c6165733235362d6374720000006c63686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d67636d406f70656e7373682e636f6d2c6165733235362d67636d406f70656e7373682e636f6d2c6165733132382d6374722c6165733139322d6374722c6165733235362d637472000000d5756d61632d36342d65746d406f70656e7373682e636f6d2c756d61632d3132382d65746d406f70656e7373682e636f6d2c686d61632d736861322d3235362d65746d406f70656e7373682e636f6d2c686d61632d736861322d3531322d65746d406f70656e7373682e636f6d2c686d61632d736861312d65746d406f70656e7373682e636f6d2c756d61632d3634406f70656e7373682e636f6d2c756d61632d313238406f70656e7373682e636f6d2c686d61632d736861322d3235362c686d61632d736861322d3531322c686d61632d73686131000000d5756d61632d36342d65746d406f70656e7373682e636f6d2c756d61632d3132382d65746d406f70656e7373682e636f6d2c686d61632d736861322d3235362d65746d406f70656e7373682e636f6d2c686d61632d736861322d3531322d65746d406f70656e7373682e636f6d2c686d61632d736861312d65746d406f70656e7373682e636f6d2c756d61632d3634406f70656e7373682e636f6d2c756d61632d313238406f70656e7373682e636f6d2c686d61632d736861322d3235362c686d61632d736861322d3531322c686d61632d73686131000000156e6f6e652c7a6c6962406f70656e7373682e636f6d000000156e6f6e652c7a6c6962406f70656e7373682e636f6d00000000000000000000000000000000000000000000"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 1040 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 1208 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 3157 bytes, frame: 0xffffff81 0x7e 0x0c 0x51
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "000004cc091f000000330000000b7373682d656432353531390000002095e8904d274af77a258b3bc914d8a6dc7c99959b0ecab5d37191366aad8cfc550000042fdd42a7003c8a93c362fdb9a7a02dc75a48bcb9a43bb34c5a82c6f58ec2dcae4e762db1b9466726a6af4c891de6408b2d222dccc91d364efa9cee262698fe5c382b3227e6460d84b68915f8c0875f8e6edb22150f9c920c46eb1b1b8522c9d6b5d0784e9eb5e00b19e4ee89e48dffa5997c9d9a38e01768dc727341b422ba550589d07f3a238c4f48d402d8dd61becbcd17b3208acf0aff336f0f6987df37e992547f3352d1397ad2b66a9c5b709bc10657409d2637de579e85306da04529f035d6b4f654f2201e069cf06f9da562aa77f11bc6c99c252cac8126a272b65cc6ad6630e3c25cc19a7764fc4a6bf09f0b03f01e1911129ee4a05d3c139c313c7f76caeb0230568701ff8314c3009282fee6c47ab67f6a917eaf3f9785bd7eef1266c0afa73a298dfdf436a6d2baa63082b48a314b7869d52f41dfc66cdcf6a9cceacb5aa7a7bf3d9f3db84074e85120fe88bd5b758010febfddecd736f9819a33689381b575e70de210cddf949e1220b2def88f314fb9dc607e43722cf92d7ea084d035c420bf65dc17058579dfd6ce83c0c3c3064f6e7b9676348f998e0e4edb858b1780ca0fdd359528558198cca6e3d66c593bf16a7b80af0578c7eba823eab3e35710a082d1ee776a5c6c81c3126cbc8e7d36caa27d7253123d030a0d67869e2a5393665afcf9a7dce943670c400cea627791c2259ef90b8e3533c7e7e04f62d4616ab118a377b169536fe10834f599520a01f8681ff31cad26e2cde21b4ac6ef3d51ec7ba89a70e10068744c96787c9894ba982dece9a8179b07a7b06a7140fa753a56fe9d45f6e67833419f5b94821af09b7cdf39f22dd32a5e0443319968af00fe2abde99cbda77e4220c89affe4593fd7387518e85e466b512cb80630c946587f81a63bbbaafddf95c2031d5c6137cedf68e3bf41d7d7f6cd813a4f6fbd05b1f1bcf0a9cefe86e405049b4883894677b52f074ef752e77d3599646c5d5ef7d87780f2ba60e027fd848d2203a36ddc9e02119e8ef6129792ce2e4f12e82fdcf909ed56acd714a77e03fdd2ed40fe11fd60018411e5760ee3c4be84f91aa6fdbd4a23e43b6d28d679daa68845510d0b053f62b092e5dfe95c3575388d821fb62c404a2d4d43b81438ddda4fcd357a39452e14bdfcf31f473874551ee333d045a0e07ece8c9eeb738047ff8fff0838500f0c4503ce95d55b45bcbad95b9f6b48db3e72afc32a76b6aea7c8131a3f854bc2ebe3bfc42bb191558f66152ed5eaeb35dd200eb6fed051f67e21d4db740a636cbcdd51b2eb8de561cc94a86c6b760d60310e827009fe43750ec2177eb7ad707361ea128d7c498ef177a872cd841571f3083a13a6fa0c3246464d88caff453d4e141bc1366269a4d516eb2834786b6b18b4bd9ec5e95af45ec244d7710596ba765f81c03c5bd285136497f6ae959ba77d5a41004711107f9bfe6469b7d1c9e2b3ae590baf8468923c2bf0c49581c138ebb0bcad5db6a5a03e051d205d6a000000530000000b7373682d6564323535313900000040adbae5a7bdd40c40b40e00a859ea1c5fbcea804406816af594fecb3c695c221cf9e20b5cd6ebf8310e63223bb3dd1b9eecd3c897577bd016f4d52fd7b2fdb4080000000000000000000000000c0a1500000000000000000000afbeb5562d8ca763119bed5b7f566a42476ccde2cc18da6434d686160a714155ae05e0db259374941977b0b47130eb55fb122e5072547f2dba214e7b5b2f982ae09e13c8d4fd418a014f079aee89c9df5dd79f855855474424b72908b4d359ce2808b01692d3c0dcffe9efe41736c9780236baf2ba1e4ce7fb32aa5a19e3eec7521c440c3efe6c2e8bbce97acfa7412d2f8aa97e317f5acfcc8ce548b4a1a6aa4514c941acd8adf7bbbf0e47bb80e6a81de9b9a2af4ffb70bafa36c76c57a300952d1db43a7682b9293903aff687f3da4fd54612fabdb5136cdc0072ddf36a9601da3bf4f959e90a0d313581b51e82e18d0564c299800f7b048dc8c92db327bb50b5be299e45fe36b990fafa263fc06a865b1fbdadaf837a332253b7"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "000004cc091f000000330000000b7373682d656432353531390000002095e8904d274af77a258b3bc914d8a6dc7c99959b0ecab5d37191366aad8cfc550000042fdd42a7003c8a93c362fdb9a7a02dc75a48bcb9a43bb34c5a82c6f58ec2dcae4e762db1b9466726a6af4c891de6408b2d222dccc91d364efa9cee262698fe5c382b3227e6460d84b68915f8c0875f8e6edb22150f9c920c46eb1b1b8522c9d6b5d0784e9eb5e00b19e4ee89e48dffa5997c9d9a38e01768dc727341b422ba550589d07f3a238c4f48d402d8dd61becbcd17b3208acf0aff336f0f6987df37e992547f3352d1397ad2b66a9c5b709bc10657409d2637de579e85306da04529f035d6b4f654f2201e069cf06f9da562aa77f11bc6c99c252cac8126a272b65cc6ad6630e3c25cc19a7764fc4a6bf09f0b03f01e1911129ee4a05d3c139c313c7f76caeb0230568701ff8314c3009282fee6c47ab67f6a917eaf3f9785bd7eef1266c0afa73a298dfdf436a6d2baa63082b48a314b7869d52f41dfc66cdcf6a9cceacb5aa7a7bf3d9f3db84074e85120fe88bd5b758010febfddecd736f9819a33689381b575e70de210cddf949e1220b2def88f314fb9dc607e43722cf92d7ea084d035c420bf65dc17058579dfd6ce83c0c3c3064f6e7b9676348f998e0e4edb858b1780ca0fdd359528558198cca6e3d66c593bf16a7b80af0578c7eba823eab3e35710a082d1ee776a5c6c81c3126cbc8e7d36caa27d7253123d030a0d67869e2a5393665afcf9a7dce943670c400cea627791c2259ef90b8e3533c7e7e04f62d4616ab118a377b169536fe10834f599520a01f8681ff31cad26e2cde21b4ac6ef3d51ec7ba89a70e10068744c96787c9894ba982dece9a8179b07a7b06a7140fa753a56fe9d45f6e67833419f5b94821af09b7cdf39f22dd32a5e0443319968af00fe2abde99cbda77e4220c89affe4593fd7387518e85e466b512cb80630c946587f81a63bbbaafddf95c2031d5c6137cedf68e3bf41d7d7f6cd813a4f6fbd05b1f1bcf0a9cefe86e405049b4883894677b52f074ef752e77d3599646c5d5ef7d87780f2ba60e027fd848d2203a36ddc9e02119e8ef6129792ce2e4f12e82fdcf909ed56acd714a77e03fdd2ed40fe11fd60018411e5760ee3c4be84f91aa6fdbd4a23e43b6d28d679daa68845510d0b053f62b092e5dfe95c3575388d821fb62c404a2d4d43b81438ddda4fcd357a39452e14bdfcf31f473874551ee333d045a0e07ece8c9eeb738047ff8fff0838500f0c4503ce95d55b45bcbad95b9f6b48db3e72afc32a76b6aea7c8131a3f854bc2ebe3bfc42bb191558f66152ed5eaeb35dd200eb6fed051f67e21d4db740a636cbcdd51b2eb8de561cc94a86c6b760d60310e827009fe43750ec2177eb7ad707361ea128d7c498ef177a872cd841571f3083a13a6fa0c3246464d88caff453d4e141bc1366269a4d516eb2834786b6b18b4bd9ec5e95af45ec244d7710596ba765f81c03c5bd285136497f6ae959ba77d5a41004711107f9bfe6469b7d1c9e2b3ae590baf8468923c2bf0c49581c138ebb0bcad5db6a5a03e051d205d6a000000530000000b7373682d6564323535313900000040adbae5a7bdd40c40b40e00a859ea1c5fbcea804406816af594fecb3c695c221cf9e20b5cd6ebf8310e63223bb3dd1b9eecd3c897577bd016f4d52fd7b2fdb4080000000000000000000000000c0a1500000000000000000000afbeb5562d8ca763119bed5b7f566a42476ccde2cc18da6434d686160a714155ae05e0db259374941977b0b47130eb55fb122e5072547f2dba214e7b5b2f982ae09e13c8d4fd418a014f079aee89c9df5dd79f855855474424b72908b4d359ce2808b01692d3c0dcffe9efe41736c9780236baf2ba1e4ce7fb32aa5a19e3eec7521c440c3efe6c2e8bbce97acfa7412d2f8aa97e317f5acfcc8ce548b4a1a6aa4514c941acd8adf7bbbf0e47bb80e6a81de9b9a2af4ffb70bafa36c76c57a300952d1db43a7682b9293903aff687f3da4fd54612fabdb5136cdc0072ddf36a9601da3bf4f959e90a0d313581b51e82e18d0564c299800f7b048dc8c92db327bb50b5be299e45fe36b990fafa263fc06a865b1fbdadaf837a332253b7"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 1532 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 84 bytes from TCP to WebSocket
[DEBUG - TCPConnection] Forwarding 44 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 181 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffb1
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "a9c08a7fe6fd67c7663a4816ce70cefbe50cc5dc5ac6087c43b53b04c9910d7fc40e97cc628d81391fce8e87"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "a9c08a7fe6fd67c7663a4816ce70cefbe50cc5dc5ac6087c43b53b04c9910d7fc40e97cc628d81391fce8e87"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 44 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 60 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 621 bytes, frame: 0xffffff81 0x7e 0x02 0x69
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "9c58082c468828e65e5a27070ae93a8a718a96de88a5b0c3a3332628d1b69ad50e1ced9b7060a46f04bfa2bada29ba37865bb206968a6ba1e57a682cb867f0eddbdec01b61ec2aaf58a550b15c846b409a5d03be246d9b58849cb5026e396b39dc7fa3a715800c22204e9c013f646bd65449d30de59d4f7317512c0c8c021fb72dd6692b5ecf252555f8a3dd4378ec4964a21ff8a22c88bd039609bf57def5701ab01fa88f690440592965132334c1f4bd7da91c6cb8bb4a0a3066b3bcc48d2d94ad1ff0268448db001a61f88247cd3f80cb41123b35e93662b7c07dfc2360052057497729537248c22ed3caad3dd4bfbba5deb23cd044a0a245bdd4ed9beb383c6448b9a41d85f8"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "9c58082c468828e65e5a27070ae93a8a718a96de88a5b0c3a3332628d1b69ad50e1ced9b7060a46f04bfa2bada29ba37865bb206968a6ba1e57a682cb867f0eddbdec01b61ec2aaf58a550b15c846b409a5d03be246d9b58849cb5026e396b39dc7fa3a715800c22204e9c013f646bd65449d30de59d4f7317512c0c8c021fb72dd6692b5ecf252555f8a3dd4378ec4964a21ff8a22c88bd039609bf57def5701ab01fa88f690440592965132334c1f4bd7da91c6cb8bb4a0a3066b3bcc48d2d94ad1ff0268448db001a61f88247cd3f80cb41123b35e93662b7c07dfc2360052057497729537248c22ed3caad3dd4bfbba5deb23cd044a0a245bdd4ed9beb383c6448b9a41d85f8"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 264 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 500 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 197 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffc1
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "c16923d87b553e333ced3801d2b5b83a7c1ea687fb1541f90171d3d4c81cc7e68ca0067b88d76c1f053be15a36b52f5ce355fe95"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "c16923d87b553e333ced3801d2b5b83a7c1ea687fb1541f90171d3d4c81cc7e68ca0067b88d76c1f053be15a36b52f5ce355fe95"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 52 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 140 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 293 bytes, frame: 0xffffff81 0x7e 0x01 0x21
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "dbace739f8eaf41d9c9fe63d2ff001027846e192c8d26905e87a67ee1e4367d35bd001a84fb269cd1b4061773be9298c787e3048975f646e15f0c2c923db110e8a8907a4d6e27e5fd5945a75c821f13c4011bb366826cf652a8ee275c52c050a1f5fbf5e"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "dbace739f8eaf41d9c9fe63d2ff001027846e192c8d26905e87a67ee1e4367d35bd001a84fb269cd1b4061773be9298c787e3048975f646e15f0c2c923db110e8a8907a4d6e27e5fd5945a75c821f13c4011bb366826cf652a8ee275c52c050a1f5fbf5e"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 100 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 308 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 149 bytes, frame: 0xffffff81 0x7e 0x00 0xffffff91
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "8db798905e29c4ae9c3b8841f94f0b110f911575f079d667770cbf0d"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "8db798905e29c4ae9c3b8841f94f0b110f911575f079d667770cbf0d"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 28 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 112 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 1653 bytes, frame: 0xffffff81 0x7e 0x06 0x71
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "40a4fa433f237eea614e66cc5d6c1312458a8a8a468136f730f3cb87bac64913de6b674bd397975d45c6bdb16af89347d8b3c2980356ea49191d911b818f97a0b5e37e89325912ec7b8e60e97270fdaada84af7a25b8d1660659afc0710c16fe6b0ed206a13754331c79757ea37da2530757eab208f68dd8cecde6f8b94f8201b2a4c247fa30d176205ff3438869e58593b0c61256a9efda7ff1797a902fa16178d20b5bc9abe48d2a616269599f33a2a640862c7e9d7978d515d902d2c6b31b3bd1f155c3e73b6cde5f089432750b6823082dbddde49c6127909866518b142c09bd1544fab7f7a279ed07459b06ada583cb869d1e7a113d809ed140c5ffbe4360ca8bde43d23834a21be08d1cded27a99ec917afb1cebec717528e3184e63de6a7e4912d2572a473f4aa3593c890d76453928350379152691d91d48ef2b60c20a56d059d232f3d5e6cc8c029106f45f38dfef309e88f5d22d8fb507e2cc7cf7ef17675371253cf6786218e87f031a9d7c8c0530023f77020df65276b8c2dd11139a3b17980425afbe1a72bfef48d238687ce2a408bc87c562f56d6c6092ae22e7082cd1f5d3767a64afd70567c204e4f625149119cf1f49ea06e315a6f4c3fc816e7f89e7f00bae1334d13f135491b39db080d7f0a5193f23dc9a87507128544407c342f3935b7b571302171bbbc74d2119831c3510c756f9924cd474811e2210527fddac8bdcc9bfa9bce6b11bcddfdca21dd570d8f3de0dd9a9b9a84427c22a9a2715adf37de1351a4110aa264bfd21ec181e58b5be616cb9fba05f25322711c03909257c14e1b94d81e62fc723be7a6e8707651b3348242050988607f998025f8e7ed722e064e9f0ee6014d1ae84506890082d76eafa95be8f8c4328d05d3f0811064c0f1c26ee97778338a76e14c80c0fc195ce526f2b7c8f5b613f0f9beab08e1fb57ef185f6efbfd5f9029e50f3a25e61c0451d89ed49b4a8f6168b300878fc46dd9094fdcbd1679dd4bc13cad00bc8a64bcd31396a7d1dba137e4ef566d1d41480b82dbff13e9072f14f714f0ba7285521b6c0b16a0aec3811d6c3404d1f207b623476d680ed2e6c"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "40a4fa433f237eea614e66cc5d6c1312458a8a8a468136f730f3cb87bac64913de6b674bd397975d45c6bdb16af89347d8b3c2980356ea49191d911b818f97a0b5e37e89325912ec7b8e60e97270fdaada84af7a25b8d1660659afc0710c16fe6b0ed206a13754331c79757ea37da2530757eab208f68dd8cecde6f8b94f8201b2a4c247fa30d176205ff3438869e58593b0c61256a9efda7ff1797a902fa16178d20b5bc9abe48d2a616269599f33a2a640862c7e9d7978d515d902d2c6b31b3bd1f155c3e73b6cde5f089432750b6823082dbddde49c6127909866518b142c09bd1544fab7f7a279ed07459b06ada583cb869d1e7a113d809ed140c5ffbe4360ca8bde43d23834a21be08d1cded27a99ec917afb1cebec717528e3184e63de6a7e4912d2572a473f4aa3593c890d76453928350379152691d91d48ef2b60c20a56d059d232f3d5e6cc8c029106f45f38dfef309e88f5d22d8fb507e2cc7cf7ef17675371253cf6786218e87f031a9d7c8c0530023f77020df65276b8c2dd11139a3b17980425afbe1a72bfef48d238687ce2a408bc87c562f56d6c6092ae22e7082cd1f5d3767a64afd70567c204e4f625149119cf1f49ea06e315a6f4c3fc816e7f89e7f00bae1334d13f135491b39db080d7f0a5193f23dc9a87507128544407c342f3935b7b571302171bbbc74d2119831c3510c756f9924cd474811e2210527fddac8bdcc9bfa9bce6b11bcddfdca21dd570d8f3de0dd9a9b9a84427c22a9a2715adf37de1351a4110aa264bfd21ec181e58b5be616cb9fba05f25322711c03909257c14e1b94d81e62fc723be7a6e8707651b3348242050988607f998025f8e7ed722e064e9f0ee6014d1ae84506890082d76eafa95be8f8c4328d05d3f0811064c0f1c26ee97778338a76e14c80c0fc195ce526f2b7c8f5b613f0f9beab08e1fb57ef185f6efbfd5f9029e50f3a25e61c0451d89ed49b4a8f6168b300878fc46dd9094fdcbd1679dd4bc13cad00bc8a64bcd31396a7d1dba137e4ef566d1d41480b82dbff13e9072f14f714f0ba7285521b6c0b16a0aec3811d6c3404d1f207b623476d680ed2e6c"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 780 bytes from buffer to local socket
[DEBUG - WebSockets] Accumulated 181 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffb1
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "498c595204e715f053ecd5474aa080dd3fd977ec3512fe9ad4579568659b0cf14bc962975ae4af1c65f54fb9"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "498c595204e715f053ecd5474aa080dd3fd977ec3512fe9ad4579568659b0cf14bc962975ae4af1c65f54fb9"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 44 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 120 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 237 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffe9
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "754eb32fffd129ac85f9561ac6751741d4b76112982662e2af6562f071ce82286ca8a825e907683644fc438648e138196ec52ddecff93a8341fb8e49451d8e811ec1279da3b6f0c0"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "754eb32fffd129ac85f9561ac6751741d4b76112982662e2af6562f071ce82286ca8a825e907683644fc438648e138196ec52ddecff93a8341fb8e49451d8e811ec1279da3b6f0c0"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 72 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 44 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 805 bytes, frame: 0xffffff81 0x7e 0x03 0x21
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "753655fe98860361f3ae9b09c249bbb1b42d76b2e6ae8919ac7a58593c817cf6097d8fb19a33506ec0f3b9589afd781bc243a44337e49bf08d85a8620d26f121ab9ea676120d0bafd3290b8e206449471ae71d778f0d6e017e6c6853da5ebedd04c2ab72c0dcbd56f55b1fca9c38488e21f7cde265301097f856b5608294735185c6461ff16c744fd11fdc9d857f90540b41061774a99ed730159caeecb3be9235b60333ee6625a083cc2e2eca0987cc8f15f795db5bdfd69eefd8932b13f24b6b740e9b8fabf8e612fe97049806e7efe38ff17751ae288109e9a9a9b39b5937fbfe95d56d9de213f5dbe98333e83771d115949ff5d3495c0e21b5c82fb3de8bf8b97e6b4e5604051b788f129cc6e2a22134f18f772a796868bb13fe43c79bdf7f55e5a3fa2821691e040807fe68cddd93f52f6224ef8df400d3328f561aed6f23cc8b1db4e28ba761894fd966f39d76b1e420d2420711c4b0687c23614deba72ec9f3bc"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "753655fe98860361f3ae9b09c249bbb1b42d76b2e6ae8919ac7a58593c817cf6097d8fb19a33506ec0f3b9589afd781bc243a44337e49bf08d85a8620d26f121ab9ea676120d0bafd3290b8e206449471ae71d778f0d6e017e6c6853da5ebedd04c2ab72c0dcbd56f55b1fca9c38488e21f7cde265301097f856b5608294735185c6461ff16c744fd11fdc9d857f90540b41061774a99ed730159caeecb3be9235b60333ee6625a083cc2e2eca0987cc8f15f795db5bdfd69eefd8932b13f24b6b740e9b8fabf8e612fe97049806e7efe38ff17751ae288109e9a9a9b39b5937fbfe95d56d9de213f5dbe98333e83771d115949ff5d3495c0e21b5c82fb3de8bf8b97e6b4e5604051b788f129cc6e2a22134f18f772a796868bb13fe43c79bdf7f55e5a3fa2821691e040807fe68cddd93f52f6224ef8df400d3328f561aed6f23cc8b1db4e28ba761894fd966f39d76b1e420d2420711c4b0687c23614deba72ec9f3bc"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 356 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 68 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 245 bytes, frame: 0xffffff81 0x7e 0x00 0xfffffff1
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "6c3382930e1f0e7ec383c65c0571ec0eb4de3ab1c37bd991f4147bcfa5f794b785b7203e2969798d4c8bfcb619b43ecbf2153ada4f3db9f836b924b343883048e2ff495515053024d64d599a"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "6c3382930e1f0e7ec383c65c0571ec0eb4de3ab1c37bd991f4147bcfa5f794b785b7203e2969798d4c8bfcb619b43ecbf2153ada4f3db9f836b924b343883048e2ff495515053024d64d599a"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 76 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 52 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 245 bytes, frame: 0xffffff81 0x7e 0x00 0xfffffff1
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "c748fe8064328df48d5ac73f696dd1ecac858b25af5eb740e02dab7246c86d394c939515c4127c26acbddedb7f4f124426bf0018c3500d8ea5240c1685819c4299909bbeea46d3932222a9b4"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "c748fe8064328df48d5ac73f696dd1ecac858b25af5eb740e02dab7246c86d394c939515c4127c26acbddedb7f4f124426bf0018c3500d8ea5240c1685819c4299909bbeea46d3932222a9b4"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 76 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 84 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 197 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffc1
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "6a4df2f41a28643198d695c1e26da535bd37625a3affe6d3e7cbb93411d43171c3ef37e6369d7ba999e531747e34c73865b5e94f"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "6a4df2f41a28643198d695c1e26da535bd37625a3affe6d3e7cbb93411d43171c3ef37e6369d7ba999e531747e34c73865b5e94f"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 52 bytes from buffer to local socket
vr_gloryhole.mp4 0% 0 0.0KB/s --:-- ETA[DEBUG - TCPConnection] Forwarding 32804 bytes from TCP to WebSocket
[DEBUG - TCPConnection] Forwarding 98412 bytes from TCP to WebSocket
[DEBUG - TCPConnection] Forwarding 130224 bytes from TCP to WebSocket
vr_gloryhole.mp4 0% 0 0.0KB/s --:-- ETA[DEBUG - WebSockets] Accumulated 165 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffa1
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "f9e8efc5603a33929b6eaa815ffbd46315ea065784621cf84115200378ceebb61b7d9cc3"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "f9e8efc5603a33929b6eaa815ffbd46315ea065784621cf84115200378ceebb61b7d9cc3"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 36 bytes from buffer to local socket
[DEBUG - WebSockets] Accumulated 165 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffa1
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "63635892bbe7367fa8feda658bd2ed3184606c47bdd33c0e6d93c35ea4a3e0171e5519d9"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "63635892bbe7367fa8feda658bd2ed3184606c47bdd33c0e6d93c35ea4a3e0171e5519d9"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 36 bytes from buffer to local socket
[DEBUG - WebSockets] Accumulated 229 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffe1
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "d9de5897876c58493035e30c47ea85e833f2f55479d6617bd18291503b5dec2dbabd22417865d0a912decb7a8a35d988f1c911ac1c4796caeefb8bb0ff5605257e9e033c"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "d9de5897876c58493035e30c47ea85e833f2f55479d6617bd18291503b5dec2dbabd22417865d0a912decb7a8a35d988f1c911ac1c4796caeefb8bb0ff5605257e9e033c"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 68 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 32804 bytes from TCP to WebSocket
[DEBUG - TCPConnection] Forwarding 852904 bytes from TCP to WebSocket
[DEBUG] Hex data too large (1705809 bytes), truncating to 1048476
[DEBUG] Hex data truncated, sent 524238 of 852904 bytes
vr_gloryhole.mp4 0% 255KB 255.0KB/s 1:55:43 ETA[DEBUG - TCPConnection] Forwarding 1048576 bytes from TCP to WebSocket
[DEBUG] Hex data too large (2097153 bytes), truncating to 1048476
[DEBUG] Hex data truncated, sent 524238 of 1048576 bytes
[DEBUG - TCPConnection] Forwarding 100600 bytes from TCP to WebSocket
vr_gloryhole.mp4 0% 255KB 167.3KB/s 2:56:22 ETA[DEBUG - WebSockets] Accumulated 237 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffe9
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "a59724bc65acd2f099226e6710a280d327e3d2d1341d1a512e7b8900a058e7ebfe7d7cc627d0ac4c88b54fceb84f2ee9c71de20c8b4867445dc9f3471c64f8fc9d9eeeeadbb5777a"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "a59724bc65acd2f099226e6710a280d327e3d2d1341d1a512e7b8900a058e7ebfe7d7cc627d0ac4c88b54fceb84f2ee9c71de20c8b4867445dc9f3471c64f8fc9d9eeeeadbb5777a"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 72 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 32804 bytes from TCP to WebSocket
[DEBUG - TCPConnection] Forwarding 195832 bytes from TCP to WebSocket
[DEBUG - WebSockets] Accumulated 237 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffe9
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "5e789ffc6a3073ebaedf7966a607fe8bf486e63c98d46ddb6213b45510e46d2b9463a144b48e1a154216a8b4efb0b34501e4f545d294bd7ed49cb703c236f2a633e33566b4c3a08a"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "5e789ffc6a3073ebaedf7966a607fe8bf486e63c98d46ddb6213b45510e46d2b9463a144b48e1a154216a8b4efb0b34501e4f545d294bd7ed49cb703c236f2a633e33566b4c3a08a"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - WebSockets] Accumulated 165 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffa1
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "faf787b71948f963b00828eab1bdd1e17cc6c9efc5ae95c90cc330350c4af94a1cfabdef"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "faf787b71948f963b00828eab1bdd1e17cc6c9efc5ae95c90cc330350c4af94a1cfabdef"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - WebSockets] Accumulated 373 bytes, frame: 0xffffff81 0x7e 0x01 0x71
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "2d92b8a506cfdd9b1fb927ba0e89924df1d4516619cf4ea0e3892913ee1d2d1580de316ad797929138191d8bbb093d63be79c7e8f120822365ff3cfab59e02f6200bda029a4607e44f72f43fa122c339a0423a7e9161951bbce20be956f5ca553cb8867233c3effe2b0f7923236f2d9324e1952f3baf96316417c35ed1f619715dad398444a8123cc8680b31"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "2d92b8a506cfdd9b1fb927ba0e89924df1d4516619cf4ea0e3892913ee1d2d1580de316ad797929138191d8bbb093d63be79c7e8f120822365ff3cfab59e02f6200bda029a4607e44f72f43fa122c339a0423a7e9161951bbce20be956f5ca553cb8867233c3effe2b0f7923236f2d9324e1952f3baf96316417c35ed1f619715dad398444a8123cc8680b31"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - WebSockets] Accumulated 229 bytes, frame: 0xffffff81 0x7e 0x00 0xffffffe1
[DEBUG - WebSockets] Received message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "e5e7eb81d8de1655913fdef0702c7c05c469f8cd0844f89afd10ec8e773ac967f8c3eea87ff280a2814c028f30fcb7d6adf7fe9a98ef096c80b79daac3b45cbcd032cd79"}
[DEBUG - WebSockets] Processing message: {"type": "tunnel_data", "request_id": "a5a9502a18dfdd97817f3f4488d691b365cc", "data": "e5e7eb81d8de1655913fdef0702c7c05c469f8cd0844f89afd10ec8e773ac967f8c3eea87ff280a2814c028f30fcb7d6adf7fe9a98ef096c80b79daac3b45cbcd032cd79"}
[DEBUG - Tunnel] Received tunnel_data message
[DEBUG] Socket selection: wsscp mode, target_sock=5 (local_sock)
[DEBUG - TCPConnection] Sent 316 bytes from buffer to local socket
[DEBUG - TCPConnection] Forwarding 32804 bytes from TCP to WebSocket
[DEBUG - TCPConnection] Forwarding 229628 bytes from TCP to WebSocket
vr_gloryhole.mp4 0% 255KB 72.0KB/s - stalled -[DEBUG - WebSockets] Accumulated 6 bytes, frame: 0xffffff89 0x04 0x38 0xffffffb5
......@@ -5,6 +5,7 @@ wsscp \- WebSocket SCP wrapper for secure file transfer
.B wsscp
[\fB\-\-local\-port\fR \fIPORT\fR]
[\fB\-\-debug\fR]
[\fB\-\-dev\-tunnel\fR]
[\fB\-\-help\fR]
[\fISCP_OPTIONS\fR...]
\fISOURCE\fR...
......@@ -20,6 +21,9 @@ Local port for tunnel establishment (default: auto-assign)
.B \-\-debug
Enable debug output for troubleshooting
.TP
.B \-\-dev\-tunnel
Development mode: setup tunnel but don't launch SCP
.TP
.B \-\-help
Display help message and exit
.SH HOSTNAME FORMAT
......
......@@ -5,6 +5,7 @@ wsssh \- WebSocket SSH wrapper for secure tunneling
.B wsssh
[\fB\-\-local\-port\fR \fIPORT\fR]
[\fB\-\-debug\fR]
[\fB\-\-dev\-tunnel\fR]
[\fB\-\-help\fR]
\fIuser\fR@\fIclient\fR.\fIdomain\fR
[\fISSH_OPTIONS\fR...]
......@@ -19,6 +20,9 @@ Local port for tunnel establishment (default: auto-assign)
.B \-\-debug
Enable debug output for troubleshooting
.TP
.B \-\-dev\-tunnel
Development mode: setup tunnel but don't launch SSH
.TP
.B \-\-help
Display help message and exit
.SH HOSTNAME FORMAT
......
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