WebSocket and UI improvements

- Fix WebSocket upgrade handshake in web interface
- Fix terminal session linking with WebSocket connections
- Add binary type setting for WebSocket messages
- Add terminal resize after session establishment
- Change 'WebSocket SSH Daemon' to 'WSSSHD control panel' in all headers
- Add GNOME-like window decorations to terminal interface
- Implement window controls: close (disconnect), maximize (fullscreen), minimize (exit fullscreen)
- Maintain window decorations in fullscreen mode
parent c9755708
......@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}WebSocket SSH Daemon{% endblock %}</title>
<title>{% block title %}WSSSHD control panel{% endblock %}</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
......@@ -68,7 +68,7 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">
<i class="fas fa-terminal"></i> WebSocket SSH Daemon
<i class="fas fa-terminal"></i> WSSSHD control panel
</a>
<div class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
......
......@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - WebSocket SSH Daemon</title>
<title>Dashboard - WSSSHD control panel</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
......@@ -12,7 +12,7 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">
<i class="fas fa-terminal"></i> WebSocket SSH Daemon</a>
<i class="fas fa-terminal"></i> WSSSHD control panel</a>
<div class="navbar-nav ms-auto">
<span class="navbar-text me-3">%s</span>
<a class="nav-link" href="/logout">Logout</a>
......
......@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - WebSocket SSH Daemon</title>
<title>Login - WSSSHD control panel</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
......@@ -12,7 +12,7 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">
<i class="fas fa-terminal"></i> WebSocket SSH Daemon</a>
<i class="fas fa-terminal"></i> WSSSHD control panel</a>
</div></nav>
<div class="container mt-4">
<div class="row justify-content-center">
......
......@@ -62,13 +62,99 @@
.terminal-input:focus {
box-shadow: none;
}
/* GNOME-like window decorations */
.terminal-window {
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.window-titlebar {
background: linear-gradient(to bottom, #f0f0f0, #e0e0e0);
border-bottom: 1px solid #ccc;
padding: 4px 8px;
display: flex;
justify-content: space-between;
align-items: center;
height: 32px;
}
.window-title {
font-weight: bold;
color: #333;
font-size: 14px;
}
.window-controls {
display: flex;
align-items: center;
gap: 4px;
}
.window-btn {
width: 24px;
height: 24px;
border: none;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.window-btn:hover {
transform: scale(1.1);
}
.close-btn {
background: #ff605c;
color: white;
}
.close-btn:hover {
background: #ff403c;
}
.maximize-btn {
background: #28ca42;
color: white;
}
.maximize-btn:hover {
background: #24b83a;
}
.minimize-btn {
background: #ffbd44;
color: white;
display: none; /* Hidden by default, shown in fullscreen */
}
.minimize-btn:hover {
background: #ffad2c;
}
.window-content {
background: #1e1e1e;
padding: 8px;
}
/* Fullscreen styles */
.terminal-window.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
border: none;
border-radius: 0;
z-index: 9999;
}
.terminal-window.fullscreen .terminal-container {
height: calc(100vh - 40px) !important;
min-height: calc(100vh - 40px) !important;
}
.terminal-window.fullscreen .minimize-btn {
display: flex;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">
<i class="fas fa-terminal"></i> WebSocket SSH Daemon</a>
<i class="fas fa-terminal"></i> WSSSHD control panel</a>
<div class="navbar-nav ms-auto">
<span class="navbar-text me-3">SSH Terminal - %s</span>
<a class="nav-link" href="/logout">Logout</a>
......@@ -77,30 +163,25 @@
<div class="container mt-4">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<a href="/" class="btn btn-outline-secondary btn-sm me-3">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
<h3 class="card-title mb-0">
<i class="fas fa-terminal"></i> SSH Terminal - %s
</h3>
<div class="terminal-window">
<div class="window-titlebar">
<div class="window-title">
<i class="fas fa-terminal"></i> SSH Terminal - %s
</div>
<div>
<input type="text" id="sshUsername" class="form-control form-control-sm d-inline-block w-auto me-2" placeholder="Username" value="root">
<button id="connectBtn" class="btn btn-success btn-sm">
<div class="window-controls">
<input type="text" id="sshUsername" class="form-control form-control-sm d-inline-block w-auto me-2" placeholder="Username" value="root" style="height: 24px; font-size: 12px; border: none; background: rgba(255,255,255,0.1); color: white;">
<button id="connectBtn" class="btn btn-success btn-sm me-1" style="height: 24px; font-size: 12px; padding: 0 8px;">
<i class="fas fa-play"></i> Connect
</button>
<button id="disconnectBtn" class="btn btn-danger btn-sm" disabled>
<button id="disconnectBtn" class="btn btn-danger btn-sm me-1" disabled style="height: 24px; font-size: 12px; padding: 0 8px;">
<i class="fas fa-stop"></i> Disconnect
</button>
<button id="fullscreenBtn" class="btn btn-secondary btn-sm" title="Toggle Fullscreen">
<i class="fas fa-expand"></i>
</button>
<button class="window-btn minimize-btn" title="Exit Fullscreen">_</button>
<button class="window-btn maximize-btn" title="Fullscreen"><span class="maximize-icon"></span></button>
<button class="window-btn close-btn" title="Disconnect">×</button>
</div>
</div>
<div class="card-body p-2">
<div class="window-content">
<div id="terminal" class="terminal-container w-100"></div>
</div>
</div>
......@@ -118,59 +199,36 @@ let term = null;
let fitAddon = null;
let connected = false;
let requestId = null;
let pollInterval = null;
let polling = false;
let websocket = null;
console.log('Terminal page loaded, adding event listeners');
document.getElementById('connectBtn').addEventListener('click', connect);
document.getElementById('disconnectBtn').addEventListener('click', disconnect);
document.getElementById('fullscreenBtn').addEventListener('click', toggleFullscreen);
// Window control buttons
document.querySelector('.close-btn').addEventListener('click', disconnect);
document.querySelector('.maximize-btn').addEventListener('click', toggleFullscreen);
document.querySelector('.minimize-btn').addEventListener('click', toggleFullscreen);
console.log('Event listeners added');
// Fullscreen functionality
function toggleFullscreen() {
const terminalContainer = document.getElementById('terminal');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const icon = fullscreenBtn.querySelector('i');
const terminalWindow = document.querySelector('.terminal-window');
const maximizeBtn = document.querySelector('.maximize-btn');
const maximizeIcon = maximizeBtn.querySelector('.maximize-icon');
if (!document.fullscreenElement) {
// Enter fullscreen
if (terminalContainer.requestFullscreen) {
terminalContainer.requestFullscreen();
} else if (terminalContainer.webkitRequestFullscreen) { // Safari
terminalContainer.webkitRequestFullscreen();
} else if (terminalContainer.msRequestFullscreen) { // IE11
terminalContainer.msRequestFullscreen();
}
} else {
if (terminalWindow.classList.contains('fullscreen')) {
// Exit fullscreen
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) { // Safari
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) { // IE11
document.msExitFullscreen();
}
}
}
// Update fullscreen button icon based on state
function updateFullscreenButton() {
const fullscreenBtn = document.getElementById('fullscreenBtn');
const icon = fullscreenBtn.querySelector('i');
if (document.fullscreenElement) {
icon.className = 'fas fa-compress';
fullscreenBtn.title = 'Exit Fullscreen';
terminalWindow.classList.remove('fullscreen');
maximizeIcon.textContent = '□';
maximizeBtn.title = 'Fullscreen';
} else {
icon.className = 'fas fa-expand';
fullscreenBtn.title = 'Enter Fullscreen';
// Enter fullscreen
terminalWindow.classList.add('fullscreen');
maximizeIcon.textContent = '▭'; // Restore icon
maximizeBtn.title = 'Exit Fullscreen';
}
}
// Listen for fullscreen changes
function handleFullscreenChange() {
updateFullscreenButton();
// Resize terminal after fullscreen change
setTimeout(() => {
if (window.fitTerminal) {
......@@ -191,15 +249,12 @@ function handleFullscreenChange() {
'&cols=' + encodeURIComponent(newCols) +
'&rows=' + encodeURIComponent(newRows)
}).catch(error => {
console.error('Resize error during fullscreen change:', error);
console.error('Resize error during fullscreen toggle:', error);
});
}
}, 100); // Small delay to ensure DOM is updated
}, 100);
}
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange); // Safari
document.addEventListener('msfullscreenchange', handleFullscreenChange); // IE11
function connect() {
console.log('Connect button clicked');
......@@ -324,72 +379,117 @@ function connect() {
rows = dimensions.rows || rows;
}
// Send connect request with terminal dimensions
const connectUrl = '/terminal/%s/xterm/connect';
console.log('Sending connect request to:', connectUrl);
console.log('Username:', username, 'Cols:', cols, 'Rows:', rows);
fetch(connectUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'username=' + encodeURIComponent(username) +
'&cols=' + encodeURIComponent(cols) +
'&rows=' + encodeURIComponent(rows),
// Add timeout and credentials
credentials: 'same-origin'
})
.then(response => {
console.log('Connect response status:', response.status, 'OK:', response.ok);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Connect response data:', data);
if (data.request_id) {
requestId = data.request_id;
if (data.command) {
console.log('Launching command:', data.command);
}
term.write('Connected successfully!\r\n');
pollInterval = setInterval(pollData, 500);
// Poll immediately to get any buffered output
pollData();
} else {
term.write('Error: ' + (data.error || 'Unknown error') + '\r\n');
disconnect();
}
})
.catch(error => {
console.error('Connection failed:', error);
term.write('Connection failed: ' + error.message + '\r\n');
disconnect();
});
// Establish WebSocket connection first
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = wsProtocol + '//' + window.location.host + '/terminal/%s/ws';
console.log('Connecting WebSocket to:', wsUrl);
// Handle input - send all keystrokes to server, let SSH handle echo
term.onKey(e => {
if (!connected || !requestId) return;
websocket = new WebSocket(wsUrl);
websocket.binaryType = 'arraybuffer';
let data = e.key;
console.log('Sending input data:', data);
websocket.onopen = function(event) {
console.log('WebSocket connected');
term.write('WebSocket connected, establishing terminal...\r\n');
// Send to server
fetch('/terminal/%s/xterm/data', {
// Now send connect request with terminal dimensions
const connectUrl = '/terminal/%s/xterm/connect';
console.log('Sending connect request to:', connectUrl);
fetch(connectUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'request_id=' + encodeURIComponent(requestId) + '&data=' + encodeURIComponent(data)
}).then(response => {
if (response.status !== 200) {
console.log('Input send response status:', response.status);
body: 'username=' + encodeURIComponent(username) +
'&cols=' + encodeURIComponent(cols) +
'&rows=' + encodeURIComponent(rows),
credentials: 'same-origin'
})
.then(response => {
console.log('Connect response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Connect response data:', data);
if (data.request_id) {
requestId = data.request_id;
if (data.command) {
console.log('Launching command:', data.command);
}
term.write('Terminal connected successfully!\r\n');
// Send resize with current dimensions to ensure correct sizing
if (fitAddon) {
const dimensions = fitAddon.proposeDimensions();
const newCols = dimensions.cols || 80;
const newRows = dimensions.rows || 24;
fetch('/terminal/%s/xterm/resize', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'request_id=' + encodeURIComponent(requestId) +
'&cols=' + encodeURIComponent(newCols) +
'&rows=' + encodeURIComponent(newRows)
}).catch(error => {
console.error('Initial resize error:', error);
});
}
} else {
term.write('Error: ' + (data.error || 'Unknown error') + '\r\n');
disconnect();
}
}).catch(error => {
console.error('Input send error:', error);
})
.catch(error => {
console.error('Terminal connection failed:', error);
term.write('Terminal connection failed: ' + error.message + '\r\n');
disconnect();
});
};
websocket.onmessage = function(event) {
console.log('WebSocket message received:', event.data);
if (typeof event.data === 'string') {
// JSON message (like session ended)
try {
const data = JSON.parse(event.data);
if (data.ended) {
console.log('Session ended');
disconnect();
}
} catch (e) {
console.error('Failed to parse JSON message:', e);
}
} else {
// Binary data (terminal output)
term.write(new Uint8Array(event.data));
}
};
websocket.onerror = function(error) {
console.error('WebSocket error:', error);
term.write('WebSocket error occurred\r\n');
disconnect();
};
websocket.onclose = function(event) {
console.log('WebSocket closed:', event.code, event.reason);
term.write('Connection closed\r\n');
disconnect();
};
// Handle input - send all keystrokes to server via WebSocket
term.onKey(e => {
if (!connected || !websocket || websocket.readyState !== WebSocket.OPEN) return;
let data = e.key;
console.log('Sending input data via WebSocket:', data);
// Send input data via WebSocket
websocket.send(data);
// Prevent local display of input
e.domEvent.preventDefault();
......@@ -402,10 +502,10 @@ function disconnect() {
document.getElementById('disconnectBtn').disabled = true;
document.getElementById('sshUsername').disabled = false;
if (pollInterval) {
clearTimeout(pollInterval);
pollInterval = null;
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.close();
}
websocket = null;
if (requestId) {
fetch('/terminal/%s/xterm/disconnect', {
......@@ -419,79 +519,13 @@ function disconnect() {
}
if (term) {
term.write('\r\nDisconnect\r\n');
term.write('\r\nDisconnected\r\n');
setTimeout(() => {
location.reload();
}, 3000);
}
}
function pollData() {
if (!requestId || polling) return;
polling = true;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
fetch('/terminal/%s/xterm/data?request_id=' + encodeURIComponent(requestId), {
signal: controller.signal
})
.then(response => {
clearTimeout(timeoutId);
if (response.status !== 200) {
console.log('Poll response status:', response.status);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('json')) {
return response.json();
} else {
return response.arrayBuffer();
}
})
.then(response => {
if (response.status !== 200) {
console.log('Poll response status:', response.status);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('json')) {
return response.json();
} else {
return response.arrayBuffer();
}
})
.then(data => {
console.log('Poll data received:', data);
if (data.ended !== undefined) {
console.log('Session ended, reloading page');
if (pollInterval) {
clearTimeout(pollInterval);
pollInterval = null;
}
location.reload();
} else {
if (data) {
console.log('Received data:', data.byteLength || data.length, 'bytes/characters');
// Write data to terminal
if (data.byteLength !== undefined) {
// Binary data
term.write(new Uint8Array(data));
} else {
// Text data
term.write(data);
}
}
// Schedule next poll
if (pollInterval) {
pollInterval = setTimeout(pollData, 1000);
}
}
})
.catch(error => {
console.error('Polling error:', error);
// Continue polling even on error, but with delay
if (pollInterval) {
pollInterval = setTimeout(pollData, 1000);
}
});
}
// Focus on terminal when connected
document.addEventListener('keydown', function(e) {
......
......@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Users - WebSocket SSH Daemon</title>
<title>Users - WSSSHD control panel</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
......@@ -12,7 +12,7 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">
<i class="fas fa-terminal"></i> WebSocket SSH Daemon</a>
<i class="fas fa-terminal"></i> WSSSHD control panel</a>
<div class="navbar-nav ms-auto">
<span class="navbar-text me-3">%s</span>
<a class="nav-link" href="/logout">Logout</a>
......
......@@ -231,6 +231,7 @@ char *terminal_get_output(terminal_session_t *session, size_t *len) {
pthread_mutex_lock(&session->output_mutex);
if (session->output_used == 0) {
// For WebSocket connections, don't wait - return immediately
pthread_mutex_unlock(&session->output_mutex);
*len = 0;
return NULL;
......
......@@ -35,6 +35,7 @@
#include "terminal.h"
#include "assets.h"
#include "websocket.h"
#include "websocket_protocol.h"
#include "html_pages/index_page.h"
#include "html_pages/login_page.h"
#include "html_pages/terminal_page.h"
......@@ -76,9 +77,163 @@ static terminal_session_t *active_terminals[MAX_ACTIVE_TERMINALS];
static int active_terminals_count = 0;
static pthread_mutex_t terminals_mutex = PTHREAD_MUTEX_INITIALIZER;
// WebSocket connections for terminals
#define MAX_WEBSOCKET_CONNECTIONS 100
typedef struct {
ws_connection_t *ws_conn;
char request_id[37];
char client_id[256];
char username[50];
bool active;
pthread_t thread;
} websocket_terminal_conn_t;
static websocket_terminal_conn_t websocket_connections[MAX_WEBSOCKET_CONNECTIONS];
static int websocket_connections_count = 0;
static pthread_mutex_t websocket_mutex = PTHREAD_MUTEX_INITIALIZER;
// JSON response for ended session
static const char ended_json[] = "{\"ended\":true}";
// WebSocket terminal connection management
static websocket_terminal_conn_t *add_websocket_connection(ws_connection_t *ws_conn, const char *request_id, const char *client_id, const char *username) {
pthread_mutex_lock(&websocket_mutex);
if (websocket_connections_count >= MAX_WEBSOCKET_CONNECTIONS) {
pthread_mutex_unlock(&websocket_mutex);
return NULL;
}
websocket_terminal_conn_t *conn = &websocket_connections[websocket_connections_count++];
conn->ws_conn = ws_conn;
if (request_id) {
strncpy(conn->request_id, request_id, sizeof(conn->request_id) - 1);
conn->request_id[sizeof(conn->request_id) - 1] = '\0';
} else {
conn->request_id[0] = '\0';
}
if (client_id) {
size_t len = strlen(client_id);
if (len >= sizeof(conn->client_id)) {
// Client ID too long, truncate safely
len = sizeof(conn->client_id) - 1;
}
memcpy(conn->client_id, client_id, len);
conn->client_id[len] = '\0';
} else {
conn->client_id[0] = '\0';
}
if (username) {
strncpy(conn->username, username, sizeof(conn->username) - 1);
conn->username[sizeof(conn->username) - 1] = '\0';
} else {
conn->username[0] = '\0';
}
conn->active = true;
pthread_mutex_unlock(&websocket_mutex);
return conn;
}
static void remove_websocket_connection(const char *request_id) {
pthread_mutex_lock(&websocket_mutex);
for (int i = 0; i < websocket_connections_count; i++) {
if (strcmp(websocket_connections[i].request_id, request_id) == 0) {
websocket_connections[i].active = false;
// Shift remaining connections
memmove(&websocket_connections[i], &websocket_connections[i + 1],
sizeof(websocket_terminal_conn_t) * (websocket_connections_count - i - 1));
websocket_connections_count--;
break;
}
}
pthread_mutex_unlock(&websocket_mutex);
}
// WebSocket terminal handler thread
static void *websocket_terminal_handler(void *arg) {
websocket_terminal_conn_t *ws_conn = (websocket_terminal_conn_t *)arg;
printf("WebSocket terminal handler started for client: %s\n", ws_conn->client_id);
// Wait for terminal session to be established (up to 30 seconds)
terminal_session_t *session = NULL;
int wait_count = 0;
const int max_wait = 300; // 30 seconds at 100ms intervals
while (ws_conn->active && ws_conn->ws_conn->state == WS_STATE_OPEN && wait_count < max_wait) {
pthread_mutex_lock(&terminals_mutex);
for (int i = 0; i < active_terminals_count; i++) {
if (strcmp(active_terminals[i]->request_id, ws_conn->request_id) == 0 && strlen(ws_conn->request_id) > 0) {
session = active_terminals[i];
break;
}
}
pthread_mutex_unlock(&terminals_mutex);
if (session) {
printf("WebSocket terminal handler found session for request_id: %s\n", ws_conn->request_id);
break;
}
usleep(100000); // 100ms
wait_count++;
}
if (!session) {
printf("WebSocket terminal handler timed out waiting for session for client: %s\n", ws_conn->client_id);
ws_send_frame(ws_conn->ws_conn, WS_OPCODE_TEXT, "{\"error\":\"Terminal session not established\"}", 45);
remove_websocket_connection(ws_conn->request_id);
return NULL;
}
printf("WebSocket terminal handler running for request_id: %s\n", ws_conn->request_id);
while (ws_conn->active && ws_conn->ws_conn->state == WS_STATE_OPEN) {
// Check if terminal session is still active
if (!terminal_is_running(session)) {
// Terminal session ended
printf("WebSocket terminal session ended for request_id: %s\n", ws_conn->request_id);
ws_send_frame(ws_conn->ws_conn, WS_OPCODE_TEXT, ended_json, sizeof(ended_json) - 1);
break;
}
// Send any available output immediately
size_t output_len = 0;
char *output = terminal_get_output(session, &output_len);
if (output && output_len > 0) {
printf("WebSocket sending %zu bytes of output for request_id: %s\n", output_len, ws_conn->request_id);
ws_send_frame(ws_conn->ws_conn, WS_OPCODE_BINARY, output, output_len);
free(output);
}
// Check for input from WebSocket (non-blocking)
uint8_t opcode = 0;
void *data = NULL;
size_t len = 0;
if (ws_receive_frame(ws_conn->ws_conn, &opcode, &data, &len)) {
if (opcode == WS_OPCODE_TEXT && len > 0) {
// Input data
printf("WebSocket received %zu bytes of input for request_id: %s\n", len, ws_conn->request_id);
terminal_send_data(session, (const char *)data);
} else if (opcode == WS_OPCODE_CLOSE) {
// Connection closed
printf("WebSocket connection closed for request_id: %s\n", ws_conn->request_id);
break;
}
if (data) free(data);
} else {
// No data available, small delay
usleep(10000); // 10ms
}
}
printf("WebSocket terminal handler ended for request_id: %s\n", ws_conn->request_id);
remove_websocket_connection(ws_conn->request_id);
return NULL;
}
// Simple hash function for passwords (not secure, but for demo)
static void simple_hash(const char *input, char *output, size_t output_size) {
unsigned long hash = 5381;
......@@ -738,7 +893,6 @@ static void parse_multipart_form_data(const char *body, const char *boundary, ch
const char *start = strstr(body, boundary_start);
if (!start) {
printf("[DEBUG] Boundary not found in body\n");
return;
}
start += strlen(boundary_start) + 2; // \r\n
......@@ -1003,7 +1157,8 @@ static int generate_users_html(const char *username, char *html, size_t max_len)
}
// Handle HTTP requests
static void handle_request(int client_fd, const http_request_t *req) {
// Returns 1 if the connection should be kept open (WebSocket), 0 if it should be closed
static int handle_request(int client_fd, const http_request_t *req) {
if (global_config && global_config->debug_web && !strstr(req->path, "/xterm/data")) {
printf("[WEB-DEBUG] Received %s request for %s\n", req->method, req->path);
if (req->query[0]) {
......@@ -1037,7 +1192,147 @@ static void handle_request(int client_fd, const http_request_t *req) {
}
// Route handling
// Handle terminal routes first (both GET and POST)
// Handle WebSocket terminal connections first
char *headers_copy = NULL;
printf("[DEBUG] Checking path: %s\n", req->path);
if (strncmp(req->path, "/terminal/", 9) == 0 && strstr(req->path, "/ws")) {
printf("[WEBSOCKET] WebSocket upgrade request detected for path: %s\n", req->path);
// WebSocket terminal connection
char path_copy[1024];
strcpy(path_copy, req->path);
char *client_id = path_copy + 9;
// Skip any leading slashes
while (*client_id == '/') client_id++;
char *ws_part = strstr(client_id, "/ws");
if (ws_part) *ws_part = '\0'; // Remove /ws from client_id
printf("[WEBSOCKET] Extracted client_id: %s\n", client_id);
if (!username) {
printf("[WEBSOCKET] No authenticated user, rejecting\n");
send_response(client_fd, 401, "Unauthorized", "text/plain", "Authentication required", 21, NULL, NULL);
return 0;
}
printf("[WEBSOCKET] User authenticated: %s\n", username);
// Check if this is a WebSocket upgrade request
const char *upgrade = NULL;
const char *connection = NULL;
const char *sec_websocket_key = NULL;
// Parse headers manually since they contain full header lines
headers_copy = strdup(req->headers);
if (!headers_copy) {
send_response(client_fd, 500, "Internal Server Error", "text/plain", "Memory allocation failed", 23, NULL, NULL);
return 0;
}
char *header_line = strtok(headers_copy, "\r\n");
while (header_line) {
if (strncasecmp(header_line, "Upgrade:", 8) == 0) {
upgrade = header_line + 8;
while (*upgrade == ' ' || *upgrade == '\t') upgrade++;
} else if (strncasecmp(header_line, "Connection:", 11) == 0) {
connection = header_line + 11;
while (*connection == ' ' || *connection == '\t') connection++;
} else if (strncasecmp(header_line, "Sec-WebSocket-Key:", 18) == 0) {
sec_websocket_key = header_line + 18;
while (*sec_websocket_key == ' ' || *sec_websocket_key == '\t') sec_websocket_key++;
}
header_line = strtok(NULL, "\r\n");
}
if (global_config && global_config->debug_web) {
printf("[WEB-DEBUG] WebSocket upgrade check: upgrade='%s', connection='%s', key='%s'\n",
upgrade ? upgrade : "NULL", connection ? connection : "NULL",
sec_websocket_key ? "(present)" : "NULL");
}
bool is_valid_upgrade = upgrade && strcasecmp(upgrade, "websocket") == 0 &&
connection && strstr(connection, "Upgrade") &&
sec_websocket_key;
if (global_config && global_config->debug_web) {
printf("[WEB-DEBUG] WebSocket upgrade valid: %s\n", is_valid_upgrade ? "yes" : "no");
}
if (is_valid_upgrade) {
// Set socket to non-blocking mode for WebSocket
int flags = fcntl(client_fd, F_GETFL, 0);
if (flags != -1) {
fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
}
// Create WebSocket connection
ws_connection_t *ws_conn = ws_connection_create(NULL, client_fd); // No SSL for web interface
if (!ws_conn) {
free(headers_copy);
send_response(client_fd, 500, "Internal Server Error", "text/plain", "Failed to create WebSocket connection", 35, NULL, NULL);
return 0;
}
// Compute WebSocket accept key
char *accept_key = ws_compute_accept_key(sec_websocket_key);
if (!accept_key) {
ws_connection_free(ws_conn);
free(headers_copy);
send_response(client_fd, 500, "Internal Server Error", "text/plain", "Failed to compute WebSocket accept key", 36, NULL, NULL);
return 0;
}
// Send WebSocket handshake response
char response[512];
int response_len = snprintf(response, sizeof(response),
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: %s\r\n"
"\r\n", accept_key);
free(accept_key);
int bytes_written = write(client_fd, response, response_len);
if (bytes_written != response_len) {
ws_connection_free(ws_conn);
free(headers_copy);
send_response(client_fd, 500, "Internal Server Error", "text/plain", "Failed to send WebSocket handshake response", 42, NULL, NULL);
return 0;
}
// Set connection state to open
ws_conn->state = WS_STATE_OPEN;
// Create WebSocket terminal connection
websocket_terminal_conn_t *ws_term_conn = add_websocket_connection(ws_conn, "", client_id, username);
if (!ws_term_conn) {
ws_connection_free(ws_conn);
send_response(client_fd, 500, "Internal Server Error", "text/plain", "Too many WebSocket connections", 30, NULL, NULL);
return 0;
}
// Start WebSocket handler thread
if (pthread_create(&ws_term_conn->thread, NULL, websocket_terminal_handler, ws_term_conn) != 0) {
remove_websocket_connection("");
ws_connection_free(ws_conn);
send_response(client_fd, 500, "Internal Server Error", "text/plain", "Failed to start WebSocket handler", 32, NULL, NULL);
return 0;
}
pthread_detach(ws_term_conn->thread);
return 1; // WebSocket connection is now handled by the thread, keep socket open
} else {
send_response(client_fd, 400, "Bad Request", "text/plain", "Invalid WebSocket upgrade request", 32, NULL, NULL);
free(headers_copy);
return 0;
}
} else {
free(headers_copy);
}
// Handle terminal routes (both GET and POST)
if (strncmp(req->path, "/terminal/", 9) == 0) {
// Extract client_id and action from path
char path_copy[1024];
......@@ -1061,7 +1356,7 @@ static void handle_request(int client_fd, const http_request_t *req) {
if (!username) {
send_response(client_fd, 302, "Found", "text/html", NULL, 0, NULL, "Location: /");
return;
return 0;
}
if (!action || *action == '\0') {
......@@ -1100,13 +1395,12 @@ static void handle_request(int client_fd, const http_request_t *req) {
char location_header[2048];
snprintf(location_header, sizeof(location_header), "Location: /?error=SSH service not available for client %s", client_id);
send_response(client_fd, 302, "Found", "text/html", NULL, 0, NULL, location_header);
return;
return 0;
}
// Generate terminal HTML with client_id
char html[32768];
int len = snprintf(html, sizeof(html), terminal_page_html,
client_id, client_id, client_id, client_id, client_id, client_id, client_id, client_id, client_id, client_id,
client_id, client_id, client_id, client_id, client_id, client_id);
client_id, client_id, client_id, client_id, client_id, client_id, client_id, client_id, client_id);
send_response(client_fd, 200, "OK", "text/html", html, len, NULL, NULL);
} else {
// Handle terminal actions (connect, data, disconnect, resize)
......@@ -1124,7 +1418,7 @@ static void handle_request(int client_fd, const http_request_t *req) {
}
if (!client_exists) {
send_response(client_fd, 404, "Not Found", "application/json", "{\"error\":\"Client not connected\"}", 31, NULL, NULL);
return;
return 0;
}
if (strcmp(req->method, "POST") == 0) {
......@@ -1166,6 +1460,31 @@ static void handle_request(int client_fd, const http_request_t *req) {
pthread_mutex_lock(&terminals_mutex);
if (active_terminals_count < MAX_ACTIVE_TERMINALS) {
active_terminals[active_terminals_count++] = session;
// Update WebSocket connection with request_id
// Find WebSocket connection by client_id (since request_id is empty initially)
pthread_mutex_lock(&websocket_mutex);
websocket_terminal_conn_t *ws_conn = NULL;
for (int i = 0; i < websocket_connections_count; i++) {
if (websocket_connections[i].active &&
strcmp(websocket_connections[i].client_id, client_id) == 0) {
ws_conn = &websocket_connections[i];
break;
}
}
if (ws_conn) {
size_t len = strlen(session->request_id);
if (len >= sizeof(ws_conn->request_id)) {
len = sizeof(ws_conn->request_id) - 1;
}
memcpy(ws_conn->request_id, session->request_id, len);
ws_conn->request_id[len] = '\0';
printf("Updated WebSocket connection for client %s with request_id: %s\n", client_id, session->request_id);
} else {
printf("Warning: Could not find WebSocket connection for client %s\n", client_id);
}
pthread_mutex_unlock(&websocket_mutex);
char json[256];
int len = snprintf(json, sizeof(json), "{\"request_id\":\"%s\",\"command\":\"%s\"}",
session->request_id, session->command);
......@@ -1175,12 +1494,13 @@ static void handle_request(int client_fd, const http_request_t *req) {
terminal_free_session(session);
pthread_mutex_unlock(&terminals_mutex);
send_response(client_fd, 500, "Internal Server Error", "application/json",
"{\"error\":\"Too many active terminals\"}", 36, NULL, NULL);
"{\"error\":\"Too many active terminals\"}", 36, NULL, NULL);
}
} else {
send_response(client_fd, 500, "Internal Server Error", "application/json",
"{\"error\":\"Failed to create terminal session\"}", 45, NULL, NULL);
"{\"error\":\"Failed to create terminal session\"}", 45, NULL, NULL);
}
return 0;
} else if (strcmp(action, "data") == 0) {
char request_id[37] = "";
char data[4096] = "";
......@@ -1222,6 +1542,9 @@ static void handle_request(int client_fd, const http_request_t *req) {
} else {
send_response(client_fd, 404, "Not Found", "application/json", "{\"error\":\"Terminal session not found\"}", 37, NULL, NULL);
}
return 0;
return 0;
return 0;
} else if (strcmp(action, "disconnect") == 0) {
char request_id[37] = "";
......@@ -1354,7 +1677,7 @@ static void handle_request(int client_fd, const http_request_t *req) {
send_response(client_fd, 405, "Method Not Allowed", "text/plain", "Method not allowed", 18, NULL, NULL);
}
}
return;
return 0;
}
if (strcmp(req->method, "GET") == 0) {
......@@ -1364,7 +1687,7 @@ static void handle_request(int client_fd, const http_request_t *req) {
char location_header[256];
snprintf(location_header, sizeof(location_header), "Location: /login");
send_response(client_fd, 302, "Found", "text/html", NULL, 0, NULL, location_header);
return;
return 0;
}
char error[256] = "";
if (req->query[0]) {
......@@ -1383,11 +1706,11 @@ static void handle_request(int client_fd, const http_request_t *req) {
send_response(client_fd, 200, "OK", "text/html", login_page_html, strlen(login_page_html), NULL, NULL);
} else if (strcmp(req->path, "/logout") == 0) {
send_response(client_fd, 302, "Found", "text/html", NULL, 0, "session_id=; Max-Age=0; Path=/", "Location: /");
return;
return 0;
} else if (strcmp(req->path, "/users") == 0) {
if (!username || !is_admin) {
send_response(client_fd, 302, "Found", "text/html", NULL, 0, NULL, "Location: /");
return;
return 0;
}
// Generate dynamic users page
char html[32768];
......@@ -1396,7 +1719,7 @@ static void handle_request(int client_fd, const http_request_t *req) {
} else if (strcmp(req->path, "/api/clients") == 0) {
if (!username) {
send_response(client_fd, 302, "Found", "text/html", NULL, 0, NULL, "Location: /");
return;
return 0;
}
char json[4096];
int len = snprintf(json, sizeof(json), "{\"clients\":{");
......@@ -1455,7 +1778,7 @@ static void handle_request(int client_fd, const http_request_t *req) {
char cookie[256];
snprintf(cookie, sizeof(cookie), "session_id=%s; Path=/; HttpOnly", new_session);
send_response(client_fd, 302, "Found", "text/html", NULL, 0, cookie, "Location: /");
return;
return 0;
} else {
if (global_config && global_config->debug_web) {
printf("[WEB-DEBUG] Login failed: invalid password for user '%s'\n", form_username);
......@@ -1471,7 +1794,7 @@ static void handle_request(int client_fd, const http_request_t *req) {
} else if (strcmp(req->path, "/add_user") == 0) {
if (!username || !is_admin) {
send_response(client_fd, 302, "Found", "text/html", NULL, 0, NULL, "Location: /");
return;
return 0;
}
char form_username[50] = "";
......@@ -1502,13 +1825,13 @@ static void handle_request(int client_fd, const http_request_t *req) {
}
if (!form_username[0] || !form_password[0]) {
send_response(client_fd, 400, "Bad Request", "application/json", "{\"success\":false,\"error\":\"Username and password are required\"}", 62, NULL, NULL);
return;
return 0;
}
// Check if username already exists
if (find_user(form_username)) {
send_response(client_fd, 400, "Bad Request", "application/json", "{\"success\":false,\"error\":\"Username already exists\"}", 50, NULL, NULL);
return;
return 0;
}
char hashed[100];
......@@ -1522,7 +1845,7 @@ static void handle_request(int client_fd, const http_request_t *req) {
} else if (strncmp(req->path, "/edit_user/", 11) == 0) {
if (!username || !is_admin) {
send_response(client_fd, 302, "Found", "text/html", NULL, 0, NULL, "Location: /");
return;
return 0;
}
int user_id = atoi(req->path + 11);
......@@ -1569,7 +1892,7 @@ static void handle_request(int client_fd, const http_request_t *req) {
} else if (strncmp(req->path, "/delete_user/", 13) == 0) {
if (!username || !is_admin) {
send_response(client_fd, 302, "Found", "text/html", NULL, 0, NULL, "Location: /");
return;
return 0;
}
int user_id = atoi(req->path + 13);
......@@ -1585,6 +1908,7 @@ static void handle_request(int client_fd, const http_request_t *req) {
} else {
send_response(client_fd, 405, "Method Not Allowed", "text/html", "Method not allowed", 18, NULL, NULL);
}
return 0;
}
// HTTP server thread function
......@@ -1631,7 +1955,11 @@ static void *http_server_thread(void *arg __attribute__((unused))) {
http_request_t req;
if (parse_http_request(client_fd, &req) == 0) {
handle_request(client_fd, &req);
int keep_open = handle_request(client_fd, &req);
if (keep_open) {
// WebSocket connection - don't close the socket
continue;
}
}
close(client_fd);
......
......@@ -182,7 +182,12 @@ static bool ws_parse_frame_header(const uint8_t *buffer, size_t len, ws_frame_he
bool ws_perform_handshake(ws_connection_t *conn) {
// Read HTTP request
char buffer[4096];
int bytes_read = SSL_read(conn->ssl, buffer, sizeof(buffer) - 1);
int bytes_read;
if (conn->ssl) {
bytes_read = SSL_read(conn->ssl, buffer, sizeof(buffer) - 1);
} else {
bytes_read = read(conn->sock_fd, buffer, sizeof(buffer) - 1);
}
if (bytes_read <= 0) return false;
buffer[bytes_read] = '\0';
......@@ -251,7 +256,12 @@ bool ws_perform_handshake(ws_connection_t *conn) {
free(accept_key);
int bytes_written = SSL_write(conn->ssl, response, strlen(response));
int bytes_written;
if (conn->ssl) {
bytes_written = SSL_write(conn->ssl, response, strlen(response));
} else {
bytes_written = write(conn->sock_fd, response, strlen(response));
}
if (bytes_written <= 0) return false;
conn->state = WS_STATE_OPEN;
......@@ -268,9 +278,7 @@ bool ws_send_frame(ws_connection_t *conn, uint8_t opcode, const void *data, size
return false;
}
if (!conn->ssl) {
return false;
}
// Allow non-SSL connections for web interface
size_t header_len = 2;
if (len >= 126) {
......@@ -318,23 +326,31 @@ bool ws_send_frame(ws_connection_t *conn, uint8_t opcode, const void *data, size
while (total_written < (int)frame_len && retry_count < max_retries) {
int to_write = frame_len - total_written;
int written = SSL_write(conn->ssl, frame + total_written, to_write);
if (written <= 0) {
int ssl_error = SSL_get_error(conn->ssl, written);
// Check for recoverable SSL errors
if ((ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE ||
ssl_error == SSL_ERROR_SSL || ssl_error == SSL_ERROR_SYSCALL) && retry_count < max_retries - 1) {
retry_count++;
// Exponential backoff: wait longer between retries
usleep(10000 * (1 << retry_count)); // 10ms, 20ms, 40ms, 80ms
continue; // Retry the write operation
} else {
// Don't mark connection as closed on send failures - let receive failures handle connection closure
int written;
if (conn->ssl) {
written = SSL_write(conn->ssl, frame + total_written, to_write);
if (written <= 0) {
int ssl_error = SSL_get_error(conn->ssl, written);
// Check for recoverable SSL errors
if ((ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE ||
ssl_error == SSL_ERROR_SSL || ssl_error == SSL_ERROR_SYSCALL) && retry_count < max_retries - 1) {
retry_count++;
// Exponential backoff: wait longer between retries
usleep(10000 * (1 << retry_count)); // 10ms, 20ms, 40ms, 80ms
continue; // Retry the write operation
} else {
// Don't mark connection as closed on send failures - let receive failures handle connection closure
free(frame);
return false;
}
}
} else {
written = write(conn->sock_fd, frame + total_written, to_write);
if (written <= 0) {
free(frame);
return false;
}
free(frame);
return false;
}
total_written += written;
retry_count = 0; // Reset retry count on successful write
......@@ -354,9 +370,20 @@ bool ws_receive_frame(ws_connection_t *conn, uint8_t *opcode, void **data, size_
// Read minimum frame header (2 bytes) to determine full header size
uint8_t header[14];
int bytes_read = SSL_read(conn->ssl, header, 2);
if (bytes_read <= 0) {
return false;
int bytes_read;
if (conn->ssl) {
bytes_read = SSL_read(conn->ssl, header, 2);
if (bytes_read <= 0) {
return false;
}
} else {
bytes_read = read(conn->sock_fd, header, 2);
if (bytes_read < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return false; // No data available, not an error
}
return false; // Real error
}
}
if (bytes_read != 2) {
return false;
......@@ -376,9 +403,19 @@ bool ws_receive_frame(ws_connection_t *conn, uint8_t *opcode, void **data, size_
if (min_header_size > 2) {
int total_read = 0;
while (total_read < (int)(min_header_size - 2)) {
bytes_read = SSL_read(conn->ssl, header + 2 + total_read, min_header_size - 2 - total_read);
if (bytes_read <= 0) {
return false;
if (conn->ssl) {
bytes_read = SSL_read(conn->ssl, header + 2 + total_read, min_header_size - 2 - total_read);
if (bytes_read <= 0) {
return false;
}
} else {
bytes_read = read(conn->sock_fd, header + 2 + total_read, min_header_size - 2 - total_read);
if (bytes_read < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return false; // No data available
}
return false; // Real error
}
}
total_read += bytes_read;
}
......@@ -388,9 +425,22 @@ bool ws_receive_frame(ws_connection_t *conn, uint8_t *opcode, void **data, size_
bool masked = (header[1] & 0x80) != 0;
size_t total_header_size = min_header_size;
if (masked) {
bytes_read = SSL_read(conn->ssl, header + min_header_size, 4);
if (bytes_read != 4) {
return false;
if (conn->ssl) {
bytes_read = SSL_read(conn->ssl, header + min_header_size, 4);
if (bytes_read != 4) {
return false;
}
} else {
bytes_read = read(conn->sock_fd, header + min_header_size, 4);
if (bytes_read < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return false; // No data available
}
return false; // Real error
}
if (bytes_read != 4) {
return false;
}
}
total_header_size += 4;
}
......@@ -432,10 +482,29 @@ bool ws_receive_frame(ws_connection_t *conn, uint8_t *opcode, void **data, size_
// Limit read size to prevent excessive blocking
size_t to_read = remaining > 8192 ? 8192 : remaining;
bytes_read = SSL_read(conn->ssl, (char *)*data + total_read, to_read);
if (bytes_read <= 0) {
free(*data);
return false;
if (conn->ssl) {
bytes_read = SSL_read(conn->ssl, (char *)*data + total_read, to_read);
if (bytes_read <= 0) {
free(*data);
return false;
}
} else {
bytes_read = read(conn->sock_fd, (char *)*data + total_read, to_read);
if (bytes_read < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// For non-blocking sockets, if we can't read the complete payload at once,
// this is an error since WebSocket frames should be complete
free(*data);
return false;
}
free(*data);
return false;
}
if (bytes_read == 0) {
// Connection closed
free(*data);
return false;
}
}
total_read += bytes_read;
}
......@@ -461,7 +530,7 @@ bool ws_receive_frame(ws_connection_t *conn, uint8_t *opcode, void **data, size_
// Check if WebSocket connection is healthy
bool ws_connection_is_healthy(ws_connection_t *conn) {
// Simple health check - just verify connection is open and has SSL context
// Simple health check - just verify connection is open and has SSL context or socket
// Actual connection health is determined by send/receive operations
return conn && conn->state == WS_STATE_OPEN && conn->ssl;
return conn && conn->state == WS_STATE_OPEN && (conn->ssl || conn->sock_fd >= 0);
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment