Files
BetterDesk/web-nodejs/views/remote.ejs
T
UNITRONIX ffe907e73a fix(web-remote): guest page 500 and cookie hijack (Refs #274)
Safe EJS bootstrap for /remote/guest; panel session wins over stale guest cookie.
2026-07-20 23:29:52 +02:00

442 lines
28 KiB
HTML

<%- include('layouts/viewer', {
title: title || _('remote.title'),
pageScripts: ['billing-report', 'remote'],
body: `
<!-- RdClient phone gate -->
<div class="rd-phone-gate" id="rd-phone-gate" hidden>
<div class="rd-phone-gate-card">
<div class="rd-phone-gate-icon">
<span class="material-icons">tablet_mac</span>
</div>
<h1 class="rd-phone-gate-title">${_('remote.phone_unsupported_title')}</h1>
<p class="rd-phone-gate-body">${_('remote.phone_unsupported_body')}</p>
<div class="rd-phone-gate-actions">
<a href="/devices" class="rd-phone-gate-btn rd-phone-gate-btn-primary">${_('remote.phone_unsupported_back_devices')}</a>
<a href="/" class="rd-phone-gate-btn rd-phone-gate-btn-secondary">${_('mobile_nav.home')}</a>
</div>
</div>
</div>
<!-- RdClient shell: tabs + viewer + toolbar (fullscreen target) -->
<div id="rd-viewer-shell" class="rd-viewer-shell">
<!-- Session Tab Bar -->
<div class="session-tab-bar session-tab-bar--slim" id="session-tab-bar">
<button type="button" class="tab-bar-back" id="btn-back-devices" title="${_('remote.back_to_devices')}">
<span class="material-icons">arrow_back</span>
</button>
<div class="session-tabs" id="session-tabs"></div>
<div class="toolbar-notch-slot toolbar-chrome-hidden" id="toolbar-notch-slot">
<div class="viewer-toolbar" id="viewer-toolbar">
<div class="toolbar-drawer" id="toolbar-drawer">
<div class="toolbar-expanded" id="toolbar-expanded">
<div class="toolbar-left">
<span class="toolbar-device-info">
<span class="toolbar-device-id" id="toolbar-device-id"></span>
<span class="toolbar-separator toolbar-status-sep">|</span>
<span class="toolbar-status" id="toolbar-status">${_('remote.connecting')}</span>
</span>
</div>
<div class="toolbar-monitor-strip" id="toolbar-monitor-strip" hidden></div>
<div class="toolbar-right">
<div class="toolbar-dropdown">
<button class="toolbar-btn" id="btn-actions" title="${_('remote.actions')}">
<span class="material-icons">flash_on</span>
</button>
<div class="toolbar-dropdown-menu" id="actions-menu">
<button class="dropdown-item" id="btn-cad">
<span class="material-icons">security</span> Ctrl+Alt+Del
</button>
<button class="dropdown-item" id="btn-lock">
<span class="material-icons">lock</span> ${_('remote.lock_screen')}
</button>
<button class="dropdown-item" id="btn-restart-remote">
<span class="material-icons">restart_alt</span> ${_('remote.restart_remote')}
</button>
<div class="dropdown-separator"></div>
<button class="dropdown-item" id="btn-refresh-screen">
<span class="material-icons">refresh</span> ${_('remote.refresh_screen')}
</button>
<button class="dropdown-item" id="btn-clipboard-paste">
<span class="material-icons">content_paste</span> ${_('remote.clipboard_paste')}
</button>
<button class="dropdown-item" id="btn-reset-keyboard">
<span class="material-icons">keyboard</span> ${_('remote.reset_keyboard')}
</button>
<div class="dropdown-separator"></div>
<button class="dropdown-item toggle-item" id="btn-block-input" data-active="false">
<span class="material-icons">block</span> ${_('remote.block_input')}
</button>
</div>
</div>
<div class="toolbar-dropdown">
<button class="toolbar-btn" id="btn-display" title="${_('remote.display_settings')}">
<span class="material-icons">tune</span>
</button>
<div class="toolbar-dropdown-menu toolbar-dropdown-wide" id="display-menu">
<div class="display-menu-grid">
<section class="display-menu-section">
<div class="dropdown-label">${_('remote.image_quality')}</div>
<button class="dropdown-item quality-item active" data-quality="Best">
<span class="material-icons">hd</span> ${_('remote.quality_best')}
</button>
<button class="dropdown-item quality-item" data-quality="Balanced">
<span class="material-icons">tune</span> ${_('remote.quality_balanced')}
</button>
<button class="dropdown-item quality-item" data-quality="Low">
<span class="material-icons">speed</span> ${_('remote.quality_low')}
</button>
</section>
<section class="display-menu-section">
<div class="dropdown-label">${_('remote.codec')}</div>
<button class="dropdown-item codec-item active" data-codec="Auto">
<span class="material-icons">auto_awesome</span> ${_('remote.codec_auto')}
</button>
<button class="dropdown-item codec-item" data-codec="VP9">
<span class="material-icons">memory</span> VP9
</button>
<button class="dropdown-item codec-item" data-codec="AV1">
<span class="material-icons">memory</span> AV1
</button>
<button class="dropdown-item codec-item" data-codec="H264">
<span class="material-icons">memory</span> H264
</button>
<button class="dropdown-item codec-item" data-codec="VP8">
<span class="material-icons">memory</span> VP8
</button>
<button class="dropdown-item codec-item" data-codec="H265">
<span class="material-icons">memory</span> H265
</button>
</section>
<section class="display-menu-section">
<div class="dropdown-label">${_('remote.keyboard_mode')}</div>
<button class="dropdown-item keyboard-mode-item active" data-keyboard-mode="Auto">
<span class="material-icons">auto_awesome</span> ${_('remote.keyboard_mode_auto')}
</button>
<button class="dropdown-item keyboard-mode-item" data-keyboard-mode="Legacy">
<span class="material-icons">translate</span> ${_('remote.keyboard_mode_legacy')}
</button>
<button class="dropdown-item keyboard-mode-item" data-keyboard-mode="Map">
<span class="material-icons">keyboard</span> ${_('remote.keyboard_mode_map')}
</button>
</section>
<section class="display-menu-section">
<div class="dropdown-label">${_('remote.scale_mode')}</div>
<button class="dropdown-item scale-item active" data-scale="fit">
<span class="material-icons">fit_screen</span> ${_('remote.scale_fit')}
</button>
<button class="dropdown-item scale-item" data-scale="fill">
<span class="material-icons">crop_free</span> ${_('remote.scale_fill')}
</button>
<button class="dropdown-item scale-item" data-scale="1:1">
<span class="material-icons">filter_1</span> ${_('remote.scale_original')}
</button>
<button class="dropdown-item scale-item" data-scale="stretch">
<span class="material-icons">open_in_full</span> ${_('remote.scale_stretch')}
</button>
</section>
<section class="display-menu-section display-menu-section--full">
<button class="dropdown-item toggle-item" id="btn-show-cursor" data-active="true">
<span class="material-icons">mouse</span> ${_('remote.show_cursor')}
</button>
<button class="dropdown-item toggle-item" id="btn-lock-session" data-active="false">
<span class="material-icons">lock_clock</span> ${_('remote.lock_after_session')}
</button>
<button class="dropdown-item toggle-item" id="btn-privacy-mode" data-active="false">
<span class="material-icons">visibility_off</span> ${_('remote.privacy_mode')}
</button>
<button class="dropdown-item toggle-item" id="btn-disable-clipboard" data-active="false">
<span class="material-icons">content_paste_off</span> ${_('remote.disable_clipboard')}
</button>
</section>
</div>
</div>
</div>
<button class="toolbar-btn" id="btn-audio" title="${_('remote.toggle_audio')}">
<span class="material-icons">volume_up</span>
</button>
<div class="toolbar-dropdown">
<button class="toolbar-btn" id="btn-monitors" title="${_('remote.monitors') || 'Monitors'}" style="display:none">
<span class="material-icons">desktop_windows</span>
</button>
<div class="toolbar-dropdown-menu" id="monitors-menu">
<div class="dropdown-label">${_('remote.select_monitor') || 'Select Monitor'}</div>
</div>
</div>
<button class="toolbar-btn" id="btn-chat" title="${_('remote.chat')}">
<span class="material-icons">chat</span>
</button>
<button class="toolbar-btn" id="btn-file-transfer" title="${_('remote.file_transfer') || 'File Transfer'}">
<span class="material-icons">folder_open</span>
</button>
<button class="toolbar-btn" id="btn-record" title="${_('remote.record')}">
<span class="material-icons">fiber_manual_record</span>
</button>
<button class="toolbar-btn" id="btn-viewonly" title="${_('remote.view_only')}">
<span class="material-icons">visibility</span>
</button>
<button class="toolbar-btn" id="btn-pin" title="${_('remote.pin_toolbar')}">
<span class="material-icons">push_pin</span>
</button>
<button class="toolbar-btn toolbar-btn-danger" id="btn-disconnect" title="${_('remote.disconnect')}">
<span class="material-icons">power_settings_new</span>
</button>
</div>
</div>
<div class="toolbar-handle" id="toolbar-handle" role="button" tabindex="0" aria-expanded="false" aria-controls="toolbar-drawer" title="${_('remote.toggle_menu')}">
<button type="button" class="handle-btn" id="btn-handle-fullscreen" title="${_('remote.fullscreen')}">
<span class="material-icons">fullscreen</span>
</button>
<span class="toolbar-handle-grip" aria-hidden="true">
<span class="material-icons">expand_less</span>
</span>
<button type="button" class="handle-btn handle-expand" id="btn-toolbar-expand" title="${_('remote.toggle_menu')}">
<span class="material-icons">expand_more</span>
</button>
</div>
</div>
</div>
</div>
<button type="button" class="tab-bar-add" id="btn-add-session" title="${_('remote.new_connection') || 'New Connection'}">
<span class="material-icons">add</span>
</button>
</div>
<!-- Session picker (address book overlay) -->
<div class="session-picker-backdrop" id="session-picker-backdrop" hidden>
<div class="session-picker-panel" id="session-picker-panel" role="dialog" aria-labelledby="session-picker-title">
<div class="session-picker-header">
<h2 class="session-picker-title" id="session-picker-title">${_('remote.session_picker.title') || 'Choose device'}</h2>
<button type="button" class="session-picker-close" id="session-picker-close" title="${_('actions.close')}">
<span class="material-icons">close</span>
</button>
</div>
<div class="session-picker-quick">
<input type="text" class="session-picker-quick-input" id="session-picker-quick-id"
placeholder="${_('remote.device_id_placeholder') || 'Enter Device ID'}" autocomplete="off" spellcheck="false">
<button type="button" class="session-picker-quick-btn" id="session-picker-quick-btn">
<span class="material-icons">play_arrow</span>
${_('remote_dashboard.connect')}
</button>
</div>
<div class="session-picker-body">
<aside class="session-picker-sidebar" id="session-picker-sidebar">
<div class="session-picker-nav-heading">${_('remote_dashboard.nav_folders')}</div>
<ul class="session-picker-nav-list" id="session-picker-folders">
<li data-static="1"><button type="button" class="rd-picker-nav-item active" data-nav="folder" data-id="all">
<span class="material-icons">devices</span>
<span class="rd-picker-nav-label">${_('remote_dashboard.filter_all')}</span>
<span class="rd-picker-nav-count" id="rd-picker-count-all">0</span>
</button></li>
<li data-static="1"><button type="button" class="rd-picker-nav-item" data-nav="folder" data-id="unassigned">
<span class="material-icons">folder_off</span>
<span class="rd-picker-nav-label">${_('remote_dashboard.unassigned')}</span>
<span class="rd-picker-nav-count" id="rd-picker-count-unassigned">0</span>
</button></li>
</ul>
<ul class="session-picker-nav-list" id="session-picker-folders-custom"></ul>
<div class="session-picker-nav-heading">${_('remote_dashboard.nav_groups')}</div>
<ul class="session-picker-nav-list" id="session-picker-groups">
<li class="hidden" data-static="1" id="session-picker-groups-empty">${_('remote_dashboard.no_groups')}</li>
</ul>
<div class="session-picker-nav-heading">${_('remote_dashboard.nav_tags')}</div>
<ul class="session-picker-nav-list" id="session-picker-tags">
<li class="hidden" data-static="1" id="session-picker-tags-empty">${_('remote_dashboard.no_tags')}</li>
</ul>
</aside>
<div class="session-picker-content" id="session-picker-content">
<div class="session-picker-toolbar">
<span class="session-picker-section-title" id="session-picker-section-title">${_('remote_dashboard.nav_address_book')}</span>
<div class="session-picker-search-wrap">
<span class="material-icons session-picker-search-icon">search</span>
<input type="search" id="session-picker-search" class="session-picker-search"
placeholder="${_('remote_dashboard.search_placeholder')}" autocomplete="off">
</div>
<div class="session-picker-filters" role="tablist">
<button type="button" class="rd-picker-filter active" data-filter="all">${_('remote_dashboard.filter_all')}</button>
<button type="button" class="rd-picker-filter" data-filter="online">${_('remote_dashboard.filter_online')}</button>
<button type="button" class="rd-picker-filter" data-filter="offline">${_('remote_dashboard.filter_offline')}</button>
</div>
<div class="session-picker-view-toggle" role="group">
<button type="button" class="rd-picker-icon-btn active" id="session-picker-view-grid" title="${_('remote_dashboard.view_grid')}">
<span class="material-icons">grid_view</span>
</button>
<button type="button" class="rd-picker-icon-btn" id="session-picker-view-list" title="${_('remote_dashboard.view_list')}">
<span class="material-icons">view_list</span>
</button>
</div>
<span class="session-picker-count" id="session-picker-count"></span>
<button type="button" class="rd-picker-icon-btn" id="session-picker-refresh" title="${_('remote_dashboard.sync_now')}">
<span class="material-icons">sync</span>
</button>
</div>
<div class="session-picker-main" id="session-picker-main">
<div class="session-picker-loading hidden" id="session-picker-loading">
<span class="material-icons">sync</span>
<span>${_('remote_dashboard.loading')}</span>
</div>
<div class="session-picker-error hidden" id="session-picker-error">
<span class="material-icons">error_outline</span>
<span id="session-picker-error-text">${_('remote_dashboard.error_load')}</span>
<button type="button" class="rd-picker-btn" id="session-picker-retry">${_('remote_dashboard.retry')}</button>
</div>
<div class="session-picker-empty hidden" id="session-picker-empty">
<span class="material-icons">devices_other</span>
<p>${_('remote_dashboard.empty')}</p>
</div>
<div class="rd-picker-grid hidden" id="session-picker-grid"></div>
<div class="rd-picker-table-wrap hidden" id="session-picker-table-wrap">
<table class="rd-picker-table">
<thead>
<tr>
<th>${_('remote_dashboard.col_status')}</th>
<th>${_('remote_dashboard.col_id')}</th>
<th>${_('remote_dashboard.col_hostname')}</th>
<th>${_('remote_dashboard.col_platform')}</th>
<th>${_('remote_dashboard.col_last_seen')}</th>
<th></th>
</tr>
</thead>
<tbody id="session-picker-tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Main Viewer Container (session panels are inserted here) -->
<div class="viewer-container" id="viewer-container"></div>
</div><!-- /#rd-viewer-shell -->
<nav class="rd-mobile-toolbar" id="rd-mobile-toolbar" aria-label="${_('remote.mobile_toolbar') || 'Remote controls'}">
<button type="button" class="rd-mobile-toolbar-btn active" id="rd-mob-input-touch" data-mode="direct" title="${_('remote.input_mode_touch')}">
<span class="material-icons">touch_app</span>
<span>${_('remote.input_mode_touch')}</span>
</button>
<button type="button" class="rd-mobile-toolbar-btn" id="rd-mob-input-touchpad" data-mode="touchpad" title="${_('remote.input_mode_touchpad')}">
<span class="material-icons">swipe</span>
<span>${_('remote.input_mode_touchpad')}</span>
</button>
<button type="button" class="rd-mobile-toolbar-btn" id="rd-mob-keyboard" title="${_('remote.show_keyboard')}">
<span class="material-icons">keyboard</span>
<span>${_('remote.show_keyboard')}</span>
</button>
<button type="button" class="rd-mobile-toolbar-btn" id="rd-mob-special" title="${_('remote.special_keys')}">
<span class="material-icons">keyboard_command_key</span>
<span>${_('remote.special_keys')}</span>
</button>
<button type="button" class="rd-mobile-toolbar-btn" id="rd-mob-cad" title="Ctrl+Alt+Del">
<span class="material-icons">security</span>
<span>CAD</span>
</button>
<button type="button" class="rd-mobile-toolbar-btn" id="rd-mob-fullscreen" title="${_('remote.fullscreen')}">
<span class="material-icons">fullscreen</span>
<span>${_('remote.fullscreen')}</span>
</button>
</nav>
<div class="rd-special-keys-panel" id="rd-special-keys-panel" role="group" aria-label="${_('remote.special_keys')}">
<button type="button" class="rd-special-key-btn" data-special="Meta">Win</button>
<button type="button" class="rd-special-key-btn" data-special="Alt+Tab">Alt+Tab</button>
<button type="button" class="rd-special-key-btn" data-special="PrintScreen">${_('remote.special_print_screen') || 'Print Screen'}</button>
<button type="button" class="rd-special-key-btn" data-special="Escape">Esc</button>
</div>
<input type="text" class="rd-keyboard-bridge" id="rd-keyboard-bridge" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" aria-hidden="true" tabindex="-1">
<!-- Session Panel Template (cloned for each session) -->
<template id="session-panel-template">
<div class="session-panel">
<div class="viewer-overlay session-connection-overlay">
<div class="overlay-card">
<div class="overlay-icon"><span class="material-icons">connected_tv</span></div>
<h2 class="overlay-title">${_('remote.title')}</h2>
<p class="overlay-device-id session-device-label"></p>
<p class="overlay-device-name session-device-name"></p>
<div class="overlay-status">
<div class="spinner"></div>
<span class="session-status-text">${_('remote.connecting')}</span>
</div>
<div class="overlay-actions session-overlay-actions" style="display:none;">
<button class="btn btn-primary session-btn-reconnect">
<span class="material-icons">refresh</span>
${_('remote.reconnect')}
</button>
<button class="btn btn-secondary session-btn-cdap-fallback" style="display:none;margin-top:8px;" title="${_('remote.cdap_fallback_hint')}">
<span class="material-icons">photo_camera</span>
${_('remote.use_cdap_fallback')}
</button>
</div>
</div>
</div>
<div class="viewer-overlay session-password-overlay" style="display:none;">
<div class="overlay-card">
<div class="overlay-icon"><span class="material-icons">lock</span></div>
<h2 class="overlay-title">${_('remote.password_required')}</h2>
<p class="overlay-device-id session-password-label"></p>
<div class="password-form">
<div class="form-group">
<input type="password" class="form-input session-password-input"
placeholder="${_('remote.password_placeholder')}" autocomplete="off">
</div>
<p class="login-error session-login-error" style="display:none;"></p>
<div class="password-form-footer">
<label class="session-remember-peer">
<input type="checkbox" class="session-remember-peer-checkbox">
<span>${_('remote.remember_peer_password') || 'Remember device password on this device'}</span>
</label>
<button class="btn btn-primary btn-full session-btn-authenticate">
<span class="material-icons">login</span>
${_('remote.connect')}
</button>
</div>
</div>
</div>
</div>
<div class="viewer-overlay session-2fa-overlay" style="display:none;">
<div class="overlay-card">
<div class="overlay-icon"><span class="material-icons">verified_user</span></div>
<h2 class="overlay-title">${_('remote.2fa_required') || '2FA Verification'}</h2>
<p class="overlay-subtitle">${_('remote.2fa_hint') || 'Enter the 6-digit code from your authenticator app'}</p>
<div class="password-form">
<div class="form-group">
<input type="text" class="form-input session-2fa-input"
placeholder="000000" autocomplete="one-time-code"
maxlength="6" inputmode="numeric" pattern="[0-9]*"
style="text-align:center;font-size:1.5rem;letter-spacing:0.5em;">
</div>
<p class="login-error session-2fa-error" style="display:none;"></p>
<button class="btn btn-primary btn-full session-btn-verify-2fa">
<span class="material-icons">verified</span>
${_('remote.verify') || 'Verify'}
</button>
</div>
</div>
</div>
<canvas class="session-canvas" tabindex="0"></canvas>
<div class="chat-panel session-chat-panel" style="display:none;">
<div class="chat-header">
<span>${_('remote.chat')}</span>
<button class="chat-close session-btn-chat-close"><span class="material-icons">close</span></button>
</div>
<div class="chat-messages session-chat-messages"></div>
<div class="chat-input-area">
<input type="text" class="chat-input session-chat-input" placeholder="${_('remote.chat_placeholder')}" />
<button class="chat-send session-btn-chat-send"><span class="material-icons">send</span></button>
</div>
</div>
</div>
</template>
<!-- Initial session data (from server route) -->
<script nonce="${cspNonce}">
window.__initialDeviceId = ` + JSON.stringify(deviceId) + `;
window.__initialDeviceName = ` + JSON.stringify(device && device.hostname ? device.hostname : '') + `;
window.__capabilities = ` + JSON.stringify(capabilities || { transport: 'rd' }) + `;
window.__guestToken = ` + JSON.stringify((typeof guestToken !== 'undefined' && guestToken) ? guestToken : '') + `;
</script>
`
}) %>