Remove Python implementations of wssshc, wsssh, wsscp

- Delete wssshc.py, wsssh.py, wsscp.py files
- Update build.sh to remove pyinstaller commands and output messages
- Update Debian packaging to exclude Python script installations
- Update documentation to reflect C-only implementations
- Update CHANGELOG.md with version 1.4.5 entry
- Focus on C implementations for better performance and minimal dependencies
parent 250a9362
...@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. ...@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4.5] - 2025-09-16
### Removed
- **Python Implementations**: 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
- Cleaned up documentation and project structure references
- Maintained C implementations in wssshtools/ directory
### Changed
- **Build System**: Updated build.sh to only build C tools and wssshd daemon
- **Documentation**: Updated README.md, DOCUMENTATION.md, and project structure to reflect C-only implementations
- **Debian Packaging**: Modified wssshtools/debian/rules to only install wssshd.py and C tools
### Technical Details
- **C Implementation Focus**: Project now focuses exclusively on C implementations for better performance and minimal dependencies
- **Build Simplification**: Removed Python binary building from build process
- **Package Structure**: wsssh-tools package now contains only C tools and wssshd daemon
## [1.4.4] - 2025-09-15 ## [1.4.4] - 2025-09-15
### Added ### Added
......
...@@ -18,20 +18,17 @@ WebSocket SSH (wsssh) is a tunneling system that enables secure SSH/SCP access t ...@@ -18,20 +18,17 @@ WebSocket SSH (wsssh) is a tunneling system that enables secure SSH/SCP access t
### Key Components ### Key Components
#### Python Implementation (Primary) #### Server Component
- **wssshd**: WebSocket SSH Daemon - Central server with web interface - **wssshd**: WebSocket SSH Daemon - Central server with web interface (Python)
- **wssshc**: WebSocket SSH Client - Registers machines with the daemon
- **wsssh**: SSH wrapper - Provides SSH access through tunnels
- **wsscp**: SCP wrapper - Provides SCP access through tunnels
#### C Implementation (Alternative) #### C Implementation (Primary)
- **wssshc**: Lightweight C client for registration (OpenSSL-based) - **wssshc**: Lightweight C client for registration (OpenSSL-based)
- **wsssh**: Native SSH wrapper with direct system integration - **wsssh**: Native SSH wrapper with direct system integration
- **wsscp**: Native SCP wrapper optimized for file transfers - **wsscp**: Native SCP wrapper optimized for file transfers
### Core Features ### Core Features
- **Dual Implementation**: Python (full-featured) and C (lightweight) versions - **C Implementation**: Lightweight and efficient C versions
- **Intelligent Hostname Parsing**: Automatic detection of client IDs and server endpoints - **Intelligent Hostname Parsing**: Automatic detection of client IDs and server endpoints
- **SSL/TLS Encryption**: Secure WebSocket communications with OpenSSL - **SSL/TLS Encryption**: Secure WebSocket communications with OpenSSL
- **Multi-client Support**: Route connections to different registered clients - **Multi-client Support**: Route connections to different registered clients
...@@ -706,9 +703,6 @@ Key log messages to monitor: ...@@ -706,9 +703,6 @@ Key log messages to monitor:
``` ```
wsssh/ wsssh/
├── wssshd.py # WebSocket SSH Daemon (Python) ├── wssshd.py # WebSocket SSH Daemon (Python)
├── wssshc.py # WebSocket SSH Client (Python)
├── wsssh.py # SSH wrapper (Python)
├── wsscp.py # SCP wrapper (Python)
├── build.sh # Build script (supports --debian, --server-only) ├── build.sh # Build script (supports --debian, --server-only)
├── clean.sh # Clean script ├── clean.sh # Clean script
├── requirements.txt # Python dependencies ├── requirements.txt # Python dependencies
...@@ -754,7 +748,7 @@ wsssh/ ...@@ -754,7 +748,7 @@ wsssh/
### Dependencies ### Dependencies
#### Python Implementation #### Server Dependencies (wssshd)
- **Python 3.7+** - **Python 3.7+**
- **websockets**: WebSocket client/server library - **websockets**: WebSocket client/server library
- **Flask**: Web framework for admin interface - **Flask**: Web framework for admin interface
...@@ -780,13 +774,13 @@ wsssh/ ...@@ -780,13 +774,13 @@ wsssh/
### Building from Source ### Building from Source
#### Python Implementation #### Server Build (wssshd)
```bash ```bash
# Install Python dependencies # Install Python dependencies
pip3 install -r requirements.txt pip3 install -r requirements.txt
# Build Python binaries # Build server binary
./build.sh ./build.sh --server-only
# Build with Debian package # Build with Debian package
./build.sh --debian ./build.sh --debian
......
...@@ -6,7 +6,7 @@ A modern SSH tunneling system that uses WebSocket connections to securely route ...@@ -6,7 +6,7 @@ A modern SSH tunneling system that uses WebSocket connections to securely route
## Features ## Features
- **Dual Implementation**: Python (full-featured) and C (lightweight) versions available - **C Implementation**: Lightweight and efficient C versions available
- **WebSocket-based Tunneling**: Secure SSH/SCP access through WebSocket connections - **WebSocket-based Tunneling**: Secure SSH/SCP access through WebSocket connections
- **Client Registration**: Register client machines with the WebSocket daemon - **Client Registration**: Register client machines with the WebSocket daemon
- **Password Authentication**: Secure client registration with configurable passwords - **Password Authentication**: Secure client registration with configurable passwords
...@@ -28,9 +28,9 @@ A modern SSH tunneling system that uses WebSocket connections to securely route ...@@ -28,9 +28,9 @@ A modern SSH tunneling system that uses WebSocket connections to securely route
## Architecture ## Architecture
The system consists of multiple components available in both Python and C implementations: The system consists of components implemented in C for optimal performance:
### Python Implementation (Primary) ### Server Component
1. **`wssshd`** - WebSocket SSH Daemon (server) 1. **`wssshd`** - WebSocket SSH Daemon (server)
- Manages WebSocket connections with SSL/TLS encryption - Manages WebSocket connections with SSL/TLS encryption
- Handles client registrations with password authentication - Handles client registrations with password authentication
...@@ -39,18 +39,19 @@ The system consists of multiple components available in both Python and C implem ...@@ -39,18 +39,19 @@ The system consists of multiple components available in both Python and C implem
- HTML5 terminal interface for SSH connections - HTML5 terminal interface for SSH connections
- Donation modal integrated into the web interface - Donation modal integrated into the web interface
2. **`wssshc`** - WebSocket SSH Client (registration) ### C Implementation (Primary)
1. **`wssshc`** - WebSocket SSH Client (registration)
- Registers client machines with the daemon - Registers client machines with the daemon
- Maintains persistent WebSocket connection - Maintains persistent WebSocket connection
- Automatic reconnection with configurable intervals - Automatic reconnection with configurable intervals
3. **`wsssh`** - SSH wrapper with tunneling 2. **`wsssh`** - SSH wrapper with tunneling
- Simplified CLI (no need to specify "ssh" command) - Simplified CLI (no need to specify "ssh" command)
- Parses SSH commands and hostnames intelligently - Parses SSH commands and hostnames intelligently
- Establishes WebSocket tunnels automatically - Establishes WebSocket tunnels automatically
- Launches SSH to local tunnel port - Launches SSH to local tunnel port
4. **`wsscp`** - SCP wrapper with tunneling 3. **`wsscp`** - SCP wrapper with tunneling
- Simplified CLI (no need to specify "scp" command) - Simplified CLI (no need to specify "scp" command)
- Similar to wsssh but optimized for SCP operations - Similar to wsssh but optimized for SCP operations
- Handles file transfers through secure tunnels - Handles file transfers through secure tunnels
...@@ -491,9 +492,6 @@ python3 -m pytest ...@@ -491,9 +492,6 @@ python3 -m pytest
``` ```
wsssh/ wsssh/
├── wssshd.py # WebSocket SSH Daemon with web interface ├── wssshd.py # WebSocket SSH Daemon with web interface
├── wssshc.py # WebSocket SSH Client
├── wsssh.py # SSH wrapper (simplified CLI)
├── wsscp.py # SCP wrapper (simplified CLI)
├── build.sh # Build script (supports --debian flag) ├── build.sh # Build script (supports --debian flag)
├── clean.sh # Clean script ├── clean.sh # Clean script
├── requirements.txt # Python dependencies ├── requirements.txt # Python dependencies
......
...@@ -136,17 +136,7 @@ if [ "$BUILD_DEBIAN_ONLY" = false ] && [ "$BUILD_WSSSHTOOLS_ONLY" = false ]; the ...@@ -136,17 +136,7 @@ if [ "$BUILD_DEBIAN_ONLY" = false ] && [ "$BUILD_WSSSHTOOLS_ONLY" = false ]; the
# Build client binaries # Build client binaries
if [ "$BUILD_SERVER_ONLY" = false ]; then if [ "$BUILD_SERVER_ONLY" = false ]; then
# Build wssshc (client) binary
pyinstaller --onefile --distpath dist --runtime-tmpdir /tmp --clean wssshc.py
# Build wsssh and wsscp binaries unless --no-server is specified
if [ "$BUILD_NO_SERVER" = false ]; then
# Build wsssh binary
pyinstaller --onefile --distpath dist --runtime-tmpdir /tmp --clean wsssh.py
# Build wsscp binary
pyinstaller --onefile --distpath dist --runtime-tmpdir /tmp --clean wsscp.py
fi
fi fi
fi fi
...@@ -272,13 +262,6 @@ else ...@@ -272,13 +262,6 @@ else
echo "- dist/wssshd (server with web interface)" echo "- dist/wssshd (server with web interface)"
fi fi
if [ "$BUILD_SERVER_ONLY" = false ]; then
echo "- dist/wssshc (client)"
if [ "$BUILD_NO_SERVER" = false ]; then
echo "- dist/wsssh (SSH wrapper)"
echo "- dist/wsscp (SCP wrapper)"
fi
fi
if [ -d "wssshtools" ] && [ -f "wssshtools/wssshc" ]; then if [ -d "wssshtools" ] && [ -f "wssshtools/wssshc" ]; then
if [ "$BUILD_NO_SERVER" = true ]; then if [ "$BUILD_NO_SERVER" = true ]; then
......
#!/usr/bin/env python3
"""
WebSocket SCP (wsscp)
SCP wrapper that uses WebSocket tunnels through wssshd.
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import argparse
import asyncio
import ssl
import websockets
import json
import socket
import subprocess
import sys
import uuid
import configparser
import os
import signal
debug = False
async def handle_tunnel(ws, local_port, request_id, ready_event=None):
"""Handle tunnel data forwarding"""
try:
# Open local TCP listener
server = await asyncio.start_server(
lambda r, w: handle_local_connection(r, w, ws, request_id),
'localhost', local_port
)
if debug: print(f"[DEBUG] Listening on localhost:{local_port}")
# Signal that server is ready
if ready_event:
ready_event.set()
async with server:
await server.serve_forever()
except Exception as e:
if debug: print(f"[DEBUG] Tunnel handler error: {e}")
async def handle_local_connection(reader, writer, ws, request_id):
"""Handle connections from local SCP client"""
try:
# Forward data between local connection and WebSocket
async def forward_to_ws():
try:
while True:
data = await reader.read(1024)
if not data:
break
await ws.send(json.dumps({
"type": "tunnel_data",
"request_id": request_id,
"data": data.hex() # Use hex encoding for binary data
}))
except Exception as e:
if debug: print(f"[DEBUG] Local to WS error: {e}")
async def forward_from_ws():
try:
async for message in ws:
data = json.loads(message)
if data.get('type') == 'tunnel_data' and data.get('request_id') == request_id:
# Decode and forward data
tunnel_data = bytes.fromhex(data['data'])
writer.write(tunnel_data)
await writer.drain()
elif data.get('type') == 'tunnel_close' and data.get('request_id') == request_id:
break
except Exception as e:
if debug: print(f"[DEBUG] WS to local error: {e}")
await asyncio.gather(forward_to_ws(), forward_from_ws())
except Exception as e:
if debug: print(f"[DEBUG] Local connection error: {e}")
finally:
writer.close()
await writer.wait_closed()
async def run_scp(server_ip, server_port, client_id, local_port, scp_args, interval=30, shutdown_event=None):
"""Connect to wssshd and run SCP with reconnection support"""
uri = f"wss://{server_ip}:{server_port}"
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
request_id = str(uuid.uuid4())
# Initial connection with retry logic
websocket = None
max_initial_attempts = 3
initial_attempt = 0
while initial_attempt < max_initial_attempts and websocket is None:
try:
if debug and initial_attempt > 0:
print(f"[DEBUG] Initial connection attempt {initial_attempt + 1}/{max_initial_attempts}")
websocket = await websockets.connect(uri, ssl=ssl_context)
if debug: print("[DEBUG] Initial WebSocket connection established")
except Exception as e:
initial_attempt += 1
if initial_attempt < max_initial_attempts:
if debug: print(f"[DEBUG] Initial connection failed, waiting {interval} seconds...")
await asyncio.sleep(interval)
else:
print(f"Connection failed after {max_initial_attempts} attempts: {e}")
return 1
try:
# Set up ping/pong handlers for explicit handling
async def ping_handler(payload):
if debug: print(f"[DEBUG] Received ping: {payload}")
# Pong is sent automatically by websockets library
async def pong_handler(payload):
if debug: print(f"[DEBUG] Received pong: {payload}")
websocket.ping_handler = ping_handler
websocket.pong_handler = pong_handler
# Request tunnel
await websocket.send(json.dumps({
"type": "tunnel_request",
"client_id": client_id,
"request_id": request_id
}))
# Wait for acknowledgment
response = await websocket.recv()
data = json.loads(response)
if data.get('type') == 'tunnel_ack':
if debug: print(f"[DEBUG] Tunnel request acknowledged: {request_id}")
# Start tunnel handler (opens listening port)
ready_event = asyncio.Event()
tunnel_task = asyncio.create_task(handle_tunnel(websocket, local_port, request_id, ready_event))
# Wait for tunnel to be ready (listening port opened)
await ready_event.wait()
# Launch SCP with modified arguments
scp_cmd = ['scp'] + scp_args
if debug: print(f"[DEBUG] Launching: {' '.join(scp_cmd)}")
# Run SCP process
process = await asyncio.create_subprocess_exec(
*scp_cmd,
stdin=sys.stdin,
stdout=sys.stdout,
stderr=sys.stderr
)
# Monitor both SCP process and WebSocket connection
tunnel_active = True
while tunnel_active:
# Check if SCP process is still running
if process.returncode is not None:
if debug: print(f"[DEBUG] SCP process finished with code: {process.returncode}")
tunnel_active = False
break
# Check for shutdown signal
if shutdown_event and shutdown_event.is_set():
if debug: print("[DEBUG] Shutdown signal received, terminating tunnel")
tunnel_active = False
break
# Check WebSocket connection and attempt reconnection if needed
try:
# Try to send a small message to test connection
await asyncio.wait_for(websocket.ping(), timeout=5.0)
except Exception as e:
if debug: print(f"[DEBUG] WebSocket connection lost, attempting reconnection...")
# Attempt WebSocket reconnection
reconnect_attempts = 0
max_reconnect_attempts = 3
reconnected = False
while reconnect_attempts < max_reconnect_attempts and not reconnected:
# Check for shutdown signal during reconnection
if shutdown_event and shutdown_event.is_set():
if debug: print("[DEBUG] Shutdown signal received during reconnection")
tunnel_active = False
break
try:
if debug: print(f"[DEBUG] WebSocket reconnection attempt {reconnect_attempts + 1}/{max_reconnect_attempts}")
# Create new WebSocket connection
new_websocket = await websockets.connect(uri, ssl=ssl_context)
new_websocket.ping_handler = ping_handler
new_websocket.pong_handler = pong_handler
# Re-request tunnel
await new_websocket.send(json.dumps({
"type": "tunnel_request",
"client_id": client_id,
"request_id": request_id
}))
# Wait for acknowledgment
response = await new_websocket.recv()
data = json.loads(response)
if data.get('type') == 'tunnel_ack':
# Update tunnel task with new WebSocket
tunnel_task.cancel()
tunnel_task = asyncio.create_task(handle_tunnel(new_websocket, local_port, request_id, None))
websocket = new_websocket
reconnected = True
if debug: print("[DEBUG] WebSocket reconnection successful")
else:
await new_websocket.close()
except Exception as reconnect_error:
reconnect_attempts += 1
if reconnect_attempts < max_reconnect_attempts:
if debug: print(f"[DEBUG] WebSocket reconnection failed, waiting 1 second...")
await asyncio.sleep(1) # Fast reconnection for WebSocket
else:
if debug: print(f"[DEBUG] All reconnection attempts failed: {reconnect_error}")
tunnel_active = False
break
if tunnel_active:
await asyncio.sleep(2) # Check every 2 seconds
# Wait for SCP to complete if still running
if process.returncode is None:
await process.wait()
# Close tunnel
try:
await websocket.send(json.dumps({
"type": "tunnel_close",
"request_id": request_id
}))
except Exception:
pass # WebSocket might already be closed
if tunnel_task and not tunnel_task.done():
tunnel_task.cancel()
elif data.get('type') == 'tunnel_error':
print(f"Error: {data.get('error', 'Unknown error')}")
return 1
except Exception as e:
print(f"Connection failed: {e}")
return 1
finally:
if websocket:
try:
await websocket.close()
except Exception:
pass
return 0
def parse_hostname(hostname, config_domain=None):
"""Parse hostname to extract CLIENT_ID and WSSSHD_HOST"""
# Split by dots to get client_id and wssshd_host
parts = hostname.split('.')
if len(parts) >= 2:
client_id = parts[0]
wssshd_host = '.'.join(parts[1:])
else:
# No domain, assume whole hostname is client_id
client_id = hostname
wssshd_host = config_domain if config_domain else 'localhost' # Default fallback
return client_id, wssshd_host
def main():
# Read config file
config = configparser.ConfigParser()
config_file = os.path.expanduser("~/.config/wsssh/wsssh.conf")
config_port = None
config_domain = None
if os.path.exists(config_file):
config.read(config_file)
if 'default' in config:
if 'port' in config['default']:
try:
config_port = int(config['default']['port'])
except ValueError:
pass
if 'domain' in config['default']:
config_domain = config['default']['domain']
parser = argparse.ArgumentParser(description='WebSocket SCP (wsscp)', add_help=False)
parser.add_argument('--local-port', type=int, default=0, help='Local port for tunnel (0 = auto)')
parser.add_argument('--interval', type=int, default=30, help='Connection retry interval in seconds (default: 30)')
parser.add_argument('--debug', action='store_true', help='Enable debug output')
# Parse our arguments first
args, remaining = parser.parse_known_args()
global debug
debug = args.debug
# Don't prepend 'scp' - we'll handle it in execvp
modified_args = remaining
# Find host arguments and -P port option in modified args (SCP can have multiple host:paths)
hosts = []
scp_port = None
for i, arg in enumerate(modified_args):
if arg == '-P' and i + 1 < len(modified_args):
try:
scp_port = int(modified_args[i + 1])
except ValueError:
pass
elif not arg.startswith('-') and ':' in arg:
# Extract host from host:path format
host_part = arg.split(':', 1)[0]
if '@' in host_part:
host = host_part.split('@', 1)[1]
else:
host = host_part
hosts.append(host)
if not hosts:
print("Error: Could not determine target host(s)")
sys.exit(1)
if not scp_port:
if config_port:
scp_port = config_port
else:
print("Error: Could not determine wssshd port from -P option or config file")
sys.exit(1)
# Use the first host for parsing
host = hosts[0]
# Parse hostname to extract client_id and wssshd_host
client_id, wssshd_host = parse_hostname(host, config_domain)
if debug:
print(f"[DEBUG] Hosts: {hosts}")
print(f"[DEBUG] Client ID: {client_id}")
print(f"[DEBUG] WSSSHD Host: {wssshd_host}")
print(f"[DEBUG] WSSSHD Port: {scp_port}")
# Find available local port
if args.local_port == 0:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('localhost', 0))
local_port = s.getsockname()[1]
else:
local_port = args.local_port
if debug: print(f"[DEBUG] Using local port: {local_port}")
# Modify modified_args to use localhost:local_port
final_args = []
i = 0
while i < len(modified_args):
arg = modified_args[i]
if arg == '-P' and i + 1 < len(modified_args):
# Skip -P and the port value as it's for wssshd, not SCP
i += 2
continue
if ':' in arg:
host_part, path_part = arg.split(':', 1)
if '@' in host_part:
user, host_in_arg = host_part.split('@', 1)
if host_in_arg == host: # Only modify the first host
final_args.append(f"{user}@localhost:{path_part}")
else:
final_args.append(arg)
else:
if host_part == host: # Only modify the first host
final_args.append(f"localhost:{path_part}")
else:
final_args.append(arg)
else:
final_args.append(arg)
i += 1
# Add StrictHostKeyChecking=no and port argument for local tunnel at the beginning
final_args = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'LogLevel ERROR', '-P', str(local_port)] + final_args
if debug: print(f"[DEBUG] Final SCP args: {final_args}")
# Set up signal handling for clean exit
shutdown_event = asyncio.Event()
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)
# Run the async SCP wrapper
exit_code = asyncio.run(run_scp(
wssshd_host, # Use parsed wssshd_host as server_ip
scp_port, # Use -P port as wssshd_port
client_id,
local_port,
final_args,
args.interval, # Pass the interval parameter
shutdown_event # Pass the shutdown event
))
sys.exit(exit_code)
if __name__ == '__main__':
main()
\ No newline at end of file
#!/usr/bin/env python3
"""
WebSocket SSH (wsssh)
SSH wrapper that uses WebSocket tunnels through wssshd.
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import argparse
import asyncio
import ssl
import websockets
import json
import socket
import subprocess
import sys
import uuid
import configparser
import os
import signal
debug = False
async def handle_tunnel(ws, local_port, request_id, ready_event=None):
"""Handle tunnel data forwarding"""
try:
# Open local TCP listener
server = await asyncio.start_server(
lambda r, w: handle_local_connection(r, w, ws, request_id),
'localhost', local_port
)
if debug: print(f"[DEBUG] Listening on localhost:{local_port}")
# Signal that server is ready
if ready_event:
ready_event.set()
async with server:
await server.serve_forever()
except Exception as e:
if debug: print(f"[DEBUG] Tunnel handler error: {e}")
async def handle_local_connection(reader, writer, ws, request_id):
"""Handle connections from local SSH client"""
try:
# Forward data between local connection and WebSocket
async def forward_to_ws():
try:
while True:
data = await reader.read(1024)
if not data:
break
await ws.send(json.dumps({
"type": "tunnel_data",
"request_id": request_id,
"data": data.hex() # Use hex encoding for binary data
}))
except Exception as e:
if debug: print(f"[DEBUG] Local to WS error: {e}")
async def forward_from_ws():
try:
async for message in ws:
data = json.loads(message)
if data.get('type') == 'tunnel_data' and data.get('request_id') == request_id:
# Decode and forward data
tunnel_data = bytes.fromhex(data['data'])
writer.write(tunnel_data)
await writer.drain()
elif data.get('type') == 'tunnel_close' and data.get('request_id') == request_id:
break
except Exception as e:
if debug: print(f"[DEBUG] WS to local error: {e}")
await asyncio.gather(forward_to_ws(), forward_from_ws())
except Exception as e:
if debug: print(f"[DEBUG] Local connection error: {e}")
finally:
writer.close()
await writer.wait_closed()
async def run_ssh(server_ip, server_port, client_id, local_port, ssh_args, interval=30, shutdown_event=None):
"""Connect to wssshd and run SSH with reconnection support"""
uri = f"wss://{server_ip}:{server_port}"
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
request_id = str(uuid.uuid4())
# Initial connection with retry logic
websocket = None
max_initial_attempts = 3
initial_attempt = 0
while initial_attempt < max_initial_attempts and websocket is None:
try:
if debug and initial_attempt > 0:
print(f"[DEBUG] Initial connection attempt {initial_attempt + 1}/{max_initial_attempts}")
websocket = await websockets.connect(uri, ssl=ssl_context)
if debug: print("[DEBUG] Initial WebSocket connection established")
except Exception as e:
initial_attempt += 1
if initial_attempt < max_initial_attempts:
if debug: print(f"[DEBUG] Initial connection failed, waiting {interval} seconds...")
await asyncio.sleep(interval)
else:
print(f"Connection failed after {max_initial_attempts} attempts: {e}")
return 1
try:
# Set up ping/pong handlers for explicit handling
async def ping_handler(payload):
if debug: print(f"[DEBUG] Received ping: {payload}")
# Pong is sent automatically by websockets library
async def pong_handler(payload):
if debug: print(f"[DEBUG] Received pong: {payload}")
websocket.ping_handler = ping_handler
websocket.pong_handler = pong_handler
# Request tunnel
await websocket.send(json.dumps({
"type": "tunnel_request",
"client_id": client_id,
"request_id": request_id
}))
# Wait for acknowledgment
response = await websocket.recv()
data = json.loads(response)
if data.get('type') == 'tunnel_ack':
if debug: print(f"[DEBUG] Tunnel request acknowledged: {request_id}")
# Start tunnel handler (opens listening port)
ready_event = asyncio.Event()
tunnel_task = asyncio.create_task(handle_tunnel(websocket, local_port, request_id, ready_event))
# Wait for tunnel to be ready (listening port opened)
await ready_event.wait()
# Launch SSH with modified arguments
ssh_cmd = ['ssh'] + ssh_args
if debug: print(f"[DEBUG] Launching: {' '.join(ssh_cmd)}")
# Run SSH process
process = await asyncio.create_subprocess_exec(
*ssh_cmd,
stdin=sys.stdin,
stdout=sys.stdout,
stderr=sys.stderr
)
# Monitor both SSH process and WebSocket connection
tunnel_active = True
while tunnel_active:
# Check if SSH process is still running
if process.returncode is not None:
if debug: print(f"[DEBUG] SSH process finished with code: {process.returncode}")
tunnel_active = False
break
# Check for shutdown signal
if shutdown_event and shutdown_event.is_set():
if debug: print("[DEBUG] Shutdown signal received, terminating tunnel")
tunnel_active = False
break
# Check WebSocket connection and attempt reconnection if needed
try:
# Try to send a small message to test connection
await asyncio.wait_for(websocket.ping(), timeout=5.0)
except Exception as e:
if debug: print(f"[DEBUG] WebSocket connection lost, attempting reconnection...")
# Attempt WebSocket reconnection
reconnect_attempts = 0
max_reconnect_attempts = 3
reconnected = False
while reconnect_attempts < max_reconnect_attempts and not reconnected:
# Check for shutdown signal during reconnection
if shutdown_event and shutdown_event.is_set():
if debug: print("[DEBUG] Shutdown signal received during reconnection")
tunnel_active = False
break
try:
if debug: print(f"[DEBUG] WebSocket reconnection attempt {reconnect_attempts + 1}/{max_reconnect_attempts}")
# Create new WebSocket connection
new_websocket = await websockets.connect(uri, ssl=ssl_context)
new_websocket.ping_handler = ping_handler
new_websocket.pong_handler = pong_handler
# Re-request tunnel
await new_websocket.send(json.dumps({
"type": "tunnel_request",
"client_id": client_id,
"request_id": request_id
}))
# Wait for acknowledgment
response = await new_websocket.recv()
data = json.loads(response)
if data.get('type') == 'tunnel_ack':
# Update tunnel task with new WebSocket
tunnel_task.cancel()
tunnel_task = asyncio.create_task(handle_tunnel(new_websocket, local_port, request_id, None))
websocket = new_websocket
reconnected = True
if debug: print("[DEBUG] WebSocket reconnection successful")
else:
await new_websocket.close()
except Exception as reconnect_error:
reconnect_attempts += 1
if reconnect_attempts < max_reconnect_attempts:
if debug: print(f"[DEBUG] WebSocket reconnection failed, waiting 1 second...")
await asyncio.sleep(1) # Fast reconnection for WebSocket
else:
if debug: print(f"[DEBUG] All reconnection attempts failed: {reconnect_error}")
tunnel_active = False
break
if tunnel_active:
await asyncio.sleep(2) # Check every 2 seconds
# Wait for SSH to complete if still running
if process.returncode is None:
await process.wait()
# Close tunnel
try:
await websocket.send(json.dumps({
"type": "tunnel_close",
"request_id": request_id
}))
except Exception:
pass # WebSocket might already be closed
if tunnel_task and not tunnel_task.done():
tunnel_task.cancel()
elif data.get('type') == 'tunnel_error':
print(f"Error: {data.get('error', 'Unknown error')}")
return 1
except Exception as e:
print(f"Connection failed: {e}")
return 1
finally:
if websocket:
try:
await websocket.close()
except Exception:
pass
return 0
def parse_hostname(hostname, config_domain=None):
"""Parse hostname to extract CLIENT_ID and WSSSHD_HOST"""
# Split by dots to get client_id and wssshd_host
parts = hostname.split('.')
if len(parts) >= 2:
client_id = parts[0]
wssshd_host = '.'.join(parts[1:])
else:
# No domain, assume whole hostname is client_id
client_id = hostname
wssshd_host = config_domain if config_domain else 'localhost' # Default fallback
return client_id, wssshd_host
def main():
# Read config file
config = configparser.ConfigParser()
config_file = os.path.expanduser("~/.config/wsssh/wsssh.conf")
config_port = None
config_domain = None
if os.path.exists(config_file):
config.read(config_file)
if 'default' in config:
if 'port' in config['default']:
try:
config_port = int(config['default']['port'])
except ValueError:
pass
if 'domain' in config['default']:
config_domain = config['default']['domain']
parser = argparse.ArgumentParser(description='WebSocket SSH (wsssh)', add_help=False)
parser.add_argument('--local-port', type=int, default=0, help='Local port for tunnel (0 = auto)')
parser.add_argument('--interval', type=int, default=30, help='Connection retry interval in seconds (default: 30)')
parser.add_argument('--debug', action='store_true', help='Enable debug output')
# Parse our arguments first
args, remaining = parser.parse_known_args()
global debug
debug = args.debug
# Don't prepend 'ssh' - we'll handle it in execvp
modified_args = remaining
# Find the host argument and -p port option in modified args
host = None
ssh_port = None
for i, arg in enumerate(modified_args):
if arg == '-p' and i + 1 < len(modified_args):
try:
ssh_port = int(modified_args[i + 1])
except ValueError:
pass
elif not arg.startswith('-') and i > 0 and modified_args[i-1] in ['-h', '--host']:
host = arg
break
elif not arg.startswith('-') and '@' in arg:
# Handle user@host format
host = arg.split('@', 1)[1]
break
elif not arg.startswith('-') and i == 0:
# First non-option argument might be host
host = arg
break
if not host:
print("Error: Could not determine target host")
sys.exit(1)
if not ssh_port:
if config_port:
ssh_port = config_port
else:
print("Error: Could not determine wssshd port from -p option or config file")
sys.exit(1)
# Parse hostname to extract client_id and wssshd_host
client_id, wssshd_host = parse_hostname(host, config_domain)
if debug:
print(f"[DEBUG] Host: {host}")
print(f"[DEBUG] Client ID: {client_id}")
print(f"[DEBUG] WSSSHD Host: {wssshd_host}")
print(f"[DEBUG] WSSSHD Port: {ssh_port}")
# Find available local port
if args.local_port == 0:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('localhost', 0))
local_port = s.getsockname()[1]
else:
local_port = args.local_port
if debug: print(f"[DEBUG] Using local port: {local_port}")
# Modify modified_args to use localhost:local_port
final_args = []
skip_next = False
for i, arg in enumerate(modified_args):
if skip_next:
skip_next = False
continue
if arg in ['-h', '--host', '-p', '--port']:
skip_next = True
continue
elif '@' in arg and arg.split('@', 1)[1] == host:
# Replace user@host with user@localhost
user = arg.split('@', 1)[0]
final_args.append(f"{user}@localhost")
continue
elif arg == host:
final_args.append('localhost')
continue
else:
final_args.append(arg)
# Add StrictHostKeyChecking=no and port argument for local tunnel
final_args.extend(['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'LogLevel ERROR', '-p', str(local_port)])
if debug: print(f"[DEBUG] Final SSH args: {final_args}")
# Set up signal handling for clean exit
shutdown_event = asyncio.Event()
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)
# Run the async SSH wrapper
exit_code = asyncio.run(run_ssh(
wssshd_host, # Use parsed wssshd_host as server_ip
ssh_port, # Use -p port as wssshd_port
client_id,
local_port,
final_args,
args.interval, # Pass the interval parameter
shutdown_event # Pass the shutdown event
))
sys.exit(exit_code)
if __name__ == '__main__':
main()
\ No newline at end of file
#!/usr/bin/env python3
"""
WebSocket SSH Client (wssshc)
Connects to wssshd server and registers as a client.
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import argparse
import asyncio
import ssl
import websockets
import json
import socket
import configparser
import os
import signal
debug = False
# Active tunnels: request_id -> {'reader': tcp_reader, 'writer': tcp_writer}
active_tunnels = {}
async def forward_tcp_to_ws(tcp_reader, websocket, request_id):
try:
while True:
data = await tcp_reader.read(1024)
if not data:
break
if debug: print(f"[DEBUG] Sending tunnel_response, data len: {len(data)}, data: {data[:50].decode('utf-8', errors='replace') if data else 'empty'}, hex: {data.hex()[:100]}...")
await websocket.send(json.dumps({
"type": "tunnel_response",
"request_id": request_id,
"data": data.hex()
}))
except Exception as e:
if debug: print(f"[DEBUG] TCP to WS error: {e}")
finally:
# Cleanup tunnel
if request_id in active_tunnels:
del active_tunnels[request_id]
if debug: print(f"[DEBUG] Tunnel {request_id} closed")
async def connect_to_server(server_ip, port, client_id, password, interval, shutdown_event=None):
uri = f"wss://{server_ip}:{port}"
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
while True:
# Check for shutdown signal
if shutdown_event and shutdown_event.is_set():
if debug: print("[DEBUG] Shutdown signal received, exiting")
break
try:
async with websockets.connect(uri, ssl=ssl_context) as websocket:
# Register
await websocket.send(json.dumps({"type": "register", "id": client_id, "password": password}))
print(f"Connected and registered as {client_id}")
# Wait for registration acknowledgment
response = await websocket.recv()
data = json.loads(response)
if data.get('type') == 'registered':
print(f"Successfully registered as {client_id}")
elif data.get('type') == 'registration_error':
print(f"Registration failed: {data.get('error', 'Unknown error')}")
return # Exit and retry
async for message in websocket:
if debug: print(f"[DEBUG] WebSocket message: {message[:100]}...")
data = json.loads(message)
if data.get('type') == 'tunnel_request':
request_id = data['request_id']
if debug: print(f"[DEBUG] Tunnel request received: {request_id}")
try:
# Connect to local SSH server
tcp_reader, tcp_writer = await asyncio.open_connection('localhost', 22)
active_tunnels[request_id] = {'reader': tcp_reader, 'writer': tcp_writer}
# Start forwarding from TCP to WS
asyncio.create_task(forward_tcp_to_ws(tcp_reader, websocket, request_id))
if debug: print(f"[DEBUG] Connected to local SSH server for tunnel {request_id}")
except Exception as e:
if debug: print(f"[DEBUG] Tunnel setup error: {e}")
elif data.get('type') == 'tunnel_data':
request_id = data['request_id']
if request_id in active_tunnels:
# Forward data to local SSH
tunnel_data = bytes.fromhex(data['data'])
if debug: print(f"[DEBUG] Received tunnel_data, hex len: {len(data['data'])}, hex: {data['data'][:100]}..., decoded: {tunnel_data[:50].decode('utf-8', errors='replace') if tunnel_data else 'empty'}")
active_tunnels[request_id]['writer'].write(tunnel_data)
await active_tunnels[request_id]['writer'].drain()
elif data.get('type') == 'tunnel_close':
request_id = data['request_id']
if request_id in active_tunnels:
# Close tunnel
active_tunnels[request_id]['writer'].close()
await active_tunnels[request_id]['writer'].wait_closed()
del active_tunnels[request_id]
if debug: print(f"[DEBUG] Tunnel {request_id} closed")
except Exception as e:
print(f"Connection failed: {e}, retrying in {interval} seconds")
await asyncio.sleep(interval)
def load_config_file(config_path):
"""Load configuration from a specific file path"""
if not os.path.exists(config_path):
return {}
config = configparser.ConfigParser()
try:
config.read(config_path)
if 'wssshc' in config:
section = config['wssshc']
defaults = {}
for key in ['password', 'id']:
if key in section:
defaults[key] = section[key]
if 'server-ip' in section:
defaults['server_ip'] = section['server-ip']
elif 'domain' in section:
defaults['server_ip'] = section['domain']
if 'port' in section:
defaults['port'] = int(section['port'])
if 'interval' in section:
defaults['interval'] = int(section['interval'])
return defaults
except Exception as e:
if debug: print(f"[DEBUG] Error reading config file {config_path}: {e}")
return {}
def main():
# Configuration hierarchy:
# 1. System config (/etc/wssshc.conf)
# 2. User config (~/.config/wsssh/wssshc.conf)
# 3. Command line arguments (highest priority)
# Load system config first (lowest priority)
system_config = '/etc/wssshc.conf'
defaults = load_config_file(system_config)
# Load user config (medium priority, overrides system)
user_config = os.path.expanduser('~/.config/wsssh/wssshc.conf')
user_defaults = load_config_file(user_config)
defaults.update(user_defaults)
# Set default values if not set by config files
defaults.setdefault('interval', 30)
defaults.setdefault('port', 9898)
# Parse command line arguments (highest priority)
parser = argparse.ArgumentParser(description='WebSocket SSH Client (wssshc)')
parser.set_defaults(**defaults)
parser.add_argument('--config', help='Configuration file path (overrides default hierarchy)')
parser.add_argument('--server-ip', help='Server IP address')
parser.add_argument('--port', type=int, help='Server port')
parser.add_argument('--id', help='Client ID')
parser.add_argument('--password', help='Registration password')
parser.add_argument('--interval', type=int, help='Reconnect interval in seconds (default: 30)')
parser.add_argument('--debug', action='store_true', help='Enable debug output')
# Parse just the config argument first to check for custom config file
temp_parser = argparse.ArgumentParser(add_help=False)
temp_parser.add_argument('--config')
temp_args, remaining = temp_parser.parse_known_args()
# If --config is specified, load that file instead of the hierarchy
if temp_args.config:
custom_defaults = load_config_file(temp_args.config)
defaults.update(custom_defaults)
parser.set_defaults(**defaults)
args = parser.parse_args()
args = parser.parse_args()
# Check required arguments
if not args.server_ip:
parser.error('--server-ip is required')
if not args.id:
parser.error('--id is required')
if not args.password:
parser.error('--password is required')
global debug
debug = args.debug
# Set up signal handling for clean exit
shutdown_event = asyncio.Event()
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)
asyncio.run(connect_to_server(args.server_ip, args.port, args.id, args.password, args.interval, shutdown_event))
if __name__ == '__main__':
main()
\ No newline at end of file
...@@ -52,10 +52,7 @@ override_dh_auto_install: ...@@ -52,10 +52,7 @@ override_dh_auto_install:
install -m 644 debian/wssshc.service debian/wsssh-tools/lib/systemd/system/ install -m 644 debian/wssshc.service debian/wsssh-tools/lib/systemd/system/
# Install Python scripts # Install Python scripts
install -m 755 ../wssshc.py debian/wsssh-tools/usr/bin/
install -m 755 ../wssshd.py debian/wsssh-tools/usr/bin/ install -m 755 ../wssshd.py debian/wsssh-tools/usr/bin/
install -m 755 ../wsssh.py debian/wsssh-tools/usr/bin/
install -m 755 ../wsscp.py debian/wsssh-tools/usr/bin/
override_dh_auto_clean: override_dh_auto_clean:
make clean make clean
......
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