From 8ff22dfc2c4f82dc064c963a768242b7d1087cc4 Mon Sep 17 00:00:00 2001 From: Serge NOEL Date: Fri, 5 Dec 2025 16:33:03 +0100 Subject: [PATCH] Test peus concluants --- docker-compose.yaml | 21 ++ rdp-broker.env | 19 ++ rdp-web-gateway.env | 13 + src/Dockerfile | 5 +- src/main.c | 2 +- src/rdp_broker.h | 12 +- src/rdp_server.c | 302 ++++++++++++++++----- src/test.sh | 2 + targets.yaml | 2 +- web-gateway/package.json | 4 +- web-gateway/public/js/app.js | 67 +++-- web-gateway/src/rdpProxyHandler.js | 20 +- web-gateway/src/rdpProxyHandler.new.js | 355 +++++++++++++++++++++++++ web-gateway/src/server.js | 6 +- 14 files changed, 720 insertions(+), 110 deletions(-) create mode 100644 docker-compose.yaml create mode 100644 rdp-broker.env create mode 100644 rdp-web-gateway.env create mode 100755 src/test.sh create mode 100644 web-gateway/src/rdpProxyHandler.new.js diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..2136440 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,21 @@ +services: + rdp-broker: + image: easylinux/rdp-broker:1.0-3 + container_name: rdp-broker + restart: unless-stopped + env_file: + - ./rdp-broker.env + volumes: + - ./targets.yaml:/etc/rdpbroker/targets.yaml:ro + ports: + - "3389:3389" + rdp-web-gateway: + image: easylinux/web-gateway:1.4 + container_name: web-gateway + restart: unless-stopped + env_file: + - ./rdp-web-gateway.env + ports: + - "8085:8080" + depends_on: + - rdp-broker diff --git a/rdp-broker.env b/rdp-broker.env new file mode 100644 index 0000000..76b95ac --- /dev/null +++ b/rdp-broker.env @@ -0,0 +1,19 @@ +# RdpBroker Configuration + +# Samba Active Directory Server +SAMBA_AD_SERVER=192.168.100.240 +SAMBA_AD_BASE_DN=DC=aipice,DC=local +SAMBA_PORT=389 + +# LDAP Base DN for user authentication +# Example: DC=easylinux,DC=lan +BASE_DN=DC=aipice,DC=local + +# RDP Listen Port +RDP_LISTEN_PORT=3389 + +# Targets configuration file +TARGETS_CONFIG=/etc/rdpbroker/targets.yaml + +# Log Level: 0=ERROR, 1=WARN, 2=INFO, 3=DEBUG +LOG_LEVEL=2 diff --git a/rdp-web-gateway.env b/rdp-web-gateway.env new file mode 100644 index 0000000..744ba34 --- /dev/null +++ b/rdp-web-gateway.env @@ -0,0 +1,13 @@ +# Web Gateway Configuration + +# Server Port +PORT=8080 +NODE_ENV=development + +# RdpBroker Connection +RDP_BROKER_HOST=rdp-broker +RDP_BROKER_PORT=3389 + +# Optional: Pre-configure RDP Targets (JSON array) +# Leave empty to get targets dynamically from RdpBroker +# RDP_TARGETS=[{"name":"Server1","host":"192.168.1.100","port":3389,"description":"Test Server"}] diff --git a/src/Dockerfile b/src/Dockerfile index 686b78e..9049281 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM alpine:3.18 AS builder +FROM alpine:3.22 AS builder # Install build dependencies RUN apk add --no-cache \ @@ -19,8 +19,7 @@ COPY *.c *.h Makefile ./ RUN make deps-alpine && make # Runtime stage -FROM alpine:3.18 - +FROM alpine:3.22 # Install runtime dependencies RUN apk add --no-cache \ libldap \ diff --git a/src/main.c b/src/main.c index e96716c..da8c14f 100644 --- a/src/main.c +++ b/src/main.c @@ -6,7 +6,7 @@ #include "rdp_broker.h" int global_log_level = LOG_INFO; -static volatile bool running = true; +volatile bool running = true; /* Global running flag */ void signal_handler(int signum) { if (signum == SIGINT || signum == SIGTERM) { diff --git a/src/rdp_broker.h b/src/rdp_broker.h index 80e4b5e..ba7610d 100644 --- a/src/rdp_broker.h +++ b/src/rdp_broker.h @@ -5,6 +5,9 @@ #include #include +/* Global running flag for graceful shutdown */ +extern volatile bool running; + #define MAX_TARGETS 100 #define MAX_SESSIONS 1000 #define MAX_USERNAME_LEN 256 @@ -69,11 +72,16 @@ int ldap_bind_check(const char *server, int port, const char *username, int start_rdp_server(broker_config_t *config, session_manager_t *sm); int handle_rdp_connection(int client_fd, broker_config_t *config, session_manager_t *sm); +int handle_auth_protocol(int client_fd, char *initial_buffer, int initial_len, + broker_config_t *config, session_manager_t *sm, + const char *client_ip); +void send_json_error(int fd, const char *message); +int send_targets_json(int fd, broker_config_t *config, const char *username); int present_login_screen(int client_fd, char *username, char *password); int present_target_menu(int client_fd, broker_config_t *config, rdp_target_t **selected_target); -int forward_rdp_connection(int client_fd, rdp_target_t *target, - session_info_t *session); +int forward_rdp_connection(int client_fd, const char *target_host, + int target_port, session_info_t *session); /* session_manager.c */ int init_session_manager(session_manager_t *sm); diff --git a/src/rdp_server.c b/src/rdp_server.c index 06ac89f..12c2b61 100644 --- a/src/rdp_server.c +++ b/src/rdp_server.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,8 @@ int start_rdp_server(broker_config_t *config, session_manager_t *sm) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); pthread_t thread_id; + struct timeval timeout; + fd_set read_fds; /* Create listening socket */ server_fd = create_listening_socket(config->rdp_listen_port); @@ -35,9 +38,30 @@ int start_rdp_server(broker_config_t *config, session_manager_t *sm) { LOG(LOG_INFO, "RDP server listening on port %d", config->rdp_listen_port); /* Accept connections in a loop */ - while (1) { + while (running) { + /* Use select with timeout to check running flag periodically */ + FD_ZERO(&read_fds); + FD_SET(server_fd, &read_fds); + timeout.tv_sec = 1; /* 1 second timeout */ + timeout.tv_usec = 0; + + int ready = select(server_fd + 1, &read_fds, NULL, NULL, &timeout); + + if (ready < 0) { + if (!running) break; /* Shutting down */ + LOG(LOG_ERROR, "Select failed: %s", strerror(errno)); + continue; + } + + if (ready == 0) { + /* Timeout - check running flag and continue */ + continue; + } + + /* Socket is ready for accept */ client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { + if (!running) break; /* Shutting down */ LOG(LOG_ERROR, "Accept failed: %s", strerror(errno)); continue; } @@ -70,6 +94,8 @@ int start_rdp_server(broker_config_t *config, session_manager_t *sm) { pthread_detach(thread_id); } + /* Clean shutdown */ + LOG(LOG_INFO, "Closing server socket..."); close(server_fd); return 0; } @@ -133,8 +159,163 @@ int create_listening_socket(int port) { return server_fd; } +/* Handle AUTH/SELECT protocol from web-gateway */ +int handle_auth_protocol(int client_fd, char *initial_buffer, int initial_len, + broker_config_t *config, session_manager_t *sm, + const char *client_ip) { + char username[MAX_USERNAME_LEN] = {0}; + char password[MAX_USERNAME_LEN] = {0}; + char target_name[MAX_HOSTNAME_LEN] = {0}; + char buffer[4096]; + rdp_target_t *selected_target = NULL; + session_info_t *session = NULL; + int ret; + + /* Parse AUTH\nusername\npassword\n from initial_buffer */ + char *line = initial_buffer; + char *lines[3]; + int line_count = 0; + + char *token = strtok(line, "\n"); + while (token != NULL && line_count < 3) { + lines[line_count++] = token; + token = strtok(NULL, "\n"); + } + + if (line_count < 3 || strcmp(lines[0], "AUTH") != 0) { + LOG(LOG_ERROR, "Invalid AUTH protocol format"); + send_json_error(client_fd, "Invalid protocol format"); + return -1; + } + + strncpy(username, lines[1], MAX_USERNAME_LEN - 1); + strncpy(password, lines[2], MAX_USERNAME_LEN - 1); + + /* Authenticate user */ + LOG(LOG_INFO, "Authenticating user: %s from %s", username, client_ip); + if (authenticate_user(username, password, config->samba_server, + config->samba_port, config->base_dn) != 0) { + LOG(LOG_WARN, "Authentication failed for user: %s", username); + send_json_error(client_fd, "Invalid credentials"); + return -1; + } + + LOG(LOG_INFO, "User %s authenticated successfully", username); + + /* Send targets list as JSON */ + if (send_targets_json(client_fd, config, username) != 0) { + LOG(LOG_ERROR, "Failed to send targets list"); + return -1; + } + + /* Wait for SELECT message */ + memset(buffer, 0, sizeof(buffer)); + ret = recv(client_fd, buffer, sizeof(buffer) - 1, 0); + if (ret <= 0) { + LOG(LOG_ERROR, "Failed to receive SELECT message"); + return -1; + } + + /* Parse SELECT\ntarget_name\n */ + line_count = 0; + line = buffer; + token = strtok(line, "\n"); + while (token != NULL && line_count < 2) { + lines[line_count++] = token; + token = strtok(NULL, "\n"); + } + + if (line_count < 2 || strcmp(lines[0], "SELECT") != 0) { + LOG(LOG_ERROR, "Invalid SELECT protocol format"); + send_json_error(client_fd, "Invalid protocol format"); + return -1; + } + + strncpy(target_name, lines[1], MAX_HOSTNAME_LEN - 1); + + /* Find target by name */ + for (int i = 0; i < config->target_count; i++) { + if (strcmp(config->targets[i].name, target_name) == 0) { + selected_target = &config->targets[i]; + break; + } + } + + if (!selected_target) { + LOG(LOG_ERROR, "Target not found: %s", target_name); + send_json_error(client_fd, "Target not found"); + return -1; + } + + LOG(LOG_INFO, "User %s selected target: %s (%s:%d)", + username, selected_target->name, selected_target->host, selected_target->port); + + /* Send RDP ready message */ + const char *ready_msg = "{\"type\":\"rdp_ready\"}\n\n"; + send(client_fd, ready_msg, strlen(ready_msg), 0); + + /* Create session */ + session = create_session(sm, username, client_ip, selected_target, client_fd); + if (!session) { + LOG(LOG_ERROR, "Failed to create session"); + return -1; + } + + /* Connect to target RDP server and forward traffic */ + LOG(LOG_INFO, "Connecting to target RDP server %s:%d", + selected_target->host, selected_target->port); + + ret = forward_rdp_connection(client_fd, selected_target->host, + selected_target->port, session); + + /* Cleanup session */ + close_session(sm, session->session_id); + + return ret; +} + +/* Send JSON error message */ +void send_json_error(int fd, const char *message) { + char buffer[512]; + snprintf(buffer, sizeof(buffer), + "{\"type\":\"auth_failed\",\"message\":\"%s\"}\n\n", message); + send(fd, buffer, strlen(buffer), 0); +} + +/* Send targets list as JSON */ +int send_targets_json(int fd, broker_config_t *config, const char *username) { + char buffer[8192]; + int offset = 0; + + /* Start JSON */ + offset += snprintf(buffer + offset, sizeof(buffer) - offset, + "{\"type\":\"auth_success\",\"targets\":["); + + /* Add targets - for now, all targets (TODO: filter by user groups) */ + for (int i = 0; i < config->target_count; i++) { + if (i > 0) { + offset += snprintf(buffer + offset, sizeof(buffer) - offset, ","); + } + + offset += snprintf(buffer + offset, sizeof(buffer) - offset, + "{\"name\":\"%s\",\"host\":\"%s\",\"port\":%d,\"description\":\"%s\"}", + config->targets[i].name, + config->targets[i].host, + config->targets[i].port, + config->targets[i].description); + } + + /* End JSON */ + offset += snprintf(buffer + offset, sizeof(buffer) - offset, "]}\n\n"); + + LOG(LOG_DEBUG, "Sending %d targets to user %s", config->target_count, username); + + return send(fd, buffer, strlen(buffer), 0) > 0 ? 0 : -1; +} + int handle_rdp_connection(int client_fd, broker_config_t *config, session_manager_t *sm) { + char buffer[4096]; char username[MAX_USERNAME_LEN] = {0}; char password[MAX_USERNAME_LEN] = {0}; rdp_target_t *selected_target = NULL; @@ -142,69 +323,32 @@ int handle_rdp_connection(int client_fd, broker_config_t *config, struct sockaddr_in addr; socklen_t addr_len = sizeof(addr); char client_ip[INET_ADDRSTRLEN] = {0}; + int ret; /* Get client IP */ if (getpeername(client_fd, (struct sockaddr *)&addr, &addr_len) == 0) { inet_ntop(AF_INET, &addr.sin_addr, client_ip, INET_ADDRSTRLEN); } - /* Step 1: Present login screen and get credentials */ - LOG(LOG_INFO, "Presenting login screen to client"); - if (present_login_screen(client_fd, username, password) != 0) { - LOG(LOG_ERROR, "Failed to get login credentials"); + /* Read first message to detect protocol type */ + memset(buffer, 0, sizeof(buffer)); + ret = recv(client_fd, buffer, sizeof(buffer) - 1, 0); + if (ret <= 0) { + LOG(LOG_ERROR, "Failed to receive initial message"); return -1; } - /* Step 2: Authenticate user */ - LOG(LOG_INFO, "Authenticating user: %s from %s", username, client_ip); - if (authenticate_user(username, password, config->samba_server, - config->samba_port, config->base_dn) != 0) { - LOG(LOG_WARN, "Authentication failed for user: %s", username); - const char *msg = "Authentication failed\n"; + /* Check if this is AUTH protocol (web-gateway) */ + if (strncmp(buffer, "AUTH\n", 5) == 0) { + LOG(LOG_INFO, "Detected AUTH protocol from web-gateway"); + return handle_auth_protocol(client_fd, buffer, ret, config, sm, client_ip); + } else { + /* Legacy RDP protocol - not supported anymore */ + LOG(LOG_WARN, "Legacy RDP protocol not supported, use web-gateway"); + const char *msg = "ERROR: Please use web-gateway for access\n"; send(client_fd, msg, strlen(msg), 0); return -1; } - - LOG(LOG_INFO, "User %s authenticated successfully", username); - - /* Step 3: Present target menu */ - LOG(LOG_INFO, "Presenting target menu to user: %s", username); - if (present_target_menu(client_fd, config, &selected_target) != 0) { - LOG(LOG_ERROR, "Failed to get target selection"); - return -1; - } - - if (!selected_target) { - LOG(LOG_ERROR, "No target selected"); - return -1; - } - - LOG(LOG_INFO, "User %s selected target: %s (%s:%d)", username, - selected_target->name, selected_target->host, selected_target->port); - - /* Step 4: Create session */ - session = create_session(sm, username, client_ip, selected_target, client_fd); - if (!session) { - LOG(LOG_ERROR, "Failed to create session"); - return -1; - } - - LOG(LOG_INFO, "Created session %u for %s -> %s", session->session_id, - username, selected_target->name); - - /* Step 5: Forward RDP connection */ - LOG(LOG_INFO, "Forwarding RDP connection for session %u", session->session_id); - if (forward_rdp_connection(client_fd, selected_target, session) != 0) { - LOG(LOG_ERROR, "Failed to forward RDP connection"); - close_session(sm, session->session_id); - return -1; - } - - /* Connection closed */ - LOG(LOG_INFO, "Session %u ended", session->session_id); - close_session(sm, session->session_id); - - return 0; } int present_login_screen(int client_fd, char *username, char *password) { @@ -307,8 +451,8 @@ int present_target_menu(int client_fd, broker_config_t *config, return 0; } -int forward_rdp_connection(int client_fd, rdp_target_t *target, - session_info_t *session) { +int forward_rdp_connection(int client_fd, const char *target_host, + int target_port, session_info_t *session) { int target_fd; struct sockaddr_in target_addr; fd_set read_fds; @@ -325,10 +469,10 @@ int forward_rdp_connection(int client_fd, rdp_target_t *target, memset(&target_addr, 0, sizeof(target_addr)); target_addr.sin_family = AF_INET; - target_addr.sin_port = htons(target->port); + target_addr.sin_port = htons(target_port); - if (inet_pton(AF_INET, target->host, &target_addr.sin_addr) <= 0) { - LOG(LOG_ERROR, "Invalid target address: %s", target->host); + if (inet_pton(AF_INET, target_host, &target_addr.sin_addr) <= 0) { + LOG(LOG_ERROR, "Invalid target address: %s", target_host); close(target_fd); return -1; } @@ -336,38 +480,53 @@ int forward_rdp_connection(int client_fd, rdp_target_t *target, if (connect(target_fd, (struct sockaddr *)&target_addr, sizeof(target_addr)) < 0) { LOG(LOG_ERROR, "Failed to connect to target %s:%d: %s", - target->host, target->port, strerror(errno)); + target_host, target_port, strerror(errno)); close(target_fd); return -1; } - LOG(LOG_INFO, "Connected to target %s:%d", target->host, target->port); + LOG(LOG_INFO, "Connected to target %s:%d", target_host, target_port); session->target_fd = target_fd; - const char *msg = "Connected to target. Starting RDP session...\n"; - send(client_fd, msg, strlen(msg), 0); - - /* Forward data bidirectionally */ + /* Forward data bidirectionally - no messages sent, just raw RDP forwarding */ max_fd = (client_fd > target_fd) ? client_fd : target_fd; - while (1) { + LOG(LOG_INFO, "Starting bidirectional forwarding loop (client_fd=%d, target_fd=%d)", + client_fd, target_fd); + + while (running) { + struct timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + FD_ZERO(&read_fds); FD_SET(client_fd, &read_fds); FD_SET(target_fd, &read_fds); - if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) < 0) { + int sel_ret = select(max_fd + 1, &read_fds, NULL, NULL, &timeout); + if (sel_ret < 0) { LOG(LOG_ERROR, "Select failed: %s", strerror(errno)); break; } + if (sel_ret == 0) { + /* Timeout - continue loop to check running flag */ + continue; + } + /* Data from client to target */ if (FD_ISSET(client_fd, &read_fds)) { n = recv(client_fd, buffer, sizeof(buffer), 0); - if (n <= 0) { - LOG(LOG_DEBUG, "Client connection closed"); + if (n < 0) { + LOG(LOG_ERROR, "Error receiving from client: %s", strerror(errno)); + break; + } + if (n == 0) { + LOG(LOG_INFO, "Client connection closed gracefully"); break; } + LOG(LOG_DEBUG, "Forwarding %d bytes from client to target", n); if (send(target_fd, buffer, n, 0) != n) { LOG(LOG_ERROR, "Failed to forward to target"); break; @@ -379,11 +538,16 @@ int forward_rdp_connection(int client_fd, rdp_target_t *target, /* Data from target to client */ if (FD_ISSET(target_fd, &read_fds)) { n = recv(target_fd, buffer, sizeof(buffer), 0); - if (n <= 0) { - LOG(LOG_DEBUG, "Target connection closed"); + if (n < 0) { + LOG(LOG_ERROR, "Error receiving from target: %s", strerror(errno)); + break; + } + if (n == 0) { + LOG(LOG_INFO, "Target connection closed gracefully"); break; } + LOG(LOG_DEBUG, "Forwarding %d bytes from target to client", n); if (send(client_fd, buffer, n, 0) != n) { LOG(LOG_ERROR, "Failed to forward to client"); break; diff --git a/src/test.sh b/src/test.sh new file mode 100755 index 0000000..1a3bde7 --- /dev/null +++ b/src/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker container run -it -e SAMBA_AD_SERVER=192.168.100.240 -e SAMBA_AD_BASE_DN="DC=aipice,DC=local" -v /data/apps/RdpBroker/targets.yaml:/etc/rdpbroker/targets.yaml -p 3389:3389 easylinux/rdp-broker:1.0-1 sh diff --git a/targets.yaml b/targets.yaml index eaa72d8..6d1eed4 100644 --- a/targets.yaml +++ b/targets.yaml @@ -16,7 +16,7 @@ targets: # Development Desktop - name: "Development Desktop" - host: "dev-machine.local" + host: "192.168.100.135" port: 3389 description: "Developer Workstation" diff --git a/web-gateway/package.json b/web-gateway/package.json index 5bff03d..26f225d 100644 --- a/web-gateway/package.json +++ b/web-gateway/package.json @@ -23,7 +23,9 @@ "dotenv": "^16.3.1", "compression": "^1.7.4", "helmet": "^7.1.0", - "cors": "^2.8.5" + "cors": "^2.8.5", + "node-rdpjs-2": "^0.3.4", + "pngjs": "^7.0.0" }, "devDependencies": { "nodemon": "^3.0.2" diff --git a/web-gateway/public/js/app.js b/web-gateway/public/js/app.js index a51fda9..7568a42 100644 --- a/web-gateway/public/js/app.js +++ b/web-gateway/public/js/app.js @@ -80,7 +80,9 @@ class RDPWebGateway { await this.authenticateAndLoadTargets(); } catch (error) { console.error('Login error:', error); - this.showError(errorMessage, 'Connection error. Please check your network and try again.'); + // Show specific error message if available + const errorMsg = error.message || 'Connection error. Please check your network and try again.'; + this.showError(errorMessage, errorMsg); loginBtn.disabled = false; btnText.style.display = 'block'; spinner.style.display = 'none'; @@ -244,7 +246,7 @@ class RDPWebGateway { } handleWebSocketMessage(event) { - if (typeof event.data === 'string') { + try { const message = JSON.parse(event.data); switch (message.type) { @@ -252,30 +254,44 @@ class RDPWebGateway { document.getElementById('loadingOverlay').style.display = 'none'; document.getElementById('connectionInfo').textContent = `Connected to ${this.currentTarget.name}`; + console.log('RDP connection established'); break; + + case 'screen': + // Render screen update (PNG image data) + this.renderScreenUpdate(message); + break; + + case 'disconnected': + this.showConnectionError('RDP connection closed'); + break; + case 'error': this.showConnectionError(message.error); break; - case 'resize': - this.canvas.width = message.width; - this.canvas.height = message.height; - break; + + default: + console.warn('Unknown message type:', message.type); } - } else { - // Binary data - frame update - this.renderFrame(event.data); + } catch (error) { + console.error('Error handling WebSocket message:', error); } } - renderFrame(data) { - // This is a simplified version - // In production, you'd decode the RDP frame data properly - const imageData = new Uint8ClampedArray(data); - - if (imageData.length === this.canvas.width * this.canvas.height * 4) { - const imgData = this.ctx.createImageData(this.canvas.width, this.canvas.height); - imgData.data.set(imageData); - this.ctx.putImageData(imgData, 0, 0); + renderScreenUpdate(update) { + try { + // Decode base64 PNG image + const img = new Image(); + img.onload = () => { + // Draw image to canvas at specified position + this.ctx.drawImage(img, update.x, update.y, update.width, update.height); + }; + img.onerror = (error) => { + console.error('Failed to load screen update image:', error); + }; + img.src = 'data:image/png;base64,' + update.data; + } catch (error) { + console.error('Error rendering screen update:', error); } } @@ -326,13 +342,18 @@ class RDPWebGateway { const x = Math.floor((event.clientX - rect.left) * (this.canvas.width / rect.width)); const y = Math.floor((event.clientY - rect.top) * (this.canvas.height / rect.height)); + // Map button: 0=left, 1=middle, 2=right + let button = 0; + if (type === 'down' || type === 'up') { + button = event.button; + } + this.ws.send(JSON.stringify({ type: 'mouse', action: type, x: x, y: y, - button: event.button, - deltaY: event.deltaY || 0, + button: button })); } @@ -342,8 +363,9 @@ class RDPWebGateway { this.ws.send(JSON.stringify({ type: 'keyboard', action: type, - key: event.key, - code: event.code, + code: event.keyCode || event.which + })); + } keyCode: event.keyCode, ctrlKey: event.ctrlKey, altKey: event.altKey, @@ -438,3 +460,4 @@ class RDPWebGateway { // Initialize the app const rdpGateway = new RDPWebGateway(); +// v1.4 diff --git a/web-gateway/src/rdpProxyHandler.js b/web-gateway/src/rdpProxyHandler.js index e675a5d..cbe62ae 100644 --- a/web-gateway/src/rdpProxyHandler.js +++ b/web-gateway/src/rdpProxyHandler.js @@ -8,6 +8,7 @@ class RDPProxyHandler { this.rdpSocket = null; this.isConnected = false; this.isAuthenticated = false; + this.isRdpMode = false; // Track when in binary RDP forwarding mode this.dataBuffer = ''; this.pendingTarget = null; } @@ -84,7 +85,15 @@ class RDPProxyHandler { } handleBrokerData(data) { - // Accumulate data in buffer + // If in RDP mode, forward binary data directly + if (this.isRdpMode) { + if (this.ws.readyState === 1) { + this.ws.send(data); + } + return; + } + + // Otherwise, accumulate and parse JSON messages this.dataBuffer += data.toString(); // Check if we have complete message (ends with \n\n or specific delimiter) @@ -98,13 +107,6 @@ class RDPProxyHandler { } }); } - - // If already authenticated and in RDP session, forward raw data to WebSocket - if (this.isAuthenticated && this.pendingTarget === null) { - if (this.ws.readyState === 1) { - this.ws.send(data); - } - } } processBrokerMessage(message) { @@ -137,6 +139,8 @@ class RDPProxyHandler { target: this.pendingTarget })); this.pendingTarget = null; + this.isRdpMode = true; // Switch to binary RDP forwarding mode + this.dataBuffer = ''; // Clear JSON buffer } else { console.warn(`${new Date().toISOString()} [WARN] Unknown broker message type:`, data.type); diff --git a/web-gateway/src/rdpProxyHandler.new.js b/web-gateway/src/rdpProxyHandler.new.js new file mode 100644 index 0000000..5d8ce1e --- /dev/null +++ b/web-gateway/src/rdpProxyHandler.new.js @@ -0,0 +1,355 @@ +const net = require('net'); +const rdp = require('node-rdpjs-2'); +const { PNG } = require('pngjs'); + +/** + * Clean RDP Proxy Handler - v1.3 + * Handles: Authentication with broker → Target selection → RDP connection with screen updates + */ +class RDPProxyHandler { + constructor(websocket, rdpBrokerHost, rdpBrokerPort) { + this.ws = websocket; + this.brokerHost = rdpBrokerHost; + this.brokerPort = rdpBrokerPort; + + // State + this.state = 'INIT'; // INIT → AUTH → TARGETS → CONNECTED + this.username = null; + this.currentTarget = null; + + // Promise callbacks for async operations + this.authResolve = null; + this.authReject = null; + + // Broker connection (for AUTH/SELECT protocol) + this.brokerSocket = null; + this.brokerBuffer = ''; + + // RDP client (for actual RDP connection to target) + this.rdpClient = null; + this.rdpConnected = false; + } + + /** + * Handle incoming WebSocket message from browser + */ + async handleMessage(message) { + try { + const msg = JSON.parse(message); + + switch (msg.type) { + case 'authenticate': + await this.handleAuthenticate(msg.username, msg.password); + break; + + case 'connect': + await this.handleConnectToTarget(msg.target); + break; + + case 'mouse': + this.handleMouseInput(msg); + break; + + case 'keyboard': + this.handleKeyboardInput(msg); + break; + + default: + console.warn(`Unknown message type: ${msg.type}`); + } + } catch (error) { + console.error('Error handling message:', error); + this.sendError('Failed to process message'); + } + } + + /** + * Phase 1: Authenticate with RdpBroker + */ + async handleAuthenticate(username, password) { + console.log(`[AUTH] Authenticating user: ${username}`); + this.username = username; + this.state = 'AUTH'; + + return new Promise((resolve, reject) => { + this.authResolve = resolve; + this.authReject = reject; + + this.brokerSocket = new net.Socket(); + + this.brokerSocket.connect(this.brokerPort, this.brokerHost, () => { + console.log('[AUTH] Connected to broker'); + // Send AUTH protocol message + const authMsg = `AUTH\n${username}\n${password}\n`; + this.brokerSocket.write(authMsg); + }); + + this.brokerSocket.on('data', (data) => { + this.brokerBuffer += data.toString(); + + // Check for complete JSON message (ends with \n\n) + if (this.brokerBuffer.includes('\n\n')) { + const messages = this.brokerBuffer.split('\n\n'); + this.brokerBuffer = messages.pop(); + + messages.forEach(msgText => { + if (msgText.trim()) { + try { + const msg = JSON.parse(msgText.trim()); + this.handleBrokerMessage(msg); + } catch (e) { + console.error('Failed to parse broker message:', e); + } + } + }); + } + }); + + this.brokerSocket.on('error', (error) => { + console.error('[AUTH] Broker connection error:', error); + this.sendError('Failed to connect to authentication server'); + if (this.authReject) { + this.authReject(error); + this.authReject = null; + this.authResolve = null; + } + }); + + this.brokerSocket.on('close', () => { + console.log('[AUTH] Broker connection closed'); + }); + }); + } + + /** + * Handle messages from RdpBroker (auth results, target lists, etc) + */ + handleBrokerMessage(msg) { + console.log('[BROKER] Received:', msg.type); + + switch (msg.type) { + case 'auth_success': + console.log(`[AUTH] Success - ${msg.targets.length} targets available`); + this.state = 'TARGETS'; + this.sendToClient({ + type: 'targets', + targets: msg.targets + }); + // Resolve the authentication promise + if (this.authResolve) { + this.authResolve(); + this.authResolve = null; + this.authReject = null; + } + break; + + case 'auth_failed': + console.log('[AUTH] Failed:', msg.message); + this.sendError(msg.message || 'Authentication failed'); + // Reject the authentication promise + if (this.authReject) { + this.authReject(new Error(msg.message)); + this.authResolve = null; + this.authReject = null; + } + this.cleanup(); + break; + + case 'rdp_ready': + console.log('[BROKER] RDP session ready signal received'); + // Broker has connected to target, but we won't use this connection + // We'll create our own RDP client connection + break; + + default: + console.warn('[BROKER] Unknown message type:', msg.type); + } + } + + /** + * Phase 2: Connect to selected target via RDP + */ + async handleConnectToTarget(target) { + if (this.state !== 'TARGETS') { + this.sendError('Must authenticate first'); + return; + } + + console.log(`[RDP] Connecting to target: ${target.name}`); + this.currentTarget = target; + + // Send SELECT message to broker (to maintain session tracking) + const selectMsg = `SELECT\n${target.name}\n`; + if (this.brokerSocket && !this.brokerSocket.destroyed) { + this.brokerSocket.write(selectMsg); + } + + // Create direct RDP connection to target + await this.connectRDP(target.host, target.port); + } + + /** + * Create RDP client connection and handle screen updates + */ + async connectRDP(host, port) { + return new Promise((resolve, reject) => { + try { + console.log(`[RDP] Creating client for ${host}:${port}`); + + this.rdpClient = rdp.createClient({ + domain: '', + userName: this.username, + password: '', // Already authenticated via broker + enablePerf: true, + autoLogin: true, + screen: { width: 1024, height: 768 }, + locale: 'en', + logLevel: 'INFO' + }).on('connect', () => { + console.log('[RDP] Connected'); + this.rdpConnected = true; + this.state = 'CONNECTED'; + + this.sendToClient({ + type: 'connected', + target: this.currentTarget + }); + + resolve(); + + }).on('bitmap', (bitmap) => { + // Received screen update from RDP server + this.handleScreenUpdate(bitmap); + + }).on('close', () => { + console.log('[RDP] Connection closed'); + this.rdpConnected = false; + this.sendToClient({ type: 'disconnected' }); + + }).on('error', (error) => { + console.error('[RDP] Error:', error); + this.sendError('RDP connection failed: ' + error.message); + reject(error); + }); + + // Connect to RDP server + this.rdpClient.connect(host, port); + + } catch (error) { + console.error('[RDP] Failed to create client:', error); + this.sendError('Failed to initialize RDP client'); + reject(error); + } + }); + } + + /** + * Handle screen bitmap updates from RDP server + */ + handleScreenUpdate(bitmap) { + try { + // Convert bitmap to PNG and send to browser + const png = new PNG({ + width: bitmap.width, + height: bitmap.height + }); + + // Copy bitmap data (assuming RGBA format) + png.data = Buffer.from(bitmap.data); + + const buffer = PNG.sync.write(png); + const base64Data = buffer.toString('base64'); + + this.sendToClient({ + type: 'screen', + x: bitmap.destLeft || 0, + y: bitmap.destTop || 0, + width: bitmap.width, + height: bitmap.height, + data: base64Data + }); + + } catch (error) { + console.error('[RDP] Error processing screen update:', error); + } + } + + /** + * Handle mouse input from browser + */ + handleMouseInput(msg) { + if (!this.rdpConnected || !this.rdpClient) return; + + try { + this.rdpClient.sendPointerEvent( + msg.x, + msg.y, + msg.button || 0, + msg.action === 'down' + ); + } catch (error) { + console.error('[RDP] Mouse input error:', error); + } + } + + /** + * Handle keyboard input from browser + */ + handleKeyboardInput(msg) { + if (!this.rdpConnected || !this.rdpClient) return; + + try { + this.rdpClient.sendKeyEventScancode( + msg.code, + msg.action === 'down' + ); + } catch (error) { + console.error('[RDP] Keyboard input error:', error); + } + } + + /** + * Send message to browser client + */ + sendToClient(message) { + if (this.ws.readyState === 1) { + this.ws.send(JSON.stringify(message)); + } + } + + /** + * Send error to browser client + */ + sendError(message) { + this.sendToClient({ + type: 'error', + error: message + }); + } + + /** + * Cleanup all connections + */ + cleanup() { + console.log('[CLEANUP] Closing connections'); + + if (this.rdpClient) { + try { + this.rdpClient.close(); + } catch (e) { + console.error('[CLEANUP] Error closing RDP client:', e); + } + this.rdpClient = null; + } + + if (this.brokerSocket && !this.brokerSocket.destroyed) { + this.brokerSocket.destroy(); + this.brokerSocket = null; + } + + this.rdpConnected = false; + this.state = 'INIT'; + } +} + +module.exports = RDPProxyHandler; diff --git a/web-gateway/src/server.js b/web-gateway/src/server.js index 4919b59..5c02ced 100644 --- a/web-gateway/src/server.js +++ b/web-gateway/src/server.js @@ -6,7 +6,7 @@ const net = require('net'); const compression = require('compression'); const helmet = require('helmet'); const cors = require('cors'); -const RDPProxyHandler = require('./rdpProxyHandler'); +const RDPProxyHandler = require('./rdpProxyHandler.new'); class RDPWebGatewayServer { constructor() { @@ -111,8 +111,8 @@ class RDPWebGatewayServer { ws.on('message', (data) => { try { - const message = JSON.parse(data.toString()); - proxyHandler.handleMessage(message); + // All messages are JSON (no binary mode) + proxyHandler.handleMessage(data.toString()); } catch (error) { console.error(`${new Date().toISOString()} [ERROR] WebSocket message error:`, error); ws.send(JSON.stringify({