Version 1.6.2: Tunnel Close Forwarding & Web Terminal Enhancements

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

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

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

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

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

Technical Details:
- Server-side tunnel close message routing in websocket.py
- JavaScript library management with robust error recovery
- Cross-browser fullscreen API implementation
- Asset management for both development and production environments
parent b3ef97b4
...@@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file. ...@@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.6.2] - 2025-09-19
### Fixed
- **Tunnel Close Forwarding**: Critical fix for tunnel closure synchronization between wssshc, server, and wsssht
- Server now properly forwards `tunnel_close` messages from wssshc to wsssht
- Prevents wsssht processes from hanging after client-initiated tunnel closure
- Ensures proper cleanup of tunnel resources on all endpoints
- Fixed server-side tunnel close message routing in `wsssd/websocket.py`
- **Web Terminal JavaScript Issues**: Comprehensive fixes for web interface terminal functionality
- Fixed `FitAddon` loading issues by switching from jsdelivr to unpkg CDN
- Resolved JavaScript variable scoping error (`fitAddon` undefined) in terminal initialization
- Added proper error handling for xterm.js library loading failures
- Enhanced debug logging for library loading and terminal initialization
- Improved cross-browser compatibility for xterm.js components
- **Logo Serving Path**: Fixed web interface logo loading from correct directory
- Updated Flask routes to serve logo files from `logos/` directory instead of root
- Added proper PyInstaller support for bundled logo assets
- Fixed favicon.ico and image.jpg serving for both development and frozen executables
### Added
- **Fullscreen Terminal Support**: Enhanced web terminal with fullscreen toggle functionality
- Added fullscreen toggle button (⛶/⛝) to terminal interface header
- Cross-browser fullscreen API support (Chrome, Firefox, Safari, IE11)
- Automatic terminal resizing when entering/exiting fullscreen mode
- Backend synchronization of terminal dimensions during fullscreen changes
- Proper event handling for fullscreen state changes
### Enhanced
- **Web Terminal User Experience**: Improved terminal interface with better controls and responsiveness
- Added fullscreen button with dynamic icon changes based on state
- Enhanced terminal resizing with proper backend dimension updates
- Improved error handling and user feedback for terminal operations
- Better visual feedback for terminal state changes
### Technical Details
- **Tunnel Close Protocol**: Server now correctly forwards tunnel close messages to both endpoints
- **JavaScript Library Management**: Robust xterm.js loading with fallback CDN and error recovery
- **Fullscreen API**: Comprehensive browser compatibility with proper event handling
- **Asset Management**: Proper static file serving for both development and production environments
## [1.6.1] - 2025-09-18 ## [1.6.1] - 2025-09-18
### Changed ### Changed
......
...@@ -658,6 +658,12 @@ python3 -m pytest tests/integration/ ...@@ -658,6 +658,12 @@ python3 -m pytest tests/integration/
## Recent Updates ## Recent Updates
### Version 1.6.2
- **Tunnel Close Forwarding**: Critical fix for tunnel closure synchronization between wssshc, server, and wsssht
- **Web Terminal JavaScript Fixes**: Comprehensive fixes for web interface terminal functionality
- **Fullscreen Terminal Support**: Enhanced web terminal with fullscreen toggle functionality
- **Logo Serving Path**: Fixed web interface logo loading from correct directory
### Version 1.6.1 ### Version 1.6.1
- **Major Code Refactoring**: Complete modularization of wsssht.c - **Major Code Refactoring**: Complete modularization of wsssht.c
- **Operating Modes**: Multiple operating modes implementation - **Operating Modes**: Multiple operating modes implementation
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
- **Drop-in SSH/SCP Replacement**: Use `wsssh` and `wsscp` as direct replacements for standard SSH/SCP commands - **Drop-in SSH/SCP Replacement**: Use `wsssh` and `wsscp` as direct replacements for standard SSH/SCP commands
- **SSL/TLS Encryption**: All communications are fully encrypted - **SSL/TLS Encryption**: All communications are fully encrypted
- **Multi-client Support**: Route connections to different registered clients - **Multi-client Support**: Route connections to different registered clients
- **Professional Web Interface**: Admin panel with user management and HTML5 terminal - **Professional Web Interface**: Admin panel with user management, HTML5 terminal, and fullscreen support
- **Cross-platform Compatibility**: Works on Linux, macOS, and Windows - **Cross-platform Compatibility**: Works on Linux, macOS, and Windows
- **Debian Packaging**: Easy installation with proper system integration - **Debian Packaging**: Easy installation with proper system integration
- **Service Management**: Complete init scripts and watchdog monitoring - **Service Management**: Complete init scripts and watchdog monitoring
......
...@@ -7,8 +7,29 @@ ...@@ -7,8 +7,29 @@
<link rel="icon" href="/image.jpg" type="image/x-icon"> <link rel="icon" href="/image.jpg" type="image/x-icon">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css"> <link rel="stylesheet" href="https://unpkg.com/xterm@5.3.0/css/xterm.css">
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script> <script src="https://unpkg.com/xterm@5.3.0/lib/xterm.js"></script>
<script src="https://unpkg.com/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
<script>
// Ensure libraries are loaded
function checkLibraries() {
if (typeof Terminal === 'undefined') {
console.error('Terminal not loaded from CDN');
return false;
}
if (typeof FitAddon === 'undefined') {
console.error('FitAddon not loaded from CDN');
return false;
}
console.log('All xterm libraries loaded successfully');
return true;
}
// Check immediately and after a delay
if (!checkLibraries()) {
setTimeout(checkLibraries, 1000);
}
</script>
<style> <style>
.navbar-brand { .navbar-brand {
font-weight: bold; font-weight: bold;
......
This diff is collapsed.
""" """
WSSSH Daemon (wssshd) - Modular implementation WSSSH Daemon (wssshd) - Modular implementation
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from .server import main from .server import main
......
""" """
Entry point for running wssshd as a module Entry point for running wssshd as a module
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from .server import main from .server import main
......
""" """
Configuration handling for wssshd Configuration handling for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import argparse import argparse
......
""" """
Main server logic for wssshd Main server logic for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import asyncio import asyncio
......
""" """
Terminal and PTY handling for wssshd Terminal and PTY handling for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os import os
...@@ -86,6 +101,12 @@ def create_terminal_session(args, username, client_id): ...@@ -86,6 +101,12 @@ def create_terminal_session(args, username, client_id):
request_id = str(uuid.uuid4()) request_id = str(uuid.uuid4())
# Force echo mode before launching wsssh # Force echo mode before launching wsssh
command = ['sh', '-c', f'stty echo && wsssh -p {args.port} {username}@{client_id}.{args.domain}'] command = ['sh', '-c', f'stty echo && wsssh -p {args.port} {username}@{client_id}.{args.domain}']
# Debug output for the command being launched
if hasattr(args, 'debug') and args.debug:
print(f"[DEBUG] [Terminal] Launching command: {' '.join(command)}")
print(f"[DEBUG] [Terminal] Request ID: {request_id}")
print(f"[DEBUG] [Terminal] Username: {username}, Client ID: {client_id}, Domain: {args.domain}")
# Spawn wsssh process with pty using fallback method # Spawn wsssh process with pty using fallback method
master, slave = openpty_with_fallback() master, slave = openpty_with_fallback()
slave_name = os.ttyname(slave) slave_name = os.ttyname(slave)
......
""" """
Tunnel object management for wssshd Tunnel object management for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import time import time
...@@ -67,6 +82,10 @@ class Tunnel: ...@@ -67,6 +82,10 @@ class Tunnel:
self.last_keepalive_from_client = time.time() # wssshc endpoint self.last_keepalive_from_client = time.time() # wssshc endpoint
self.last_keepalive_from_tool = time.time() # wsssht/wsscp endpoint self.last_keepalive_from_tool = time.time() # wsssht/wsscp endpoint
# Keep-alive forwarding failure counters
self.keepalive_forward_failures = 0 # Consecutive forwarding failures
self.keepalive_ack_forward_failures = 0 # Consecutive ACK forwarding failures
def update_status(self, new_status, error_message=None): def update_status(self, new_status, error_message=None):
"""Update tunnel status and timestamp""" """Update tunnel status and timestamp"""
self.status = new_status self.status = new_status
......
""" """
Flask web interface for wssshd Flask web interface for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os import os
...@@ -158,9 +173,13 @@ def delete_user(user_id): ...@@ -158,9 +173,13 @@ def delete_user(user_id):
@app.route('/terminal/<client_id>') @app.route('/terminal/<client_id>')
@login_required @login_required
def terminal(client_id): def terminal(client_id):
print(f"[DEBUG] [Web] Terminal page requested for client {client_id}")
print(f"[DEBUG] [Web] Available clients: {list(clients.keys())}")
if client_id not in clients: if client_id not in clients:
print(f"[ERROR] [Web] Client '{client_id}' not found, redirecting to index")
flash('Client not connected') flash('Client not connected')
return redirect(url_for('index')) return redirect(url_for('index'))
print(f"[DEBUG] [Web] Rendering terminal template for client {client_id}")
return render_template('terminal.html', client_id=client_id) return render_template('terminal.html', client_id=client_id)
...@@ -179,36 +198,87 @@ def get_clients(): ...@@ -179,36 +198,87 @@ def get_clients():
}) })
@app.route('/favicon.ico')
def favicon():
if getattr(sys, 'frozen', False):
# Running as bundled executable
logos_dir = os.path.join(sys._MEIPASS, 'logos')
else:
# Running as script
logos_dir = 'logos'
return send_from_directory(logos_dir, 'favicon.ico')
@app.route('/image.jpg') @app.route('/image.jpg')
def logo_file(): def logo_file():
return send_from_directory('.', 'image.jpg') if getattr(sys, 'frozen', False):
# Running as bundled executable
logos_dir = os.path.join(sys._MEIPASS, 'logos')
else:
# Running as script
logos_dir = 'logos'
return send_from_directory(logos_dir, 'logo-128.png')
@app.route('/terminal/<client_id>/connect', methods=['POST']) @app.route('/terminal/<client_id>/connect', methods=['POST'])
@login_required @login_required
def connect_terminal(client_id): def connect_terminal(client_id):
print(f"[DEBUG] [Web] === TERMINAL CONNECT REQUEST START ===")
print(f"[DEBUG] [Web] Raw request method: {request.method}")
print(f"[DEBUG] [Web] Raw request URL: {request.url}")
print(f"[DEBUG] [Web] Raw request data: {request.get_data(as_text=True)}")
print(f"[DEBUG] [Web] Current user authenticated: {current_user.is_authenticated}")
if hasattr(current_user, 'username'):
print(f"[DEBUG] [Web] Current user: {current_user.username}")
from .config import load_config from .config import load_config
args = load_config() args = load_config()
username = request.form.get('username', 'root') username = request.form.get('username', 'root')
cols = int(request.form.get('cols', 80)) cols = int(request.form.get('cols', 80))
rows = int(request.form.get('rows', 24)) rows = int(request.form.get('rows', 24))
terminal_session = create_terminal_session(args, username, client_id) print(f"[DEBUG] [Web] Terminal connect request received for client {client_id}")
request_id = terminal_session['request_id'] print(f"[DEBUG] [Web] Parameters: username={username}, cols={cols}, rows={rows}")
print(f"[DEBUG] [Web] Available clients: {list(clients.keys())}")
print(f"[DEBUG] [Web] Client '{client_id}' in clients: {client_id in clients}")
if client_id not in clients:
print(f"[ERROR] [Web] Client '{client_id}' not found in connected clients")
return jsonify({'error': f'Client {client_id} not connected'}), 404
# Store terminal session if hasattr(args, 'debug') and args.debug:
active_terminals[request_id] = { print(f"[DEBUG] [Web] Creating terminal session for client {client_id}, username {username}")
'client_id': client_id,
'username': username,
'proc': terminal_session['proc'],
'output_buffer': terminal_session['output_buffer'],
'master': terminal_session['master']
}
return jsonify({ try:
'request_id': request_id, terminal_session = create_terminal_session(args, username, client_id)
'command': terminal_session['command'] request_id = terminal_session['request_id']
})
print(f"[DEBUG] [Web] Terminal session created successfully with request_id {request_id}")
# Store terminal session
active_terminals[request_id] = {
'client_id': client_id,
'username': username,
'proc': terminal_session['proc'],
'output_buffer': terminal_session['output_buffer'],
'master': terminal_session['master']
}
if hasattr(args, 'debug') and args.debug:
print(f"[DEBUG] [Web] Terminal session stored with request_id {request_id}")
response_data = {
'request_id': request_id,
'command': terminal_session['command']
}
print(f"[DEBUG] [Web] Returning response: {response_data}")
print(f"[DEBUG] [Web] === TERMINAL CONNECT REQUEST END ===")
return jsonify(response_data)
except Exception as e:
print(f"[ERROR] [Web] Failed to create terminal session: {e}")
import traceback
traceback.print_exc()
return jsonify({'error': str(e)}), 500
@app.route('/terminal/<client_id>/data', methods=['GET', 'POST']) @app.route('/terminal/<client_id>/data', methods=['GET', 'POST'])
......
""" """
WebSocket handling for wssshd WebSocket handling for wssshd
Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import asyncio import asyncio
...@@ -22,7 +37,7 @@ TUNNEL_ACK_MSG = '{"type": "tunnel_ack", "request_id": "%s"}' ...@@ -22,7 +37,7 @@ TUNNEL_ACK_MSG = '{"type": "tunnel_ack", "request_id": "%s"}'
TUNNEL_CLOSE_MSG = '{"type": "tunnel_close", "request_id": "%s"}' TUNNEL_CLOSE_MSG = '{"type": "tunnel_close", "request_id": "%s"}'
TUNNEL_REQUEST_MSG = '{"type": "tunnel_request", "request_id": "%s"}' TUNNEL_REQUEST_MSG = '{"type": "tunnel_request", "request_id": "%s"}'
TUNNEL_ERROR_MSG = '{"type": "tunnel_error", "request_id": "%s", "error": "%s"}' TUNNEL_ERROR_MSG = '{"type": "tunnel_error", "request_id": "%s", "error": "%s"}'
TUNNEL_KEEPALIVE_MSG = '{"type": "tunnel_keepalive", "request_id": "%s", "total_bytes": %llu, "rate_bps": %.2f}' TUNNEL_KEEPALIVE_MSG = '{"type": "tunnel_keepalive", "request_id": "%s", "total_bytes": %d, "rate_bps": %.2f}'
TUNNEL_KEEPALIVE_ACK_MSG = '{"type": "tunnel_keepalive_ack", "request_id": "%s"}' TUNNEL_KEEPALIVE_ACK_MSG = '{"type": "tunnel_keepalive_ack", "request_id": "%s"}'
REGISTERED_MSG = '{"type": "registered", "id": "%s"}' REGISTERED_MSG = '{"type": "registered", "id": "%s"}'
REGISTRATION_ERROR_MSG = '{"type": "registration_error", "error": "%s"}' REGISTRATION_ERROR_MSG = '{"type": "registration_error", "error": "%s"}'
...@@ -332,16 +347,31 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_ ...@@ -332,16 +347,31 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_
# Update tunnel status to closing # Update tunnel status to closing
tunnel.update_status(TunnelStatus.CLOSING) tunnel.update_status(TunnelStatus.CLOSING)
# Forward close to client if still active # Forward close to the other endpoint (bidirectional)
client_info = clients.get(tunnel.client_id) # If message comes from client (wssshc), forward to wsssht
if client_info and client_info['status'] == 'active': # If message comes from wsssht, forward to client (wssshc)
try:
close_msg = TUNNEL_CLOSE_MSG % request_id if websocket == tunnel.client_ws:
if debug_flag: print(f"[DEBUG] [WebSocket] Sending tunnel close to client: {close_msg}") # Message from wssshc, forward to wsssht
await tunnel.client_ws.send(close_msg) if tunnel.wsssh_ws:
except Exception: try:
# Silent failure for performance close_msg = TUNNEL_CLOSE_MSG % request_id
pass if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding tunnel close from wssshc to wsssht: {close_msg}")
await tunnel.wsssh_ws.send(close_msg)
except Exception:
# Silent failure for performance
pass
elif websocket == tunnel.wsssh_ws:
# Message from wsssht, forward to wssshc
client_info = clients.get(tunnel.client_id)
if client_info and client_info['status'] == 'active':
try:
close_msg = TUNNEL_CLOSE_MSG % request_id
if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding tunnel close from wsssht to wssshc: {close_msg}")
await tunnel.client_ws.send(close_msg)
except Exception:
# Silent failure for performance
pass
# Update tunnel status to closed and clean up # Update tunnel status to closed and clean up
tunnel.update_status(TunnelStatus.CLOSED) tunnel.update_status(TunnelStatus.CLOSED)
...@@ -386,8 +416,34 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_ ...@@ -386,8 +416,34 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_
keepalive_msg = TUNNEL_KEEPALIVE_MSG % (request_id, total_bytes, rate_bps) keepalive_msg = TUNNEL_KEEPALIVE_MSG % (request_id, total_bytes, rate_bps)
if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding keep-alive to {forward_name}: {keepalive_msg}") if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding keep-alive to {forward_name}: {keepalive_msg}")
await forward_ws.send(keepalive_msg) await forward_ws.send(keepalive_msg)
# Reset failure counter on successful forward
tunnel.keepalive_forward_failures = 0
except Exception as e: except Exception as e:
if debug_flag: print(f"[DEBUG] [WebSocket] Failed to forward keep-alive: {e}") if debug_flag: print(f"[DEBUG] [WebSocket] Failed to forward keep-alive: {e}")
# Increment failure counter
tunnel.keepalive_forward_failures += 1
if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive forward failure count: {tunnel.keepalive_forward_failures}")
# Close tunnel if 3 consecutive failures
if tunnel.keepalive_forward_failures >= 3:
if debug_flag: print(f"[DEBUG] [WebSocket] Closing tunnel {request_id} due to 3 consecutive keep-alive forwarding failures")
# Send close messages to both ends
try:
close_msg = TUNNEL_CLOSE_MSG % request_id
if tunnel.client_ws:
await tunnel.client_ws.send(close_msg)
if tunnel.wsssh_ws:
await tunnel.wsssh_ws.send(close_msg)
except Exception:
pass # Silent failure for cleanup
# Update tunnel status and clean up
tunnel.update_status(TunnelStatus.CLOSED, "Keep-alive forwarding failed 3 times")
if request_id in active_tunnels:
del active_tunnels[request_id]
if not debug:
print(f"[EVENT] Tunnel {request_id} closed due to keep-alive forwarding failures")
continue
# Send ACK response back to sender # Send ACK response back to sender
try: try:
...@@ -422,8 +478,34 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_ ...@@ -422,8 +478,34 @@ async def handle_websocket(websocket, path=None, *, server_password=None, debug_
ack_msg = TUNNEL_KEEPALIVE_ACK_MSG % request_id ack_msg = TUNNEL_KEEPALIVE_ACK_MSG % request_id
if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding keep-alive ACK to {forward_name}: {ack_msg}") if debug_flag: print(f"[DEBUG] [WebSocket] Forwarding keep-alive ACK to {forward_name}: {ack_msg}")
await forward_ws.send(ack_msg) await forward_ws.send(ack_msg)
# Reset failure counter on successful forward
tunnel.keepalive_ack_forward_failures = 0
except Exception as e: except Exception as e:
if debug_flag: print(f"[DEBUG] [WebSocket] Failed to forward keep-alive ACK: {e}") if debug_flag: print(f"[DEBUG] [WebSocket] Failed to forward keep-alive ACK: {e}")
# Increment failure counter
tunnel.keepalive_ack_forward_failures += 1
if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive ACK forward failure count: {tunnel.keepalive_ack_forward_failures}")
# Close tunnel if 3 consecutive failures
if tunnel.keepalive_ack_forward_failures >= 3:
if debug_flag: print(f"[DEBUG] [WebSocket] Closing tunnel {request_id} due to 3 consecutive keep-alive ACK forwarding failures")
# Send close messages to both ends
try:
close_msg = TUNNEL_CLOSE_MSG % request_id
if tunnel.client_ws:
await tunnel.client_ws.send(close_msg)
if tunnel.wsssh_ws:
await tunnel.wsssh_ws.send(close_msg)
except Exception:
pass # Silent failure for cleanup
# Update tunnel status and clean up
tunnel.update_status(TunnelStatus.CLOSED, "Keep-alive ACK forwarding failed 3 times")
if request_id in active_tunnels:
del active_tunnels[request_id]
if not debug:
print(f"[EVENT] Tunnel {request_id} closed due to keep-alive ACK forwarding failures")
continue
else: else:
if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive ACK received for unknown tunnel {request_id}") if debug_flag: print(f"[DEBUG] [WebSocket] Keep-alive ACK received for unknown tunnel {request_id}")
except websockets.exceptions.ConnectionClosed: except websockets.exceptions.ConnectionClosed:
......
wsssh-server (1.6.1-1) unstable; urgency=medium
* Version 1.6.1: Major code refactoring and documentation updates
* Complete modularization of wsssht.c into libwsssht/ components
* Split monolithic 2769-line wsssht.c into modular structure:
- utils.h/c: Utility functions (print_usage, parse_connection_string, parse_args)
- modes.h/c: Operating mode implementations (bridge, script, daemon modes)
- threads.h/c: Thread-related functions and structures
- wsssht.h: Main header with includes and declarations
* Reduced main wsssht.c from 2769 to 674 lines (75% size reduction)
* Updated configure.sh and Makefile for new modular structure
* Maintained 100% backward compatibility and functionality
* Improved developer experience with logical code grouping
* Easier debugging, testing, and feature development
* Complete documentation rewrite and README update
* Updated project description from WebSocket-based to universal tunneling system
* Enhanced README.md with comprehensive features and installation guide
* Rewritten DOCUMENTATION.md with detailed technical specifications
* Updated TODO.md with current project status and future enhancements
* Ensured all command-line examples are accurate and exclude hidden options
* Maintained consistency across all documentation files
-- Stefy Lanza <stefy@nexlab.net> Wed, 18 Sep 2025 08:47:00 +0200
wsssh-server (1.4.4-1) unstable; urgency=medium wsssh-server (1.4.4-1) unstable; urgency=medium
* New upstream release 1.4.4 * New upstream release 1.4.4
......
This diff is collapsed.
/*
* WSSSH Library - Control Channel Messages
*
* Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef CONTROL_MESSAGES_H
#define CONTROL_MESSAGES_H
#include <openssl/ssl.h>
#include "tunnel.h"
// Function declarations for control channel messages
int send_json_message(SSL *ssl, const char *type, const char *client_id, const char *request_id);
int send_registration_message(SSL *ssl, const char *client_id, const char *password, const char *tunnel, const char *tunnel_control, const char *wssshd_private_ip);
int send_tunnel_request_message(SSL *ssl, const char *client_id, const char *request_id, const char *tunnel, const char *tunnel_control, const char *service);
int send_tunnel_close_message(SSL *ssl, const char *request_id, int debug);
int send_tunnel_keepalive_message(SSL *ssl, tunnel_t *tunnel, int debug);
int send_tunnel_keepalive_ack_message(SSL *ssl, const char *request_id, int debug);
int send_ping_frame(SSL *ssl, const char *ping_payload, int payload_len);
int send_pong_frame(SSL *ssl, const char *ping_payload, int payload_len);
#endif // CONTROL_MESSAGES_H
\ No newline at end of file
/*
* WSSSH Library - Data Channel Messages Implementation
*
* Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "data_messages.h"
#include "websocket.h"
#include "tunnel.h"
#include <openssl/ssl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int send_tunnel_data_message(SSL *ssl, const char *request_id, const char *data_hex, int debug) {
// Send as tunnel_data
size_t msg_size = strlen("{\"type\":\"tunnel_data\",\"request_id\":\"\",\"data\":\"\"}") + strlen(request_id) + strlen(data_hex) + 1;
char *message = malloc(msg_size);
if (!message) {
if (debug) {
printf("[DEBUG] Failed to allocate memory for tunnel_data message (%zu bytes)\n", msg_size);
fflush(stdout);
}
return 0;
}
snprintf(message, msg_size,
"{\"type\":\"tunnel_data\",\"request_id\":\"%s\",\"data\":\"%s\"}",
request_id, data_hex);
if (!send_websocket_frame(ssl, message)) {
if (debug) {
printf("[DEBUG] Failed to send tunnel_data WebSocket frame\n");
fflush(stdout);
}
free(message);
return 0;
}
free(message);
return 1;
}
int send_tunnel_response_message(SSL *ssl, const char *request_id, const char *data_hex, int debug) {
// Send as tunnel_response (from target back to WebSocket)
size_t msg_size = strlen("{\"type\":\"tunnel_response\",\"request_id\":\"\",\"data\":\"\"}") + strlen(request_id) + strlen(data_hex) + 1;
char *message = malloc(msg_size);
if (!message) {
if (debug) {
printf("[DEBUG] Failed to allocate memory for tunnel_response message (%zu bytes)\n", msg_size);
fflush(stdout);
}
return 0;
}
snprintf(message, msg_size,
"{\"type\":\"tunnel_response\",\"request_id\":\"%s\",\"data\":\"%s\"}",
request_id, data_hex);
if (!send_websocket_frame(ssl, message)) {
if (debug) {
printf("[DEBUG] Failed to send tunnel_response WebSocket frame\n");
fflush(stdout);
}
free(message);
return 0;
}
free(message);
return 1;
}
\ No newline at end of file
/*
* WSSSH Library - Data Channel Messages
*
* Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef DATA_MESSAGES_H
#define DATA_MESSAGES_H
#include <openssl/ssl.h>
// Function declarations for data channel messages
int send_tunnel_data_message(SSL *ssl, const char *request_id, const char *data_hex, int debug);
int send_tunnel_response_message(SSL *ssl, const char *request_id, const char *data_hex, int debug);
#endif // DATA_MESSAGES_H
\ No newline at end of file
...@@ -43,6 +43,9 @@ ...@@ -43,6 +43,9 @@
#include "control_messages.h" #include "control_messages.h"
#include "data_messages.h" #include "data_messages.h"
// External signal handling variables from wsssht.c
extern volatile sig_atomic_t graceful_shutdown;
int run_bridge_mode(wsssh_config_t *config, const char *client_id, const char *wssshd_host, int wssshd_port) { int run_bridge_mode(wsssh_config_t *config, const char *client_id, const char *wssshd_host, int wssshd_port) {
// Bridge mode: Pure transport layer - no tunnel setup, just WebSocket transport // Bridge mode: Pure transport layer - no tunnel setup, just WebSocket transport
if (config->debug) { if (config->debug) {
...@@ -813,6 +816,23 @@ int run_pipe_mode(wsssh_config_t *config, const char *client_id, const char *wss ...@@ -813,6 +816,23 @@ int run_pipe_mode(wsssh_config_t *config, const char *client_id, const char *wss
static int frame_buffer_used = 0; static int frame_buffer_used = 0;
while (1) { while (1) {
// Check for graceful shutdown signal
if (graceful_shutdown) {
fprintf(stderr, "Sending tunnel_close on graceful shutdown...\n");
if (config->debug) {
fprintf(stderr, "[DEBUG] Graceful shutdown requested, sending tunnel_close\n");
}
// Send tunnel_close before exiting
send_tunnel_close(ws_ssl, request_id, config->debug);
fprintf(stderr, "Tunnel close message sent\n");
// Small delay to allow parent process (scp) to finish cleanly
// This prevents "Broken pipe" errors from scp trying to write after we exit
usleep(200000); // 200ms delay
break;
}
// Check tunnel status // Check tunnel status
pthread_mutex_lock(&tunnel_mutex); pthread_mutex_lock(&tunnel_mutex);
int tunnel_active = pipe_tunnel && pipe_tunnel->active; int tunnel_active = pipe_tunnel && pipe_tunnel->active;
...@@ -842,11 +862,17 @@ int run_pipe_mode(wsssh_config_t *config, const char *client_id, const char *wss ...@@ -842,11 +862,17 @@ int run_pipe_mode(wsssh_config_t *config, const char *client_id, const char *wss
ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer)); ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer));
if (bytes_read <= 0) { if (bytes_read <= 0) {
// EOF or error on stdin // EOF or error on stdin
fprintf(stderr, "EOF detected on stdin, sending tunnel_close...\n");
if (config->debug) { if (config->debug) {
fprintf(stderr, "[DEBUG] EOF on stdin, sending tunnel_close\n"); fprintf(stderr, "[DEBUG] EOF on stdin, sending tunnel_close\n");
} }
// Send tunnel_close before exiting // Send tunnel_close before exiting
send_tunnel_close(ws_ssl, request_id, config->debug); send_tunnel_close(ws_ssl, request_id, config->debug);
fprintf(stderr, "Tunnel close message sent\n");
// Small delay to allow parent process (scp) to finish cleanly
usleep(500000); // 500ms delay
break; break;
} }
......
/* /*
* WebSocket SCP (wsscp) - C Implementation * WSSSH SCP (wsscp) - C Implementation
* SCP wrapper with WebSocket ProxyCommand support. * SCP Wrapper with wsssht ProxyCommand support.
* *
* Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me * Copyright (C) 2024 Stefy Lanza <stefy@nexlab.net> and SexHack.me
* *
......
...@@ -35,7 +35,7 @@ void print_wsssh_usage(const char *program_name) { ...@@ -35,7 +35,7 @@ void print_wsssh_usage(const char *program_name) {
fprintf(stderr, " --help Show this help\n"); fprintf(stderr, " --help Show this help\n");
fprintf(stderr, " --clientid ID Client ID of the registered wssshc endpoint\n"); fprintf(stderr, " --clientid ID Client ID of the registered wssshc endpoint\n");
fprintf(stderr, " --wssshd-host HOST wssshd relay host\n"); fprintf(stderr, " --wssshd-host HOST wssshd relay host\n");
fprintf(stderr, " --wssshd-port PORT wssshd relay websocket port (default: 9898)\n"); fprintf(stderr, " -p, --wssshd-port PORT wssshd relay websocket port (default: 9898)\n");
fprintf(stderr, " --debug Enable debug output\n"); fprintf(stderr, " --debug Enable debug output\n");
fprintf(stderr, " --tunnel TRANSPORT Select data channel transport (comma-separated or 'any')\n"); fprintf(stderr, " --tunnel TRANSPORT Select data channel transport (comma-separated or 'any')\n");
fprintf(stderr, " --tunnel-control TRANSPORT Select control channel transport (comma-separated or 'any')\n"); fprintf(stderr, " --tunnel-control TRANSPORT Select control channel transport (comma-separated or 'any')\n");
...@@ -69,7 +69,7 @@ int parse_wsssh_args(int argc, char *argv[], wsssh_config_t *config) { ...@@ -69,7 +69,7 @@ int parse_wsssh_args(int argc, char *argv[], wsssh_config_t *config) {
} else if (strcmp(argv[i], "--wssshd-host") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--wssshd-host") == 0 && i + 1 < argc) {
config->wssshd_host = strdup(argv[i + 1]); config->wssshd_host = strdup(argv[i + 1]);
i++; i++;
} else if (strcmp(argv[i], "--wssshd-port") == 0 && i + 1 < argc) { } else if ((strcmp(argv[i], "--wssshd-port") == 0 || strcmp(argv[i], "-p") == 0) && i + 1 < argc) {
config->wssshd_port = atoi(argv[i + 1]); config->wssshd_port = atoi(argv[i + 1]);
config->wssshd_port_explicit = 1; config->wssshd_port_explicit = 1;
i++; i++;
......
...@@ -45,20 +45,46 @@ ...@@ -45,20 +45,46 @@
volatile sig_atomic_t sigint_count = 0; volatile sig_atomic_t sigint_count = 0;
volatile sig_atomic_t graceful_shutdown = 0; volatile sig_atomic_t graceful_shutdown = 0;
volatile sig_atomic_t force_exit = 0;
volatile sig_atomic_t tunnel_close_sent = 0;
void sigint_handler(int sig __attribute__((unused))) { void signal_handler(int sig __attribute__((unused))) {
sigint_count++; sigint_count++;
if (sigint_count == 1) { if (sigint_count == 1) {
fprintf(stderr, "\nReceived SIGINT, attempting graceful shutdown...\n"); fprintf(stderr, "\nReceived signal %d, attempting graceful shutdown...\n", sig);
fflush(stderr); fflush(stderr);
graceful_shutdown = 1; graceful_shutdown = 1;
} else if (sigint_count >= 2) { } else if (sigint_count >= 2) {
fprintf(stderr, "\nReceived second SIGINT, exiting immediately...\n"); fprintf(stderr, "\nReceived second signal, exiting immediately...\n");
fflush(stderr); fflush(stderr);
force_exit = 1;
exit(1); exit(1);
} }
} }
void cleanup_on_exit(void) {
// Only attempt cleanup if we haven't been forced to exit and haven't already sent tunnel_close
if (!force_exit && !tunnel_close_sent) {
pthread_mutex_lock(&tunnel_mutex);
if (active_tunnel && active_tunnel->active && active_tunnel->ssl) {
fprintf(stderr, "Sending tunnel_close on exit...\n");
send_tunnel_close(active_tunnel->ssl, active_tunnel->request_id, 0); // debug=0 for cleaner output
tunnel_close_sent = 1;
// Wait up to 3 seconds for tunnel close to complete, but allow interruption
time_t shutdown_start = time(NULL);
while (time(NULL) - shutdown_start < 3 && !force_exit) {
if (!active_tunnel->active) {
fprintf(stderr, "Tunnel closed gracefully\n");
break;
}
usleep(100000); // Sleep 100ms before checking again
}
}
pthread_mutex_unlock(&tunnel_mutex);
}
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
// Read config from wsssht.conf // Read config from wsssht.conf
char *config_domain = read_config_value_from_file("wssshd-host", "wsssht"); char *config_domain = read_config_value_from_file("wssshd-host", "wsssht");
...@@ -165,8 +191,12 @@ int main(int argc, char *argv[]) { ...@@ -165,8 +191,12 @@ int main(int argc, char *argv[]) {
pthread_mutex_init(&tunnel_mutex, NULL); pthread_mutex_init(&tunnel_mutex, NULL);
pthread_mutex_init(&ssl_mutex, NULL); pthread_mutex_init(&ssl_mutex, NULL);
// Set up signal handler for SIGINT // Register cleanup function to run on exit
signal(SIGINT, sigint_handler); atexit(cleanup_on_exit);
// Set up signal handlers for SIGINT and SIGTERM
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// Parse wsssht arguments // Parse wsssht arguments
int remaining_argc; int remaining_argc;
...@@ -425,17 +455,25 @@ int main(int argc, char *argv[]) { ...@@ -425,17 +455,25 @@ int main(int argc, char *argv[]) {
fflush(stdout); fflush(stdout);
} }
send_tunnel_close(active_tunnel->ssl, active_tunnel->request_id, config.debug); send_tunnel_close(active_tunnel->ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
// Wait up to 3 seconds for tunnel close to complete // Wait up to 3 seconds for tunnel close to complete, but allow interruption by second signal
fprintf(stderr, "Sent close message for tunnel, waiting up to 3 seconds...\n"); fprintf(stderr, "Sent close message for tunnel, waiting up to 3 seconds...\n");
time_t shutdown_start = time(NULL); time_t shutdown_start = time(NULL);
while (time(NULL) - shutdown_start < 3) { while (time(NULL) - shutdown_start < 3 && !force_exit) {
if (!active_tunnel->active) { if (!active_tunnel->active) {
fprintf(stderr, "Tunnel closed gracefully\n"); fprintf(stderr, "Tunnel closed gracefully\n");
break; break;
} }
usleep(100000); // Sleep 100ms before checking again usleep(100000); // Sleep 100ms before checking again
} }
// If we were forced to exit, break immediately
if (force_exit) {
fprintf(stderr, "Forced exit during tunnel close wait\n");
pthread_mutex_unlock(&tunnel_mutex);
break;
}
} }
pthread_mutex_unlock(&tunnel_mutex); pthread_mutex_unlock(&tunnel_mutex);
break; break;
...@@ -475,6 +513,7 @@ int main(int argc, char *argv[]) { ...@@ -475,6 +513,7 @@ int main(int argc, char *argv[]) {
fflush(stdout); fflush(stdout);
} }
send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug); send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
pthread_mutex_unlock(&tunnel_mutex); pthread_mutex_unlock(&tunnel_mutex);
goto cleanup_and_exit; goto cleanup_and_exit;
} }
...@@ -491,6 +530,7 @@ int main(int argc, char *argv[]) { ...@@ -491,6 +530,7 @@ int main(int argc, char *argv[]) {
active_tunnel->broken = 1; active_tunnel->broken = 1;
// Send tunnel_close notification // Send tunnel_close notification
send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug); send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
pthread_mutex_unlock(&tunnel_mutex); pthread_mutex_unlock(&tunnel_mutex);
goto cleanup_and_exit; goto cleanup_and_exit;
} }
...@@ -516,6 +556,7 @@ int main(int argc, char *argv[]) { ...@@ -516,6 +556,7 @@ int main(int argc, char *argv[]) {
fflush(stdout); fflush(stdout);
} }
send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug); send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
goto cleanup_and_exit; goto cleanup_and_exit;
} else if (retval == 0) { } else if (retval == 0) {
// Timeout, check if tunnel became inactive // Timeout, check if tunnel became inactive
...@@ -681,6 +722,7 @@ int main(int argc, char *argv[]) { ...@@ -681,6 +722,7 @@ int main(int argc, char *argv[]) {
fflush(stdout); fflush(stdout);
} }
send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug); send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
goto cleanup_and_exit; goto cleanup_and_exit;
} }
...@@ -715,6 +757,7 @@ int main(int argc, char *argv[]) { ...@@ -715,6 +757,7 @@ int main(int argc, char *argv[]) {
fflush(stdout); fflush(stdout);
} }
send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug); send_tunnel_close(current_ssl, active_tunnel->request_id, config.debug);
tunnel_close_sent = 1;
goto cleanup_and_exit; goto cleanup_and_exit;
} else if (frame_type == 0x89) { // Ping frame } else if (frame_type == 0x89) { // Ping frame
if (config.debug) { if (config.debug) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment