CI / Build & Test (push) Successful in 2m52s
CRITICAL:
- certificate-authority.js: declare `regenerated` variable in installRootCA Windows path to prevent ReferenceError crash
HIGH:
- virtual-hosts.js/service-tunnels.js: call hs.removeAllListeners() in catch blocks to prevent stale listeners on failed Holesail instances
- https-proxy.js: destroy rawSocket in TLS error handler to prevent file descriptor exhaustion
- message-router.js (native): move setEventEmitter() to module-level init instead of re-calling on every message
- ssh-manager.js: add error handler to WS server to prevent unhandled error crashes
- init.js: store setInterval ID and clear on beforeunload to prevent interval accumulation
- logs.js: store and remove chrome.runtime.onMessage listener on beforeunload; add duplicate-call guard
- events.js: move pending++ before async sendMessage call to fix SSH/RDP-only import showing "Nothing to import"
- ssh.js: store resizeTimer on activeSshSession and clear in disconnectSsh; fix auto-reconnect race with _sshConnecting lock
- native-messaging.js: track retry timer IDs in array and cancel all on disconnect
- rdp.js: reuse single offscreen canvas per session instead of allocating per bitmap
MEDIUM:
- virtual-hosts.js/service-tunnels.js: clear existing.reconnectTimer before replacing tunnel entries
- message-router.js (native): destroy pingTunnel socket on error path; clear 15s fallback timer via finally()
- startup.js: wrap setImmediate body in try/finally to always resolve proxiesReadyPromise
- https-proxy.js: fix pre-connect upstream error handler to avoid writing raw HTTP into piped TLS stream; move HOP_BY_HOP to module-level constant
- connect-proxy.js: destroy upstreamSocket on clientSocket close; track and destroy active sockets in stop()
- ssh-manager.js: call cancelPasswordWatch on WS disconnect during password collection
- rdp-manager.js: remove dead remotePort variable; add error handlers to both WS servers
- backup-manager.js: log cleanupStaging errors and non-zero exit codes
- rdp.js: null out ws callbacks before closing in disconnectRdp; disconnect MutationObserver on beforeunload
- proxy-ca.js: prune stale entries from validationResults Map in renderValidatorTable
- refresh.js: deduplicate in-flight pings per port via Set
- messaging.js: read chrome.runtime.lastError in sendToNative callback
- servers.js (dashboard): add null check for $('serverEditId') element
LOW:
- port-allocator.js: add dedup check before pushing to tunnelPortFreeList
- servers.js (native): add error listener to server-mode Holesail instances
- virtual-hosts.js: remove dead prevReconnectDelay variable
- tab-lifecycle.js: change swarmRefCount fallback from || 1 to || 0 to prevent premature swarm destroy
- ssh.js/rdp.js: disconnect MutationObservers on beforeunload
506 lines
19 KiB
JavaScript
506 lines
19 KiB
JavaScript
// Depends on: core/utils.js ($, escapeHtml, log), core/messaging.js (sendToNative),
|
||
// ui/toast.js (showToast), ui/modal.js (openModal, closeModal, showModalError)
|
||
|
||
let rdpConnections = [];
|
||
let activeRdpSession = null; // { sessionId, wsPort, type, ws, rfb, conn }
|
||
|
||
// Reusable offscreen canvas for bitmap rendering — resized only when dimensions change.
|
||
let _rdpOffscreen = null;
|
||
let _rdpOffscreenCtx = null;
|
||
let _rdpOffscreenW = 0;
|
||
let _rdpOffscreenH = 0;
|
||
|
||
function generateRdpId() {
|
||
return 'rdp-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 7);
|
||
}
|
||
|
||
function saveRdpConnections(cb) {
|
||
// Encode password as base64 before persisting so it survives page reloads
|
||
const toSave = rdpConnections.map(c => {
|
||
const { password, ...rest } = c; // eslint-disable-line no-unused-vars
|
||
if (password) rest.passwordB64 = btoa(unescape(encodeURIComponent(password)));
|
||
else delete rest.passwordB64;
|
||
return rest;
|
||
});
|
||
chrome.runtime.sendMessage(
|
||
{ target: 'holesail-native', action: 'send', payload: { type: 'setRdpConnections', payload: { connections: toSave } } },
|
||
(response) => {
|
||
if (response && !response.ok) log('saveRdpConnections failed:', response.error);
|
||
renderRdpGrid();
|
||
const countEl = $('rdpCount');
|
||
if (countEl) countEl.textContent = rdpConnections.length;
|
||
if (cb) cb();
|
||
}
|
||
);
|
||
}
|
||
|
||
function renderRdpGrid() {
|
||
const grid = $('rdpGrid');
|
||
if (!grid) return;
|
||
const countEl = $('rdpCount');
|
||
if (countEl) countEl.textContent = rdpConnections.length;
|
||
if (rdpConnections.length === 0) {
|
||
grid.innerHTML = `
|
||
<div class="empty-state" style="grid-column:1/-1">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||
<div class="empty-state-title">No remote desktop connections</div>
|
||
<div class="empty-state-desc">Add a VNC or RDP connection. You'll need an hs:// key for the remote peer.</div>
|
||
</div>`;
|
||
return;
|
||
}
|
||
grid.innerHTML = rdpConnections.map(conn => {
|
||
const typeBadge = conn.type === 'rdp'
|
||
? `<span class="badge badge-amber" style="font-size:10px;padding:2px 7px;">RDP</span>`
|
||
: `<span class="badge badge-cyan" style="font-size:10px;padding:2px 7px;">VNC</span>`;
|
||
return `
|
||
<div class="rdp-conn-card" data-rdp-id="${escapeHtml(conn.id)}">
|
||
<div class="rdp-conn-card-top">
|
||
<div class="rdp-conn-icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="2" y="3" width="20" height="14" rx="2"/>
|
||
<line x1="8" y1="21" x2="16" y2="21"/>
|
||
<line x1="12" y1="17" x2="12" y2="21"/>
|
||
</svg>
|
||
</div>
|
||
<div class="rdp-conn-info">
|
||
<div class="rdp-conn-label">${escapeHtml(conn.label || (conn.type === 'rdp' ? 'RDP Desktop' : 'VNC Desktop'))} ${typeBadge}</div>
|
||
</div>
|
||
<div class="rdp-conn-actions">
|
||
<button class="btn btn-primary" data-rdp-connect="${escapeHtml(conn.id)}" style="padding:6px 14px;font-size:12px;">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px;"><polyline points="5,12 19,12"/><polyline points="12,5 19,12 12,19"/></svg>
|
||
Connect
|
||
</button>
|
||
<button class="btn btn-ghost" data-rdp-edit="${escapeHtml(conn.id)}" style="padding:6px 10px;" title="Edit">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||
</button>
|
||
<button class="btn btn-ghost" data-rdp-remove="${escapeHtml(conn.id)}" style="padding:6px 10px;color:var(--red);" title="Remove">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><polyline points="3,6 5,6 21,6"/><path d="M19,6l-1,14a2,2,0,0,1-2,2H8a2,2,0,0,1-2-2L5,6"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
grid.querySelectorAll('[data-rdp-connect]').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const conn = rdpConnections.find(c => c.id === btn.dataset.rdpConnect);
|
||
if (conn) connectRdp(conn);
|
||
});
|
||
});
|
||
grid.querySelectorAll('[data-rdp-edit]').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const conn = rdpConnections.find(c => c.id === btn.dataset.rdpEdit);
|
||
if (conn) openAddRdpModal(conn);
|
||
});
|
||
});
|
||
grid.querySelectorAll('[data-rdp-remove]').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const conn = rdpConnections.find(c => c.id === btn.dataset.rdpRemove);
|
||
if (conn) {
|
||
$('removeRdpName').textContent = conn.label || conn.type.toUpperCase() + ' Desktop';
|
||
$('removeRdpConfirm').dataset.rdpId = conn.id;
|
||
openModal('modal-removeRdp');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function openAddRdpModal(conn) {
|
||
const isEdit = !!conn;
|
||
$('modal-addRdp-title').textContent = isEdit ? 'Edit Remote Desktop Connection' : 'Add Remote Desktop Connection';
|
||
$('rdpConnLabel').value = conn ? (conn.label || '') : '';
|
||
$('rdpConnHsUrl').value = conn ? conn.hsUrl : '';
|
||
$('rdpConnPort').value = conn ? conn.port : 5900;
|
||
$('rdpConnWidth').value = conn ? (conn.width || 1280) : 1280;
|
||
$('rdpConnHeight').value = conn ? (conn.height || 720) : 720;
|
||
$('rdpConnUsername').value = conn ? (conn.username || '') : '';
|
||
$('rdpConnPassword').value = conn ? (conn.password || '') : '';
|
||
$('rdpConnEditId').value = conn ? conn.id : '';
|
||
$('rdpConnSubmit').textContent = isEdit ? 'Save Changes' : 'Save Connection';
|
||
|
||
const type = conn ? conn.type : 'vnc';
|
||
document.querySelector('input[name="rdpProtocol"][value="' + type + '"]').checked = true;
|
||
updateRdpProtocolUI(type);
|
||
openModal('modal-addRdp');
|
||
}
|
||
|
||
function updateRdpProtocolUI(type) {
|
||
const portEl = $('rdpConnPort');
|
||
const usernameGroup = $('rdpUsernameGroup');
|
||
if (type === 'rdp') {
|
||
if (portEl && portEl.value === '5900') portEl.value = '3389';
|
||
if (usernameGroup) usernameGroup.style.display = '';
|
||
} else {
|
||
if (portEl && portEl.value === '3389') portEl.value = '5900';
|
||
if (usernameGroup) usernameGroup.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// ── VNC viewer (noVNC RFB) ────────────────────────────────────────────────────
|
||
|
||
function initVncViewer(wsPort, conn) {
|
||
const container = $('rdpViewerContainer');
|
||
if (!container) return null;
|
||
container.innerHTML = '';
|
||
|
||
const RFB = window.RFB;
|
||
if (!RFB) {
|
||
container.innerHTML = '<div style="color:var(--red);padding:20px;font-size:13px;">noVNC (RFB) not loaded. Check vendor/novnc.js.</div>';
|
||
return null;
|
||
}
|
||
|
||
let rfb;
|
||
try {
|
||
rfb = new RFB(container, 'ws://127.0.0.1:' + wsPort, {
|
||
credentials: conn.password ? { password: conn.password } : undefined
|
||
});
|
||
rfb.scaleViewport = true;
|
||
rfb.resizeSession = false;
|
||
rfb.viewOnly = false;
|
||
rfb.clipViewport = false;
|
||
rfb.dragViewport = false;
|
||
rfb.focusOnClick = true;
|
||
rfb.background = '#000';
|
||
} catch (e) {
|
||
container.innerHTML = '<div style="color:var(--red);padding:20px;font-size:13px;">Failed to init noVNC: ' + escapeHtml(e.message) + '</div>';
|
||
return null;
|
||
}
|
||
|
||
rfb.addEventListener('connect', () => {
|
||
$('rdpStatusDot').className = 'terminal-status-dot';
|
||
$('rdpStateDisplay').textContent = 'Connected';
|
||
$('rdpStateDisplay').style.color = 'var(--green)';
|
||
const w = rfb._fbWidth || conn.width || '?';
|
||
const h = rfb._fbHeight || conn.height || '?';
|
||
$('rdpResDisplay').textContent = w + '×' + h;
|
||
});
|
||
|
||
rfb.addEventListener('disconnect', (e) => {
|
||
$('rdpStatusDot').className = 'terminal-status-dot disconnected';
|
||
$('rdpStateDisplay').textContent = e.detail && e.detail.clean ? 'Disconnected' : 'Connection lost';
|
||
$('rdpStateDisplay').style.color = 'var(--text3)';
|
||
});
|
||
|
||
rfb.addEventListener('desktopname', (e) => {
|
||
if (e.detail && e.detail.name) $('rdpViewerInfo').textContent = e.detail.name;
|
||
});
|
||
|
||
rfb.addEventListener('credentialsrequired', () => {
|
||
const pw = prompt('VNC password required:');
|
||
if (pw !== null) rfb.sendCredentials({ password: pw });
|
||
});
|
||
|
||
return rfb;
|
||
}
|
||
|
||
// ── RDP viewer (node-rdpjs bitmap renderer) ───────────────────────────────────
|
||
|
||
function initRdpViewer(wsPort, conn) {
|
||
const container = $('rdpViewerContainer');
|
||
if (!container) return null;
|
||
container.innerHTML = '';
|
||
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = conn.width || 1280;
|
||
canvas.height = conn.height || 720;
|
||
canvas.style.display = 'block';
|
||
canvas.style.cursor = 'default';
|
||
container.appendChild(canvas);
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
$('rdpResDisplay').textContent = canvas.width + '×' + canvas.height;
|
||
|
||
let ws;
|
||
try {
|
||
ws = new WebSocket('ws://127.0.0.1:' + wsPort);
|
||
} catch (e) {
|
||
container.innerHTML = '<div style="color:var(--red);padding:20px;font-size:13px;">WebSocket failed: ' + escapeHtml(e.message) + '</div>';
|
||
return null;
|
||
}
|
||
|
||
ws.onopen = () => {
|
||
$('rdpStateDisplay').textContent = 'Waiting for RDP…';
|
||
$('rdpStateDisplay').style.color = 'var(--amber)';
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
let msg;
|
||
try { msg = JSON.parse(event.data); } catch (_) { return; }
|
||
|
||
if (msg.type === 'connected') {
|
||
$('rdpStatusDot').className = 'terminal-status-dot';
|
||
$('rdpStateDisplay').textContent = 'Connected';
|
||
$('rdpStateDisplay').style.color = 'var(--green)';
|
||
if (msg.width && msg.height) {
|
||
canvas.width = msg.width;
|
||
canvas.height = msg.height;
|
||
$('rdpResDisplay').textContent = msg.width + '×' + msg.height;
|
||
}
|
||
} else if (msg.type === 'bitmap') {
|
||
renderRdpBitmap(ctx, msg);
|
||
} else if (msg.type === 'close') {
|
||
$('rdpStatusDot').className = 'terminal-status-dot disconnected';
|
||
$('rdpStateDisplay').textContent = 'Disconnected';
|
||
$('rdpStateDisplay').style.color = 'var(--text3)';
|
||
} else if (msg.type === 'error') {
|
||
$('rdpStatusDot').className = 'terminal-status-dot disconnected';
|
||
$('rdpStateDisplay').textContent = 'Error: ' + (msg.message || 'unknown');
|
||
$('rdpStateDisplay').style.color = 'var(--red)';
|
||
}
|
||
};
|
||
|
||
ws.onclose = () => {
|
||
$('rdpStatusDot').className = 'terminal-status-dot disconnected';
|
||
$('rdpStateDisplay').textContent = 'Disconnected';
|
||
$('rdpStateDisplay').style.color = 'var(--text3)';
|
||
};
|
||
|
||
ws.onerror = () => {
|
||
$('rdpStateDisplay').textContent = 'WebSocket error';
|
||
$('rdpStateDisplay').style.color = 'var(--red)';
|
||
};
|
||
|
||
canvas.addEventListener('mousemove', (e) => {
|
||
if (ws.readyState !== WebSocket.OPEN) return;
|
||
const r = canvas.getBoundingClientRect();
|
||
const scaleX = canvas.width / r.width;
|
||
const scaleY = canvas.height / r.height;
|
||
ws.send(JSON.stringify({ type: 'mouseMove', x: Math.round((e.clientX - r.left) * scaleX), y: Math.round((e.clientY - r.top) * scaleY) }));
|
||
});
|
||
|
||
canvas.addEventListener('mousedown', (e) => {
|
||
if (ws.readyState !== WebSocket.OPEN) return;
|
||
const r = canvas.getBoundingClientRect();
|
||
const scaleX = canvas.width / r.width;
|
||
const scaleY = canvas.height / r.height;
|
||
const btn = e.button === 2 ? 2 : e.button === 1 ? 3 : 1;
|
||
ws.send(JSON.stringify({ type: 'mouseButton', x: Math.round((e.clientX - r.left) * scaleX), y: Math.round((e.clientY - r.top) * scaleY), button: btn, isDown: true }));
|
||
});
|
||
|
||
canvas.addEventListener('mouseup', (e) => {
|
||
if (ws.readyState !== WebSocket.OPEN) return;
|
||
const r = canvas.getBoundingClientRect();
|
||
const scaleX = canvas.width / r.width;
|
||
const scaleY = canvas.height / r.height;
|
||
const btn = e.button === 2 ? 2 : e.button === 1 ? 3 : 1;
|
||
ws.send(JSON.stringify({ type: 'mouseButton', x: Math.round((e.clientX - r.left) * scaleX), y: Math.round((e.clientY - r.top) * scaleY), button: btn, isDown: false }));
|
||
});
|
||
|
||
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
||
|
||
canvas.setAttribute('tabindex', '0');
|
||
canvas.addEventListener('keydown', (e) => {
|
||
e.preventDefault();
|
||
if (ws.readyState !== WebSocket.OPEN) return;
|
||
ws.send(JSON.stringify({ type: 'keyUnicode', code: e.key.charCodeAt(0) || 0, isDown: true }));
|
||
});
|
||
canvas.addEventListener('keyup', (e) => {
|
||
e.preventDefault();
|
||
if (ws.readyState !== WebSocket.OPEN) return;
|
||
ws.send(JSON.stringify({ type: 'keyUnicode', code: e.key.charCodeAt(0) || 0, isDown: false }));
|
||
});
|
||
|
||
return ws;
|
||
}
|
||
|
||
function renderRdpBitmap(ctx, bitmap) {
|
||
const { destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, data } = bitmap;
|
||
if (!data) return;
|
||
|
||
const raw = atob(data);
|
||
const bytes = new Uint8Array(raw.length);
|
||
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
|
||
|
||
const drawW = destRight - destLeft;
|
||
const drawH = destBottom - destTop;
|
||
if (drawW <= 0 || drawH <= 0) return;
|
||
|
||
const imgData = ctx.createImageData(width, height);
|
||
const pixels = imgData.data;
|
||
|
||
if (bitsPerPixel === 32) {
|
||
for (let i = 0, p = 0; i < bytes.length && p < pixels.length; i += 4, p += 4) {
|
||
pixels[p] = bytes[i + 2]; // R (BGRA → RGBA)
|
||
pixels[p + 1] = bytes[i + 1]; // G
|
||
pixels[p + 2] = bytes[i]; // B
|
||
pixels[p + 3] = 255;
|
||
}
|
||
} else if (bitsPerPixel === 24) {
|
||
for (let i = 0, p = 0; i < bytes.length && p < pixels.length; i += 3, p += 4) {
|
||
pixels[p] = bytes[i + 2]; // R
|
||
pixels[p + 1] = bytes[i + 1]; // G
|
||
pixels[p + 2] = bytes[i]; // B
|
||
pixels[p + 3] = 255;
|
||
}
|
||
} else if (bitsPerPixel === 16) {
|
||
for (let i = 0, p = 0; i < bytes.length - 1 && p < pixels.length; i += 2, p += 4) {
|
||
const v = bytes[i] | (bytes[i + 1] << 8);
|
||
pixels[p] = ((v >> 11) & 0x1f) << 3;
|
||
pixels[p + 1] = ((v >> 5) & 0x3f) << 2;
|
||
pixels[p + 2] = (v & 0x1f) << 3;
|
||
pixels[p + 3] = 255;
|
||
}
|
||
} else {
|
||
return; // unsupported depth
|
||
}
|
||
|
||
if (!_rdpOffscreen || _rdpOffscreenW !== width || _rdpOffscreenH !== height) {
|
||
_rdpOffscreen = document.createElement('canvas');
|
||
_rdpOffscreen.width = width;
|
||
_rdpOffscreen.height = height;
|
||
_rdpOffscreenCtx = _rdpOffscreen.getContext('2d');
|
||
_rdpOffscreenW = width;
|
||
_rdpOffscreenH = height;
|
||
}
|
||
_rdpOffscreenCtx.putImageData(imgData, 0, 0);
|
||
ctx.drawImage(_rdpOffscreen, destLeft, destTop, drawW, drawH);
|
||
}
|
||
|
||
async function connectRdp(conn) {
|
||
openModal('modal-rdpViewer');
|
||
|
||
const labelEl = $('rdpViewerLabel');
|
||
if (labelEl) labelEl.textContent = conn.label || (conn.type === 'rdp' ? 'RDP Desktop' : 'VNC Desktop');
|
||
|
||
const protoBadge = $('rdpProtocolBadge');
|
||
if (protoBadge) {
|
||
protoBadge.textContent = conn.type.toUpperCase();
|
||
protoBadge.className = 'badge ' + (conn.type === 'rdp' ? 'badge-amber' : 'badge-cyan');
|
||
}
|
||
|
||
$('rdpStatusDot').className = 'terminal-status-dot';
|
||
$('rdpStateDisplay').textContent = 'Connecting…';
|
||
$('rdpStateDisplay').style.color = 'var(--amber)';
|
||
$('rdpResDisplay').textContent = (conn.width || 1280) + '×' + (conn.height || 720);
|
||
$('rdpViewerInfo').textContent = '';
|
||
|
||
await disconnectRdp();
|
||
|
||
const result = await sendToNative('startRdpSession', {
|
||
type: conn.type,
|
||
hsUrl: conn.hsUrl,
|
||
port: conn.port,
|
||
username: conn.username || '',
|
||
password: conn.password || '',
|
||
domain: '',
|
||
width: conn.width || 1280,
|
||
height: conn.height || 720,
|
||
label: conn.label || ''
|
||
});
|
||
|
||
if (!result || !result.ok) {
|
||
$('rdpStatusDot').className = 'terminal-status-dot disconnected';
|
||
$('rdpStateDisplay').textContent = 'Failed: ' + ((result && result.error) || 'Unknown error');
|
||
$('rdpStateDisplay').style.color = 'var(--red)';
|
||
return;
|
||
}
|
||
|
||
const { sessionId, wsPort } = result;
|
||
let viewer = null;
|
||
|
||
if (conn.type === 'vnc') {
|
||
viewer = initVncViewer(wsPort, conn);
|
||
activeRdpSession = { sessionId, wsPort, type: 'vnc', rfb: viewer, ws: null, conn };
|
||
} else {
|
||
viewer = initRdpViewer(wsPort, conn);
|
||
activeRdpSession = { sessionId, wsPort, type: 'rdp', rfb: null, ws: viewer, conn };
|
||
}
|
||
}
|
||
|
||
async function disconnectRdp() {
|
||
if (!activeRdpSession) return;
|
||
const { sessionId, rfb, ws } = activeRdpSession;
|
||
activeRdpSession = null;
|
||
|
||
if (rfb) { try { rfb.disconnect(); } catch (_) {} }
|
||
if (ws) {
|
||
ws.onclose = null;
|
||
ws.onerror = null;
|
||
ws.onmessage = null;
|
||
try { ws.close(); } catch (_) {}
|
||
}
|
||
|
||
const container = $('rdpViewerContainer');
|
||
if (container) container.innerHTML = '';
|
||
|
||
if (sessionId) {
|
||
await sendToNative('stopRdpSession', { sessionId });
|
||
}
|
||
}
|
||
|
||
function setupRdpEvents() {
|
||
$('addRdpBtn')?.addEventListener('click', () => openAddRdpModal(null));
|
||
|
||
document.querySelectorAll('input[name="rdpProtocol"]').forEach(radio => {
|
||
radio.addEventListener('change', () => updateRdpProtocolUI(radio.value));
|
||
});
|
||
|
||
$('rdpConnSubmit')?.addEventListener('click', () => {
|
||
const label = $('rdpConnLabel').value.trim();
|
||
const hsUrl = $('rdpConnHsUrl').value.trim();
|
||
const type = document.querySelector('input[name="rdpProtocol"]:checked')?.value || 'vnc';
|
||
const port = parseInt($('rdpConnPort').value, 10) || (type === 'rdp' ? 3389 : 5900);
|
||
const width = parseInt($('rdpConnWidth').value, 10) || 1280;
|
||
const height = parseInt($('rdpConnHeight').value, 10) || 720;
|
||
const username = $('rdpConnUsername').value.trim();
|
||
const password = $('rdpConnPassword').value;
|
||
const editId = $('rdpConnEditId').value;
|
||
|
||
if (!hsUrl) { showModalError('modal-addRdp', 'rdpConnError', 'Holesail key is required'); return; }
|
||
if (!hsUrl.startsWith('hs://')) { showModalError('modal-addRdp', 'rdpConnError', 'Key must start with hs://'); return; }
|
||
|
||
const entry = { id: editId || generateRdpId(), label, hsUrl, type, port, width, height, username, password };
|
||
|
||
if (editId) {
|
||
const idx = rdpConnections.findIndex(c => c.id === editId);
|
||
if (idx !== -1) rdpConnections[idx] = entry;
|
||
} else {
|
||
rdpConnections.push(entry);
|
||
}
|
||
saveRdpConnections();
|
||
closeModal('modal-addRdp');
|
||
showToast(editId ? 'Connection updated' : 'Connection saved', 'success');
|
||
});
|
||
|
||
$('removeRdpConfirm')?.addEventListener('click', () => {
|
||
const id = $('removeRdpConfirm').dataset.rdpId;
|
||
rdpConnections = rdpConnections.filter(c => c.id !== id);
|
||
saveRdpConnections();
|
||
closeModal('modal-removeRdp');
|
||
showToast('Connection removed', 'success');
|
||
});
|
||
|
||
$('rdpDisconnectBtn')?.addEventListener('click', async () => {
|
||
await disconnectRdp();
|
||
closeModal('modal-rdpViewer');
|
||
});
|
||
|
||
$('rdpFullscreenBtn')?.addEventListener('click', () => {
|
||
const modal = document.querySelector('#modal-rdpViewer .modal');
|
||
if (!modal) return;
|
||
if (modal.style.width === '100vw') {
|
||
modal.style.width = '';
|
||
modal.style.height = '';
|
||
modal.style.borderRadius = '';
|
||
} else {
|
||
modal.style.width = '100vw';
|
||
modal.style.height = '100vh';
|
||
modal.style.borderRadius = '0';
|
||
}
|
||
});
|
||
|
||
const viewerModal = $('modal-rdpViewer');
|
||
if (viewerModal) {
|
||
const observer = new MutationObserver(() => {
|
||
if (!viewerModal.classList.contains('open') && activeRdpSession) {
|
||
disconnectRdp();
|
||
}
|
||
});
|
||
observer.observe(viewerModal, { attributes: true, attributeFilter: ['class'] });
|
||
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
|
||
}
|
||
}
|