Fix RDP page connection by adding keyboard and mouse event handling from example.html

- Added WallixModule initialization with logging
- Integrated keyboard classes (Keyboard, EmulatedKeyboard, UnicodeKeyboard) from example
- Added mouse event handling functions
- Implemented event listeners for canvas and focus
- Updated connect function to properly set up RDP client with input handling
- Added updateInput and keyboard layout management
- Cleaned up event listeners on disconnect
parent cf9c5ea9
...@@ -3,4 +3,4 @@ ...@@ -3,4 +3,4 @@
- the Makefile is generated by the script configure.sh - the Makefile is generated by the script configure.sh
- in wssshd webinterface, the vnc page and the rdp page uses different websocket implementation, looks at the specifics of web.c and vnc.c or rdp.c, the file websocket.c is user ALSO for the websockets in the communication port with wssshc and wsssht, if you modify something for vnc or rdp or anything in the web interface, don't break the implementation for the wssshc-wssshd-wsssht communication channel/protocol - in wssshd webinterface, the vnc page and the rdp page uses different websocket implementation, looks at the specifics of web.c and vnc.c or rdp.c, the file websocket.c is user ALSO for the websockets in the communication port with wssshc and wsssht, if you modify something for vnc or rdp or anything in the web interface, don't break the implementation for the wssshc-wssshd-wsssht communication channel/protocol
- vnc page on wssshd uses the project noVNC - vnc page on wssshd uses the project noVNC
- rdp page on wssshd uses rdp.wasm, project page: https://github.com/sitepi/rdp.wasm - rdp page on wssshd uses rdp.wasm, project page: https://github.com/sitepi/rdp.wasm, you can check the example page at https://github.com/sitepi/rdp.wasm/blob/main/example.html
...@@ -2,3 +2,5 @@ ...@@ -2,3 +2,5 @@
- the script embed_assets.sh is launched by the Makefile - the script embed_assets.sh is launched by the Makefile
- the Makefile is generated by the script configure.sh - the Makefile is generated by the script configure.sh
- in wssshd webinterface, the vnc page and the rdp page uses different websocket implementation, looks at the specifics of web.c and vnc.c or rdp.c, the file websocket.c is user ALSO for the websockets in the communication port with wssshc and wsssht, if you modify something for vnc or rdp or anything in the web interface, don't break the implementation for the wssshc-wssshd-wsssht communication channel/protocol - in wssshd webinterface, the vnc page and the rdp page uses different websocket implementation, looks at the specifics of web.c and vnc.c or rdp.c, the file websocket.c is user ALSO for the websockets in the communication port with wssshc and wsssht, if you modify something for vnc or rdp or anything in the web interface, don't break the implementation for the wssshc-wssshd-wsssht communication channel/protocol
- vnc page on wssshd uses the project noVNC
- rdp page on wssshd uses rdp.wasm, project page: https://github.com/sitepi/rdp.wasm
...@@ -1549,6 +1549,10 @@ const char *rdp_rdp_wasm_js = ...@@ -1549,6 +1549,10 @@ const char *rdp_rdp_wasm_js =
" define([], function() { return WallixModule; });\n" " define([], function() { return WallixModule; });\n"
"else if (typeof exports === 'object')\n" "else if (typeof exports === 'object')\n"
" exports[\"WallixModule\"] = WallixModule;\n" " exports[\"WallixModule\"] = WallixModule;\n"
"else {\n"
" window.WallixModule = WallixModule;\n"
" window.Module = WallixModule;\n"
"}\n"
"\n" "\n"
; ;
if (strcmp(path, "/rdpwasm/rdp.wasm.js") == 0) { if (strcmp(path, "/rdpwasm/rdp.wasm.js") == 0) {
...@@ -2975,7 +2979,7 @@ const char *rdp_rdp_graphics_js = ...@@ -2975,7 +2979,7 @@ const char *rdp_rdp_graphics_js =
" return ctx ? createCtx(ctx, newRdpPointer(canvasElement, module)) : undefined;\n" " return ctx ? createCtx(ctx, newRdpPointer(canvasElement, module)) : undefined;\n"
"};\n" "};\n"
"\n" "\n"
"try {\n" "if (typeof module !== 'undefined' && module.exports) {\n"
" module.exports.newRdpGraphics2D = newRdpGraphics2D;\n" " module.exports.newRdpGraphics2D = newRdpGraphics2D;\n"
" module.exports.newRdpGraphicsGL = newRdpGraphicsGL;\n" " module.exports.newRdpGraphicsGL = newRdpGraphicsGL;\n"
" module.exports.newRdpGraphicsGL2 = newRdpGraphicsGL2;\n" " module.exports.newRdpGraphicsGL2 = newRdpGraphicsGL2;\n"
...@@ -2984,9 +2988,15 @@ const char *rdp_rdp_graphics_js = ...@@ -2984,9 +2988,15 @@ const char *rdp_rdp_graphics_js =
" module.exports.newRdpGL = newRdpGL;\n" " module.exports.newRdpGL = newRdpGL;\n"
" module.exports.newRdpGL2 = newRdpGL2;\n" " module.exports.newRdpGL2 = newRdpGL2;\n"
" module.exports.newRdpPointer = newRdpPointer;\n" " module.exports.newRdpPointer = newRdpPointer;\n"
"}\n" "} else {\n"
"catch (e) {\n" " window.newRdpGraphics2D = newRdpGraphics2D;\n"
" // module not found\n" " window.newRdpGraphicsGL = newRdpGraphicsGL;\n"
" window.newRdpGraphicsGL2 = newRdpGraphicsGL2;\n"
" window.newRdpGraphics = newRdpGraphics;\n"
" window.newRdpCanvas = newRdpCanvas;\n"
" window.newRdpGL = newRdpGL;\n"
" window.newRdpGL2 = newRdpGL2;\n"
" window.newRdpPointer = newRdpPointer;\n"
"}\n" "}\n"
"\n" "\n"
; ;
...@@ -103,7 +103,7 @@ function updateClients() { ...@@ -103,7 +103,7 @@ function updateClients() {
actionsHtml += `<a href="/vnc/${clientId}" class="btn btn-info btn-sm me-1"><i class="fas fa-desktop"></i> VNC</a>`; actionsHtml += `<a href="/vnc/${clientId}" class="btn btn-info btn-sm me-1"><i class="fas fa-desktop"></i> VNC</a>`;
} }
if (services.includes('rdp')) { if (services.includes('rdp')) {
actionsHtml += `<a href="/rdp/${clientId}" class="btn btn-primary btn-sm me-1"><i class="fas fa-windows"></i> RDP</a>`; actionsHtml += `<a href="/rdp/${clientId}" class="btn btn-primary btn-sm me-1"><i class="fas fa-desktop"></i> RDP</a>`;
} }
clientListHtml += ` clientListHtml += `
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
......
...@@ -158,6 +158,113 @@ ...@@ -158,6 +158,113 @@
<a class="nav-link" href="/logout">Logout</a> <a class="nav-link" href="/logout">Logout</a>
</div></div></nav> </div></div></nav>
<div id="notification-area" class="position-fixed top-0 end-0 p-3" style="z-index: 1050;"></div> <div id="notification-area" class="position-fixed top-0 end-0 p-3" style="z-index: 1050;"></div>
<!-- RDP Configuration Modal -->
<div class="modal fade" id="rdpConfigModal" tabindex="-1" aria-labelledby="rdpConfigModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="rdpConfigModalLabel">RDP Connection Configuration</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="rdpConfigForm">
<div class="row">
<div class="col-md-6">
<fieldset class="border p-3 mb-3">
<legend class="w-auto">Authentication</legend>
<div class="mb-3">
<label for="rdpUsername" class="form-label">Username</label>
<input type="text" class="form-control" id="rdpUsername" placeholder="Username">
</div>
<div class="mb-3">
<label for="rdpPassword" class="form-label">Password</label>
<input type="password" class="form-control" id="rdpPassword" placeholder="Password">
</div>
<div class="mb-3">
<label for="rdpDomain" class="form-label">Domain</label>
<input type="text" class="form-control" id="rdpDomain" placeholder="Domain (optional)">
</div>
</fieldset>
</div>
<div class="col-md-6">
<fieldset class="border p-3 mb-3">
<legend class="w-auto">Display</legend>
<div class="mb-3">
<label for="rdpWidth" class="form-label">Width</label>
<input type="number" class="form-control" id="rdpWidth" value="1024" min="640" max="4096">
</div>
<div class="mb-3">
<label for="rdpHeight" class="form-label">Height</label>
<input type="number" class="form-control" id="rdpHeight" value="768" min="480" max="2160">
</div>
<div class="mb-3">
<label for="rdpBpp" class="form-label">Color Depth (BPP)</label>
<select class="form-select" id="rdpBpp">
<option value="8">8-bit</option>
<option value="15">15-bit</option>
<option value="16" selected>16-bit</option>
<option value="24">24-bit</option>
<option value="32">32-bit</option>
</select>
</div>
<div class="mb-3">
<label for="rdpGraphicsImpl" class="form-label">Graphics Implementation</label>
<select class="form-select" id="rdpGraphicsImpl">
<option value="2d" selected>Canvas 2D</option>
<option value="webgl">WebGL</option>
<option value="webgl2">WebGL2</option>
</select>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col-md-6">
<fieldset class="border p-3 mb-3">
<legend class="w-auto">Keyboard</legend>
<div class="mb-3">
<label for="rdpKeyboardMethod" class="form-label">Keyboard Method</label>
<select class="form-select" id="rdpKeyboardMethod">
<option value="codeToScancode" selected>KeyboardEvent.code → Scancode</option>
<option value="emulateScancode">KeyboardEvent.key → Scancode or Unicode</option>
<option value="unicode">KeyboardEvent.key → Unicode</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="rdpAltGrCtrlAlt" checked>
<label class="form-check-label" for="rdpAltGrCtrlAlt">
Ctrl + Alt = AltGr
</label>
</div>
</div>
</fieldset>
</div>
<div class="col-md-6">
<fieldset class="border p-3 mb-3">
<legend class="w-auto">Advanced</legend>
<div class="mb-3">
<label for="rdpVerbosity" class="form-label">Verbosity Level</label>
<input type="number" class="form-control" id="rdpVerbosity" value="0" min="0">
</div>
<div class="mb-3">
<label for="rdpJsonConfig" class="form-label">JSON Configuration (Advanced)</label>
<textarea class="form-control" id="rdpJsonConfig" rows="3" placeholder='{"key": "value"}'></textarea>
</div>
</fieldset>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="startRdpBtn">Start RDP Session</button>
</div>
</div>
</div>
</div>
<div class="container mt-4"> <div class="container mt-4">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
...@@ -200,16 +307,439 @@ ...@@ -200,16 +307,439 @@
</div> </div>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
// init kbdlayout_input
let defaultLayoutId;
for (let i = 0; i < layouts.length; ++i) {
const layout = layouts[i];
const displayName = layout.displayName;
const opt = document.createElement('option');
opt.textContent = `${layout.localeName} 0x${layout.klid.toString(16).padStart(5, '0')} (${displayName})`;
opt.value = i;
if (displayName === 'United States - English') {
opt.selected = true;
defaultLayoutId = i;
}
// Since we don't have kbdlayout_input in this simplified version, we'll use default
}
let kbdInputLayoutEvent = () => {};
let _Module;
WallixModule({
// INITIAL_MEMORY: 16777216, // 16**6
// INITIAL_MEMORY: 268435456, // 16**7
}).then((Module) => {
_Module = Module;
// optional
const LogLevel = Module.LogLevel;
Module.log = function(priority, msg) {
const logger = (priority === LogLevel.Info) ? console.log
: (priority === LogLevel.Warning) ? console.warn
: (priority === LogLevel.Error) ? console.error
: (priority === LogLevel.Debug) ? (s) => {
console.debug("%c%s", 'color:yellow', s)
}
: console.info;
logger(msg);
};
}).catch((e) => {
console.error('Failed to load WebAssembly module:', e);
showNotification('Failed to load RDP WebAssembly module', 'danger');
});
const RdpClient = _Module ? _Module.RdpClient : null;
const ClipboardChannel = _Module ? _Module.ClipboardChannel : null;
const MouseFlags = _Module ? _Module.MouseFlags : null;
const InputFlags = _Module ? _Module.InputFlags : null;
function MultiSelectToInt(e, constants)
{
let flags = 0;
for (let i = 0; i < e.length; i++) {
if (e.options[i].selected) {
flags |= constants[e.options[i].value] || 0;
}
}
return flags;
}
const gdImplTable = {
'2d':newRdpGraphics2D,
webgl:newRdpGraphicsGL,
webgl2:newRdpGraphicsGL2,
};
class Keyboard
{
// driver = class {
// sendScancodes(scancodes:Array[Number], evt:KeyEvent);
// syncKbdLocks(syncFlags:Number)
// }
constructor(keymap, driver) {
this._driver = driver;
this._keymap = keymap;
this._syncState = 0;
// on window, when left + right shift are pressed, onkeyup is only triggered for the last one
this._shiftDown = 0;
}
copyInternalStateFrom(other) {
this._syncState = other._syncState;
this._shiftDown = other._shiftDown;
this._keymap = other._keymap;
}
activeModsSync() {
this._syncState = 1;
}
keyup(evt) {
if (!this._syncState && evt.key === 'Shift') {
this._shiftDown &= ~this._codeToShiftMod(evt.code);
// shift is released but one of the sides is still considered pressed
if (this._shiftDown && !evt.getModifierState('Shift')) {
const scancodes = [];
if (this._shiftDown & SyncFlags.ShiftRight) {
scancodes.push(ScancodeByMod.ShiftRight | KeyRelease);
}
if (this._shiftDown & SyncFlags.ShiftLeft) {
scancodes.push(ScancodeByMod.ShiftLeft | KeyRelease);
}
this._shiftDown = 0;
this._driver.sendScancodes(scancodes, evt);
return;
}
}
this._keyEvent(evt, KeyRelease);
}
keydown(evt) {
if (!this._syncState && evt.key === 'Shift') {
this._shiftDown |= this._codeToShiftMod(evt.code);
}
this._keyEvent(evt, KeyAcquire);
}
_keyEvent(evt, flag) {
if (!this._preprocessKeyEvent(evt, flag)) return;
// numpad
if (evt.location === KeyboardEvent.DOM_KEY_LOCATION_NUMPAD) {
const scancode = numpadCodeToScancode(evt.code);
this._driver.sendScancodes([scancode | flag], evt);
return;
}
const scancodes = codeToScancodes(evt.key, flag) || codeToScancodes(evt.code, flag);
if (scancodes) {
this._driver.sendScancodes(scancodes, evt);
}
}
/// \return false when key processing should stop
_preprocessKeyEvent(evt, flag) {
if (!this._syncState) return true;
let syncFlags = 0;
// locks
if (evt.getModifierState("NumLock")) syncFlags |= SyncFlags.NumLock;
if (evt.getModifierState("CapsLock")) syncFlags |= SyncFlags.CapsLock;
if (evt.getModifierState("KanaLock")) syncFlags |= SyncFlags.KanaLock;
if (evt.getModifierState("ScrollLock")) syncFlags |= SyncFlags.ScrollLock;
// locks are always properly synchronized and only need to be sent once
if (this._syncState === 1) {
this._driver.syncKbdLocks(syncFlags);
this._syncState = 2;
}
if (evt.altKey) syncFlags |= SyncFlags.AltLeft;
if (evt.getModifierState("AltGraph")) syncFlags |= SyncFlags.AltRight;
// ambiguously mods
let leftOrRight = 0;
if (evt.metaKey) leftOrRight |= (evt.code === 'OSRight' && flag === KeyAcquire) ? SyncFlags.OSRight : SyncFlags.OSLeft;
if (evt.ctrlKey) leftOrRight |= (evt.code === 'ControlRight' && flag === KeyAcquire) ? SyncFlags.ControlRight : SyncFlags.ControlLeft;
if (evt.shiftKey) leftOrRight |= (evt.code === 'ShiftRight' && flag === KeyAcquire) ? SyncFlags.ShiftRight : SyncFlags.ShiftLeft;
syncFlags |= leftOrRight;
this._driver.sendScancodes(scancodesForSynchronizedMods(syncFlags), evt);
this._keymap.sync(syncFlags);
this._shiftDown = leftOrRight & (SyncFlags.ShiftRight | SyncFlags.ShiftLeft);
// mods are unambiguously synchronized
if (!leftOrRight) {
this._syncState = 0;
}
switch (evt.code) {
// mods are already processed, no need to continue
case "OSLeft":
case "OSRight":
case "ShiftLeft":
case "ShiftRight":
case "ControlLeft":
case "ControlRight":
return false;
}
return true;
}
_codeToShiftMod(code) {
if (code === 'ShiftRight') return SyncFlags.ShiftRight;
if (code === 'ShiftLeft') return SyncFlags.ShiftLeft;
return 0;
}
}
class EmulatedKeyboard extends Keyboard
{
// driver = class {
// sendUnicode(unicode:String, flag:Number, evt:KeyEvent)
// sendScancodes(scancodes:Array[Number], evt:KeyEvent);
// syncKbdLocks(syncFlags:Number)
// }
constructor(keymap, driver) {
super(keymap, driver)
this._hasUnicodeSupport = false;
}
setUnicodeSupport(enable) {
this._hasUnicodeSupport = enable;
}
compositeKeyEvent(evt, text) {
if (!this._preprocessKeyEvent(evt, flag)) return;
const scancodes = this._keymap.toScancodesAndFlags(text, text, KeyAcquire);
if (scancodes) {
this._driver.sendScancodes(this._unstatedScancodes(scancodes), evt);
}
else if (this._hasUnicodeSupport) {
this._driver.sendUnicode(text, KeyAcquire, evt);
this._driver.sendUnicode(text, KeyRelease, evt);
}
}
// release pressed keys and press released keys
_unstatedScancodes(scancodes) {
const set = new Set();
for (const scancode of scancodes) {
if (scancode & KeyRelease)
set.delete(scancode & ~KeyRelease);
else
set.add(scancode);
}
const len = scancodes.length;
for (let i = 0; i < len; ++i) {
const scancode = scancodes[i];
if (!(scancode & KeyRelease) && set.has(scancode)) {
scancodes.push(scancode & KeyRelease);
}
}
return scancodes;
}
_keyEvent(evt, flag) {
if (!this._preprocessKeyEvent(evt, flag)) return;
if (isComposing(evt)) {
return;
}
// numpad
if (evt.location === KeyboardEvent.DOM_KEY_LOCATION_NUMPAD) {
const scancode = numpadCodeToScancode(evt.code);
this._driver.sendScancodes([scancode | flag], evt);
return;
}
const scancodes = this._keymap.toScancodesAndFlags(evt.key, evt.code, flag);
if (scancodes) {
this._driver.sendScancodes(scancodes, evt);
}
else if (this._hasUnicodeSupport) {
this._driver.sendUnicode(evt.key, flag, evt);
}
}
}
class UnicodeKeyboard extends Keyboard
{
// driver = class {
// sendUnicode(unicode:String, flag:Number, evt:KeyEvent)
// sendScancodes(scancodes:Array[Number], evt:KeyEvent);
// syncKbdLocks(syncFlags:Number)
// }
constructor(keymap, driver) {
super(keymap, driver)
}
_keyEvent(evt, flag) {
if (!this._preprocessKeyEvent(evt, flag)) return;
if (isComposing(evt)) {
return;
}
if (evt.key.length === 1 || evt.key.charCodeAt(0) > 127) {
this._driver.sendUnicode(evt.key, flag, evt);
}
else {
const scancodes = codeToScancodes(evt.key, flag) || codeToScancodes(evt.code, flag);
if (scancodes) {
this._driver.sendScancodes(scancodes, evt);
}
}
}
}
let rdpclient = null; let rdpclient = null;
let socket = null; let socket = null;
let connected = false; let connected = false;
document.getElementById('connectBtn').addEventListener('click', connect); let hWheelSupport = false;
let MouseXSupport = false;
const sendData = function() {
if (socket && socket.readyState === WebSocket.OPEN && rdpclient) {
const out = rdpclient.getOutputData();
if (out.length) {
socket.send(out);
rdpclient.resetOutputData();
}
}
};
const sendScancodes = function(scancodes, evt) {
if (scancodes) {
evt.preventDefault();
evt.stopImmediatePropagation();
for (let i = 0; i < scancodes.length; ++i) {
const scancode = scancodes[i];
console.log('Scancode: send', scancode, '0x'+scancode.toString(16));
rdpclient.writeScancodeEvent(scancode);
}
sendData();
}
else {
console.warn('Scancode: unknown keycode', evt.key, evt.code)
}
}
const sendUnicode = function(unicode, flag, evt) {
const scancode = keycodeToSingleScancode(unicode); // actionLayout[unicode];
if (scancode) {
sendScancodes([scancode | flag], evt)
}
else {
evt.preventDefault();
evt.stopImmediatePropagation();
for (let i = 0; i < unicode.length; ++i) {
const code = unicode.charCodeAt(i);
console.log('Unicode: send', code, `(0x${code.toString(16)})`, 'flag:', flag, `(0x${flag.toString(16)})`);
rdpclient.writeUnicodeEvent(code, flag);
}
sendData();
}
}
const isComposing = function(evt) {
switch (evt.key) {
case 'Control':
case 'Shift':
case 'Alt':
case 'AltGraph':
case 'NumLock':
case 'ScrollLock':
case 'CapsLock':
case 'OS':
return false;
case 'Dead':
case 'Compose':
case 'Process':
// case 'Unidentified':
return true;
}
return evt.isComposing;
};
const currentLayout = function() {
const kbdLayoutId = defaultLayoutId; // since we don't have input, use default
const layout = layouts[kbdLayoutId];
return layout;
}
const keyboardDriver = {
sendUnicode,
sendScancodes,
syncKbdLocks: function(syncFlags) {
rdpclient.syncKbdLocks(syncFlags);
},
};
const keymap = new ReversedKeymap(currentLayout());
const scancodeKeyboard = new Keyboard(keymap, keyboardDriver);
const emulatedKeyboard = new EmulatedKeyboard(keymap, keyboardDriver);
const unicodeKeyboard = new UnicodeKeyboard(keymap, keyboardDriver);
let keyboardForEvent = scancodeKeyboard;
const onKeyUp = (evt) => { keyboardForEvent.keyup(evt); };
const onKeyDown = (evt) => { keyboardForEvent.keydown(evt); };
function sendMouseEvent(evt, flags)
{
evt.preventDefault();
evt.stopImmediatePropagation();
rdpclient.writeMouseEvent(evt.offsetX, evt.offsetY, flags);
sendData();
}
function sendMouseButton(evt, flag)
{
switch (evt.button) {
case 0: return sendMouseEvent(evt, flag | MouseFlags.LeftButton);
case 1: return sendMouseEvent(evt, flag | MouseFlags.MiddleButton);
case 2: return sendMouseEvent(evt, flag | MouseFlags.RightButton);
case 3: if (MouseXSupport) return sendMouseEvent(evt, flag | MouseFlags.Button4); break;
case 4: if (MouseXSupport) return sendMouseEvent(evt, flag | MouseFlags.Button5); break;
}
}
const onMouseMove = (evt) => sendMouseEvent(evt, MouseFlags.Move);
const onMouseDown = (evt) => sendMouseButton(evt, MouseFlags.Down);
const onMouseUp = (evt) => sendMouseButton(evt, MouseFlags.Up);
const onMouseWheel = (evt) => {
if (evt.deltaY) {
const delta = (evt.deltaY < 0)
? MouseFlags.WheelRotationMask
: MouseFlags.WheelNegative;
sendMouseEvent(evt, MouseFlags.VerticalWheel | delta);
}
else if (evt.deltaX && hWheelSupport) {
const delta = (evt.deltaX < 0)
? MouseFlags.WheelRotationMask
: MouseFlags.WheelNegative;
sendMouseEvent(evt, MouseFlags.HorizontalWheel | delta);
}
};
document.getElementById('connectBtn').addEventListener('click', showRdpConfig);
document.getElementById('disconnectBtn').addEventListener('click', disconnect); document.getElementById('disconnectBtn').addEventListener('click', disconnect);
document.getElementById('cancelConnectBtn').addEventListener('click', cancelConnect); document.getElementById('cancelConnectBtn').addEventListener('click', cancelConnect);
document.getElementById('startRdpBtn').addEventListener('click', startRdpConnection);
// Window control buttons // Window control buttons
document.querySelector('.close-btn').addEventListener('click', disconnect); document.querySelector('.close-btn').addEventListener('click', disconnect);
...@@ -288,6 +818,23 @@ function toggleZoom() { ...@@ -288,6 +818,23 @@ function toggleZoom() {
} }
} }
function showRdpConfig() {
if (connected) return;
// Show the RDP configuration modal
const modal = new bootstrap.Modal(document.getElementById('rdpConfigModal'));
modal.show();
}
function startRdpConnection() {
// Hide the modal
const modal = bootstrap.Modal.getInstance(document.getElementById('rdpConfigModal'));
modal.hide();
// Start the actual connection
connect();
}
function connect() { function connect() {
if (connected) return; if (connected) return;
...@@ -304,41 +851,170 @@ function connect() { ...@@ -304,41 +851,170 @@ function connect() {
socket.binaryType = 'arraybuffer'; socket.binaryType = 'arraybuffer';
socket.onopen = () => { socket.onopen = () => {
// Check if WebAssembly module functions are available console.info('Graphics implementation: ' + document.getElementById('rdpGraphicsImpl').value);
if (typeof newRdpGraphics2D === 'undefined') { const gd = gdImplTable[document.getElementById('rdpGraphicsImpl').value](ecanvas, _Module);
console.error('WebAssembly RDP functions not loaded'); gd.drawRect(0, 0, gd.width, gd.height, 0);
showNotification('Failed to load RDP client', 'danger');
disconnect();
return;
}
const canvas = document.getElementById('canvas'); // Get form values
const gd = newRdpGraphics2D(canvas, Module);
const config = { const config = {
width: 1024, width: parseInt(document.getElementById('rdpWidth').value) || 1024,
height: 768, height: parseInt(document.getElementById('rdpHeight').value) || 768,
username: '', username: document.getElementById('rdpUsername').value || '',
password: '', password: document.getElementById('rdpPassword').value || '',
domain: '', domain: document.getElementById('rdpDomain').value || '',
bpp: 32, bpp: parseInt(document.getElementById('rdpBpp').value) || 32,
verbosity: 0 verbosity: parseInt(document.getElementById('rdpVerbosity').value) || 0,
keylayout: currentLayout().klid,
};
// Add JSON config if provided
const jsonConfig = document.getElementById('rdpJsonConfig').value.trim();
if (jsonConfig) {
try {
const extraConfig = JSON.parse(jsonConfig);
Object.assign(config, extraConfig);
} catch (e) {
console.warn('Invalid JSON config, ignoring:', e);
}
}
console.table(config);
rdpclient = new RdpClient(gd, config);
window.rdpclient = rdpclient;
window.sendData = sendData;
// For now, skip clipboard setup as it's complex
// const cliprdr = new Cliprdr(cbFiles, sendData, _Module);
// const clipboard = new ClipboardChannel(
// rdpclient.getCallbackAsVoidPtr(),
// cliprdr,
// /*verbosity = */0x04000000);
// cliprdr.setEmcChannel(clipboard);
// addChannel(clipboard);
socket.onmessage = function(event) {
rdpclient.processInputData(event.data);
sendData();
}; };
rdpclient = new Module.RdpClient(gd, config);
rdpclient.writeFirstPacket(); rdpclient.writeFirstPacket();
sendData(); sendData();
connected = true; connected = true;
document.getElementById('connectBtn').disabled = true; document.getElementById('connectBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = false; document.getElementById('disconnectBtn').disabled = false;
// Hide loading message // Hide loading message
document.getElementById('rdp_loading').style.display = 'none'; document.getElementById('rdp_loading').style.display = 'none';
showNotification('RDP session connected', 'success'); showNotification('RDP session connected', 'success');
};
socket.onmessage = (event) => { // Set up input handling
rdpclient.processInputData(event.data); updateInput();
sendData();
}; };
function updateInput()
{
if (!rdpclient) return;
const inputFlags = rdpclient.getInputFlags();
const keyboardLayout = rdpclient.getKeyboardLayout();
console.log(`keyboardLayout: 0x${keyboardLayout.toString(16)}`);
console.group(`inputFlags: 0x${inputFlags.toString(16)}`);
for (const k in InputFlags) {
console.log(`${k}: ${(inputFlags & InputFlags[k]) ? 1 : 0} (0x${InputFlags[k].toString(16)})`);
}
console.groupEnd();
hWheelSupport = Boolean(inputFlags & InputFlags.HorizontalWheel);
MouseXSupport = Boolean(inputFlags & InputFlags.MouseX);
updateKbdInput(inputFlags);
kbdInputLayoutEvent = () => {
if (!rdpclient) return;
updateKbdInput(inputFlags);
}
canvasStopEvents();
canvasStartEvents();
}
function updateKbdInput(inputFlags)
{
const kbdMethodId = document.getElementById('rdpKeyboardMethod').selectedIndex;
const altGrIsCtrlAndAlt = document.getElementById('rdpAltGrCtrlAlt').checked;
const kbdMethod = document.getElementById('rdpKeyboardMethod')[kbdMethodId].value;
const hasUnicode = !!(inputFlags & InputFlags.Unicode);
const layout = currentLayout();
console.group('keyboard');
console.log(`unicode support: ${hasUnicode}`);
console.log(`method: (${kbdMethodId}) ${kbdMethod}`);
console.log(`layout: (0x${layout.klid.toString(16)}) ${layout.displayName}`);
console.log(`AltGr = Ctrl + Alt: ${altGrIsCtrlAndAlt}`);
console.groupEnd();
keymap.altGrIsCtrlAndAlt = altGrIsCtrlAndAlt;
keymap.layout = layout;
emulatedKeyboard.setUnicodeSupport(hasUnicode);
const newKeyboardForEvent = (kbdMethod === 'emulateScancode') ? emulatedKeyboard
: (kbdMethod === 'unicode') ? unicodeKeyboard
: scancodeKeyboard;
newKeyboardForEvent.copyInternalStateFrom(keyboardForEvent);
keyboardForEvent = newKeyboardForEvent;
keyboardForEvent.activeModsSync();
}
function canvasStartEvents()
{
ecanvas.addEventListener('mousemove', onMouseMove);
ecanvas.addEventListener('mousedown', onMouseDown);
ecanvas.addEventListener('mouseup', onMouseUp);
ecanvas.addEventListener('wheel', onMouseWheel);
ecanvasFocus.addEventListener('keydown', onKeyDown);
ecanvasFocus.addEventListener('keyup', onKeyUp);
canvasEnableFocus();
}
function canvasStopEvents()
{
ecanvas.removeEventListener('mousemove', onMouseMove);
ecanvas.removeEventListener('mousedown', onMouseDown);
ecanvas.removeEventListener('mouseup', onMouseUp);
ecanvas.removeEventListener('wheel', onMouseWheel);
ecanvasFocus.removeEventListener('keydown', onKeyDown);
ecanvasFocus.removeEventListener('keyup', onKeyUp);
}
function canvasFocus(evt)
{
console.log('focus');
ecanvasFocus.addEventListener('keydown', onKeyDown);
ecanvasFocus.addEventListener('keyup', onKeyUp);
ecanvas.onclick = (e) => {
e.preventDefault();
e.stopImmediatePropagation();
};
keyboardForEvent.activeModsSync();
}
function canvasEnableFocus()
{
// preventScroll don't work with firefox
ecanvasFocus.focus({preventScroll: true});
}
function canvasBlur()
{
console.log('blur');
ecanvasFocus.removeEventListener('keydown', onKeyDown);
ecanvasFocus.removeEventListener('keyup', onKeyUp);
ecanvas.onclick = canvasEnableFocus;
}
ecanvasFocus.onblur = canvasBlur;
ecanvasFocus.onfocus = canvasFocus;
socket.onclose = () => { socket.onclose = () => {
connected = false; connected = false;
document.getElementById('connectBtn').disabled = false; document.getElementById('connectBtn').disabled = false;
...@@ -348,6 +1024,7 @@ function connect() { ...@@ -348,6 +1024,7 @@ function connect() {
document.getElementById('rdp_loading').style.display = 'none'; document.getElementById('rdp_loading').style.display = 'none';
document.getElementById('cancelConnectBtn').style.display = 'none'; document.getElementById('cancelConnectBtn').style.display = 'none';
showNotification('RDP session disconnected', 'info'); showNotification('RDP session disconnected', 'info');
canvasStopEvents();
rdpclient = null; rdpclient = null;
socket = null; socket = null;
}; };
...@@ -361,15 +1038,6 @@ function connect() { ...@@ -361,15 +1038,6 @@ function connect() {
console.error('Failed to create RDP client:', e); console.error('Failed to create RDP client:', e);
showNotification('Failed to connect RDP: ' + e.message, 'danger'); showNotification('Failed to connect RDP: ' + e.message, 'danger');
} }
function sendData() {
if (socket && socket.readyState === WebSocket.OPEN && rdpclient) {
const data = rdpclient.getOutputData();
if (data && data.length > 0) {
socket.send(data);
}
}
}
} }
function disconnect() { function disconnect() {
...@@ -387,9 +1055,8 @@ function disconnect() { ...@@ -387,9 +1055,8 @@ function disconnect() {
document.getElementById('rdp_status').style.display = 'block'; document.getElementById('rdp_status').style.display = 'block';
document.getElementById('rdp_loading').style.display = 'none'; document.getElementById('rdp_loading').style.display = 'none';
document.getElementById('cancelConnectBtn').style.display = 'none'; document.getElementById('cancelConnectBtn').style.display = 'none';
setTimeout(() => { canvasStopEvents();
location.reload(); location.reload();
}, 3000);
} }
function cancelConnect() { function cancelConnect() {
......
...@@ -19,3 +19,7 @@ else if (typeof define === 'function' && define['amd']) ...@@ -19,3 +19,7 @@ else if (typeof define === 'function' && define['amd'])
define([], function() { return WallixModule; }); define([], function() { return WallixModule; });
else if (typeof exports === 'object') else if (typeof exports === 'object')
exports["WallixModule"] = WallixModule; exports["WallixModule"] = WallixModule;
else {
window.WallixModule = WallixModule;
window.Module = WallixModule;
}
...@@ -1417,7 +1417,7 @@ const newRdpGraphics = function(canvasElement, module, ropError) { ...@@ -1417,7 +1417,7 @@ const newRdpGraphics = function(canvasElement, module, ropError) {
return ctx ? createCtx(ctx, newRdpPointer(canvasElement, module)) : undefined; return ctx ? createCtx(ctx, newRdpPointer(canvasElement, module)) : undefined;
}; };
try { if (typeof module !== 'undefined' && module.exports) {
module.exports.newRdpGraphics2D = newRdpGraphics2D; module.exports.newRdpGraphics2D = newRdpGraphics2D;
module.exports.newRdpGraphicsGL = newRdpGraphicsGL; module.exports.newRdpGraphicsGL = newRdpGraphicsGL;
module.exports.newRdpGraphicsGL2 = newRdpGraphicsGL2; module.exports.newRdpGraphicsGL2 = newRdpGraphicsGL2;
...@@ -1426,7 +1426,13 @@ try { ...@@ -1426,7 +1426,13 @@ try {
module.exports.newRdpGL = newRdpGL; module.exports.newRdpGL = newRdpGL;
module.exports.newRdpGL2 = newRdpGL2; module.exports.newRdpGL2 = newRdpGL2;
module.exports.newRdpPointer = newRdpPointer; module.exports.newRdpPointer = newRdpPointer;
} } else {
catch (e) { window.newRdpGraphics2D = newRdpGraphics2D;
// module not found window.newRdpGraphicsGL = newRdpGraphicsGL;
window.newRdpGraphicsGL2 = newRdpGraphicsGL2;
window.newRdpGraphics = newRdpGraphics;
window.newRdpCanvas = newRdpCanvas;
window.newRdpGL = newRdpGL;
window.newRdpGL2 = newRdpGL2;
window.newRdpPointer = newRdpPointer;
} }
...@@ -437,7 +437,40 @@ function connect() { ...@@ -437,7 +437,40 @@ function connect() {
rfb.addEventListener('securityfailure', (e) => { rfb.addEventListener('securityfailure', (e) => {
console.error('VNC security failure:', e.detail); console.error('VNC security failure:', e.detail);
showNotification('VNC security failure: ' + e.detail.reason, 'danger'); // Show password modal again for retry
const passwordModal = new bootstrap.Modal(document.getElementById('passwordModal'));
document.getElementById('passwordModalLabel').textContent = 'VNC Authentication Failed';
document.querySelector('#passwordModal .modal-body p').textContent = 'The password you entered is incorrect. Please try again:';
document.getElementById('vncPassword').value = ''; // Clear previous password
passwordModal.show();
// Handle password resubmission
const submitPassword = () => {
const password = document.getElementById('vncPassword').value;
if (password) {
rfb.sendCredentials({ password: password });
passwordModal.hide();
document.getElementById('vncPassword').value = ''; // Clear password field
}
};
document.getElementById('submitPasswordBtn').onclick = submitPassword;
// Allow Enter key to submit
document.getElementById('vncPassword').onkeydown = (e) => {
if (e.key === 'Enter') {
submitPassword();
}
};
// Handle modal close without password
document.getElementById('passwordModal').addEventListener('hidden.bs.modal', () => {
if (!rfb._rfbCredentials.password) {
// If no password was provided, disconnect
showNotification('VNC connection cancelled - password required', 'warning');
disconnect();
}
});
}); });
rfb.addEventListener('clipboard', (e) => { rfb.addEventListener('clipboard', (e) => {
...@@ -462,9 +495,7 @@ function disconnect() { ...@@ -462,9 +495,7 @@ function disconnect() {
document.getElementById('noVNC_status').style.display = 'block'; document.getElementById('noVNC_status').style.display = 'block';
document.getElementById('noVNC_loading').style.display = 'none'; document.getElementById('noVNC_loading').style.display = 'none';
document.getElementById('cancelConnectBtn').style.display = 'none'; document.getElementById('cancelConnectBtn').style.display = 'none';
setTimeout(() => { location.reload();
location.reload();
}, 3000);
} }
function cancelConnect() { function cancelConnect() {
......
...@@ -2571,6 +2571,7 @@ static int handle_request(int client_fd, const http_request_t *req) { ...@@ -2571,6 +2571,7 @@ static int handle_request(int client_fd, const http_request_t *req) {
if (strstr(req->path, ".html")) content_type = "text/html"; if (strstr(req->path, ".html")) content_type = "text/html";
else if (strstr(req->path, ".css")) content_type = "text/css"; else if (strstr(req->path, ".css")) content_type = "text/css";
else if (strstr(req->path, ".js")) content_type = "application/javascript"; else if (strstr(req->path, ".js")) content_type = "application/javascript";
else if (strstr(req->path, ".wasm")) content_type = "application/wasm";
else if (strstr(req->path, ".jpg") || strstr(req->path, ".jpeg")) content_type = "image/jpeg"; else if (strstr(req->path, ".jpg") || strstr(req->path, ".jpeg")) content_type = "image/jpeg";
else if (strstr(req->path, ".png")) content_type = "image/png"; else if (strstr(req->path, ".png")) content_type = "image/png";
else if (strstr(req->path, ".ico")) content_type = "image/x-icon"; else if (strstr(req->path, ".ico")) content_type = "image/x-icon";
......
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