Bundle dev version of the web-ui
@ -1,179 +1,48 @@
|
||||
GNS3 WebUI is web implementation of user interface for GNS3 software.
|
||||
|
||||
Current version: 2.2.24
|
||||
Current version: 3.1.0-dev.1
|
||||
|
||||
Bug Fixes & enhancements
|
||||
- security fixes
|
||||
---
|
||||
|
||||
Current version: 2020.4.0-beta.1
|
||||
## What's New
|
||||
|
||||
Bug Fixes & enhancements
|
||||
- symbol is not properly selected in change symbol dialog
|
||||
- issue when using the scroll wheel on the web console
|
||||
- missing settings for Docker nodes
|
||||
- error on servers page
|
||||
- **Web Wireshark Inline Display**: Open Wireshark directly in the project map canvas
|
||||
- Context menu integration: "Start Web Wireshark (Inline)"
|
||||
- Support for multiple concurrent inline windows
|
||||
- Draggable and resizable inline windows
|
||||
- Minimize/restore with unified taskbar
|
||||
- xpra-html5 client integration (MPL-2.0 licensed)
|
||||
|
||||
What's new
|
||||
- double click nodes to open the console
|
||||
- **AI Chat Feature**: Integrated AI assistant for network automation
|
||||
- LLM model configuration (OpenAI, Anthropic, local models)
|
||||
- Profile management for different AI providers
|
||||
- Context-aware assistance for network topology
|
||||
|
||||
Current version: 2020.3.0-beta.3
|
||||
- **Unified Window Management**: Modern floating window system
|
||||
- Unified taskbar for all minimized windows (Console, Wireshark)
|
||||
- Focus-based z-index management (click to bring window to front)
|
||||
- Minimize/restore functionality for all floating panels
|
||||
- Window position persistence across sessions
|
||||
|
||||
Bug Fixes & enhancements
|
||||
- direct download URL in template dialog
|
||||
- fix for issues with suspnded nodes
|
||||
- fix for bug with deleting templates
|
||||
- fix for importing images
|
||||
- **Modern UI Framework**: Angular 21 Zoneless architecture
|
||||
- 100% migration to Angular 21 standalone components
|
||||
- Zone.js removed for better performance
|
||||
- Signal-based reactive state management
|
||||
- OnPush change detection strategy
|
||||
|
||||
What's new
|
||||
- Option to resize console
|
||||
- Improvements in creating templates
|
||||
- **Material Design 3**: Complete visual refresh
|
||||
- Material Design 3 color system and theming
|
||||
- Dynamic theme switching (light/dark)
|
||||
- CSS variable-based theming architecture
|
||||
- Centralized dialog styling system
|
||||
|
||||
GNS3 Web UI 2020.3.0-beta.1
|
||||
## Bug Fixes & Enhancements
|
||||
|
||||
Bug Fixes & enhancements
|
||||
- refreshing list of templates after adding new template from project map
|
||||
- link to preferences from project page
|
||||
- disallow user to create Qemu template when binary is not selected
|
||||
- extending the time for notification to appear
|
||||
- open first settings menu at start
|
||||
- the menu for the map rearranged
|
||||
- restyling SystemStatus page
|
||||
- marking files which already exist in appliance wizard
|
||||
|
||||
What's new
|
||||
- Option to import appliances
|
||||
|
||||
GNS3 Web UI 2020.2.0-beta.5
|
||||
|
||||
Bug Fixes
|
||||
|
||||
- Bug in symbol selection
|
||||
- Same question is asked after going back to project
|
||||
- Cannot read property 'forEach' of undefined
|
||||
- Error when selecting existing Docker image
|
||||
- Invalid property when adding VMware VM template
|
||||
- Invalid type for adapters field when adding Docker template
|
||||
- Prevent user to move to another step when adding template
|
||||
- Web UI cannot set flag "Leave this project running in the background after closing"
|
||||
|
||||
What's new
|
||||
- Default values in templates
|
||||
- New option for Qemu VMs
|
||||
- Ability to quickly change Hostname from right click
|
||||
- Progress bar for node creation
|
||||
|
||||
GNS3 Web UI 2020.2.0-beta.4
|
||||
|
||||
Bug Fixes
|
||||
- New port setting for GNS3 VM preferences
|
||||
- Option to auto-hide menu toolbar on the left side
|
||||
-Controller type in template preferences
|
||||
- Error when selecting existing Docker image
|
||||
- Default values in templates
|
||||
- TypeError: Cannot read property 'message' of undefined
|
||||
- TypeError: e.error is undefined
|
||||
- TypeError: Cannot read property 'placements' of null
|
||||
- Creating IOS templates -> fix for platforms and network adapters
|
||||
|
||||
GNS3 Web UI 2020.2.0-beta.2
|
||||
|
||||
What's New
|
||||
- Drag & drop to add new nodes on topology
|
||||
- Option to minimize/maximize and hide console widget
|
||||
- Ability to add IOS templates
|
||||
- Node names in HTTP console tabs
|
||||
- Default settings for templates
|
||||
- Support for adding IOS images
|
||||
- Node dialog updated
|
||||
- Messages with description in toasts
|
||||
- Adding interfaces to cloud nodes
|
||||
- Changes in notification box mechanism (once per day option)
|
||||
- Additional tooltips added
|
||||
- Copy/paste options in console (only Chrome full support)
|
||||
- More details for server failed connections
|
||||
|
||||
Bug Fixes
|
||||
- Fix for console icons
|
||||
- Fix for creating ethernet switches and hubs
|
||||
- Fix for opening console from context menu
|
||||
- Qemu configurator now works properly
|
||||
- Fixes in snap to grid option
|
||||
- Symbols preview now works correctly
|
||||
- Error messages in preferences should be displayed
|
||||
- Default values for New Ethernet devices in configurator
|
||||
- Fix for wrong adapter types in Qemu
|
||||
- Fix for fit in view option on Firefox
|
||||
- Fix for navigation errors
|
||||
|
||||
GNS3 Web UI 2020.2.0-beta.1
|
||||
|
||||
What's New
|
||||
- Support for suspended status added
|
||||
- Suport for 404 page
|
||||
- Actions for group of nodes added
|
||||
- Updating packages
|
||||
- Button to close project added
|
||||
- Opening ads in new window
|
||||
- New dialog for adding nodes
|
||||
- Option to import config
|
||||
- Support for light theme added
|
||||
|
||||
Bug Fixes
|
||||
- Fix for navigating to project that doesn't exist
|
||||
- Fix for AdButler errors
|
||||
- Fix for screenshot issue
|
||||
- Proper centering of icons
|
||||
- Fix for adding custom symbols
|
||||
- Fix for return command in console
|
||||
- Fix for deleting links
|
||||
- Fix for duplicating any node type
|
||||
- Fix for console errors on servers page
|
||||
- Fix for console errors on projects page
|
||||
|
||||
GNS3 Web UI 2019.2.0 v10
|
||||
|
||||
What's New
|
||||
- Qemu image configurator
|
||||
- Custom console for particular node
|
||||
- Option to connect console to all nodes
|
||||
- Option to start Winpcap
|
||||
- Filtering devices with packet filters on topology summary
|
||||
- Filtering devices with captures on topology summary
|
||||
- View options taken from map configuration
|
||||
- Servers summary widget
|
||||
- Ability to lock single item on the map
|
||||
- Editing & import & export config files
|
||||
- Context menu for inserted drawings
|
||||
- Ability to drag topology summary & servers summary & console widgets
|
||||
- Ability to resize topology summary & servers summary & console widgets
|
||||
- Option to show the grid
|
||||
- Option to snap to grid
|
||||
- Usage instructions available from context menu
|
||||
- Errors & warnings visible as notifications
|
||||
- Fit in view options
|
||||
- Support for global variables
|
||||
- Support for layers
|
||||
- Extending template preferences
|
||||
|
||||
Bug Fixes
|
||||
- Input validation in styles editor
|
||||
- Fix for saving map as image
|
||||
- Removing errors with uncorrect subscriptions
|
||||
|
||||
GNS3 Web UI 2019.2.0 v9
|
||||
|
||||
What's New
|
||||
- Editing interface labels on double click
|
||||
- Support for keyboard shortcuts
|
||||
- Menu extended with option to delete currently opened project, export & import project
|
||||
- Possibility to save current state of project
|
||||
- Ability to duplicate project from projects page
|
||||
- Node information dialog available from context menu
|
||||
- Topology summary widget on map view
|
||||
- Improvements in dialog styles
|
||||
|
||||
Bug Fixes
|
||||
- Removing issues with opening console
|
||||
- Context menu now is correctly placed
|
||||
- Text validation in dialogs
|
||||
- Removing errors with creating WebSockets
|
||||
- Test environment stability improvements (Vitest isolation)
|
||||
- Hardcoded color protection mechanism with multi-layer validation
|
||||
- Symbol loading with JWT authentication
|
||||
- Custom adapters support for template configuration
|
||||
- Compute status real-time updates via WebSocket
|
||||
- Curve drawing tool improvements
|
||||
- Window boundary constraint service for better UX
|
||||
- Console devices panel (v2.0.0) with improved device management
|
||||
|
||||
3
gns3server/static/web-ui/assets/github-icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 816 B |
53
gns3server/static/web-ui/assets/gns3-loading-logo.svg
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
@ -17,8 +17,8 @@
|
||||
enable-background="new 0 0 792 612"
|
||||
xml:space="preserve"
|
||||
inkscape:version="0.92.2 (30a58d1, 2017-08-07)"
|
||||
width="100%"
|
||||
height="100%"
|
||||
|
||||
|
||||
sodipodi:docname="gns3_icon.svg"
|
||||
inkscape:export-filename="/home/grossmj/Downloads/gns3_icon_128x128.png"
|
||||
inkscape:export-xdpi="45"
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
779
gns3server/static/web-ui/assets/vnc-console/index.html
Normal file
@ -0,0 +1,779 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="x-ua-compatible" content="IE=edge" />
|
||||
<title>VNC Console</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
#vnc-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: padding-top 0.3s ease;
|
||||
}
|
||||
|
||||
#vnc-container.with-toolbar {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
#vnc-container > canvas {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
#status-bar {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
z-index: 1000;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#status-bar.success {
|
||||
background: rgba(76, 175, 80, 0.9);
|
||||
}
|
||||
|
||||
#status-bar.error {
|
||||
background: rgba(244, 67, 54, 0.9);
|
||||
}
|
||||
|
||||
#status-bar.warning {
|
||||
background: rgba(255, 152, 0, 0.9);
|
||||
}
|
||||
|
||||
#error-dialog {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
max-width: 450px;
|
||||
display: none;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
#error-dialog h3 {
|
||||
color: #d32f2f;
|
||||
margin-bottom: 12px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#error-dialog p {
|
||||
color: #555;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#error-dialog .button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#error-dialog button {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
#error-dialog button:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
|
||||
#error-dialog button.secondary {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#error-dialog button.secondary:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
/* Loading indicator */
|
||||
.loading-spinner {
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Toolbar styles */
|
||||
#vnc-toolbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 1000;
|
||||
transition: transform 0.3s ease;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#vnc-toolbar.hidden {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
#vnc-toolbar .toolbar-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
#vnc-toolbar .toolbar-section:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Dropdown menu */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: -35px;
|
||||
margin-top: 4px;
|
||||
background: rgba(40, 40, 40, 0.98);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
min-width: 320px;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
display: none;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
display: block;
|
||||
animation: dropdownFadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes dropdownFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu .menu-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.dropdown-menu .menu-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dropdown-menu .menu-section-label {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dropdown-menu .button-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dropdown-menu .button-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Modifier checkboxes */
|
||||
.modifier-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modifier-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.modifier-checkbox:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.modifier-checkbox input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
accent-color: #1976d2;
|
||||
}
|
||||
|
||||
.modifier-checkbox input[type='checkbox']:checked + span {
|
||||
color: #1976d2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modifier-checkbox.active {
|
||||
background: rgba(25, 118, 210, 0.2);
|
||||
}
|
||||
|
||||
.function-keys-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.function-keys-grid button {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
min-width: 40px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dropdown-menu button {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
min-width: 40px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dropdown-menu button:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dropdown-toggle:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
content: '▼';
|
||||
font-size: 10px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.dropdown.active .dropdown-toggle::after {
|
||||
content: '▲';
|
||||
}
|
||||
|
||||
/* Record mode select */
|
||||
#record-mode {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#record-mode option {
|
||||
background: rgba(40, 40, 40, 0.98);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#vnc-toolbar button {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#vnc-toolbar button:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
#vnc-toolbar button:active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
#vnc-toolbar button.danger {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
border-color: rgba(244, 67, 54, 0.4);
|
||||
}
|
||||
|
||||
#vnc-toolbar button.danger:hover {
|
||||
background: rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
#vnc-toolbar button.success {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
border-color: rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
#vnc-toolbar button.success:hover {
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
#vnc-toolbar button.recording {
|
||||
background: rgba(244, 67, 54, 0.3);
|
||||
border-color: rgba(244, 67, 54, 0.6);
|
||||
animation: recording-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes recording-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.4);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 8px rgba(244, 67, 54, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Recording indicator - no background bar */
|
||||
#recording-indicator {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: #fff;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 2000;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#recording-indicator.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#recording-indicator .recording-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
animation: dot-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes dot-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
#vnc-toolbar .toolbar-label {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#vnc-toolbar .connection-status {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 12px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* Toolbar toggle button */
|
||||
#toolbar-toggle {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 100px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
#toolbar-toggle:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
/* Change icon when toolbar is visible */
|
||||
#toolbar-toggle.active::before {
|
||||
content: '▼';
|
||||
}
|
||||
|
||||
#toolbar-toggle:not(.active)::before {
|
||||
content: '⌨';
|
||||
}
|
||||
|
||||
/* Move toggle button down when toolbar is visible */
|
||||
#vnc-toolbar:not(.hidden) ~ #toolbar-toggle {
|
||||
top: 58px;
|
||||
}
|
||||
|
||||
/* Clipboard modal */
|
||||
#clipboard-modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #2a2a2a;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
min-width: 400px;
|
||||
z-index: 1002;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#clipboard-modal h3 {
|
||||
color: #fff;
|
||||
margin-bottom: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#clipboard-modal textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
resize: vertical;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#clipboard-modal .button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#clipboard-modal button {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
#clipboard-modal button:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
|
||||
#clipboard-modal button.secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
#clipboard-modal button.secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Modal overlay */
|
||||
#modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1001;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="vnc-container">
|
||||
<!-- Camera preview overlay -->
|
||||
<video
|
||||
id="camera-preview"
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
style="
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
right: 10px;
|
||||
width: 180px;
|
||||
height: 135px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8px;
|
||||
z-index: 50;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
"
|
||||
></video>
|
||||
<div class="loading-container" id="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">Initializing VNC Console...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar Toggle Button -->
|
||||
<button id="toolbar-toggle" title="Toggle Toolbar"></button>
|
||||
|
||||
<!-- Recording Indicator -->
|
||||
<div id="recording-indicator">
|
||||
<span class="recording-dot"></span>
|
||||
<span id="recording-time">00:00</span>
|
||||
</div>
|
||||
|
||||
<!-- VNC Toolbar -->
|
||||
<div id="vnc-toolbar" class="hidden">
|
||||
<!-- Connection Controls -->
|
||||
<div class="toolbar-section">
|
||||
<span class="connection-status" id="connection-status">● Connecting...</span>
|
||||
<button id="btn-fullscreen" title="Toggle Fullscreen (F11)">⛶ Fullscreen</button>
|
||||
<button id="btn-connect" class="success" title="Connect/Reconnect">↻ Connect</button>
|
||||
<button id="btn-disconnect" class="danger" title="Disconnect">✕ Disconnect</button>
|
||||
</div>
|
||||
|
||||
<!-- Send Keys - Dropdown -->
|
||||
<div class="toolbar-section dropdown" id="send-keys-dropdown">
|
||||
<span class="toolbar-label dropdown-toggle" id="send-keys-toggle">Send Keys</span>
|
||||
<div class="dropdown-menu" id="send-keys-menu">
|
||||
<!-- Modifiers -->
|
||||
<div class="menu-section">
|
||||
<div class="menu-section-label">Modifiers (hold Ctrl/Alt to select multiple)</div>
|
||||
<div class="modifier-row">
|
||||
<label class="modifier-checkbox">
|
||||
<input type="checkbox" id="mod-ctrl" value="ctrl" />
|
||||
<span>Ctrl</span>
|
||||
</label>
|
||||
<label class="modifier-checkbox">
|
||||
<input type="checkbox" id="mod-alt" value="alt" />
|
||||
<span>Alt</span>
|
||||
</label>
|
||||
<label class="modifier-checkbox">
|
||||
<input type="checkbox" id="mod-shift" value="shift" />
|
||||
<span>Shift</span>
|
||||
</label>
|
||||
<label class="modifier-checkbox">
|
||||
<input type="checkbox" id="mod-ctrl-alt" value="ctrl+alt" />
|
||||
<span>Ctrl+Alt</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Function Keys -->
|
||||
<div class="menu-section">
|
||||
<div class="menu-section-label">Function Keys</div>
|
||||
<div class="function-keys-grid">
|
||||
<button id="btn-f1" title="Send F1">F1</button>
|
||||
<button id="btn-f2" title="Send F2">F2</button>
|
||||
<button id="btn-f3" title="Send F3">F3</button>
|
||||
<button id="btn-f4" title="Send F4">F4</button>
|
||||
<button id="btn-f5" title="Send F5">F5</button>
|
||||
<button id="btn-f6" title="Send F6">F6</button>
|
||||
<button id="btn-f7" title="Send F7">F7</button>
|
||||
<button id="btn-f8" title="Send F8">F8</button>
|
||||
<button id="btn-f9" title="Send F9">F9</button>
|
||||
<button id="btn-f10" title="Send F10">F10</button>
|
||||
<button id="btn-f11" title="Send F11">F11</button>
|
||||
<button id="btn-f12" title="Send F12">F12</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="menu-section">
|
||||
<div class="menu-section-label">Quick Actions</div>
|
||||
<div class="quick-actions">
|
||||
<button id="btn-ctrlaltdel" title="Send Ctrl+Alt+Del">Ctrl+Alt+Del</button>
|
||||
<button id="btn-ctrlaltbs" title="Send Ctrl+Alt+Backspace">Ctrl+Alt+BS</button>
|
||||
<button id="btn-tab" title="Send Tab">Tab</button>
|
||||
<button id="btn-esc" title="Send Esc">Esc</button>
|
||||
<button id="btn-print" title="Send Print Screen">Print</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scale Controls -->
|
||||
<div class="toolbar-section">
|
||||
<button id="btn-scale-up" title="Scale Up">+</button>
|
||||
<button id="btn-scale-down" title="Scale Down">−</button>
|
||||
<button id="btn-scale-fit" title="Scale to Fit">Fit</button>
|
||||
</div>
|
||||
|
||||
<!-- Screen Capture -->
|
||||
<div class="toolbar-section">
|
||||
<span class="toolbar-label">Capture</span>
|
||||
<select id="record-mode" title="Recording Mode">
|
||||
<option value="vnc">VNC</option>
|
||||
<option value="vnc-camera">VNC + Camera</option>
|
||||
<option value="camera">Camera</option>
|
||||
</select>
|
||||
<button id="btn-mute" title="Toggle Microphone">🔊 Mic</button>
|
||||
<button id="btn-screenshot" title="Take Screenshot">📷 Screenshot</button>
|
||||
<button id="btn-record-pause" title="Start/Pause Recording">⏺ Record</button>
|
||||
<button id="btn-record-stop" class="danger" title="Stop Recording" style="display: none">⏹ Stop</button>
|
||||
</div>
|
||||
|
||||
<!-- Clipboard -->
|
||||
<div class="toolbar-section">
|
||||
<button id="btn-clipboard" title="Open Clipboard">📋 Clipboard</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clipboard Modal -->
|
||||
<div id="modal-overlay"></div>
|
||||
<div id="clipboard-modal">
|
||||
<h3>Clipboard</h3>
|
||||
<textarea id="clipboard-text" placeholder="Paste text here to send to remote VNC server..."></textarea>
|
||||
<div class="button-group">
|
||||
<button class="secondary" id="btn-clipboard-cancel">Cancel</button>
|
||||
<button id="btn-clipboard-send">Send to Remote</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status-bar">Connecting...</div>
|
||||
|
||||
<div id="error-dialog">
|
||||
<h3 id="error-title">Connection Failed</h3>
|
||||
<p id="error-message"></p>
|
||||
<div class="button-group">
|
||||
<button class="secondary" onclick="window.close()">Close</button>
|
||||
<button onclick="location.reload()">Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- noVNC RFB library will be loaded as ES module -->
|
||||
<script type="module" src="./vnc-controller.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
103
gns3server/static/web-ui/assets/vnc-console/novnc/core/base64.js
Normal file
@ -0,0 +1,103 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js
|
||||
|
||||
import * as Log from './util/logging.js';
|
||||
|
||||
export default {
|
||||
/* Convert data (an array of integers) to a Base64 string. */
|
||||
toBase64Table: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''),
|
||||
base64Pad: '=',
|
||||
|
||||
encode(data) {
|
||||
'use strict';
|
||||
let result = '';
|
||||
const length = data.length;
|
||||
const lengthpad = length % 3;
|
||||
// Convert every three bytes to 4 ascii characters.
|
||||
|
||||
for (let i = 0; i < length - 2; i += 3) {
|
||||
result += this.toBase64Table[data[i] >> 2];
|
||||
result += this.toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)];
|
||||
result += this.toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)];
|
||||
result += this.toBase64Table[data[i + 2] & 0x3f];
|
||||
}
|
||||
|
||||
// Convert the remaining 1 or 2 bytes, pad out to 4 characters.
|
||||
const j = length - lengthpad;
|
||||
if (lengthpad === 2) {
|
||||
result += this.toBase64Table[data[j] >> 2];
|
||||
result += this.toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)];
|
||||
result += this.toBase64Table[(data[j + 1] & 0x0f) << 2];
|
||||
result += this.toBase64Table[64];
|
||||
} else if (lengthpad === 1) {
|
||||
result += this.toBase64Table[data[j] >> 2];
|
||||
result += this.toBase64Table[(data[j] & 0x03) << 4];
|
||||
result += this.toBase64Table[64];
|
||||
result += this.toBase64Table[64];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/* Convert Base64 data to a string */
|
||||
/* eslint-disable comma-spacing */
|
||||
toBinaryTable: [
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
|
||||
-1, -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
|
||||
25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
|
||||
48, 49, 50, 51, -1, -1, -1, -1, -1,
|
||||
],
|
||||
/* eslint-enable comma-spacing */
|
||||
|
||||
decode(data, offset = 0) {
|
||||
let dataLength = data.indexOf('=') - offset;
|
||||
if (dataLength < 0) {
|
||||
dataLength = data.length - offset;
|
||||
}
|
||||
|
||||
/* Every four characters is 3 resulting numbers */
|
||||
const resultLength = (dataLength >> 2) * 3 + Math.floor((dataLength % 4) / 1.5);
|
||||
const result = new Array(resultLength);
|
||||
|
||||
// Convert one by one.
|
||||
|
||||
let leftbits = 0; // number of bits decoded, but yet to be appended
|
||||
let leftdata = 0; // bits decoded, but yet to be appended
|
||||
for (let idx = 0, i = offset; i < data.length; i++) {
|
||||
const c = this.toBinaryTable[data.charCodeAt(i) & 0x7f];
|
||||
const padding = data.charAt(i) === this.base64Pad;
|
||||
// Skip illegal characters and whitespace
|
||||
if (c === -1) {
|
||||
Log.Error('Illegal character code ' + data.charCodeAt(i) + ' at position ' + i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect data into leftdata, update bitcount
|
||||
leftdata = (leftdata << 6) | c;
|
||||
leftbits += 6;
|
||||
|
||||
// If we have 8 or more bits, append 8 bits to the result
|
||||
if (leftbits >= 8) {
|
||||
leftbits -= 8;
|
||||
// Append if not padding.
|
||||
if (!padding) {
|
||||
result[idx++] = (leftdata >> leftbits) & 0xff;
|
||||
}
|
||||
leftdata &= (1 << leftbits) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are any bits left, the base64 string was corrupted
|
||||
if (leftbits) {
|
||||
const err = new Error('Corrupted base64 string');
|
||||
err.name = 'Base64-Error';
|
||||
throw err;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
}; /* End of Base64 namespace */
|
||||
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class CopyRectDecoder {
|
||||
decodeRect(x, y, width, height, sock, display, depth) {
|
||||
if (sock.rQwait('COPYRECT', 4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let deltaX = sock.rQshift16();
|
||||
let deltaY = sock.rQshift16();
|
||||
|
||||
if (width === 0 || height === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
display.copyImage(deltaX, deltaY, x, y, width, height);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,199 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
*/
|
||||
|
||||
import * as Log from '../util/logging.js';
|
||||
|
||||
export default class HextileDecoder {
|
||||
constructor() {
|
||||
this._tiles = 0;
|
||||
this._lastsubencoding = 0;
|
||||
this._tileBuffer = new Uint8Array(16 * 16 * 4);
|
||||
}
|
||||
|
||||
decodeRect(x, y, width, height, sock, display, depth) {
|
||||
if (this._tiles === 0) {
|
||||
this._tilesX = Math.ceil(width / 16);
|
||||
this._tilesY = Math.ceil(height / 16);
|
||||
this._totalTiles = this._tilesX * this._tilesY;
|
||||
this._tiles = this._totalTiles;
|
||||
}
|
||||
|
||||
while (this._tiles > 0) {
|
||||
let bytes = 1;
|
||||
|
||||
if (sock.rQwait('HEXTILE', bytes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let rQ = sock.rQ;
|
||||
let rQi = sock.rQi;
|
||||
|
||||
let subencoding = rQ[rQi]; // Peek
|
||||
if (subencoding > 30) {
|
||||
// Raw
|
||||
throw new Error('Illegal hextile subencoding (subencoding: ' + subencoding + ')');
|
||||
}
|
||||
|
||||
const currTile = this._totalTiles - this._tiles;
|
||||
const tileX = currTile % this._tilesX;
|
||||
const tileY = Math.floor(currTile / this._tilesX);
|
||||
const tx = x + tileX * 16;
|
||||
const ty = y + tileY * 16;
|
||||
const tw = Math.min(16, x + width - tx);
|
||||
const th = Math.min(16, y + height - ty);
|
||||
|
||||
// Figure out how much we are expecting
|
||||
if (subencoding & 0x01) {
|
||||
// Raw
|
||||
bytes += tw * th * 4;
|
||||
} else {
|
||||
if (subencoding & 0x02) {
|
||||
// Background
|
||||
bytes += 4;
|
||||
}
|
||||
if (subencoding & 0x04) {
|
||||
// Foreground
|
||||
bytes += 4;
|
||||
}
|
||||
if (subencoding & 0x08) {
|
||||
// AnySubrects
|
||||
bytes++; // Since we aren't shifting it off
|
||||
|
||||
if (sock.rQwait('HEXTILE', bytes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let subrects = rQ[rQi + bytes - 1]; // Peek
|
||||
if (subencoding & 0x10) {
|
||||
// SubrectsColoured
|
||||
bytes += subrects * (4 + 2);
|
||||
} else {
|
||||
bytes += subrects * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sock.rQwait('HEXTILE', bytes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We know the encoding and have a whole tile
|
||||
rQi++;
|
||||
if (subencoding === 0) {
|
||||
if (this._lastsubencoding & 0x01) {
|
||||
// Weird: ignore blanks are RAW
|
||||
Log.Debug(' Ignoring blank after RAW');
|
||||
} else {
|
||||
display.fillRect(tx, ty, tw, th, this._background);
|
||||
}
|
||||
} else if (subencoding & 0x01) {
|
||||
// Raw
|
||||
let pixels = tw * th;
|
||||
// Max sure the image is fully opaque
|
||||
for (let i = 0; i < pixels; i++) {
|
||||
rQ[rQi + i * 4 + 3] = 255;
|
||||
}
|
||||
display.blitImage(tx, ty, tw, th, rQ, rQi);
|
||||
rQi += bytes - 1;
|
||||
} else {
|
||||
if (subencoding & 0x02) {
|
||||
// Background
|
||||
this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
|
||||
rQi += 4;
|
||||
}
|
||||
if (subencoding & 0x04) {
|
||||
// Foreground
|
||||
this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
|
||||
rQi += 4;
|
||||
}
|
||||
|
||||
this._startTile(tx, ty, tw, th, this._background);
|
||||
if (subencoding & 0x08) {
|
||||
// AnySubrects
|
||||
let subrects = rQ[rQi];
|
||||
rQi++;
|
||||
|
||||
for (let s = 0; s < subrects; s++) {
|
||||
let color;
|
||||
if (subencoding & 0x10) {
|
||||
// SubrectsColoured
|
||||
color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
|
||||
rQi += 4;
|
||||
} else {
|
||||
color = this._foreground;
|
||||
}
|
||||
const xy = rQ[rQi];
|
||||
rQi++;
|
||||
const sx = xy >> 4;
|
||||
const sy = xy & 0x0f;
|
||||
|
||||
const wh = rQ[rQi];
|
||||
rQi++;
|
||||
const sw = (wh >> 4) + 1;
|
||||
const sh = (wh & 0x0f) + 1;
|
||||
|
||||
this._subTile(sx, sy, sw, sh, color);
|
||||
}
|
||||
}
|
||||
this._finishTile(display);
|
||||
}
|
||||
sock.rQi = rQi;
|
||||
this._lastsubencoding = subencoding;
|
||||
this._tiles--;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// start updating a tile
|
||||
_startTile(x, y, width, height, color) {
|
||||
this._tileX = x;
|
||||
this._tileY = y;
|
||||
this._tileW = width;
|
||||
this._tileH = height;
|
||||
|
||||
const red = color[0];
|
||||
const green = color[1];
|
||||
const blue = color[2];
|
||||
|
||||
const data = this._tileBuffer;
|
||||
for (let i = 0; i < width * height * 4; i += 4) {
|
||||
data[i] = red;
|
||||
data[i + 1] = green;
|
||||
data[i + 2] = blue;
|
||||
data[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
// update sub-rectangle of the current tile
|
||||
_subTile(x, y, w, h, color) {
|
||||
const red = color[0];
|
||||
const green = color[1];
|
||||
const blue = color[2];
|
||||
const xend = x + w;
|
||||
const yend = y + h;
|
||||
|
||||
const data = this._tileBuffer;
|
||||
const width = this._tileW;
|
||||
for (let j = y; j < yend; j++) {
|
||||
for (let i = x; i < xend; i++) {
|
||||
const p = (i + j * width) * 4;
|
||||
data[p] = red;
|
||||
data[p + 1] = green;
|
||||
data[p + 2] = blue;
|
||||
data[p + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw the current tile to the screen
|
||||
_finishTile(display) {
|
||||
display.blitImage(this._tileX, this._tileY, this._tileW, this._tileH, this._tileBuffer, 0);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class JPEGDecoder {
|
||||
constructor() {
|
||||
// RealVNC will reuse the quantization tables
|
||||
// and Huffman tables, so we need to cache them.
|
||||
this._quantTables = [];
|
||||
this._huffmanTables = [];
|
||||
this._cachedQuantTables = [];
|
||||
this._cachedHuffmanTables = [];
|
||||
|
||||
this._jpegLength = 0;
|
||||
this._segments = [];
|
||||
}
|
||||
|
||||
decodeRect(x, y, width, height, sock, display, depth) {
|
||||
// A rect of JPEG encodings is simply a JPEG file
|
||||
if (!this._parseJPEG(sock.rQslice(0))) {
|
||||
return false;
|
||||
}
|
||||
const data = sock.rQshiftBytes(this._jpegLength);
|
||||
if (this._quantTables.length != 0 && this._huffmanTables.length != 0) {
|
||||
// If there are quantization tables and Huffman tables in the JPEG
|
||||
// image, we can directly render it.
|
||||
display.imageRect(x, y, width, height, 'image/jpeg', data);
|
||||
return true;
|
||||
} else {
|
||||
// Otherwise we need to insert cached tables.
|
||||
const sofIndex = this._segments.findIndex((x) => x[1] == 0xc0 || x[1] == 0xc2);
|
||||
if (sofIndex == -1) {
|
||||
throw new Error('Illegal JPEG image without SOF');
|
||||
}
|
||||
let segments = this._segments.slice(0, sofIndex);
|
||||
segments = segments.concat(this._quantTables.length ? this._quantTables : this._cachedQuantTables);
|
||||
segments.push(this._segments[sofIndex]);
|
||||
segments = segments.concat(
|
||||
this._huffmanTables.length ? this._huffmanTables : this._cachedHuffmanTables,
|
||||
this._segments.slice(sofIndex + 1)
|
||||
);
|
||||
let length = 0;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
length += segments[i].length;
|
||||
}
|
||||
const data = new Uint8Array(length);
|
||||
length = 0;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
data.set(segments[i], length);
|
||||
length += segments[i].length;
|
||||
}
|
||||
display.imageRect(x, y, width, height, 'image/jpeg', data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_parseJPEG(buffer) {
|
||||
if (this._quantTables.length != 0) {
|
||||
this._cachedQuantTables = this._quantTables;
|
||||
}
|
||||
if (this._huffmanTables.length != 0) {
|
||||
this._cachedHuffmanTables = this._huffmanTables;
|
||||
}
|
||||
this._quantTables = [];
|
||||
this._huffmanTables = [];
|
||||
this._segments = [];
|
||||
let i = 0;
|
||||
let bufferLength = buffer.length;
|
||||
while (true) {
|
||||
let j = i;
|
||||
if (j + 2 > bufferLength) {
|
||||
return false;
|
||||
}
|
||||
if (buffer[j] != 0xff) {
|
||||
throw new Error('Illegal JPEG marker received (byte: ' + buffer[j] + ')');
|
||||
}
|
||||
const type = buffer[j + 1];
|
||||
j += 2;
|
||||
if (type == 0xd9) {
|
||||
this._jpegLength = j;
|
||||
this._segments.push(buffer.slice(i, j));
|
||||
return true;
|
||||
} else if (type == 0xda) {
|
||||
// start of scan
|
||||
let hasFoundEndOfScan = false;
|
||||
for (let k = j + 3; k + 1 < bufferLength; k++) {
|
||||
if (buffer[k] == 0xff && buffer[k + 1] != 0x00 && !(buffer[k + 1] >= 0xd0 && buffer[k + 1] <= 0xd7)) {
|
||||
j = k;
|
||||
hasFoundEndOfScan = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasFoundEndOfScan) {
|
||||
return false;
|
||||
}
|
||||
this._segments.push(buffer.slice(i, j));
|
||||
i = j;
|
||||
continue;
|
||||
} else if ((type >= 0xd0 && type < 0xd9) || type == 0x01) {
|
||||
// No length after marker
|
||||
this._segments.push(buffer.slice(i, j));
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (j + 2 > bufferLength) {
|
||||
return false;
|
||||
}
|
||||
const length = (buffer[j] << 8) + buffer[j + 1] - 2;
|
||||
if (length < 0) {
|
||||
throw new Error('Illegal JPEG length received (length: ' + length + ')');
|
||||
}
|
||||
j += 2;
|
||||
if (j + length > bufferLength) {
|
||||
return false;
|
||||
}
|
||||
j += length;
|
||||
const segment = buffer.slice(i, j);
|
||||
if (type == 0xc4) {
|
||||
// Huffman tables
|
||||
this._huffmanTables.push(segment);
|
||||
} else if (type == 0xdb) {
|
||||
// Quantization tables
|
||||
this._quantTables.push(segment);
|
||||
}
|
||||
this._segments.push(segment);
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class RawDecoder {
|
||||
constructor() {
|
||||
this._lines = 0;
|
||||
}
|
||||
|
||||
decodeRect(x, y, width, height, sock, display, depth) {
|
||||
if (width === 0 || height === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._lines === 0) {
|
||||
this._lines = height;
|
||||
}
|
||||
|
||||
const pixelSize = depth == 8 ? 1 : 4;
|
||||
const bytesPerLine = width * pixelSize;
|
||||
|
||||
if (sock.rQwait('RAW', bytesPerLine)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const curY = y + (height - this._lines);
|
||||
const currHeight = Math.min(this._lines, Math.floor(sock.rQlen / bytesPerLine));
|
||||
const pixels = width * currHeight;
|
||||
|
||||
let data = sock.rQ;
|
||||
let index = sock.rQi;
|
||||
|
||||
// Convert data if needed
|
||||
if (depth == 8) {
|
||||
const newdata = new Uint8Array(pixels * 4);
|
||||
for (let i = 0; i < pixels; i++) {
|
||||
newdata[i * 4 + 0] = (((data[index + i] >> 0) & 0x3) * 255) / 3;
|
||||
newdata[i * 4 + 1] = (((data[index + i] >> 2) & 0x3) * 255) / 3;
|
||||
newdata[i * 4 + 2] = (((data[index + i] >> 4) & 0x3) * 255) / 3;
|
||||
newdata[i * 4 + 3] = 255;
|
||||
}
|
||||
data = newdata;
|
||||
index = 0;
|
||||
}
|
||||
|
||||
// Max sure the image is fully opaque
|
||||
for (let i = 0; i < pixels; i++) {
|
||||
data[index + i * 4 + 3] = 255;
|
||||
}
|
||||
|
||||
display.blitImage(x, curY, width, currHeight, data, index);
|
||||
sock.rQskipBytes(currHeight * bytesPerLine);
|
||||
this._lines -= currHeight;
|
||||
if (this._lines > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class RREDecoder {
|
||||
constructor() {
|
||||
this._subrects = 0;
|
||||
}
|
||||
|
||||
decodeRect(x, y, width, height, sock, display, depth) {
|
||||
if (this._subrects === 0) {
|
||||
if (sock.rQwait('RRE', 4 + 4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._subrects = sock.rQshift32();
|
||||
|
||||
let color = sock.rQshiftBytes(4); // Background
|
||||
display.fillRect(x, y, width, height, color);
|
||||
}
|
||||
|
||||
while (this._subrects > 0) {
|
||||
if (sock.rQwait('RRE', 4 + 8)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let color = sock.rQshiftBytes(4);
|
||||
let sx = sock.rQshift16();
|
||||
let sy = sock.rQshift16();
|
||||
let swidth = sock.rQshift16();
|
||||
let sheight = sock.rQshift16();
|
||||
display.fillRect(x + sx, y + sy, swidth, sheight, color);
|
||||
|
||||
this._subrects--;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,321 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
*/
|
||||
|
||||
import * as Log from '../util/logging.js';
|
||||
import Inflator from '../inflator.js';
|
||||
|
||||
export default class TightDecoder {
|
||||
constructor() {
|
||||
this._ctl = null;
|
||||
this._filter = null;
|
||||
this._numColors = 0;
|
||||
this._palette = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel)
|
||||
this._len = 0;
|
||||
|
||||
this._zlibs = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this._zlibs[i] = new Inflator();
|
||||
}
|
||||
}
|
||||
|
||||
decodeRect(x, y, width, height, sock, display, depth) {
|
||||
if (this._ctl === null) {
|
||||
if (sock.rQwait('TIGHT compression-control', 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._ctl = sock.rQshift8();
|
||||
|
||||
// Reset streams if the server requests it
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if ((this._ctl >> i) & 1) {
|
||||
this._zlibs[i].reset();
|
||||
Log.Info('Reset zlib stream ' + i);
|
||||
}
|
||||
}
|
||||
|
||||
// Figure out filter
|
||||
this._ctl = this._ctl >> 4;
|
||||
}
|
||||
|
||||
let ret;
|
||||
|
||||
if (this._ctl === 0x08) {
|
||||
ret = this._fillRect(x, y, width, height, sock, display, depth);
|
||||
} else if (this._ctl === 0x09) {
|
||||
ret = this._jpegRect(x, y, width, height, sock, display, depth);
|
||||
} else if (this._ctl === 0x0a) {
|
||||
ret = this._pngRect(x, y, width, height, sock, display, depth);
|
||||
} else if ((this._ctl & 0x08) == 0) {
|
||||
ret = this._basicRect(this._ctl, x, y, width, height, sock, display, depth);
|
||||
} else {
|
||||
throw new Error('Illegal tight compression received (ctl: ' + this._ctl + ')');
|
||||
}
|
||||
|
||||
if (ret) {
|
||||
this._ctl = null;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
_fillRect(x, y, width, height, sock, display, depth) {
|
||||
if (sock.rQwait('TIGHT', 3)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rQi = sock.rQi;
|
||||
const rQ = sock.rQ;
|
||||
|
||||
display.fillRect(x, y, width, height, [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false);
|
||||
sock.rQskipBytes(3);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_jpegRect(x, y, width, height, sock, display, depth) {
|
||||
let data = this._readData(sock);
|
||||
if (data === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
display.imageRect(x, y, width, height, 'image/jpeg', data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_pngRect(x, y, width, height, sock, display, depth) {
|
||||
throw new Error('PNG received in standard Tight rect');
|
||||
}
|
||||
|
||||
_basicRect(ctl, x, y, width, height, sock, display, depth) {
|
||||
if (this._filter === null) {
|
||||
if (ctl & 0x4) {
|
||||
if (sock.rQwait('TIGHT', 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._filter = sock.rQshift8();
|
||||
} else {
|
||||
// Implicit CopyFilter
|
||||
this._filter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let streamId = ctl & 0x3;
|
||||
|
||||
let ret;
|
||||
|
||||
switch (this._filter) {
|
||||
case 0: // CopyFilter
|
||||
ret = this._copyFilter(streamId, x, y, width, height, sock, display, depth);
|
||||
break;
|
||||
case 1: // PaletteFilter
|
||||
ret = this._paletteFilter(streamId, x, y, width, height, sock, display, depth);
|
||||
break;
|
||||
case 2: // GradientFilter
|
||||
ret = this._gradientFilter(streamId, x, y, width, height, sock, display, depth);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Illegal tight filter received (ctl: ' + this._filter + ')');
|
||||
}
|
||||
|
||||
if (ret) {
|
||||
this._filter = null;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
_copyFilter(streamId, x, y, width, height, sock, display, depth) {
|
||||
const uncompressedSize = width * height * 3;
|
||||
let data;
|
||||
|
||||
if (uncompressedSize === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (uncompressedSize < 12) {
|
||||
if (sock.rQwait('TIGHT', uncompressedSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data = sock.rQshiftBytes(uncompressedSize);
|
||||
} else {
|
||||
data = this._readData(sock);
|
||||
if (data === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._zlibs[streamId].setInput(data);
|
||||
data = this._zlibs[streamId].inflate(uncompressedSize);
|
||||
this._zlibs[streamId].setInput(null);
|
||||
}
|
||||
|
||||
let rgbx = new Uint8Array(width * height * 4);
|
||||
for (let i = 0, j = 0; i < width * height * 4; i += 4, j += 3) {
|
||||
rgbx[i] = data[j];
|
||||
rgbx[i + 1] = data[j + 1];
|
||||
rgbx[i + 2] = data[j + 2];
|
||||
rgbx[i + 3] = 255; // Alpha
|
||||
}
|
||||
|
||||
display.blitImage(x, y, width, height, rgbx, 0, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_paletteFilter(streamId, x, y, width, height, sock, display, depth) {
|
||||
if (this._numColors === 0) {
|
||||
if (sock.rQwait('TIGHT palette', 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const numColors = sock.rQpeek8() + 1;
|
||||
const paletteSize = numColors * 3;
|
||||
|
||||
if (sock.rQwait('TIGHT palette', 1 + paletteSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._numColors = numColors;
|
||||
sock.rQskipBytes(1);
|
||||
|
||||
sock.rQshiftTo(this._palette, paletteSize);
|
||||
}
|
||||
|
||||
const bpp = this._numColors <= 2 ? 1 : 8;
|
||||
const rowSize = Math.floor((width * bpp + 7) / 8);
|
||||
const uncompressedSize = rowSize * height;
|
||||
|
||||
let data;
|
||||
|
||||
if (uncompressedSize === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (uncompressedSize < 12) {
|
||||
if (sock.rQwait('TIGHT', uncompressedSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data = sock.rQshiftBytes(uncompressedSize);
|
||||
} else {
|
||||
data = this._readData(sock);
|
||||
if (data === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._zlibs[streamId].setInput(data);
|
||||
data = this._zlibs[streamId].inflate(uncompressedSize);
|
||||
this._zlibs[streamId].setInput(null);
|
||||
}
|
||||
|
||||
// Convert indexed (palette based) image data to RGB
|
||||
if (this._numColors == 2) {
|
||||
this._monoRect(x, y, width, height, data, this._palette, display);
|
||||
} else {
|
||||
this._paletteRect(x, y, width, height, data, this._palette, display);
|
||||
}
|
||||
|
||||
this._numColors = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_monoRect(x, y, width, height, data, palette, display) {
|
||||
// Convert indexed (palette based) image data to RGB
|
||||
// TODO: reduce number of calculations inside loop
|
||||
const dest = this._getScratchBuffer(width * height * 4);
|
||||
const w = Math.floor((width + 7) / 8);
|
||||
const w1 = Math.floor(width / 8);
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
let dp, sp, x;
|
||||
for (x = 0; x < w1; x++) {
|
||||
for (let b = 7; b >= 0; b--) {
|
||||
dp = (y * width + x * 8 + 7 - b) * 4;
|
||||
sp = ((data[y * w + x] >> b) & 1) * 3;
|
||||
dest[dp] = palette[sp];
|
||||
dest[dp + 1] = palette[sp + 1];
|
||||
dest[dp + 2] = palette[sp + 2];
|
||||
dest[dp + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
for (let b = 7; b >= 8 - (width % 8); b--) {
|
||||
dp = (y * width + x * 8 + 7 - b) * 4;
|
||||
sp = ((data[y * w + x] >> b) & 1) * 3;
|
||||
dest[dp] = palette[sp];
|
||||
dest[dp + 1] = palette[sp + 1];
|
||||
dest[dp + 2] = palette[sp + 2];
|
||||
dest[dp + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
display.blitImage(x, y, width, height, dest, 0, false);
|
||||
}
|
||||
|
||||
_paletteRect(x, y, width, height, data, palette, display) {
|
||||
// Convert indexed (palette based) image data to RGB
|
||||
const dest = this._getScratchBuffer(width * height * 4);
|
||||
const total = width * height * 4;
|
||||
for (let i = 0, j = 0; i < total; i += 4, j++) {
|
||||
const sp = data[j] * 3;
|
||||
dest[i] = palette[sp];
|
||||
dest[i + 1] = palette[sp + 1];
|
||||
dest[i + 2] = palette[sp + 2];
|
||||
dest[i + 3] = 255;
|
||||
}
|
||||
|
||||
display.blitImage(x, y, width, height, dest, 0, false);
|
||||
}
|
||||
|
||||
_gradientFilter(streamId, x, y, width, height, sock, display, depth) {
|
||||
throw new Error('Gradient filter not implemented');
|
||||
}
|
||||
|
||||
_readData(sock) {
|
||||
if (this._len === 0) {
|
||||
if (sock.rQwait('TIGHT', 3)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let byte;
|
||||
|
||||
byte = sock.rQshift8();
|
||||
this._len = byte & 0x7f;
|
||||
if (byte & 0x80) {
|
||||
byte = sock.rQshift8();
|
||||
this._len |= (byte & 0x7f) << 7;
|
||||
if (byte & 0x80) {
|
||||
byte = sock.rQshift8();
|
||||
this._len |= byte << 14;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sock.rQwait('TIGHT', this._len)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let data = sock.rQshiftBytes(this._len);
|
||||
this._len = 0;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
_getScratchBuffer(size) {
|
||||
if (!this._scratchBuffer || this._scratchBuffer.length < size) {
|
||||
this._scratchBuffer = new Uint8Array(size);
|
||||
}
|
||||
return this._scratchBuffer;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
*/
|
||||
|
||||
import TightDecoder from './tight.js';
|
||||
|
||||
export default class TightPNGDecoder extends TightDecoder {
|
||||
_pngRect(x, y, width, height, sock, display, depth) {
|
||||
let data = this._readData(sock);
|
||||
if (data === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
display.imageRect(x, y, width, height, 'image/png', data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_basicRect(ctl, x, y, width, height, sock, display, depth) {
|
||||
throw new Error('BasicCompression received in TightPNG rect');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,187 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2021 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
*/
|
||||
|
||||
import Inflate from '../inflator.js';
|
||||
|
||||
const ZRLE_TILE_WIDTH = 64;
|
||||
const ZRLE_TILE_HEIGHT = 64;
|
||||
|
||||
export default class ZRLEDecoder {
|
||||
constructor() {
|
||||
this._length = 0;
|
||||
this._inflator = new Inflate();
|
||||
|
||||
this._pixelBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
|
||||
this._tileBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
|
||||
}
|
||||
|
||||
decodeRect(x, y, width, height, sock, display, depth) {
|
||||
if (this._length === 0) {
|
||||
if (sock.rQwait('ZLib data length', 4)) {
|
||||
return false;
|
||||
}
|
||||
this._length = sock.rQshift32();
|
||||
}
|
||||
if (sock.rQwait('Zlib data', this._length)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = sock.rQshiftBytes(this._length);
|
||||
|
||||
this._inflator.setInput(data);
|
||||
|
||||
for (let ty = y; ty < y + height; ty += ZRLE_TILE_HEIGHT) {
|
||||
let th = Math.min(ZRLE_TILE_HEIGHT, y + height - ty);
|
||||
|
||||
for (let tx = x; tx < x + width; tx += ZRLE_TILE_WIDTH) {
|
||||
let tw = Math.min(ZRLE_TILE_WIDTH, x + width - tx);
|
||||
|
||||
const tileSize = tw * th;
|
||||
const subencoding = this._inflator.inflate(1)[0];
|
||||
if (subencoding === 0) {
|
||||
// raw data
|
||||
const data = this._readPixels(tileSize);
|
||||
display.blitImage(tx, ty, tw, th, data, 0, false);
|
||||
} else if (subencoding === 1) {
|
||||
// solid
|
||||
const background = this._readPixels(1);
|
||||
display.fillRect(tx, ty, tw, th, [background[0], background[1], background[2]]);
|
||||
} else if (subencoding >= 2 && subencoding <= 16) {
|
||||
const data = this._decodePaletteTile(subencoding, tileSize, tw, th);
|
||||
display.blitImage(tx, ty, tw, th, data, 0, false);
|
||||
} else if (subencoding === 128) {
|
||||
const data = this._decodeRLETile(tileSize);
|
||||
display.blitImage(tx, ty, tw, th, data, 0, false);
|
||||
} else if (subencoding >= 130 && subencoding <= 255) {
|
||||
const data = this._decodeRLEPaletteTile(subencoding - 128, tileSize);
|
||||
display.blitImage(tx, ty, tw, th, data, 0, false);
|
||||
} else {
|
||||
throw new Error('Unknown subencoding: ' + subencoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._length = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
_getBitsPerPixelInPalette(paletteSize) {
|
||||
if (paletteSize <= 2) {
|
||||
return 1;
|
||||
} else if (paletteSize <= 4) {
|
||||
return 2;
|
||||
} else if (paletteSize <= 16) {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
_readPixels(pixels) {
|
||||
let data = this._pixelBuffer;
|
||||
const buffer = this._inflator.inflate(3 * pixels);
|
||||
for (let i = 0, j = 0; i < pixels * 4; i += 4, j += 3) {
|
||||
data[i] = buffer[j];
|
||||
data[i + 1] = buffer[j + 1];
|
||||
data[i + 2] = buffer[j + 2];
|
||||
data[i + 3] = 255; // Add the Alpha
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
_decodePaletteTile(paletteSize, tileSize, tilew, tileh) {
|
||||
const data = this._tileBuffer;
|
||||
const palette = this._readPixels(paletteSize);
|
||||
const bitsPerPixel = this._getBitsPerPixelInPalette(paletteSize);
|
||||
const mask = (1 << bitsPerPixel) - 1;
|
||||
|
||||
let offset = 0;
|
||||
let encoded = this._inflator.inflate(1)[0];
|
||||
|
||||
for (let y = 0; y < tileh; y++) {
|
||||
let shift = 8 - bitsPerPixel;
|
||||
for (let x = 0; x < tilew; x++) {
|
||||
if (shift < 0) {
|
||||
shift = 8 - bitsPerPixel;
|
||||
encoded = this._inflator.inflate(1)[0];
|
||||
}
|
||||
let indexInPalette = (encoded >> shift) & mask;
|
||||
|
||||
data[offset] = palette[indexInPalette * 4];
|
||||
data[offset + 1] = palette[indexInPalette * 4 + 1];
|
||||
data[offset + 2] = palette[indexInPalette * 4 + 2];
|
||||
data[offset + 3] = palette[indexInPalette * 4 + 3];
|
||||
offset += 4;
|
||||
shift -= bitsPerPixel;
|
||||
}
|
||||
if (shift < 8 - bitsPerPixel && y < tileh - 1) {
|
||||
encoded = this._inflator.inflate(1)[0];
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
_decodeRLETile(tileSize) {
|
||||
const data = this._tileBuffer;
|
||||
let i = 0;
|
||||
while (i < tileSize) {
|
||||
const pixel = this._readPixels(1);
|
||||
const length = this._readRLELength();
|
||||
for (let j = 0; j < length; j++) {
|
||||
data[i * 4] = pixel[0];
|
||||
data[i * 4 + 1] = pixel[1];
|
||||
data[i * 4 + 2] = pixel[2];
|
||||
data[i * 4 + 3] = pixel[3];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
_decodeRLEPaletteTile(paletteSize, tileSize) {
|
||||
const data = this._tileBuffer;
|
||||
|
||||
// palette
|
||||
const palette = this._readPixels(paletteSize);
|
||||
|
||||
let offset = 0;
|
||||
while (offset < tileSize) {
|
||||
let indexInPalette = this._inflator.inflate(1)[0];
|
||||
let length = 1;
|
||||
if (indexInPalette >= 128) {
|
||||
indexInPalette -= 128;
|
||||
length = this._readRLELength();
|
||||
}
|
||||
if (indexInPalette > paletteSize) {
|
||||
throw new Error('Too big index in palette: ' + indexInPalette + ', palette size: ' + paletteSize);
|
||||
}
|
||||
if (offset + length > tileSize) {
|
||||
throw new Error(
|
||||
'Too big rle length in palette mode: ' + length + ', allowed length is: ' + (tileSize - offset)
|
||||
);
|
||||
}
|
||||
|
||||
for (let j = 0; j < length; j++) {
|
||||
data[offset * 4] = palette[indexInPalette * 4];
|
||||
data[offset * 4 + 1] = palette[indexInPalette * 4 + 1];
|
||||
data[offset * 4 + 2] = palette[indexInPalette * 4 + 2];
|
||||
data[offset * 4 + 3] = palette[indexInPalette * 4 + 3];
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
_readRLELength() {
|
||||
let length = 0;
|
||||
let current = 0;
|
||||
do {
|
||||
current = this._inflator.inflate(1)[0];
|
||||
length += current;
|
||||
} while (current === 255);
|
||||
return length + 1;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2020 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
import { deflateInit, deflate } from '../vendor/pako/lib/zlib/deflate.js';
|
||||
import { Z_FULL_FLUSH } from '../vendor/pako/lib/zlib/deflate.js';
|
||||
import ZStream from '../vendor/pako/lib/zlib/zstream.js';
|
||||
|
||||
export default class Deflator {
|
||||
constructor() {
|
||||
this.strm = new ZStream();
|
||||
this.chunkSize = 1024 * 10 * 10;
|
||||
this.outputBuffer = new Uint8Array(this.chunkSize);
|
||||
this.windowBits = 5;
|
||||
|
||||
deflateInit(this.strm, this.windowBits);
|
||||
}
|
||||
|
||||
deflate(inData) {
|
||||
/* eslint-disable camelcase */
|
||||
this.strm.input = inData;
|
||||
this.strm.avail_in = this.strm.input.length;
|
||||
this.strm.next_in = 0;
|
||||
this.strm.output = this.outputBuffer;
|
||||
this.strm.avail_out = this.chunkSize;
|
||||
this.strm.next_out = 0;
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
let lastRet = deflate(this.strm, Z_FULL_FLUSH);
|
||||
let outData = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
|
||||
|
||||
if (lastRet < 0) {
|
||||
throw new Error('zlib deflate failed');
|
||||
}
|
||||
|
||||
if (this.strm.avail_in > 0) {
|
||||
// Read chunks until done
|
||||
|
||||
let chunks = [outData];
|
||||
let totalLen = outData.length;
|
||||
do {
|
||||
/* eslint-disable camelcase */
|
||||
this.strm.output = new Uint8Array(this.chunkSize);
|
||||
this.strm.next_out = 0;
|
||||
this.strm.avail_out = this.chunkSize;
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
lastRet = deflate(this.strm, Z_FULL_FLUSH);
|
||||
|
||||
if (lastRet < 0) {
|
||||
throw new Error('zlib deflate failed');
|
||||
}
|
||||
|
||||
let chunk = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
|
||||
totalLen += chunk.length;
|
||||
chunks.push(chunk);
|
||||
} while (this.strm.avail_in > 0);
|
||||
|
||||
// Combine chunks into a single data
|
||||
|
||||
let newData = new Uint8Array(totalLen);
|
||||
let offset = 0;
|
||||
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
newData.set(chunks[i], offset);
|
||||
offset += chunks[i].length;
|
||||
}
|
||||
|
||||
outData = newData;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
this.strm.input = null;
|
||||
this.strm.avail_in = 0;
|
||||
this.strm.next_in = 0;
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
return outData;
|
||||
}
|
||||
}
|
||||
810
gns3server/static/web-ui/assets/vnc-console/novnc/core/des.js
Normal file
@ -0,0 +1,810 @@
|
||||
/*
|
||||
* Ported from Flashlight VNC ActionScript implementation:
|
||||
* http://www.wizhelp.com/flashlight-vnc/
|
||||
*
|
||||
* Full attribution follows:
|
||||
*
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
* This DES class has been extracted from package Acme.Crypto for use in VNC.
|
||||
* The unnecessary odd parity code has been removed.
|
||||
*
|
||||
* These changes are:
|
||||
* Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*
|
||||
|
||||
* DesCipher - the DES encryption method
|
||||
*
|
||||
* The meat of this code is by Dave Zimmerman <dzimm@widget.com>, and is:
|
||||
*
|
||||
* Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved.
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software
|
||||
* and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and
|
||||
* without fee is hereby granted, provided that this copyright notice is kept
|
||||
* intact.
|
||||
*
|
||||
* WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY
|
||||
* OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
* TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
* PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE
|
||||
* FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
|
||||
* DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
|
||||
*
|
||||
* THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE
|
||||
* CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE
|
||||
* PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT
|
||||
* NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE
|
||||
* SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE
|
||||
* SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE
|
||||
* PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP
|
||||
* SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR
|
||||
* HIGH RISK ACTIVITIES.
|
||||
*
|
||||
*
|
||||
* The rest is:
|
||||
*
|
||||
* Copyright (C) 1996 by Jef Poskanzer <jef@acme.com>. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*
|
||||
* Visit the ACME Labs Java page for up-to-date versions of this and other
|
||||
* fine Java utilities: http://www.acme.com/java/
|
||||
*/
|
||||
|
||||
/* eslint-disable comma-spacing */
|
||||
|
||||
// Tables, permutations, S-boxes, etc.
|
||||
const PC2 = [
|
||||
13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, 51, 30, 36, 46, 54, 29,
|
||||
39, 50, 44, 32, 47, 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31,
|
||||
],
|
||||
totrot = [1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28];
|
||||
|
||||
const z = 0x0;
|
||||
let a, b, c, d, e, f;
|
||||
a = 1 << 16;
|
||||
b = 1 << 24;
|
||||
c = a | b;
|
||||
d = 1 << 2;
|
||||
e = 1 << 10;
|
||||
f = d | e;
|
||||
const SP1 = [
|
||||
c | e,
|
||||
z | z,
|
||||
a | z,
|
||||
c | f,
|
||||
c | d,
|
||||
a | f,
|
||||
z | d,
|
||||
a | z,
|
||||
z | e,
|
||||
c | e,
|
||||
c | f,
|
||||
z | e,
|
||||
b | f,
|
||||
c | d,
|
||||
b | z,
|
||||
z | d,
|
||||
z | f,
|
||||
b | e,
|
||||
b | e,
|
||||
a | e,
|
||||
a | e,
|
||||
c | z,
|
||||
c | z,
|
||||
b | f,
|
||||
a | d,
|
||||
b | d,
|
||||
b | d,
|
||||
a | d,
|
||||
z | z,
|
||||
z | f,
|
||||
a | f,
|
||||
b | z,
|
||||
a | z,
|
||||
c | f,
|
||||
z | d,
|
||||
c | z,
|
||||
c | e,
|
||||
b | z,
|
||||
b | z,
|
||||
z | e,
|
||||
c | d,
|
||||
a | z,
|
||||
a | e,
|
||||
b | d,
|
||||
z | e,
|
||||
z | d,
|
||||
b | f,
|
||||
a | f,
|
||||
c | f,
|
||||
a | d,
|
||||
c | z,
|
||||
b | f,
|
||||
b | d,
|
||||
z | f,
|
||||
a | f,
|
||||
c | e,
|
||||
z | f,
|
||||
b | e,
|
||||
b | e,
|
||||
z | z,
|
||||
a | d,
|
||||
a | e,
|
||||
z | z,
|
||||
c | d,
|
||||
];
|
||||
a = 1 << 20;
|
||||
b = 1 << 31;
|
||||
c = a | b;
|
||||
d = 1 << 5;
|
||||
e = 1 << 15;
|
||||
f = d | e;
|
||||
const SP2 = [
|
||||
c | f,
|
||||
b | e,
|
||||
z | e,
|
||||
a | f,
|
||||
a | z,
|
||||
z | d,
|
||||
c | d,
|
||||
b | f,
|
||||
b | d,
|
||||
c | f,
|
||||
c | e,
|
||||
b | z,
|
||||
b | e,
|
||||
a | z,
|
||||
z | d,
|
||||
c | d,
|
||||
a | e,
|
||||
a | d,
|
||||
b | f,
|
||||
z | z,
|
||||
b | z,
|
||||
z | e,
|
||||
a | f,
|
||||
c | z,
|
||||
a | d,
|
||||
b | d,
|
||||
z | z,
|
||||
a | e,
|
||||
z | f,
|
||||
c | e,
|
||||
c | z,
|
||||
z | f,
|
||||
z | z,
|
||||
a | f,
|
||||
c | d,
|
||||
a | z,
|
||||
b | f,
|
||||
c | z,
|
||||
c | e,
|
||||
z | e,
|
||||
c | z,
|
||||
b | e,
|
||||
z | d,
|
||||
c | f,
|
||||
a | f,
|
||||
z | d,
|
||||
z | e,
|
||||
b | z,
|
||||
z | f,
|
||||
c | e,
|
||||
a | z,
|
||||
b | d,
|
||||
a | d,
|
||||
b | f,
|
||||
b | d,
|
||||
a | d,
|
||||
a | e,
|
||||
z | z,
|
||||
b | e,
|
||||
z | f,
|
||||
b | z,
|
||||
c | d,
|
||||
c | f,
|
||||
a | e,
|
||||
];
|
||||
a = 1 << 17;
|
||||
b = 1 << 27;
|
||||
c = a | b;
|
||||
d = 1 << 3;
|
||||
e = 1 << 9;
|
||||
f = d | e;
|
||||
const SP3 = [
|
||||
z | f,
|
||||
c | e,
|
||||
z | z,
|
||||
c | d,
|
||||
b | e,
|
||||
z | z,
|
||||
a | f,
|
||||
b | e,
|
||||
a | d,
|
||||
b | d,
|
||||
b | d,
|
||||
a | z,
|
||||
c | f,
|
||||
a | d,
|
||||
c | z,
|
||||
z | f,
|
||||
b | z,
|
||||
z | d,
|
||||
c | e,
|
||||
z | e,
|
||||
a | e,
|
||||
c | z,
|
||||
c | d,
|
||||
a | f,
|
||||
b | f,
|
||||
a | e,
|
||||
a | z,
|
||||
b | f,
|
||||
z | d,
|
||||
c | f,
|
||||
z | e,
|
||||
b | z,
|
||||
c | e,
|
||||
b | z,
|
||||
a | d,
|
||||
z | f,
|
||||
a | z,
|
||||
c | e,
|
||||
b | e,
|
||||
z | z,
|
||||
z | e,
|
||||
a | d,
|
||||
c | f,
|
||||
b | e,
|
||||
b | d,
|
||||
z | e,
|
||||
z | z,
|
||||
c | d,
|
||||
b | f,
|
||||
a | z,
|
||||
b | z,
|
||||
c | f,
|
||||
z | d,
|
||||
a | f,
|
||||
a | e,
|
||||
b | d,
|
||||
c | z,
|
||||
b | f,
|
||||
z | f,
|
||||
c | z,
|
||||
a | f,
|
||||
z | d,
|
||||
c | d,
|
||||
a | e,
|
||||
];
|
||||
a = 1 << 13;
|
||||
b = 1 << 23;
|
||||
c = a | b;
|
||||
d = 1 << 0;
|
||||
e = 1 << 7;
|
||||
f = d | e;
|
||||
const SP4 = [
|
||||
c | d,
|
||||
a | f,
|
||||
a | f,
|
||||
z | e,
|
||||
c | e,
|
||||
b | f,
|
||||
b | d,
|
||||
a | d,
|
||||
z | z,
|
||||
c | z,
|
||||
c | z,
|
||||
c | f,
|
||||
z | f,
|
||||
z | z,
|
||||
b | e,
|
||||
b | d,
|
||||
z | d,
|
||||
a | z,
|
||||
b | z,
|
||||
c | d,
|
||||
z | e,
|
||||
b | z,
|
||||
a | d,
|
||||
a | e,
|
||||
b | f,
|
||||
z | d,
|
||||
a | e,
|
||||
b | e,
|
||||
a | z,
|
||||
c | e,
|
||||
c | f,
|
||||
z | f,
|
||||
b | e,
|
||||
b | d,
|
||||
c | z,
|
||||
c | f,
|
||||
z | f,
|
||||
z | z,
|
||||
z | z,
|
||||
c | z,
|
||||
a | e,
|
||||
b | e,
|
||||
b | f,
|
||||
z | d,
|
||||
c | d,
|
||||
a | f,
|
||||
a | f,
|
||||
z | e,
|
||||
c | f,
|
||||
z | f,
|
||||
z | d,
|
||||
a | z,
|
||||
b | d,
|
||||
a | d,
|
||||
c | e,
|
||||
b | f,
|
||||
a | d,
|
||||
a | e,
|
||||
b | z,
|
||||
c | d,
|
||||
z | e,
|
||||
b | z,
|
||||
a | z,
|
||||
c | e,
|
||||
];
|
||||
a = 1 << 25;
|
||||
b = 1 << 30;
|
||||
c = a | b;
|
||||
d = 1 << 8;
|
||||
e = 1 << 19;
|
||||
f = d | e;
|
||||
const SP5 = [
|
||||
z | d,
|
||||
a | f,
|
||||
a | e,
|
||||
c | d,
|
||||
z | e,
|
||||
z | d,
|
||||
b | z,
|
||||
a | e,
|
||||
b | f,
|
||||
z | e,
|
||||
a | d,
|
||||
b | f,
|
||||
c | d,
|
||||
c | e,
|
||||
z | f,
|
||||
b | z,
|
||||
a | z,
|
||||
b | e,
|
||||
b | e,
|
||||
z | z,
|
||||
b | d,
|
||||
c | f,
|
||||
c | f,
|
||||
a | d,
|
||||
c | e,
|
||||
b | d,
|
||||
z | z,
|
||||
c | z,
|
||||
a | f,
|
||||
a | z,
|
||||
c | z,
|
||||
z | f,
|
||||
z | e,
|
||||
c | d,
|
||||
z | d,
|
||||
a | z,
|
||||
b | z,
|
||||
a | e,
|
||||
c | d,
|
||||
b | f,
|
||||
a | d,
|
||||
b | z,
|
||||
c | e,
|
||||
a | f,
|
||||
b | f,
|
||||
z | d,
|
||||
a | z,
|
||||
c | e,
|
||||
c | f,
|
||||
z | f,
|
||||
c | z,
|
||||
c | f,
|
||||
a | e,
|
||||
z | z,
|
||||
b | e,
|
||||
c | z,
|
||||
z | f,
|
||||
a | d,
|
||||
b | d,
|
||||
z | e,
|
||||
z | z,
|
||||
b | e,
|
||||
a | f,
|
||||
b | d,
|
||||
];
|
||||
a = 1 << 22;
|
||||
b = 1 << 29;
|
||||
c = a | b;
|
||||
d = 1 << 4;
|
||||
e = 1 << 14;
|
||||
f = d | e;
|
||||
const SP6 = [
|
||||
b | d,
|
||||
c | z,
|
||||
z | e,
|
||||
c | f,
|
||||
c | z,
|
||||
z | d,
|
||||
c | f,
|
||||
a | z,
|
||||
b | e,
|
||||
a | f,
|
||||
a | z,
|
||||
b | d,
|
||||
a | d,
|
||||
b | e,
|
||||
b | z,
|
||||
z | f,
|
||||
z | z,
|
||||
a | d,
|
||||
b | f,
|
||||
z | e,
|
||||
a | e,
|
||||
b | f,
|
||||
z | d,
|
||||
c | d,
|
||||
c | d,
|
||||
z | z,
|
||||
a | f,
|
||||
c | e,
|
||||
z | f,
|
||||
a | e,
|
||||
c | e,
|
||||
b | z,
|
||||
b | e,
|
||||
z | d,
|
||||
c | d,
|
||||
a | e,
|
||||
c | f,
|
||||
a | z,
|
||||
z | f,
|
||||
b | d,
|
||||
a | z,
|
||||
b | e,
|
||||
b | z,
|
||||
z | f,
|
||||
b | d,
|
||||
c | f,
|
||||
a | e,
|
||||
c | z,
|
||||
a | f,
|
||||
c | e,
|
||||
z | z,
|
||||
c | d,
|
||||
z | d,
|
||||
z | e,
|
||||
c | z,
|
||||
a | f,
|
||||
z | e,
|
||||
a | d,
|
||||
b | f,
|
||||
z | z,
|
||||
c | e,
|
||||
b | z,
|
||||
a | d,
|
||||
b | f,
|
||||
];
|
||||
a = 1 << 21;
|
||||
b = 1 << 26;
|
||||
c = a | b;
|
||||
d = 1 << 1;
|
||||
e = 1 << 11;
|
||||
f = d | e;
|
||||
const SP7 = [
|
||||
a | z,
|
||||
c | d,
|
||||
b | f,
|
||||
z | z,
|
||||
z | e,
|
||||
b | f,
|
||||
a | f,
|
||||
c | e,
|
||||
c | f,
|
||||
a | z,
|
||||
z | z,
|
||||
b | d,
|
||||
z | d,
|
||||
b | z,
|
||||
c | d,
|
||||
z | f,
|
||||
b | e,
|
||||
a | f,
|
||||
a | d,
|
||||
b | e,
|
||||
b | d,
|
||||
c | z,
|
||||
c | e,
|
||||
a | d,
|
||||
c | z,
|
||||
z | e,
|
||||
z | f,
|
||||
c | f,
|
||||
a | e,
|
||||
z | d,
|
||||
b | z,
|
||||
a | e,
|
||||
b | z,
|
||||
a | e,
|
||||
a | z,
|
||||
b | f,
|
||||
b | f,
|
||||
c | d,
|
||||
c | d,
|
||||
z | d,
|
||||
a | d,
|
||||
b | z,
|
||||
b | e,
|
||||
a | z,
|
||||
c | e,
|
||||
z | f,
|
||||
a | f,
|
||||
c | e,
|
||||
z | f,
|
||||
b | d,
|
||||
c | f,
|
||||
c | z,
|
||||
a | e,
|
||||
z | z,
|
||||
z | d,
|
||||
c | f,
|
||||
z | z,
|
||||
a | f,
|
||||
c | z,
|
||||
z | e,
|
||||
b | d,
|
||||
b | e,
|
||||
z | e,
|
||||
a | d,
|
||||
];
|
||||
a = 1 << 18;
|
||||
b = 1 << 28;
|
||||
c = a | b;
|
||||
d = 1 << 6;
|
||||
e = 1 << 12;
|
||||
f = d | e;
|
||||
const SP8 = [
|
||||
b | f,
|
||||
z | e,
|
||||
a | z,
|
||||
c | f,
|
||||
b | z,
|
||||
b | f,
|
||||
z | d,
|
||||
b | z,
|
||||
a | d,
|
||||
c | z,
|
||||
c | f,
|
||||
a | e,
|
||||
c | e,
|
||||
a | f,
|
||||
z | e,
|
||||
z | d,
|
||||
c | z,
|
||||
b | d,
|
||||
b | e,
|
||||
z | f,
|
||||
a | e,
|
||||
a | d,
|
||||
c | d,
|
||||
c | e,
|
||||
z | f,
|
||||
z | z,
|
||||
z | z,
|
||||
c | d,
|
||||
b | d,
|
||||
b | e,
|
||||
a | f,
|
||||
a | z,
|
||||
a | f,
|
||||
a | z,
|
||||
c | e,
|
||||
z | e,
|
||||
z | d,
|
||||
c | d,
|
||||
z | e,
|
||||
a | f,
|
||||
b | e,
|
||||
z | d,
|
||||
b | d,
|
||||
c | z,
|
||||
c | d,
|
||||
b | z,
|
||||
a | z,
|
||||
b | f,
|
||||
z | z,
|
||||
c | f,
|
||||
a | d,
|
||||
b | d,
|
||||
c | z,
|
||||
b | e,
|
||||
b | f,
|
||||
z | z,
|
||||
c | f,
|
||||
a | e,
|
||||
a | e,
|
||||
z | f,
|
||||
z | f,
|
||||
a | d,
|
||||
b | z,
|
||||
c | e,
|
||||
];
|
||||
|
||||
/* eslint-enable comma-spacing */
|
||||
|
||||
export default class DES {
|
||||
constructor(password) {
|
||||
this.keys = [];
|
||||
|
||||
// Set the key.
|
||||
const pc1m = [],
|
||||
pcr = [],
|
||||
kn = [];
|
||||
|
||||
for (let j = 0, l = 56; j < 56; ++j, l -= 8) {
|
||||
l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1
|
||||
const m = l & 0x7;
|
||||
pc1m[j] = (password[l >>> 3] & (1 << m)) !== 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 16; ++i) {
|
||||
const m = i << 1;
|
||||
const n = m + 1;
|
||||
kn[m] = kn[n] = 0;
|
||||
for (let o = 28; o < 59; o += 28) {
|
||||
for (let j = o - 28; j < o; ++j) {
|
||||
const l = j + totrot[i];
|
||||
pcr[j] = l < o ? pc1m[l] : pc1m[l - 28];
|
||||
}
|
||||
}
|
||||
for (let j = 0; j < 24; ++j) {
|
||||
if (pcr[PC2[j]] !== 0) {
|
||||
kn[m] |= 1 << (23 - j);
|
||||
}
|
||||
if (pcr[PC2[j + 24]] !== 0) {
|
||||
kn[n] |= 1 << (23 - j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cookey
|
||||
for (let i = 0, rawi = 0, KnLi = 0; i < 16; ++i) {
|
||||
const raw0 = kn[rawi++];
|
||||
const raw1 = kn[rawi++];
|
||||
this.keys[KnLi] = (raw0 & 0x00fc0000) << 6;
|
||||
this.keys[KnLi] |= (raw0 & 0x00000fc0) << 10;
|
||||
this.keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10;
|
||||
this.keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6;
|
||||
++KnLi;
|
||||
this.keys[KnLi] = (raw0 & 0x0003f000) << 12;
|
||||
this.keys[KnLi] |= (raw0 & 0x0000003f) << 16;
|
||||
this.keys[KnLi] |= (raw1 & 0x0003f000) >>> 4;
|
||||
this.keys[KnLi] |= raw1 & 0x0000003f;
|
||||
++KnLi;
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt 8 bytes of text
|
||||
enc8(text) {
|
||||
const b = text.slice();
|
||||
let i = 0,
|
||||
l,
|
||||
r,
|
||||
x; // left, right, accumulator
|
||||
|
||||
// Squash 8 bytes to 2 ints
|
||||
l = (b[i++] << 24) | (b[i++] << 16) | (b[i++] << 8) | b[i++];
|
||||
r = (b[i++] << 24) | (b[i++] << 16) | (b[i++] << 8) | b[i++];
|
||||
|
||||
x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
|
||||
r ^= x;
|
||||
l ^= x << 4;
|
||||
x = ((l >>> 16) ^ r) & 0x0000ffff;
|
||||
r ^= x;
|
||||
l ^= x << 16;
|
||||
x = ((r >>> 2) ^ l) & 0x33333333;
|
||||
l ^= x;
|
||||
r ^= x << 2;
|
||||
x = ((r >>> 8) ^ l) & 0x00ff00ff;
|
||||
l ^= x;
|
||||
r ^= x << 8;
|
||||
r = (r << 1) | ((r >>> 31) & 1);
|
||||
x = (l ^ r) & 0xaaaaaaaa;
|
||||
l ^= x;
|
||||
r ^= x;
|
||||
l = (l << 1) | ((l >>> 31) & 1);
|
||||
|
||||
for (let i = 0, keysi = 0; i < 8; ++i) {
|
||||
x = (r << 28) | (r >>> 4);
|
||||
x ^= this.keys[keysi++];
|
||||
let fval = SP7[x & 0x3f];
|
||||
fval |= SP5[(x >>> 8) & 0x3f];
|
||||
fval |= SP3[(x >>> 16) & 0x3f];
|
||||
fval |= SP1[(x >>> 24) & 0x3f];
|
||||
x = r ^ this.keys[keysi++];
|
||||
fval |= SP8[x & 0x3f];
|
||||
fval |= SP6[(x >>> 8) & 0x3f];
|
||||
fval |= SP4[(x >>> 16) & 0x3f];
|
||||
fval |= SP2[(x >>> 24) & 0x3f];
|
||||
l ^= fval;
|
||||
x = (l << 28) | (l >>> 4);
|
||||
x ^= this.keys[keysi++];
|
||||
fval = SP7[x & 0x3f];
|
||||
fval |= SP5[(x >>> 8) & 0x3f];
|
||||
fval |= SP3[(x >>> 16) & 0x3f];
|
||||
fval |= SP1[(x >>> 24) & 0x3f];
|
||||
x = l ^ this.keys[keysi++];
|
||||
fval |= SP8[x & 0x0000003f];
|
||||
fval |= SP6[(x >>> 8) & 0x3f];
|
||||
fval |= SP4[(x >>> 16) & 0x3f];
|
||||
fval |= SP2[(x >>> 24) & 0x3f];
|
||||
r ^= fval;
|
||||
}
|
||||
|
||||
r = (r << 31) | (r >>> 1);
|
||||
x = (l ^ r) & 0xaaaaaaaa;
|
||||
l ^= x;
|
||||
r ^= x;
|
||||
l = (l << 31) | (l >>> 1);
|
||||
x = ((l >>> 8) ^ r) & 0x00ff00ff;
|
||||
r ^= x;
|
||||
l ^= x << 8;
|
||||
x = ((l >>> 2) ^ r) & 0x33333333;
|
||||
r ^= x;
|
||||
l ^= x << 2;
|
||||
x = ((r >>> 16) ^ l) & 0x0000ffff;
|
||||
l ^= x;
|
||||
r ^= x << 16;
|
||||
x = ((r >>> 4) ^ l) & 0x0f0f0f0f;
|
||||
l ^= x;
|
||||
r ^= x << 4;
|
||||
|
||||
// Spread ints to bytes
|
||||
x = [r, l];
|
||||
for (i = 0; i < 8; i++) {
|
||||
b[i] = (x[i >>> 2] >>> (8 * (3 - (i % 4)))) % 256;
|
||||
if (b[i] < 0) {
|
||||
b[i] += 256;
|
||||
} // unsigned
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
// Encrypt 16 bytes of text using passwd as key
|
||||
encrypt(t) {
|
||||
return this.enc8(t.slice(0, 8)).concat(this.enc8(t.slice(8, 16)));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,521 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
import * as Log from './util/logging.js';
|
||||
import Base64 from './base64.js';
|
||||
import { toSigned32bit } from './util/int.js';
|
||||
|
||||
export default class Display {
|
||||
constructor(target) {
|
||||
this._drawCtx = null;
|
||||
|
||||
this._renderQ = []; // queue drawing actions for in-oder rendering
|
||||
this._flushing = false;
|
||||
|
||||
// the full frame buffer (logical canvas) size
|
||||
this._fbWidth = 0;
|
||||
this._fbHeight = 0;
|
||||
|
||||
this._prevDrawStyle = '';
|
||||
|
||||
Log.Debug('>> Display.constructor');
|
||||
|
||||
// The visible canvas
|
||||
this._target = target;
|
||||
|
||||
if (!this._target) {
|
||||
throw new Error('Target must be set');
|
||||
}
|
||||
|
||||
if (typeof this._target === 'string') {
|
||||
throw new Error('target must be a DOM element');
|
||||
}
|
||||
|
||||
if (!this._target.getContext) {
|
||||
throw new Error('no getContext method');
|
||||
}
|
||||
|
||||
this._targetCtx = this._target.getContext('2d');
|
||||
|
||||
// the visible canvas viewport (i.e. what actually gets seen)
|
||||
this._viewportLoc = { x: 0, y: 0, w: this._target.width, h: this._target.height };
|
||||
|
||||
// The hidden canvas, where we do the actual rendering
|
||||
this._backbuffer = document.createElement('canvas');
|
||||
this._drawCtx = this._backbuffer.getContext('2d');
|
||||
|
||||
this._damageBounds = { left: 0, top: 0, right: this._backbuffer.width, bottom: this._backbuffer.height };
|
||||
|
||||
Log.Debug('User Agent: ' + navigator.userAgent);
|
||||
|
||||
Log.Debug('<< Display.constructor');
|
||||
|
||||
// ===== PROPERTIES =====
|
||||
|
||||
this._scale = 1.0;
|
||||
this._clipViewport = false;
|
||||
|
||||
// ===== EVENT HANDLERS =====
|
||||
|
||||
this.onflush = () => {}; // A flush request has finished
|
||||
}
|
||||
|
||||
// ===== PROPERTIES =====
|
||||
|
||||
get scale() {
|
||||
return this._scale;
|
||||
}
|
||||
set scale(scale) {
|
||||
this._rescale(scale);
|
||||
}
|
||||
|
||||
get clipViewport() {
|
||||
return this._clipViewport;
|
||||
}
|
||||
set clipViewport(viewport) {
|
||||
this._clipViewport = viewport;
|
||||
// May need to readjust the viewport dimensions
|
||||
const vp = this._viewportLoc;
|
||||
this.viewportChangeSize(vp.w, vp.h);
|
||||
this.viewportChangePos(0, 0);
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this._fbWidth;
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this._fbHeight;
|
||||
}
|
||||
|
||||
// ===== PUBLIC METHODS =====
|
||||
|
||||
viewportChangePos(deltaX, deltaY) {
|
||||
const vp = this._viewportLoc;
|
||||
deltaX = Math.floor(deltaX);
|
||||
deltaY = Math.floor(deltaY);
|
||||
|
||||
if (!this._clipViewport) {
|
||||
deltaX = -vp.w; // clamped later of out of bounds
|
||||
deltaY = -vp.h;
|
||||
}
|
||||
|
||||
const vx2 = vp.x + vp.w - 1;
|
||||
const vy2 = vp.y + vp.h - 1;
|
||||
|
||||
// Position change
|
||||
|
||||
if (deltaX < 0 && vp.x + deltaX < 0) {
|
||||
deltaX = -vp.x;
|
||||
}
|
||||
if (vx2 + deltaX >= this._fbWidth) {
|
||||
deltaX -= vx2 + deltaX - this._fbWidth + 1;
|
||||
}
|
||||
|
||||
if (vp.y + deltaY < 0) {
|
||||
deltaY = -vp.y;
|
||||
}
|
||||
if (vy2 + deltaY >= this._fbHeight) {
|
||||
deltaY -= vy2 + deltaY - this._fbHeight + 1;
|
||||
}
|
||||
|
||||
if (deltaX === 0 && deltaY === 0) {
|
||||
return;
|
||||
}
|
||||
Log.Debug('viewportChange deltaX: ' + deltaX + ', deltaY: ' + deltaY);
|
||||
|
||||
vp.x += deltaX;
|
||||
vp.y += deltaY;
|
||||
|
||||
this._damage(vp.x, vp.y, vp.w, vp.h);
|
||||
|
||||
this.flip();
|
||||
}
|
||||
|
||||
viewportChangeSize(width, height) {
|
||||
if (!this._clipViewport || typeof width === 'undefined' || typeof height === 'undefined') {
|
||||
Log.Debug('Setting viewport to full display region');
|
||||
width = this._fbWidth;
|
||||
height = this._fbHeight;
|
||||
}
|
||||
|
||||
width = Math.floor(width);
|
||||
height = Math.floor(height);
|
||||
|
||||
if (width > this._fbWidth) {
|
||||
width = this._fbWidth;
|
||||
}
|
||||
if (height > this._fbHeight) {
|
||||
height = this._fbHeight;
|
||||
}
|
||||
|
||||
const vp = this._viewportLoc;
|
||||
if (vp.w !== width || vp.h !== height) {
|
||||
vp.w = width;
|
||||
vp.h = height;
|
||||
|
||||
const canvas = this._target;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
// The position might need to be updated if we've grown
|
||||
this.viewportChangePos(0, 0);
|
||||
|
||||
this._damage(vp.x, vp.y, vp.w, vp.h);
|
||||
this.flip();
|
||||
|
||||
// Update the visible size of the target canvas
|
||||
this._rescale(this._scale);
|
||||
}
|
||||
}
|
||||
|
||||
absX(x) {
|
||||
if (this._scale === 0) {
|
||||
return 0;
|
||||
}
|
||||
return toSigned32bit(x / this._scale + this._viewportLoc.x);
|
||||
}
|
||||
|
||||
absY(y) {
|
||||
if (this._scale === 0) {
|
||||
return 0;
|
||||
}
|
||||
return toSigned32bit(y / this._scale + this._viewportLoc.y);
|
||||
}
|
||||
|
||||
resize(width, height) {
|
||||
this._prevDrawStyle = '';
|
||||
|
||||
this._fbWidth = width;
|
||||
this._fbHeight = height;
|
||||
|
||||
const canvas = this._backbuffer;
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
// We have to save the canvas data since changing the size will clear it
|
||||
let saveImg = null;
|
||||
if (canvas.width > 0 && canvas.height > 0) {
|
||||
saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
if (canvas.width !== width) {
|
||||
canvas.width = width;
|
||||
}
|
||||
if (canvas.height !== height) {
|
||||
canvas.height = height;
|
||||
}
|
||||
|
||||
if (saveImg) {
|
||||
this._drawCtx.putImageData(saveImg, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Readjust the viewport as it may be incorrectly sized
|
||||
// and positioned
|
||||
const vp = this._viewportLoc;
|
||||
this.viewportChangeSize(vp.w, vp.h);
|
||||
this.viewportChangePos(0, 0);
|
||||
}
|
||||
|
||||
getImageData() {
|
||||
return this._drawCtx.getImageData(0, 0, this.width, this.height);
|
||||
}
|
||||
|
||||
toDataURL(type, encoderOptions) {
|
||||
return this._backbuffer.toDataURL(type, encoderOptions);
|
||||
}
|
||||
|
||||
toBlob(callback, type, quality) {
|
||||
return this._backbuffer.toBlob(callback, type, quality);
|
||||
}
|
||||
|
||||
// Track what parts of the visible canvas that need updating
|
||||
_damage(x, y, w, h) {
|
||||
if (x < this._damageBounds.left) {
|
||||
this._damageBounds.left = x;
|
||||
}
|
||||
if (y < this._damageBounds.top) {
|
||||
this._damageBounds.top = y;
|
||||
}
|
||||
if (x + w > this._damageBounds.right) {
|
||||
this._damageBounds.right = x + w;
|
||||
}
|
||||
if (y + h > this._damageBounds.bottom) {
|
||||
this._damageBounds.bottom = y + h;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the visible canvas with the contents of the
|
||||
// rendering canvas
|
||||
flip(fromQueue) {
|
||||
if (this._renderQ.length !== 0 && !fromQueue) {
|
||||
this._renderQPush({
|
||||
type: 'flip',
|
||||
});
|
||||
} else {
|
||||
let x = this._damageBounds.left;
|
||||
let y = this._damageBounds.top;
|
||||
let w = this._damageBounds.right - x;
|
||||
let h = this._damageBounds.bottom - y;
|
||||
|
||||
let vx = x - this._viewportLoc.x;
|
||||
let vy = y - this._viewportLoc.y;
|
||||
|
||||
if (vx < 0) {
|
||||
w += vx;
|
||||
x -= vx;
|
||||
vx = 0;
|
||||
}
|
||||
if (vy < 0) {
|
||||
h += vy;
|
||||
y -= vy;
|
||||
vy = 0;
|
||||
}
|
||||
|
||||
if (vx + w > this._viewportLoc.w) {
|
||||
w = this._viewportLoc.w - vx;
|
||||
}
|
||||
if (vy + h > this._viewportLoc.h) {
|
||||
h = this._viewportLoc.h - vy;
|
||||
}
|
||||
|
||||
if (w > 0 && h > 0) {
|
||||
// FIXME: We may need to disable image smoothing here
|
||||
// as well (see copyImage()), but we haven't
|
||||
// noticed any problem yet.
|
||||
this._targetCtx.drawImage(this._backbuffer, x, y, w, h, vx, vy, w, h);
|
||||
}
|
||||
|
||||
this._damageBounds.left = this._damageBounds.top = 65535;
|
||||
this._damageBounds.right = this._damageBounds.bottom = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pending() {
|
||||
return this._renderQ.length > 0;
|
||||
}
|
||||
|
||||
flush() {
|
||||
if (this._renderQ.length === 0) {
|
||||
this.onflush();
|
||||
} else {
|
||||
this._flushing = true;
|
||||
}
|
||||
}
|
||||
|
||||
fillRect(x, y, width, height, color, fromQueue) {
|
||||
if (this._renderQ.length !== 0 && !fromQueue) {
|
||||
this._renderQPush({
|
||||
type: 'fill',
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
});
|
||||
} else {
|
||||
this._setFillColor(color);
|
||||
this._drawCtx.fillRect(x, y, width, height);
|
||||
this._damage(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {
|
||||
if (this._renderQ.length !== 0 && !fromQueue) {
|
||||
this._renderQPush({
|
||||
type: 'copy',
|
||||
oldX: oldX,
|
||||
oldY: oldY,
|
||||
x: newX,
|
||||
y: newY,
|
||||
width: w,
|
||||
height: h,
|
||||
});
|
||||
} else {
|
||||
// Due to this bug among others [1] we need to disable the image-smoothing to
|
||||
// avoid getting a blur effect when copying data.
|
||||
//
|
||||
// 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
|
||||
//
|
||||
// We need to set these every time since all properties are reset
|
||||
// when the the size is changed
|
||||
this._drawCtx.mozImageSmoothingEnabled = false;
|
||||
this._drawCtx.webkitImageSmoothingEnabled = false;
|
||||
this._drawCtx.msImageSmoothingEnabled = false;
|
||||
this._drawCtx.imageSmoothingEnabled = false;
|
||||
|
||||
this._drawCtx.drawImage(this._backbuffer, oldX, oldY, w, h, newX, newY, w, h);
|
||||
this._damage(newX, newY, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
imageRect(x, y, width, height, mime, arr) {
|
||||
/* The internal logic cannot handle empty images, so bail early */
|
||||
if (width === 0 || height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.src = 'data: ' + mime + ';base64,' + Base64.encode(arr);
|
||||
|
||||
this._renderQPush({
|
||||
type: 'img',
|
||||
img: img,
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
}
|
||||
|
||||
blitImage(x, y, width, height, arr, offset, fromQueue) {
|
||||
if (this._renderQ.length !== 0 && !fromQueue) {
|
||||
// NB(directxman12): it's technically more performant here to use preallocated arrays,
|
||||
// but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
|
||||
// this probably isn't getting called *nearly* as much
|
||||
const newArr = new Uint8Array(width * height * 4);
|
||||
newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
|
||||
this._renderQPush({
|
||||
type: 'blit',
|
||||
data: newArr,
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
} else {
|
||||
// NB(directxman12): arr must be an Type Array view
|
||||
let data = new Uint8ClampedArray(arr.buffer, arr.byteOffset + offset, width * height * 4);
|
||||
let img = new ImageData(data, width, height);
|
||||
this._drawCtx.putImageData(img, x, y);
|
||||
this._damage(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
drawImage(img, x, y) {
|
||||
this._drawCtx.drawImage(img, x, y);
|
||||
this._damage(x, y, img.width, img.height);
|
||||
}
|
||||
|
||||
autoscale(containerWidth, containerHeight) {
|
||||
let scaleRatio;
|
||||
|
||||
if (containerWidth === 0 || containerHeight === 0) {
|
||||
scaleRatio = 0;
|
||||
} else {
|
||||
const vp = this._viewportLoc;
|
||||
const targetAspectRatio = containerWidth / containerHeight;
|
||||
const fbAspectRatio = vp.w / vp.h;
|
||||
|
||||
if (fbAspectRatio >= targetAspectRatio) {
|
||||
scaleRatio = containerWidth / vp.w;
|
||||
} else {
|
||||
scaleRatio = containerHeight / vp.h;
|
||||
}
|
||||
}
|
||||
|
||||
this._rescale(scaleRatio);
|
||||
}
|
||||
|
||||
// ===== PRIVATE METHODS =====
|
||||
|
||||
_rescale(factor) {
|
||||
this._scale = factor;
|
||||
const vp = this._viewportLoc;
|
||||
|
||||
// NB(directxman12): If you set the width directly, or set the
|
||||
// style width to a number, the canvas is cleared.
|
||||
// However, if you set the style width to a string
|
||||
// ('NNNpx'), the canvas is scaled without clearing.
|
||||
const width = factor * vp.w + 'px';
|
||||
const height = factor * vp.h + 'px';
|
||||
|
||||
if (this._target.style.width !== width || this._target.style.height !== height) {
|
||||
this._target.style.width = width;
|
||||
this._target.style.height = height;
|
||||
}
|
||||
}
|
||||
|
||||
_setFillColor(color) {
|
||||
const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
|
||||
if (newStyle !== this._prevDrawStyle) {
|
||||
this._drawCtx.fillStyle = newStyle;
|
||||
this._prevDrawStyle = newStyle;
|
||||
}
|
||||
}
|
||||
|
||||
_renderQPush(action) {
|
||||
this._renderQ.push(action);
|
||||
if (this._renderQ.length === 1) {
|
||||
// If this can be rendered immediately it will be, otherwise
|
||||
// the scanner will wait for the relevant event
|
||||
this._scanRenderQ();
|
||||
}
|
||||
}
|
||||
|
||||
_resumeRenderQ() {
|
||||
// "this" is the object that is ready, not the
|
||||
// display object
|
||||
this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);
|
||||
this._noVNCDisplay._scanRenderQ();
|
||||
}
|
||||
|
||||
_scanRenderQ() {
|
||||
let ready = true;
|
||||
while (ready && this._renderQ.length > 0) {
|
||||
const a = this._renderQ[0];
|
||||
switch (a.type) {
|
||||
case 'flip':
|
||||
this.flip(true);
|
||||
break;
|
||||
case 'copy':
|
||||
this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);
|
||||
break;
|
||||
case 'fill':
|
||||
this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
|
||||
break;
|
||||
case 'blit':
|
||||
this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
|
||||
break;
|
||||
case 'img':
|
||||
if (a.img.complete) {
|
||||
if (a.img.width !== a.width || a.img.height !== a.height) {
|
||||
Log.Error(
|
||||
'Decoded image has incorrect dimensions. Got ' +
|
||||
a.img.width +
|
||||
'x' +
|
||||
a.img.height +
|
||||
'. Expected ' +
|
||||
a.width +
|
||||
'x' +
|
||||
a.height +
|
||||
'.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.drawImage(a.img, a.x, a.y);
|
||||
} else {
|
||||
a.img._noVNCDisplay = this;
|
||||
a.img.addEventListener('load', this._resumeRenderQ);
|
||||
// We need to wait for this image to 'load'
|
||||
// to keep things in-order
|
||||
ready = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (ready) {
|
||||
this._renderQ.shift();
|
||||
}
|
||||
}
|
||||
|
||||
if (this._renderQ.length === 0 && this._flushing) {
|
||||
this._flushing = false;
|
||||
this.onflush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
export const encodings = {
|
||||
encodingRaw: 0,
|
||||
encodingCopyRect: 1,
|
||||
encodingRRE: 2,
|
||||
encodingHextile: 5,
|
||||
encodingTight: 7,
|
||||
encodingZRLE: 16,
|
||||
encodingTightPNG: -260,
|
||||
encodingJPEG: 21,
|
||||
|
||||
pseudoEncodingQualityLevel9: -23,
|
||||
pseudoEncodingQualityLevel0: -32,
|
||||
pseudoEncodingDesktopSize: -223,
|
||||
pseudoEncodingLastRect: -224,
|
||||
pseudoEncodingCursor: -239,
|
||||
pseudoEncodingQEMUExtendedKeyEvent: -258,
|
||||
pseudoEncodingDesktopName: -307,
|
||||
pseudoEncodingExtendedDesktopSize: -308,
|
||||
pseudoEncodingXvp: -309,
|
||||
pseudoEncodingFence: -312,
|
||||
pseudoEncodingContinuousUpdates: -313,
|
||||
pseudoEncodingCompressLevel9: -247,
|
||||
pseudoEncodingCompressLevel0: -256,
|
||||
pseudoEncodingVMwareCursor: 0x574d5664,
|
||||
pseudoEncodingExtendedClipboard: 0xc0a1e5ce,
|
||||
};
|
||||
|
||||
export function encodingName(num) {
|
||||
switch (num) {
|
||||
case encodings.encodingRaw:
|
||||
return 'Raw';
|
||||
case encodings.encodingCopyRect:
|
||||
return 'CopyRect';
|
||||
case encodings.encodingRRE:
|
||||
return 'RRE';
|
||||
case encodings.encodingHextile:
|
||||
return 'Hextile';
|
||||
case encodings.encodingTight:
|
||||
return 'Tight';
|
||||
case encodings.encodingZRLE:
|
||||
return 'ZRLE';
|
||||
case encodings.encodingTightPNG:
|
||||
return 'TightPNG';
|
||||
case encodings.encodingJPEG:
|
||||
return 'JPEG';
|
||||
default:
|
||||
return '[unknown encoding ' + num + ']';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2020 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
import { inflateInit, inflate, inflateReset } from '../vendor/pako/lib/zlib/inflate.js';
|
||||
import ZStream from '../vendor/pako/lib/zlib/zstream.js';
|
||||
|
||||
export default class Inflate {
|
||||
constructor() {
|
||||
this.strm = new ZStream();
|
||||
this.chunkSize = 1024 * 10 * 10;
|
||||
this.strm.output = new Uint8Array(this.chunkSize);
|
||||
this.windowBits = 5;
|
||||
|
||||
inflateInit(this.strm, this.windowBits);
|
||||
}
|
||||
|
||||
setInput(data) {
|
||||
if (!data) {
|
||||
//FIXME: flush remaining data.
|
||||
/* eslint-disable camelcase */
|
||||
this.strm.input = null;
|
||||
this.strm.avail_in = 0;
|
||||
this.strm.next_in = 0;
|
||||
} else {
|
||||
this.strm.input = data;
|
||||
this.strm.avail_in = this.strm.input.length;
|
||||
this.strm.next_in = 0;
|
||||
/* eslint-enable camelcase */
|
||||
}
|
||||
}
|
||||
|
||||
inflate(expected) {
|
||||
// resize our output buffer if it's too small
|
||||
// (we could just use multiple chunks, but that would cause an extra
|
||||
// allocation each time to flatten the chunks)
|
||||
if (expected > this.chunkSize) {
|
||||
this.chunkSize = expected;
|
||||
this.strm.output = new Uint8Array(this.chunkSize);
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
this.strm.next_out = 0;
|
||||
this.strm.avail_out = expected;
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
let ret = inflate(this.strm, 0); // Flush argument not used.
|
||||
if (ret < 0) {
|
||||
throw new Error('zlib inflate failed');
|
||||
}
|
||||
|
||||
if (this.strm.next_out != expected) {
|
||||
throw new Error('Incomplete zlib block');
|
||||
}
|
||||
|
||||
return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
|
||||
}
|
||||
|
||||
reset() {
|
||||
inflateReset(this.strm);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,311 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2018 The noVNC Authors
|
||||
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
import KeyTable from './keysym.js';
|
||||
|
||||
/*
|
||||
* Mapping between HTML key values and VNC/X11 keysyms for "special"
|
||||
* keys that cannot be handled via their Unicode codepoint.
|
||||
*
|
||||
* See https://www.w3.org/TR/uievents-key/ for possible values.
|
||||
*/
|
||||
|
||||
const DOMKeyTable = {};
|
||||
|
||||
function addStandard(key, standard) {
|
||||
if (standard === undefined) throw new Error('Undefined keysym for key "' + key + '"');
|
||||
if (key in DOMKeyTable) throw new Error('Duplicate entry for key "' + key + '"');
|
||||
DOMKeyTable[key] = [standard, standard, standard, standard];
|
||||
}
|
||||
|
||||
function addLeftRight(key, left, right) {
|
||||
if (left === undefined) throw new Error('Undefined keysym for key "' + key + '"');
|
||||
if (right === undefined) throw new Error('Undefined keysym for key "' + key + '"');
|
||||
if (key in DOMKeyTable) throw new Error('Duplicate entry for key "' + key + '"');
|
||||
DOMKeyTable[key] = [left, left, right, left];
|
||||
}
|
||||
|
||||
function addNumpad(key, standard, numpad) {
|
||||
if (standard === undefined) throw new Error('Undefined keysym for key "' + key + '"');
|
||||
if (numpad === undefined) throw new Error('Undefined keysym for key "' + key + '"');
|
||||
if (key in DOMKeyTable) throw new Error('Duplicate entry for key "' + key + '"');
|
||||
DOMKeyTable[key] = [standard, standard, standard, numpad];
|
||||
}
|
||||
|
||||
// 3.2. Modifier Keys
|
||||
|
||||
addLeftRight('Alt', KeyTable.XK_Alt_L, KeyTable.XK_Alt_R);
|
||||
addStandard('AltGraph', KeyTable.XK_ISO_Level3_Shift);
|
||||
addStandard('CapsLock', KeyTable.XK_Caps_Lock);
|
||||
addLeftRight('Control', KeyTable.XK_Control_L, KeyTable.XK_Control_R);
|
||||
// - Fn
|
||||
// - FnLock
|
||||
addLeftRight('Meta', KeyTable.XK_Super_L, KeyTable.XK_Super_R);
|
||||
addStandard('NumLock', KeyTable.XK_Num_Lock);
|
||||
addStandard('ScrollLock', KeyTable.XK_Scroll_Lock);
|
||||
addLeftRight('Shift', KeyTable.XK_Shift_L, KeyTable.XK_Shift_R);
|
||||
// - Symbol
|
||||
// - SymbolLock
|
||||
// - Hyper
|
||||
// - Super
|
||||
|
||||
// 3.3. Whitespace Keys
|
||||
|
||||
addNumpad('Enter', KeyTable.XK_Return, KeyTable.XK_KP_Enter);
|
||||
addStandard('Tab', KeyTable.XK_Tab);
|
||||
addNumpad(' ', KeyTable.XK_space, KeyTable.XK_KP_Space);
|
||||
|
||||
// 3.4. Navigation Keys
|
||||
|
||||
addNumpad('ArrowDown', KeyTable.XK_Down, KeyTable.XK_KP_Down);
|
||||
addNumpad('ArrowLeft', KeyTable.XK_Left, KeyTable.XK_KP_Left);
|
||||
addNumpad('ArrowRight', KeyTable.XK_Right, KeyTable.XK_KP_Right);
|
||||
addNumpad('ArrowUp', KeyTable.XK_Up, KeyTable.XK_KP_Up);
|
||||
addNumpad('End', KeyTable.XK_End, KeyTable.XK_KP_End);
|
||||
addNumpad('Home', KeyTable.XK_Home, KeyTable.XK_KP_Home);
|
||||
addNumpad('PageDown', KeyTable.XK_Next, KeyTable.XK_KP_Next);
|
||||
addNumpad('PageUp', KeyTable.XK_Prior, KeyTable.XK_KP_Prior);
|
||||
|
||||
// 3.5. Editing Keys
|
||||
|
||||
addStandard('Backspace', KeyTable.XK_BackSpace);
|
||||
// Browsers send "Clear" for the numpad 5 without NumLock because
|
||||
// Windows uses VK_Clear for that key. But Unix expects KP_Begin for
|
||||
// that scenario.
|
||||
addNumpad('Clear', KeyTable.XK_Clear, KeyTable.XK_KP_Begin);
|
||||
addStandard('Copy', KeyTable.XF86XK_Copy);
|
||||
// - CrSel
|
||||
addStandard('Cut', KeyTable.XF86XK_Cut);
|
||||
addNumpad('Delete', KeyTable.XK_Delete, KeyTable.XK_KP_Delete);
|
||||
// - EraseEof
|
||||
// - ExSel
|
||||
addNumpad('Insert', KeyTable.XK_Insert, KeyTable.XK_KP_Insert);
|
||||
addStandard('Paste', KeyTable.XF86XK_Paste);
|
||||
addStandard('Redo', KeyTable.XK_Redo);
|
||||
addStandard('Undo', KeyTable.XK_Undo);
|
||||
|
||||
// 3.6. UI Keys
|
||||
|
||||
// - Accept
|
||||
// - Again (could just be XK_Redo)
|
||||
// - Attn
|
||||
addStandard('Cancel', KeyTable.XK_Cancel);
|
||||
addStandard('ContextMenu', KeyTable.XK_Menu);
|
||||
addStandard('Escape', KeyTable.XK_Escape);
|
||||
addStandard('Execute', KeyTable.XK_Execute);
|
||||
addStandard('Find', KeyTable.XK_Find);
|
||||
addStandard('Help', KeyTable.XK_Help);
|
||||
addStandard('Pause', KeyTable.XK_Pause);
|
||||
// - Play
|
||||
// - Props
|
||||
addStandard('Select', KeyTable.XK_Select);
|
||||
addStandard('ZoomIn', KeyTable.XF86XK_ZoomIn);
|
||||
addStandard('ZoomOut', KeyTable.XF86XK_ZoomOut);
|
||||
|
||||
// 3.7. Device Keys
|
||||
|
||||
addStandard('BrightnessDown', KeyTable.XF86XK_MonBrightnessDown);
|
||||
addStandard('BrightnessUp', KeyTable.XF86XK_MonBrightnessUp);
|
||||
addStandard('Eject', KeyTable.XF86XK_Eject);
|
||||
addStandard('LogOff', KeyTable.XF86XK_LogOff);
|
||||
addStandard('Power', KeyTable.XF86XK_PowerOff);
|
||||
addStandard('PowerOff', KeyTable.XF86XK_PowerDown);
|
||||
addStandard('PrintScreen', KeyTable.XK_Print);
|
||||
addStandard('Hibernate', KeyTable.XF86XK_Hibernate);
|
||||
addStandard('Standby', KeyTable.XF86XK_Standby);
|
||||
addStandard('WakeUp', KeyTable.XF86XK_WakeUp);
|
||||
|
||||
// 3.8. IME and Composition Keys
|
||||
|
||||
addStandard('AllCandidates', KeyTable.XK_MultipleCandidate);
|
||||
addStandard('Alphanumeric', KeyTable.XK_Eisu_toggle);
|
||||
addStandard('CodeInput', KeyTable.XK_Codeinput);
|
||||
addStandard('Compose', KeyTable.XK_Multi_key);
|
||||
addStandard('Convert', KeyTable.XK_Henkan);
|
||||
// - Dead
|
||||
// - FinalMode
|
||||
addStandard('GroupFirst', KeyTable.XK_ISO_First_Group);
|
||||
addStandard('GroupLast', KeyTable.XK_ISO_Last_Group);
|
||||
addStandard('GroupNext', KeyTable.XK_ISO_Next_Group);
|
||||
addStandard('GroupPrevious', KeyTable.XK_ISO_Prev_Group);
|
||||
// - ModeChange (XK_Mode_switch is often used for AltGr)
|
||||
// - NextCandidate
|
||||
addStandard('NonConvert', KeyTable.XK_Muhenkan);
|
||||
addStandard('PreviousCandidate', KeyTable.XK_PreviousCandidate);
|
||||
// - Process
|
||||
addStandard('SingleCandidate', KeyTable.XK_SingleCandidate);
|
||||
addStandard('HangulMode', KeyTable.XK_Hangul);
|
||||
addStandard('HanjaMode', KeyTable.XK_Hangul_Hanja);
|
||||
addStandard('JunjaMode', KeyTable.XK_Hangul_Jeonja);
|
||||
addStandard('Eisu', KeyTable.XK_Eisu_toggle);
|
||||
addStandard('Hankaku', KeyTable.XK_Hankaku);
|
||||
addStandard('Hiragana', KeyTable.XK_Hiragana);
|
||||
addStandard('HiraganaKatakana', KeyTable.XK_Hiragana_Katakana);
|
||||
addStandard('KanaMode', KeyTable.XK_Kana_Shift); // could also be _Kana_Lock
|
||||
addStandard('KanjiMode', KeyTable.XK_Kanji);
|
||||
addStandard('Katakana', KeyTable.XK_Katakana);
|
||||
addStandard('Romaji', KeyTable.XK_Romaji);
|
||||
addStandard('Zenkaku', KeyTable.XK_Zenkaku);
|
||||
addStandard('ZenkakuHankaku', KeyTable.XK_Zenkaku_Hankaku);
|
||||
|
||||
// 3.9. General-Purpose Function Keys
|
||||
|
||||
addStandard('F1', KeyTable.XK_F1);
|
||||
addStandard('F2', KeyTable.XK_F2);
|
||||
addStandard('F3', KeyTable.XK_F3);
|
||||
addStandard('F4', KeyTable.XK_F4);
|
||||
addStandard('F5', KeyTable.XK_F5);
|
||||
addStandard('F6', KeyTable.XK_F6);
|
||||
addStandard('F7', KeyTable.XK_F7);
|
||||
addStandard('F8', KeyTable.XK_F8);
|
||||
addStandard('F9', KeyTable.XK_F9);
|
||||
addStandard('F10', KeyTable.XK_F10);
|
||||
addStandard('F11', KeyTable.XK_F11);
|
||||
addStandard('F12', KeyTable.XK_F12);
|
||||
addStandard('F13', KeyTable.XK_F13);
|
||||
addStandard('F14', KeyTable.XK_F14);
|
||||
addStandard('F15', KeyTable.XK_F15);
|
||||
addStandard('F16', KeyTable.XK_F16);
|
||||
addStandard('F17', KeyTable.XK_F17);
|
||||
addStandard('F18', KeyTable.XK_F18);
|
||||
addStandard('F19', KeyTable.XK_F19);
|
||||
addStandard('F20', KeyTable.XK_F20);
|
||||
addStandard('F21', KeyTable.XK_F21);
|
||||
addStandard('F22', KeyTable.XK_F22);
|
||||
addStandard('F23', KeyTable.XK_F23);
|
||||
addStandard('F24', KeyTable.XK_F24);
|
||||
addStandard('F25', KeyTable.XK_F25);
|
||||
addStandard('F26', KeyTable.XK_F26);
|
||||
addStandard('F27', KeyTable.XK_F27);
|
||||
addStandard('F28', KeyTable.XK_F28);
|
||||
addStandard('F29', KeyTable.XK_F29);
|
||||
addStandard('F30', KeyTable.XK_F30);
|
||||
addStandard('F31', KeyTable.XK_F31);
|
||||
addStandard('F32', KeyTable.XK_F32);
|
||||
addStandard('F33', KeyTable.XK_F33);
|
||||
addStandard('F34', KeyTable.XK_F34);
|
||||
addStandard('F35', KeyTable.XK_F35);
|
||||
// - Soft1...
|
||||
|
||||
// 3.10. Multimedia Keys
|
||||
|
||||
// - ChannelDown
|
||||
// - ChannelUp
|
||||
addStandard('Close', KeyTable.XF86XK_Close);
|
||||
addStandard('MailForward', KeyTable.XF86XK_MailForward);
|
||||
addStandard('MailReply', KeyTable.XF86XK_Reply);
|
||||
addStandard('MailSend', KeyTable.XF86XK_Send);
|
||||
// - MediaClose
|
||||
addStandard('MediaFastForward', KeyTable.XF86XK_AudioForward);
|
||||
addStandard('MediaPause', KeyTable.XF86XK_AudioPause);
|
||||
addStandard('MediaPlay', KeyTable.XF86XK_AudioPlay);
|
||||
// - MediaPlayPause
|
||||
addStandard('MediaRecord', KeyTable.XF86XK_AudioRecord);
|
||||
addStandard('MediaRewind', KeyTable.XF86XK_AudioRewind);
|
||||
addStandard('MediaStop', KeyTable.XF86XK_AudioStop);
|
||||
addStandard('MediaTrackNext', KeyTable.XF86XK_AudioNext);
|
||||
addStandard('MediaTrackPrevious', KeyTable.XF86XK_AudioPrev);
|
||||
addStandard('New', KeyTable.XF86XK_New);
|
||||
addStandard('Open', KeyTable.XF86XK_Open);
|
||||
addStandard('Print', KeyTable.XK_Print);
|
||||
addStandard('Save', KeyTable.XF86XK_Save);
|
||||
addStandard('SpellCheck', KeyTable.XF86XK_Spell);
|
||||
|
||||
// 3.11. Multimedia Numpad Keys
|
||||
|
||||
// - Key11
|
||||
// - Key12
|
||||
|
||||
// 3.12. Audio Keys
|
||||
|
||||
// - AudioBalanceLeft
|
||||
// - AudioBalanceRight
|
||||
// - AudioBassBoostDown
|
||||
// - AudioBassBoostToggle
|
||||
// - AudioBassBoostUp
|
||||
// - AudioFaderFront
|
||||
// - AudioFaderRear
|
||||
// - AudioSurroundModeNext
|
||||
// - AudioTrebleDown
|
||||
// - AudioTrebleUp
|
||||
addStandard('AudioVolumeDown', KeyTable.XF86XK_AudioLowerVolume);
|
||||
addStandard('AudioVolumeUp', KeyTable.XF86XK_AudioRaiseVolume);
|
||||
addStandard('AudioVolumeMute', KeyTable.XF86XK_AudioMute);
|
||||
// - MicrophoneToggle
|
||||
// - MicrophoneVolumeDown
|
||||
// - MicrophoneVolumeUp
|
||||
addStandard('MicrophoneVolumeMute', KeyTable.XF86XK_AudioMicMute);
|
||||
|
||||
// 3.13. Speech Keys
|
||||
|
||||
// - SpeechCorrectionList
|
||||
// - SpeechInputToggle
|
||||
|
||||
// 3.14. Application Keys
|
||||
|
||||
addStandard('LaunchApplication1', KeyTable.XF86XK_MyComputer);
|
||||
addStandard('LaunchApplication2', KeyTable.XF86XK_Calculator);
|
||||
addStandard('LaunchCalendar', KeyTable.XF86XK_Calendar);
|
||||
// - LaunchContacts
|
||||
addStandard('LaunchMail', KeyTable.XF86XK_Mail);
|
||||
addStandard('LaunchMediaPlayer', KeyTable.XF86XK_AudioMedia);
|
||||
addStandard('LaunchMusicPlayer', KeyTable.XF86XK_Music);
|
||||
addStandard('LaunchPhone', KeyTable.XF86XK_Phone);
|
||||
addStandard('LaunchScreenSaver', KeyTable.XF86XK_ScreenSaver);
|
||||
addStandard('LaunchSpreadsheet', KeyTable.XF86XK_Excel);
|
||||
addStandard('LaunchWebBrowser', KeyTable.XF86XK_WWW);
|
||||
addStandard('LaunchWebCam', KeyTable.XF86XK_WebCam);
|
||||
addStandard('LaunchWordProcessor', KeyTable.XF86XK_Word);
|
||||
|
||||
// 3.15. Browser Keys
|
||||
|
||||
addStandard('BrowserBack', KeyTable.XF86XK_Back);
|
||||
addStandard('BrowserFavorites', KeyTable.XF86XK_Favorites);
|
||||
addStandard('BrowserForward', KeyTable.XF86XK_Forward);
|
||||
addStandard('BrowserHome', KeyTable.XF86XK_HomePage);
|
||||
addStandard('BrowserRefresh', KeyTable.XF86XK_Refresh);
|
||||
addStandard('BrowserSearch', KeyTable.XF86XK_Search);
|
||||
addStandard('BrowserStop', KeyTable.XF86XK_Stop);
|
||||
|
||||
// 3.16. Mobile Phone Keys
|
||||
|
||||
// - A whole bunch...
|
||||
|
||||
// 3.17. TV Keys
|
||||
|
||||
// - A whole bunch...
|
||||
|
||||
// 3.18. Media Controller Keys
|
||||
|
||||
// - A whole bunch...
|
||||
addStandard('Dimmer', KeyTable.XF86XK_BrightnessAdjust);
|
||||
addStandard('MediaAudioTrack', KeyTable.XF86XK_AudioCycleTrack);
|
||||
addStandard('RandomToggle', KeyTable.XF86XK_AudioRandomPlay);
|
||||
addStandard('SplitScreenToggle', KeyTable.XF86XK_SplitScreen);
|
||||
addStandard('Subtitle', KeyTable.XF86XK_Subtitle);
|
||||
addStandard('VideoModeNext', KeyTable.XF86XK_Next_VMode);
|
||||
|
||||
// Extra: Numpad
|
||||
|
||||
addNumpad('=', KeyTable.XK_equal, KeyTable.XK_KP_Equal);
|
||||
addNumpad('+', KeyTable.XK_plus, KeyTable.XK_KP_Add);
|
||||
addNumpad('-', KeyTable.XK_minus, KeyTable.XK_KP_Subtract);
|
||||
addNumpad('*', KeyTable.XK_asterisk, KeyTable.XK_KP_Multiply);
|
||||
addNumpad('/', KeyTable.XK_slash, KeyTable.XK_KP_Divide);
|
||||
addNumpad('.', KeyTable.XK_period, KeyTable.XK_KP_Decimal);
|
||||
addNumpad(',', KeyTable.XK_comma, KeyTable.XK_KP_Separator);
|
||||
addNumpad('0', KeyTable.XK_0, KeyTable.XK_KP_0);
|
||||
addNumpad('1', KeyTable.XK_1, KeyTable.XK_KP_1);
|
||||
addNumpad('2', KeyTable.XK_2, KeyTable.XK_KP_2);
|
||||
addNumpad('3', KeyTable.XK_3, KeyTable.XK_KP_3);
|
||||
addNumpad('4', KeyTable.XK_4, KeyTable.XK_KP_4);
|
||||
addNumpad('5', KeyTable.XK_5, KeyTable.XK_KP_5);
|
||||
addNumpad('6', KeyTable.XK_6, KeyTable.XK_KP_6);
|
||||
addNumpad('7', KeyTable.XK_7, KeyTable.XK_KP_7);
|
||||
addNumpad('8', KeyTable.XK_8, KeyTable.XK_KP_8);
|
||||
addNumpad('9', KeyTable.XK_9, KeyTable.XK_KP_9);
|
||||
|
||||
export default DOMKeyTable;
|
||||
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2018 The noVNC Authors
|
||||
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
/*
|
||||
* Fallback mapping between HTML key codes (physical keys) and
|
||||
* HTML key values. This only works for keys that don't vary
|
||||
* between layouts. We also omit those who manage fine by mapping the
|
||||
* Unicode representation.
|
||||
*
|
||||
* See https://www.w3.org/TR/uievents-code/ for possible codes.
|
||||
* See https://www.w3.org/TR/uievents-key/ for possible values.
|
||||
*/
|
||||
|
||||
/* eslint-disable key-spacing */
|
||||
|
||||
export default {
|
||||
// 3.1.1.1. Writing System Keys
|
||||
|
||||
Backspace: 'Backspace',
|
||||
|
||||
// 3.1.1.2. Functional Keys
|
||||
|
||||
AltLeft: 'Alt',
|
||||
AltRight: 'Alt', // This could also be 'AltGraph'
|
||||
CapsLock: 'CapsLock',
|
||||
ContextMenu: 'ContextMenu',
|
||||
ControlLeft: 'Control',
|
||||
ControlRight: 'Control',
|
||||
Enter: 'Enter',
|
||||
MetaLeft: 'Meta',
|
||||
MetaRight: 'Meta',
|
||||
ShiftLeft: 'Shift',
|
||||
ShiftRight: 'Shift',
|
||||
Tab: 'Tab',
|
||||
// FIXME: Japanese/Korean keys
|
||||
|
||||
// 3.1.2. Control Pad Section
|
||||
|
||||
Delete: 'Delete',
|
||||
End: 'End',
|
||||
Help: 'Help',
|
||||
Home: 'Home',
|
||||
Insert: 'Insert',
|
||||
PageDown: 'PageDown',
|
||||
PageUp: 'PageUp',
|
||||
|
||||
// 3.1.3. Arrow Pad Section
|
||||
|
||||
ArrowDown: 'ArrowDown',
|
||||
ArrowLeft: 'ArrowLeft',
|
||||
ArrowRight: 'ArrowRight',
|
||||
ArrowUp: 'ArrowUp',
|
||||
|
||||
// 3.1.4. Numpad Section
|
||||
|
||||
NumLock: 'NumLock',
|
||||
NumpadBackspace: 'Backspace',
|
||||
NumpadClear: 'Clear',
|
||||
|
||||
// 3.1.5. Function Section
|
||||
|
||||
Escape: 'Escape',
|
||||
F1: 'F1',
|
||||
F2: 'F2',
|
||||
F3: 'F3',
|
||||
F4: 'F4',
|
||||
F5: 'F5',
|
||||
F6: 'F6',
|
||||
F7: 'F7',
|
||||
F8: 'F8',
|
||||
F9: 'F9',
|
||||
F10: 'F10',
|
||||
F11: 'F11',
|
||||
F12: 'F12',
|
||||
F13: 'F13',
|
||||
F14: 'F14',
|
||||
F15: 'F15',
|
||||
F16: 'F16',
|
||||
F17: 'F17',
|
||||
F18: 'F18',
|
||||
F19: 'F19',
|
||||
F20: 'F20',
|
||||
F21: 'F21',
|
||||
F22: 'F22',
|
||||
F23: 'F23',
|
||||
F24: 'F24',
|
||||
F25: 'F25',
|
||||
F26: 'F26',
|
||||
F27: 'F27',
|
||||
F28: 'F28',
|
||||
F29: 'F29',
|
||||
F30: 'F30',
|
||||
F31: 'F31',
|
||||
F32: 'F32',
|
||||
F33: 'F33',
|
||||
F34: 'F34',
|
||||
F35: 'F35',
|
||||
PrintScreen: 'PrintScreen',
|
||||
ScrollLock: 'ScrollLock',
|
||||
Pause: 'Pause',
|
||||
|
||||
// 3.1.6. Media Keys
|
||||
|
||||
BrowserBack: 'BrowserBack',
|
||||
BrowserFavorites: 'BrowserFavorites',
|
||||
BrowserForward: 'BrowserForward',
|
||||
BrowserHome: 'BrowserHome',
|
||||
BrowserRefresh: 'BrowserRefresh',
|
||||
BrowserSearch: 'BrowserSearch',
|
||||
BrowserStop: 'BrowserStop',
|
||||
Eject: 'Eject',
|
||||
LaunchApp1: 'LaunchMyComputer',
|
||||
LaunchApp2: 'LaunchCalendar',
|
||||
LaunchMail: 'LaunchMail',
|
||||
MediaPlayPause: 'MediaPlay',
|
||||
MediaStop: 'MediaStop',
|
||||
MediaTrackNext: 'MediaTrackNext',
|
||||
MediaTrackPrevious: 'MediaTrackPrevious',
|
||||
Power: 'Power',
|
||||
Sleep: 'Sleep',
|
||||
AudioVolumeDown: 'AudioVolumeDown',
|
||||
AudioVolumeMute: 'AudioVolumeMute',
|
||||
AudioVolumeUp: 'AudioVolumeUp',
|
||||
WakeUp: 'WakeUp',
|
||||
};
|
||||
@ -0,0 +1,546 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2020 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
*/
|
||||
|
||||
const GH_NOGESTURE = 0;
|
||||
const GH_ONETAP = 1;
|
||||
const GH_TWOTAP = 2;
|
||||
const GH_THREETAP = 4;
|
||||
const GH_DRAG = 8;
|
||||
const GH_LONGPRESS = 16;
|
||||
const GH_TWODRAG = 32;
|
||||
const GH_PINCH = 64;
|
||||
|
||||
const GH_INITSTATE = 127;
|
||||
|
||||
const GH_MOVE_THRESHOLD = 50;
|
||||
const GH_ANGLE_THRESHOLD = 90; // Degrees
|
||||
|
||||
// Timeout when waiting for gestures (ms)
|
||||
const GH_MULTITOUCH_TIMEOUT = 250;
|
||||
|
||||
// Maximum time between press and release for a tap (ms)
|
||||
const GH_TAP_TIMEOUT = 1000;
|
||||
|
||||
// Timeout when waiting for longpress (ms)
|
||||
const GH_LONGPRESS_TIMEOUT = 1000;
|
||||
|
||||
// Timeout when waiting to decide between PINCH and TWODRAG (ms)
|
||||
const GH_TWOTOUCH_TIMEOUT = 50;
|
||||
|
||||
export default class GestureHandler {
|
||||
constructor() {
|
||||
this._target = null;
|
||||
|
||||
this._state = GH_INITSTATE;
|
||||
|
||||
this._tracked = [];
|
||||
this._ignored = [];
|
||||
|
||||
this._waitingRelease = false;
|
||||
this._releaseStart = 0.0;
|
||||
|
||||
this._longpressTimeoutId = null;
|
||||
this._twoTouchTimeoutId = null;
|
||||
|
||||
this._boundEventHandler = this._eventHandler.bind(this);
|
||||
}
|
||||
|
||||
attach(target) {
|
||||
this.detach();
|
||||
|
||||
this._target = target;
|
||||
this._target.addEventListener('touchstart', this._boundEventHandler);
|
||||
this._target.addEventListener('touchmove', this._boundEventHandler);
|
||||
this._target.addEventListener('touchend', this._boundEventHandler);
|
||||
this._target.addEventListener('touchcancel', this._boundEventHandler);
|
||||
}
|
||||
|
||||
detach() {
|
||||
if (!this._target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._stopLongpressTimeout();
|
||||
this._stopTwoTouchTimeout();
|
||||
|
||||
this._target.removeEventListener('touchstart', this._boundEventHandler);
|
||||
this._target.removeEventListener('touchmove', this._boundEventHandler);
|
||||
this._target.removeEventListener('touchend', this._boundEventHandler);
|
||||
this._target.removeEventListener('touchcancel', this._boundEventHandler);
|
||||
this._target = null;
|
||||
}
|
||||
|
||||
_eventHandler(e) {
|
||||
let fn;
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
switch (e.type) {
|
||||
case 'touchstart':
|
||||
fn = this._touchStart;
|
||||
break;
|
||||
case 'touchmove':
|
||||
fn = this._touchMove;
|
||||
break;
|
||||
case 'touchend':
|
||||
case 'touchcancel':
|
||||
fn = this._touchEnd;
|
||||
break;
|
||||
}
|
||||
|
||||
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||
let touch = e.changedTouches[i];
|
||||
fn.call(this, touch.identifier, touch.clientX, touch.clientY);
|
||||
}
|
||||
}
|
||||
|
||||
_touchStart(id, x, y) {
|
||||
// Ignore any new touches if there is already an active gesture,
|
||||
// or we're in a cleanup state
|
||||
if (this._hasDetectedGesture() || this._state === GH_NOGESTURE) {
|
||||
this._ignored.push(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Did it take too long between touches that we should no longer
|
||||
// consider this a single gesture?
|
||||
if (this._tracked.length > 0 && Date.now() - this._tracked[0].started > GH_MULTITOUCH_TIMEOUT) {
|
||||
this._state = GH_NOGESTURE;
|
||||
this._ignored.push(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're waiting for fingers to release then we should no longer
|
||||
// recognize new touches
|
||||
if (this._waitingRelease) {
|
||||
this._state = GH_NOGESTURE;
|
||||
this._ignored.push(id);
|
||||
return;
|
||||
}
|
||||
|
||||
this._tracked.push({
|
||||
id: id,
|
||||
started: Date.now(),
|
||||
active: true,
|
||||
firstX: x,
|
||||
firstY: y,
|
||||
lastX: x,
|
||||
lastY: y,
|
||||
angle: 0,
|
||||
});
|
||||
|
||||
switch (this._tracked.length) {
|
||||
case 1:
|
||||
this._startLongpressTimeout();
|
||||
break;
|
||||
|
||||
case 2:
|
||||
this._state &= ~(GH_ONETAP | GH_DRAG | GH_LONGPRESS);
|
||||
this._stopLongpressTimeout();
|
||||
break;
|
||||
|
||||
case 3:
|
||||
this._state &= ~(GH_TWOTAP | GH_TWODRAG | GH_PINCH);
|
||||
break;
|
||||
|
||||
default:
|
||||
this._state = GH_NOGESTURE;
|
||||
}
|
||||
}
|
||||
|
||||
_touchMove(id, x, y) {
|
||||
let touch = this._tracked.find((t) => t.id === id);
|
||||
|
||||
// If this is an update for a touch we're not tracking, ignore it
|
||||
if (touch === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the touches last position with the event coordinates
|
||||
touch.lastX = x;
|
||||
touch.lastY = y;
|
||||
|
||||
let deltaX = x - touch.firstX;
|
||||
let deltaY = y - touch.firstY;
|
||||
|
||||
// Update angle when the touch has moved
|
||||
if (touch.firstX !== touch.lastX || touch.firstY !== touch.lastY) {
|
||||
touch.angle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
|
||||
}
|
||||
|
||||
if (!this._hasDetectedGesture()) {
|
||||
// Ignore moves smaller than the minimum threshold
|
||||
if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't be a tap or long press as we've seen movement
|
||||
this._state &= ~(GH_ONETAP | GH_TWOTAP | GH_THREETAP | GH_LONGPRESS);
|
||||
this._stopLongpressTimeout();
|
||||
|
||||
if (this._tracked.length !== 1) {
|
||||
this._state &= ~GH_DRAG;
|
||||
}
|
||||
if (this._tracked.length !== 2) {
|
||||
this._state &= ~(GH_TWODRAG | GH_PINCH);
|
||||
}
|
||||
|
||||
// We need to figure out which of our different two touch gestures
|
||||
// this might be
|
||||
if (this._tracked.length === 2) {
|
||||
// The other touch is the one where the id doesn't match
|
||||
let prevTouch = this._tracked.find((t) => t.id !== id);
|
||||
|
||||
// How far the previous touch point has moved since start
|
||||
let prevDeltaMove = Math.hypot(prevTouch.firstX - prevTouch.lastX, prevTouch.firstY - prevTouch.lastY);
|
||||
|
||||
// We know that the current touch moved far enough,
|
||||
// but unless both touches moved further than their
|
||||
// threshold we don't want to disqualify any gestures
|
||||
if (prevDeltaMove > GH_MOVE_THRESHOLD) {
|
||||
// The angle difference between the direction of the touch points
|
||||
let deltaAngle = Math.abs(touch.angle - prevTouch.angle);
|
||||
deltaAngle = Math.abs(((deltaAngle + 180) % 360) - 180);
|
||||
|
||||
// PINCH or TWODRAG can be eliminated depending on the angle
|
||||
if (deltaAngle > GH_ANGLE_THRESHOLD) {
|
||||
this._state &= ~GH_TWODRAG;
|
||||
} else {
|
||||
this._state &= ~GH_PINCH;
|
||||
}
|
||||
|
||||
if (this._isTwoTouchTimeoutRunning()) {
|
||||
this._stopTwoTouchTimeout();
|
||||
}
|
||||
} else if (!this._isTwoTouchTimeoutRunning()) {
|
||||
// We can't determine the gesture right now, let's
|
||||
// wait and see if more events are on their way
|
||||
this._startTwoTouchTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._hasDetectedGesture()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pushEvent('gesturestart');
|
||||
}
|
||||
|
||||
this._pushEvent('gesturemove');
|
||||
}
|
||||
|
||||
_touchEnd(id, x, y) {
|
||||
// Check if this is an ignored touch
|
||||
if (this._ignored.indexOf(id) !== -1) {
|
||||
// Remove this touch from ignored
|
||||
this._ignored.splice(this._ignored.indexOf(id), 1);
|
||||
|
||||
// And reset the state if there are no more touches
|
||||
if (this._ignored.length === 0 && this._tracked.length === 0) {
|
||||
this._state = GH_INITSTATE;
|
||||
this._waitingRelease = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// We got a touchend before the timer triggered,
|
||||
// this cannot result in a gesture anymore.
|
||||
if (!this._hasDetectedGesture() && this._isTwoTouchTimeoutRunning()) {
|
||||
this._stopTwoTouchTimeout();
|
||||
this._state = GH_NOGESTURE;
|
||||
}
|
||||
|
||||
// Some gestures don't trigger until a touch is released
|
||||
if (!this._hasDetectedGesture()) {
|
||||
// Can't be a gesture that relies on movement
|
||||
this._state &= ~(GH_DRAG | GH_TWODRAG | GH_PINCH);
|
||||
// Or something that relies on more time
|
||||
this._state &= ~GH_LONGPRESS;
|
||||
this._stopLongpressTimeout();
|
||||
|
||||
if (!this._waitingRelease) {
|
||||
this._releaseStart = Date.now();
|
||||
this._waitingRelease = true;
|
||||
|
||||
// Can't be a tap that requires more touches than we current have
|
||||
switch (this._tracked.length) {
|
||||
case 1:
|
||||
this._state &= ~(GH_TWOTAP | GH_THREETAP);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
this._state &= ~(GH_ONETAP | GH_THREETAP);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Waiting for all touches to release? (i.e. some tap)
|
||||
if (this._waitingRelease) {
|
||||
// Were all touches released at roughly the same time?
|
||||
if (Date.now() - this._releaseStart > GH_MULTITOUCH_TIMEOUT) {
|
||||
this._state = GH_NOGESTURE;
|
||||
}
|
||||
|
||||
// Did too long time pass between press and release?
|
||||
if (this._tracked.some((t) => Date.now() - t.started > GH_TAP_TIMEOUT)) {
|
||||
this._state = GH_NOGESTURE;
|
||||
}
|
||||
|
||||
let touch = this._tracked.find((t) => t.id === id);
|
||||
touch.active = false;
|
||||
|
||||
// Are we still waiting for more releases?
|
||||
if (this._hasDetectedGesture()) {
|
||||
this._pushEvent('gesturestart');
|
||||
} else {
|
||||
// Have we reached a dead end?
|
||||
if (this._state !== GH_NOGESTURE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._hasDetectedGesture()) {
|
||||
this._pushEvent('gestureend');
|
||||
}
|
||||
|
||||
// Ignore any remaining touches until they are ended
|
||||
for (let i = 0; i < this._tracked.length; i++) {
|
||||
if (this._tracked[i].active) {
|
||||
this._ignored.push(this._tracked[i].id);
|
||||
}
|
||||
}
|
||||
this._tracked = [];
|
||||
|
||||
this._state = GH_NOGESTURE;
|
||||
|
||||
// Remove this touch from ignored if it's in there
|
||||
if (this._ignored.indexOf(id) !== -1) {
|
||||
this._ignored.splice(this._ignored.indexOf(id), 1);
|
||||
}
|
||||
|
||||
// We reset the state if ignored is empty
|
||||
if (this._ignored.length === 0) {
|
||||
this._state = GH_INITSTATE;
|
||||
this._waitingRelease = false;
|
||||
}
|
||||
}
|
||||
|
||||
_hasDetectedGesture() {
|
||||
if (this._state === GH_NOGESTURE) {
|
||||
return false;
|
||||
}
|
||||
// Check to see if the bitmask value is a power of 2
|
||||
// (i.e. only one bit set). If it is, we have a state.
|
||||
if (this._state & (this._state - 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For taps we also need to have all touches released
|
||||
// before we've fully detected the gesture
|
||||
if (this._state & (GH_ONETAP | GH_TWOTAP | GH_THREETAP)) {
|
||||
if (this._tracked.some((t) => t.active)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_startLongpressTimeout() {
|
||||
this._stopLongpressTimeout();
|
||||
this._longpressTimeoutId = setTimeout(() => this._longpressTimeout(), GH_LONGPRESS_TIMEOUT);
|
||||
}
|
||||
|
||||
_stopLongpressTimeout() {
|
||||
clearTimeout(this._longpressTimeoutId);
|
||||
this._longpressTimeoutId = null;
|
||||
}
|
||||
|
||||
_longpressTimeout() {
|
||||
if (this._hasDetectedGesture()) {
|
||||
throw new Error('A longpress gesture failed, conflict with a different gesture');
|
||||
}
|
||||
|
||||
this._state = GH_LONGPRESS;
|
||||
this._pushEvent('gesturestart');
|
||||
}
|
||||
|
||||
_startTwoTouchTimeout() {
|
||||
this._stopTwoTouchTimeout();
|
||||
this._twoTouchTimeoutId = setTimeout(() => this._twoTouchTimeout(), GH_TWOTOUCH_TIMEOUT);
|
||||
}
|
||||
|
||||
_stopTwoTouchTimeout() {
|
||||
clearTimeout(this._twoTouchTimeoutId);
|
||||
this._twoTouchTimeoutId = null;
|
||||
}
|
||||
|
||||
_isTwoTouchTimeoutRunning() {
|
||||
return this._twoTouchTimeoutId !== null;
|
||||
}
|
||||
|
||||
_twoTouchTimeout() {
|
||||
if (this._tracked.length === 0) {
|
||||
throw new Error('A pinch or two drag gesture failed, no tracked touches');
|
||||
}
|
||||
|
||||
// How far each touch point has moved since start
|
||||
let avgM = this._getAverageMovement();
|
||||
let avgMoveH = Math.abs(avgM.x);
|
||||
let avgMoveV = Math.abs(avgM.y);
|
||||
|
||||
// The difference in the distance between where
|
||||
// the touch points started and where they are now
|
||||
let avgD = this._getAverageDistance();
|
||||
let deltaTouchDistance = Math.abs(Math.hypot(avgD.first.x, avgD.first.y) - Math.hypot(avgD.last.x, avgD.last.y));
|
||||
|
||||
if (avgMoveV < deltaTouchDistance && avgMoveH < deltaTouchDistance) {
|
||||
this._state = GH_PINCH;
|
||||
} else {
|
||||
this._state = GH_TWODRAG;
|
||||
}
|
||||
|
||||
this._pushEvent('gesturestart');
|
||||
this._pushEvent('gesturemove');
|
||||
}
|
||||
|
||||
_pushEvent(type) {
|
||||
let detail = { type: this._stateToGesture(this._state) };
|
||||
|
||||
// For most gesture events the current (average) position is the
|
||||
// most useful
|
||||
let avg = this._getPosition();
|
||||
let pos = avg.last;
|
||||
|
||||
// However we have a slight distance to detect gestures, so for the
|
||||
// first gesture event we want to use the first positions we saw
|
||||
if (type === 'gesturestart') {
|
||||
pos = avg.first;
|
||||
}
|
||||
|
||||
// For these gestures, we always want the event coordinates
|
||||
// to be where the gesture began, not the current touch location.
|
||||
switch (this._state) {
|
||||
case GH_TWODRAG:
|
||||
case GH_PINCH:
|
||||
pos = avg.first;
|
||||
break;
|
||||
}
|
||||
|
||||
detail['clientX'] = pos.x;
|
||||
detail['clientY'] = pos.y;
|
||||
|
||||
// FIXME: other coordinates?
|
||||
|
||||
// Some gestures also have a magnitude
|
||||
if (this._state === GH_PINCH) {
|
||||
let distance = this._getAverageDistance();
|
||||
if (type === 'gesturestart') {
|
||||
detail['magnitudeX'] = distance.first.x;
|
||||
detail['magnitudeY'] = distance.first.y;
|
||||
} else {
|
||||
detail['magnitudeX'] = distance.last.x;
|
||||
detail['magnitudeY'] = distance.last.y;
|
||||
}
|
||||
} else if (this._state === GH_TWODRAG) {
|
||||
if (type === 'gesturestart') {
|
||||
detail['magnitudeX'] = 0.0;
|
||||
detail['magnitudeY'] = 0.0;
|
||||
} else {
|
||||
let movement = this._getAverageMovement();
|
||||
detail['magnitudeX'] = movement.x;
|
||||
detail['magnitudeY'] = movement.y;
|
||||
}
|
||||
}
|
||||
|
||||
let gev = new CustomEvent(type, { detail: detail });
|
||||
this._target.dispatchEvent(gev);
|
||||
}
|
||||
|
||||
_stateToGesture(state) {
|
||||
switch (state) {
|
||||
case GH_ONETAP:
|
||||
return 'onetap';
|
||||
case GH_TWOTAP:
|
||||
return 'twotap';
|
||||
case GH_THREETAP:
|
||||
return 'threetap';
|
||||
case GH_DRAG:
|
||||
return 'drag';
|
||||
case GH_LONGPRESS:
|
||||
return 'longpress';
|
||||
case GH_TWODRAG:
|
||||
return 'twodrag';
|
||||
case GH_PINCH:
|
||||
return 'pinch';
|
||||
}
|
||||
|
||||
throw new Error('Unknown gesture state: ' + state);
|
||||
}
|
||||
|
||||
_getPosition() {
|
||||
if (this._tracked.length === 0) {
|
||||
throw new Error('Failed to get gesture position, no tracked touches');
|
||||
}
|
||||
|
||||
let size = this._tracked.length;
|
||||
let fx = 0,
|
||||
fy = 0,
|
||||
lx = 0,
|
||||
ly = 0;
|
||||
|
||||
for (let i = 0; i < this._tracked.length; i++) {
|
||||
fx += this._tracked[i].firstX;
|
||||
fy += this._tracked[i].firstY;
|
||||
lx += this._tracked[i].lastX;
|
||||
ly += this._tracked[i].lastY;
|
||||
}
|
||||
|
||||
return { first: { x: fx / size, y: fy / size }, last: { x: lx / size, y: ly / size } };
|
||||
}
|
||||
|
||||
_getAverageMovement() {
|
||||
if (this._tracked.length === 0) {
|
||||
throw new Error('Failed to get gesture movement, no tracked touches');
|
||||
}
|
||||
|
||||
let totalH, totalV;
|
||||
totalH = totalV = 0;
|
||||
let size = this._tracked.length;
|
||||
|
||||
for (let i = 0; i < this._tracked.length; i++) {
|
||||
totalH += this._tracked[i].lastX - this._tracked[i].firstX;
|
||||
totalV += this._tracked[i].lastY - this._tracked[i].firstY;
|
||||
}
|
||||
|
||||
return { x: totalH / size, y: totalV / size };
|
||||
}
|
||||
|
||||
_getAverageDistance() {
|
||||
if (this._tracked.length === 0) {
|
||||
throw new Error('Failed to get gesture distance, no tracked touches');
|
||||
}
|
||||
|
||||
// Distance between the first and last tracked touches
|
||||
|
||||
let first = this._tracked[0];
|
||||
let last = this._tracked[this._tracked.length - 1];
|
||||
|
||||
let fdx = Math.abs(last.firstX - first.firstX);
|
||||
let fdy = Math.abs(last.firstY - first.firstY);
|
||||
|
||||
let ldx = Math.abs(last.lastX - first.lastX);
|
||||
let ldy = Math.abs(last.lastY - first.lastY);
|
||||
|
||||
return { first: { x: fdx, y: fdy }, last: { x: ldx, y: ldy } };
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,278 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
import * as Log from '../util/logging.js';
|
||||
import { stopEvent } from '../util/events.js';
|
||||
import * as KeyboardUtil from './util.js';
|
||||
import KeyTable from './keysym.js';
|
||||
import * as browser from '../util/browser.js';
|
||||
|
||||
//
|
||||
// Keyboard event handler
|
||||
//
|
||||
|
||||
export default class Keyboard {
|
||||
constructor(target) {
|
||||
this._target = target || null;
|
||||
|
||||
this._keyDownList = {}; // List of depressed keys
|
||||
// (even if they are happy)
|
||||
this._altGrArmed = false; // Windows AltGr detection
|
||||
|
||||
// keep these here so we can refer to them later
|
||||
this._eventHandlers = {
|
||||
keyup: this._handleKeyUp.bind(this),
|
||||
keydown: this._handleKeyDown.bind(this),
|
||||
blur: this._allKeysUp.bind(this),
|
||||
};
|
||||
|
||||
// ===== EVENT HANDLERS =====
|
||||
|
||||
this.onkeyevent = () => {}; // Handler for key press/release
|
||||
}
|
||||
|
||||
// ===== PRIVATE METHODS =====
|
||||
|
||||
_sendKeyEvent(keysym, code, down) {
|
||||
if (down) {
|
||||
this._keyDownList[code] = keysym;
|
||||
} else {
|
||||
// Do we really think this key is down?
|
||||
if (!(code in this._keyDownList)) {
|
||||
return;
|
||||
}
|
||||
delete this._keyDownList[code];
|
||||
}
|
||||
|
||||
Log.Debug('onkeyevent ' + (down ? 'down' : 'up') + ', keysym: ' + keysym, ', code: ' + code);
|
||||
this.onkeyevent(keysym, code, down);
|
||||
}
|
||||
|
||||
_getKeyCode(e) {
|
||||
const code = KeyboardUtil.getKeycode(e);
|
||||
if (code !== 'Unidentified') {
|
||||
return code;
|
||||
}
|
||||
|
||||
// Unstable, but we don't have anything else to go on
|
||||
if (e.keyCode) {
|
||||
// 229 is used for composition events
|
||||
if (e.keyCode !== 229) {
|
||||
return 'Platform' + e.keyCode;
|
||||
}
|
||||
}
|
||||
|
||||
// A precursor to the final DOM3 standard. Unfortunately it
|
||||
// is not layout independent, so it is as bad as using keyCode
|
||||
if (e.keyIdentifier) {
|
||||
// Non-character key?
|
||||
if (e.keyIdentifier.substr(0, 2) !== 'U+') {
|
||||
return e.keyIdentifier;
|
||||
}
|
||||
|
||||
const codepoint = parseInt(e.keyIdentifier.substr(2), 16);
|
||||
const char = String.fromCharCode(codepoint).toUpperCase();
|
||||
|
||||
return 'Platform' + char.charCodeAt();
|
||||
}
|
||||
|
||||
return 'Unidentified';
|
||||
}
|
||||
|
||||
_handleKeyDown(e) {
|
||||
const code = this._getKeyCode(e);
|
||||
let keysym = KeyboardUtil.getKeysym(e);
|
||||
|
||||
// Windows doesn't have a proper AltGr, but handles it using
|
||||
// fake Ctrl+Alt. However the remote end might not be Windows,
|
||||
// so we need to merge those in to a single AltGr event. We
|
||||
// detect this case by seeing the two key events directly after
|
||||
// each other with a very short time between them (<50ms).
|
||||
if (this._altGrArmed) {
|
||||
this._altGrArmed = false;
|
||||
clearTimeout(this._altGrTimeout);
|
||||
|
||||
if (code === 'AltRight' && e.timeStamp - this._altGrCtrlTime < 50) {
|
||||
// FIXME: We fail to detect this if either Ctrl key is
|
||||
// first manually pressed as Windows then no
|
||||
// longer sends the fake Ctrl down event. It
|
||||
// does however happily send real Ctrl events
|
||||
// even when AltGr is already down. Some
|
||||
// browsers detect this for us though and set the
|
||||
// key to "AltGraph".
|
||||
keysym = KeyTable.XK_ISO_Level3_Shift;
|
||||
} else {
|
||||
this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true);
|
||||
}
|
||||
}
|
||||
|
||||
// We cannot handle keys we cannot track, but we also need
|
||||
// to deal with virtual keyboards which omit key info
|
||||
if (code === 'Unidentified') {
|
||||
if (keysym) {
|
||||
// If it's a virtual keyboard then it should be
|
||||
// sufficient to just send press and release right
|
||||
// after each other
|
||||
this._sendKeyEvent(keysym, code, true);
|
||||
this._sendKeyEvent(keysym, code, false);
|
||||
}
|
||||
|
||||
stopEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alt behaves more like AltGraph on macOS, so shuffle the
|
||||
// keys around a bit to make things more sane for the remote
|
||||
// server. This method is used by RealVNC and TigerVNC (and
|
||||
// possibly others).
|
||||
if (browser.isMac() || browser.isIOS()) {
|
||||
switch (keysym) {
|
||||
case KeyTable.XK_Super_L:
|
||||
keysym = KeyTable.XK_Alt_L;
|
||||
break;
|
||||
case KeyTable.XK_Super_R:
|
||||
keysym = KeyTable.XK_Super_L;
|
||||
break;
|
||||
case KeyTable.XK_Alt_L:
|
||||
keysym = KeyTable.XK_Mode_switch;
|
||||
break;
|
||||
case KeyTable.XK_Alt_R:
|
||||
keysym = KeyTable.XK_ISO_Level3_Shift;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Is this key already pressed? If so, then we must use the
|
||||
// same keysym or we'll confuse the server
|
||||
if (code in this._keyDownList) {
|
||||
keysym = this._keyDownList[code];
|
||||
}
|
||||
|
||||
// macOS doesn't send proper key releases if a key is pressed
|
||||
// while meta is held down
|
||||
if ((browser.isMac() || browser.isIOS()) && e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight') {
|
||||
this._sendKeyEvent(keysym, code, true);
|
||||
this._sendKeyEvent(keysym, code, false);
|
||||
stopEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// macOS doesn't send proper key events for modifiers, only
|
||||
// state change events. That gets extra confusing for CapsLock
|
||||
// which toggles on each press, but not on release. So pretend
|
||||
// it was a quick press and release of the button.
|
||||
if ((browser.isMac() || browser.isIOS()) && code === 'CapsLock') {
|
||||
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
|
||||
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
|
||||
stopEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Windows doesn't send proper key releases for a bunch of
|
||||
// Japanese IM keys so we have to fake the release right away
|
||||
const jpBadKeys = [
|
||||
KeyTable.XK_Zenkaku_Hankaku,
|
||||
KeyTable.XK_Eisu_toggle,
|
||||
KeyTable.XK_Katakana,
|
||||
KeyTable.XK_Hiragana,
|
||||
KeyTable.XK_Romaji,
|
||||
];
|
||||
if (browser.isWindows() && jpBadKeys.includes(keysym)) {
|
||||
this._sendKeyEvent(keysym, code, true);
|
||||
this._sendKeyEvent(keysym, code, false);
|
||||
stopEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
stopEvent(e);
|
||||
|
||||
// Possible start of AltGr sequence? (see above)
|
||||
if (code === 'ControlLeft' && browser.isWindows() && !('ControlLeft' in this._keyDownList)) {
|
||||
this._altGrArmed = true;
|
||||
this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100);
|
||||
this._altGrCtrlTime = e.timeStamp;
|
||||
return;
|
||||
}
|
||||
|
||||
this._sendKeyEvent(keysym, code, true);
|
||||
}
|
||||
|
||||
_handleKeyUp(e) {
|
||||
stopEvent(e);
|
||||
|
||||
const code = this._getKeyCode(e);
|
||||
|
||||
// We can't get a release in the middle of an AltGr sequence, so
|
||||
// abort that detection
|
||||
if (this._altGrArmed) {
|
||||
this._altGrArmed = false;
|
||||
clearTimeout(this._altGrTimeout);
|
||||
this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true);
|
||||
}
|
||||
|
||||
// See comment in _handleKeyDown()
|
||||
if ((browser.isMac() || browser.isIOS()) && code === 'CapsLock') {
|
||||
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
|
||||
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
|
||||
return;
|
||||
}
|
||||
|
||||
this._sendKeyEvent(this._keyDownList[code], code, false);
|
||||
|
||||
// Windows has a rather nasty bug where it won't send key
|
||||
// release events for a Shift button if the other Shift is still
|
||||
// pressed
|
||||
if (browser.isWindows() && (code === 'ShiftLeft' || code === 'ShiftRight')) {
|
||||
if ('ShiftRight' in this._keyDownList) {
|
||||
this._sendKeyEvent(this._keyDownList['ShiftRight'], 'ShiftRight', false);
|
||||
}
|
||||
if ('ShiftLeft' in this._keyDownList) {
|
||||
this._sendKeyEvent(this._keyDownList['ShiftLeft'], 'ShiftLeft', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleAltGrTimeout() {
|
||||
this._altGrArmed = false;
|
||||
clearTimeout(this._altGrTimeout);
|
||||
this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true);
|
||||
}
|
||||
|
||||
_allKeysUp() {
|
||||
Log.Debug('>> Keyboard.allKeysUp');
|
||||
for (let code in this._keyDownList) {
|
||||
this._sendKeyEvent(this._keyDownList[code], code, false);
|
||||
}
|
||||
Log.Debug('<< Keyboard.allKeysUp');
|
||||
}
|
||||
|
||||
// ===== PUBLIC METHODS =====
|
||||
|
||||
grab() {
|
||||
//Log.Debug(">> Keyboard.grab");
|
||||
|
||||
this._target.addEventListener('keydown', this._eventHandlers.keydown);
|
||||
this._target.addEventListener('keyup', this._eventHandlers.keyup);
|
||||
|
||||
// Release (key up) if window loses focus
|
||||
window.addEventListener('blur', this._eventHandlers.blur);
|
||||
|
||||
//Log.Debug("<< Keyboard.grab");
|
||||
}
|
||||
|
||||
ungrab() {
|
||||
//Log.Debug(">> Keyboard.ungrab");
|
||||
|
||||
this._target.removeEventListener('keydown', this._eventHandlers.keydown);
|
||||
this._target.removeEventListener('keyup', this._eventHandlers.keyup);
|
||||
window.removeEventListener('blur', this._eventHandlers.blur);
|
||||
|
||||
// Release (key up) all keys that are in a down state
|
||||
this._allKeysUp();
|
||||
|
||||
//Log.Debug(">> Keyboard.ungrab");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,615 @@
|
||||
/* eslint-disable key-spacing */
|
||||
|
||||
export default {
|
||||
XK_VoidSymbol: 0xffffff /* Void symbol */,
|
||||
|
||||
XK_BackSpace: 0xff08 /* Back space, back char */,
|
||||
XK_Tab: 0xff09,
|
||||
XK_Linefeed: 0xff0a /* Linefeed, LF */,
|
||||
XK_Clear: 0xff0b,
|
||||
XK_Return: 0xff0d /* Return, enter */,
|
||||
XK_Pause: 0xff13 /* Pause, hold */,
|
||||
XK_Scroll_Lock: 0xff14,
|
||||
XK_Sys_Req: 0xff15,
|
||||
XK_Escape: 0xff1b,
|
||||
XK_Delete: 0xffff /* Delete, rubout */,
|
||||
|
||||
/* International & multi-key character composition */
|
||||
|
||||
XK_Multi_key: 0xff20 /* Multi-key character compose */,
|
||||
XK_Codeinput: 0xff37,
|
||||
XK_SingleCandidate: 0xff3c,
|
||||
XK_MultipleCandidate: 0xff3d,
|
||||
XK_PreviousCandidate: 0xff3e,
|
||||
|
||||
/* Japanese keyboard support */
|
||||
|
||||
XK_Kanji: 0xff21 /* Kanji, Kanji convert */,
|
||||
XK_Muhenkan: 0xff22 /* Cancel Conversion */,
|
||||
XK_Henkan_Mode: 0xff23 /* Start/Stop Conversion */,
|
||||
XK_Henkan: 0xff23 /* Alias for Henkan_Mode */,
|
||||
XK_Romaji: 0xff24 /* to Romaji */,
|
||||
XK_Hiragana: 0xff25 /* to Hiragana */,
|
||||
XK_Katakana: 0xff26 /* to Katakana */,
|
||||
XK_Hiragana_Katakana: 0xff27 /* Hiragana/Katakana toggle */,
|
||||
XK_Zenkaku: 0xff28 /* to Zenkaku */,
|
||||
XK_Hankaku: 0xff29 /* to Hankaku */,
|
||||
XK_Zenkaku_Hankaku: 0xff2a /* Zenkaku/Hankaku toggle */,
|
||||
XK_Touroku: 0xff2b /* Add to Dictionary */,
|
||||
XK_Massyo: 0xff2c /* Delete from Dictionary */,
|
||||
XK_Kana_Lock: 0xff2d /* Kana Lock */,
|
||||
XK_Kana_Shift: 0xff2e /* Kana Shift */,
|
||||
XK_Eisu_Shift: 0xff2f /* Alphanumeric Shift */,
|
||||
XK_Eisu_toggle: 0xff30 /* Alphanumeric toggle */,
|
||||
XK_Kanji_Bangou: 0xff37 /* Codeinput */,
|
||||
XK_Zen_Koho: 0xff3d /* Multiple/All Candidate(s) */,
|
||||
XK_Mae_Koho: 0xff3e /* Previous Candidate */,
|
||||
|
||||
/* Cursor control & motion */
|
||||
|
||||
XK_Home: 0xff50,
|
||||
XK_Left: 0xff51 /* Move left, left arrow */,
|
||||
XK_Up: 0xff52 /* Move up, up arrow */,
|
||||
XK_Right: 0xff53 /* Move right, right arrow */,
|
||||
XK_Down: 0xff54 /* Move down, down arrow */,
|
||||
XK_Prior: 0xff55 /* Prior, previous */,
|
||||
XK_Page_Up: 0xff55,
|
||||
XK_Next: 0xff56 /* Next */,
|
||||
XK_Page_Down: 0xff56,
|
||||
XK_End: 0xff57 /* EOL */,
|
||||
XK_Begin: 0xff58 /* BOL */,
|
||||
|
||||
/* Misc functions */
|
||||
|
||||
XK_Select: 0xff60 /* Select, mark */,
|
||||
XK_Print: 0xff61,
|
||||
XK_Execute: 0xff62 /* Execute, run, do */,
|
||||
XK_Insert: 0xff63 /* Insert, insert here */,
|
||||
XK_Undo: 0xff65,
|
||||
XK_Redo: 0xff66 /* Redo, again */,
|
||||
XK_Menu: 0xff67,
|
||||
XK_Find: 0xff68 /* Find, search */,
|
||||
XK_Cancel: 0xff69 /* Cancel, stop, abort, exit */,
|
||||
XK_Help: 0xff6a /* Help */,
|
||||
XK_Break: 0xff6b,
|
||||
XK_Mode_switch: 0xff7e /* Character set switch */,
|
||||
XK_script_switch: 0xff7e /* Alias for mode_switch */,
|
||||
XK_Num_Lock: 0xff7f,
|
||||
|
||||
/* Keypad functions, keypad numbers cleverly chosen to map to ASCII */
|
||||
|
||||
XK_KP_Space: 0xff80 /* Space */,
|
||||
XK_KP_Tab: 0xff89,
|
||||
XK_KP_Enter: 0xff8d /* Enter */,
|
||||
XK_KP_F1: 0xff91 /* PF1, KP_A, ... */,
|
||||
XK_KP_F2: 0xff92,
|
||||
XK_KP_F3: 0xff93,
|
||||
XK_KP_F4: 0xff94,
|
||||
XK_KP_Home: 0xff95,
|
||||
XK_KP_Left: 0xff96,
|
||||
XK_KP_Up: 0xff97,
|
||||
XK_KP_Right: 0xff98,
|
||||
XK_KP_Down: 0xff99,
|
||||
XK_KP_Prior: 0xff9a,
|
||||
XK_KP_Page_Up: 0xff9a,
|
||||
XK_KP_Next: 0xff9b,
|
||||
XK_KP_Page_Down: 0xff9b,
|
||||
XK_KP_End: 0xff9c,
|
||||
XK_KP_Begin: 0xff9d,
|
||||
XK_KP_Insert: 0xff9e,
|
||||
XK_KP_Delete: 0xff9f,
|
||||
XK_KP_Equal: 0xffbd /* Equals */,
|
||||
XK_KP_Multiply: 0xffaa,
|
||||
XK_KP_Add: 0xffab,
|
||||
XK_KP_Separator: 0xffac /* Separator, often comma */,
|
||||
XK_KP_Subtract: 0xffad,
|
||||
XK_KP_Decimal: 0xffae,
|
||||
XK_KP_Divide: 0xffaf,
|
||||
|
||||
XK_KP_0: 0xffb0,
|
||||
XK_KP_1: 0xffb1,
|
||||
XK_KP_2: 0xffb2,
|
||||
XK_KP_3: 0xffb3,
|
||||
XK_KP_4: 0xffb4,
|
||||
XK_KP_5: 0xffb5,
|
||||
XK_KP_6: 0xffb6,
|
||||
XK_KP_7: 0xffb7,
|
||||
XK_KP_8: 0xffb8,
|
||||
XK_KP_9: 0xffb9,
|
||||
|
||||
/*
|
||||
* Auxiliary functions; note the duplicate definitions for left and right
|
||||
* function keys; Sun keyboards and a few other manufacturers have such
|
||||
* function key groups on the left and/or right sides of the keyboard.
|
||||
* We've not found a keyboard with more than 35 function keys total.
|
||||
*/
|
||||
|
||||
XK_F1: 0xffbe,
|
||||
XK_F2: 0xffbf,
|
||||
XK_F3: 0xffc0,
|
||||
XK_F4: 0xffc1,
|
||||
XK_F5: 0xffc2,
|
||||
XK_F6: 0xffc3,
|
||||
XK_F7: 0xffc4,
|
||||
XK_F8: 0xffc5,
|
||||
XK_F9: 0xffc6,
|
||||
XK_F10: 0xffc7,
|
||||
XK_F11: 0xffc8,
|
||||
XK_L1: 0xffc8,
|
||||
XK_F12: 0xffc9,
|
||||
XK_L2: 0xffc9,
|
||||
XK_F13: 0xffca,
|
||||
XK_L3: 0xffca,
|
||||
XK_F14: 0xffcb,
|
||||
XK_L4: 0xffcb,
|
||||
XK_F15: 0xffcc,
|
||||
XK_L5: 0xffcc,
|
||||
XK_F16: 0xffcd,
|
||||
XK_L6: 0xffcd,
|
||||
XK_F17: 0xffce,
|
||||
XK_L7: 0xffce,
|
||||
XK_F18: 0xffcf,
|
||||
XK_L8: 0xffcf,
|
||||
XK_F19: 0xffd0,
|
||||
XK_L9: 0xffd0,
|
||||
XK_F20: 0xffd1,
|
||||
XK_L10: 0xffd1,
|
||||
XK_F21: 0xffd2,
|
||||
XK_R1: 0xffd2,
|
||||
XK_F22: 0xffd3,
|
||||
XK_R2: 0xffd3,
|
||||
XK_F23: 0xffd4,
|
||||
XK_R3: 0xffd4,
|
||||
XK_F24: 0xffd5,
|
||||
XK_R4: 0xffd5,
|
||||
XK_F25: 0xffd6,
|
||||
XK_R5: 0xffd6,
|
||||
XK_F26: 0xffd7,
|
||||
XK_R6: 0xffd7,
|
||||
XK_F27: 0xffd8,
|
||||
XK_R7: 0xffd8,
|
||||
XK_F28: 0xffd9,
|
||||
XK_R8: 0xffd9,
|
||||
XK_F29: 0xffda,
|
||||
XK_R9: 0xffda,
|
||||
XK_F30: 0xffdb,
|
||||
XK_R10: 0xffdb,
|
||||
XK_F31: 0xffdc,
|
||||
XK_R11: 0xffdc,
|
||||
XK_F32: 0xffdd,
|
||||
XK_R12: 0xffdd,
|
||||
XK_F33: 0xffde,
|
||||
XK_R13: 0xffde,
|
||||
XK_F34: 0xffdf,
|
||||
XK_R14: 0xffdf,
|
||||
XK_F35: 0xffe0,
|
||||
XK_R15: 0xffe0,
|
||||
|
||||
/* Modifiers */
|
||||
|
||||
XK_Shift_L: 0xffe1 /* Left shift */,
|
||||
XK_Shift_R: 0xffe2 /* Right shift */,
|
||||
XK_Control_L: 0xffe3 /* Left control */,
|
||||
XK_Control_R: 0xffe4 /* Right control */,
|
||||
XK_Caps_Lock: 0xffe5 /* Caps lock */,
|
||||
XK_Shift_Lock: 0xffe6 /* Shift lock */,
|
||||
|
||||
XK_Meta_L: 0xffe7 /* Left meta */,
|
||||
XK_Meta_R: 0xffe8 /* Right meta */,
|
||||
XK_Alt_L: 0xffe9 /* Left alt */,
|
||||
XK_Alt_R: 0xffea /* Right alt */,
|
||||
XK_Super_L: 0xffeb /* Left super */,
|
||||
XK_Super_R: 0xffec /* Right super */,
|
||||
XK_Hyper_L: 0xffed /* Left hyper */,
|
||||
XK_Hyper_R: 0xffee /* Right hyper */,
|
||||
|
||||
/*
|
||||
* Keyboard (XKB) Extension function and modifier keys
|
||||
* (from Appendix C of "The X Keyboard Extension: Protocol Specification")
|
||||
* Byte 3 = 0xfe
|
||||
*/
|
||||
|
||||
XK_ISO_Level3_Shift: 0xfe03 /* AltGr */,
|
||||
XK_ISO_Next_Group: 0xfe08,
|
||||
XK_ISO_Prev_Group: 0xfe0a,
|
||||
XK_ISO_First_Group: 0xfe0c,
|
||||
XK_ISO_Last_Group: 0xfe0e,
|
||||
|
||||
/*
|
||||
* Latin 1
|
||||
* (ISO/IEC 8859-1: Unicode U+0020..U+00FF)
|
||||
* Byte 3: 0
|
||||
*/
|
||||
|
||||
XK_space: 0x0020 /* U+0020 SPACE */,
|
||||
XK_exclam: 0x0021 /* U+0021 EXCLAMATION MARK */,
|
||||
XK_quotedbl: 0x0022 /* U+0022 QUOTATION MARK */,
|
||||
XK_numbersign: 0x0023 /* U+0023 NUMBER SIGN */,
|
||||
XK_dollar: 0x0024 /* U+0024 DOLLAR SIGN */,
|
||||
XK_percent: 0x0025 /* U+0025 PERCENT SIGN */,
|
||||
XK_ampersand: 0x0026 /* U+0026 AMPERSAND */,
|
||||
XK_apostrophe: 0x0027 /* U+0027 APOSTROPHE */,
|
||||
XK_quoteright: 0x0027 /* deprecated */,
|
||||
XK_parenleft: 0x0028 /* U+0028 LEFT PARENTHESIS */,
|
||||
XK_parenright: 0x0029 /* U+0029 RIGHT PARENTHESIS */,
|
||||
XK_asterisk: 0x002a /* U+002A ASTERISK */,
|
||||
XK_plus: 0x002b /* U+002B PLUS SIGN */,
|
||||
XK_comma: 0x002c /* U+002C COMMA */,
|
||||
XK_minus: 0x002d /* U+002D HYPHEN-MINUS */,
|
||||
XK_period: 0x002e /* U+002E FULL STOP */,
|
||||
XK_slash: 0x002f /* U+002F SOLIDUS */,
|
||||
XK_0: 0x0030 /* U+0030 DIGIT ZERO */,
|
||||
XK_1: 0x0031 /* U+0031 DIGIT ONE */,
|
||||
XK_2: 0x0032 /* U+0032 DIGIT TWO */,
|
||||
XK_3: 0x0033 /* U+0033 DIGIT THREE */,
|
||||
XK_4: 0x0034 /* U+0034 DIGIT FOUR */,
|
||||
XK_5: 0x0035 /* U+0035 DIGIT FIVE */,
|
||||
XK_6: 0x0036 /* U+0036 DIGIT SIX */,
|
||||
XK_7: 0x0037 /* U+0037 DIGIT SEVEN */,
|
||||
XK_8: 0x0038 /* U+0038 DIGIT EIGHT */,
|
||||
XK_9: 0x0039 /* U+0039 DIGIT NINE */,
|
||||
XK_colon: 0x003a /* U+003A COLON */,
|
||||
XK_semicolon: 0x003b /* U+003B SEMICOLON */,
|
||||
XK_less: 0x003c /* U+003C LESS-THAN SIGN */,
|
||||
XK_equal: 0x003d /* U+003D EQUALS SIGN */,
|
||||
XK_greater: 0x003e /* U+003E GREATER-THAN SIGN */,
|
||||
XK_question: 0x003f /* U+003F QUESTION MARK */,
|
||||
XK_at: 0x0040 /* U+0040 COMMERCIAL AT */,
|
||||
XK_A: 0x0041 /* U+0041 LATIN CAPITAL LETTER A */,
|
||||
XK_B: 0x0042 /* U+0042 LATIN CAPITAL LETTER B */,
|
||||
XK_C: 0x0043 /* U+0043 LATIN CAPITAL LETTER C */,
|
||||
XK_D: 0x0044 /* U+0044 LATIN CAPITAL LETTER D */,
|
||||
XK_E: 0x0045 /* U+0045 LATIN CAPITAL LETTER E */,
|
||||
XK_F: 0x0046 /* U+0046 LATIN CAPITAL LETTER F */,
|
||||
XK_G: 0x0047 /* U+0047 LATIN CAPITAL LETTER G */,
|
||||
XK_H: 0x0048 /* U+0048 LATIN CAPITAL LETTER H */,
|
||||
XK_I: 0x0049 /* U+0049 LATIN CAPITAL LETTER I */,
|
||||
XK_J: 0x004a /* U+004A LATIN CAPITAL LETTER J */,
|
||||
XK_K: 0x004b /* U+004B LATIN CAPITAL LETTER K */,
|
||||
XK_L: 0x004c /* U+004C LATIN CAPITAL LETTER L */,
|
||||
XK_M: 0x004d /* U+004D LATIN CAPITAL LETTER M */,
|
||||
XK_N: 0x004e /* U+004E LATIN CAPITAL LETTER N */,
|
||||
XK_O: 0x004f /* U+004F LATIN CAPITAL LETTER O */,
|
||||
XK_P: 0x0050 /* U+0050 LATIN CAPITAL LETTER P */,
|
||||
XK_Q: 0x0051 /* U+0051 LATIN CAPITAL LETTER Q */,
|
||||
XK_R: 0x0052 /* U+0052 LATIN CAPITAL LETTER R */,
|
||||
XK_S: 0x0053 /* U+0053 LATIN CAPITAL LETTER S */,
|
||||
XK_T: 0x0054 /* U+0054 LATIN CAPITAL LETTER T */,
|
||||
XK_U: 0x0055 /* U+0055 LATIN CAPITAL LETTER U */,
|
||||
XK_V: 0x0056 /* U+0056 LATIN CAPITAL LETTER V */,
|
||||
XK_W: 0x0057 /* U+0057 LATIN CAPITAL LETTER W */,
|
||||
XK_X: 0x0058 /* U+0058 LATIN CAPITAL LETTER X */,
|
||||
XK_Y: 0x0059 /* U+0059 LATIN CAPITAL LETTER Y */,
|
||||
XK_Z: 0x005a /* U+005A LATIN CAPITAL LETTER Z */,
|
||||
XK_bracketleft: 0x005b /* U+005B LEFT SQUARE BRACKET */,
|
||||
XK_backslash: 0x005c /* U+005C REVERSE SOLIDUS */,
|
||||
XK_bracketright: 0x005d /* U+005D RIGHT SQUARE BRACKET */,
|
||||
XK_asciicircum: 0x005e /* U+005E CIRCUMFLEX ACCENT */,
|
||||
XK_underscore: 0x005f /* U+005F LOW LINE */,
|
||||
XK_grave: 0x0060 /* U+0060 GRAVE ACCENT */,
|
||||
XK_quoteleft: 0x0060 /* deprecated */,
|
||||
XK_a: 0x0061 /* U+0061 LATIN SMALL LETTER A */,
|
||||
XK_b: 0x0062 /* U+0062 LATIN SMALL LETTER B */,
|
||||
XK_c: 0x0063 /* U+0063 LATIN SMALL LETTER C */,
|
||||
XK_d: 0x0064 /* U+0064 LATIN SMALL LETTER D */,
|
||||
XK_e: 0x0065 /* U+0065 LATIN SMALL LETTER E */,
|
||||
XK_f: 0x0066 /* U+0066 LATIN SMALL LETTER F */,
|
||||
XK_g: 0x0067 /* U+0067 LATIN SMALL LETTER G */,
|
||||
XK_h: 0x0068 /* U+0068 LATIN SMALL LETTER H */,
|
||||
XK_i: 0x0069 /* U+0069 LATIN SMALL LETTER I */,
|
||||
XK_j: 0x006a /* U+006A LATIN SMALL LETTER J */,
|
||||
XK_k: 0x006b /* U+006B LATIN SMALL LETTER K */,
|
||||
XK_l: 0x006c /* U+006C LATIN SMALL LETTER L */,
|
||||
XK_m: 0x006d /* U+006D LATIN SMALL LETTER M */,
|
||||
XK_n: 0x006e /* U+006E LATIN SMALL LETTER N */,
|
||||
XK_o: 0x006f /* U+006F LATIN SMALL LETTER O */,
|
||||
XK_p: 0x0070 /* U+0070 LATIN SMALL LETTER P */,
|
||||
XK_q: 0x0071 /* U+0071 LATIN SMALL LETTER Q */,
|
||||
XK_r: 0x0072 /* U+0072 LATIN SMALL LETTER R */,
|
||||
XK_s: 0x0073 /* U+0073 LATIN SMALL LETTER S */,
|
||||
XK_t: 0x0074 /* U+0074 LATIN SMALL LETTER T */,
|
||||
XK_u: 0x0075 /* U+0075 LATIN SMALL LETTER U */,
|
||||
XK_v: 0x0076 /* U+0076 LATIN SMALL LETTER V */,
|
||||
XK_w: 0x0077 /* U+0077 LATIN SMALL LETTER W */,
|
||||
XK_x: 0x0078 /* U+0078 LATIN SMALL LETTER X */,
|
||||
XK_y: 0x0079 /* U+0079 LATIN SMALL LETTER Y */,
|
||||
XK_z: 0x007a /* U+007A LATIN SMALL LETTER Z */,
|
||||
XK_braceleft: 0x007b /* U+007B LEFT CURLY BRACKET */,
|
||||
XK_bar: 0x007c /* U+007C VERTICAL LINE */,
|
||||
XK_braceright: 0x007d /* U+007D RIGHT CURLY BRACKET */,
|
||||
XK_asciitilde: 0x007e /* U+007E TILDE */,
|
||||
|
||||
XK_nobreakspace: 0x00a0 /* U+00A0 NO-BREAK SPACE */,
|
||||
XK_exclamdown: 0x00a1 /* U+00A1 INVERTED EXCLAMATION MARK */,
|
||||
XK_cent: 0x00a2 /* U+00A2 CENT SIGN */,
|
||||
XK_sterling: 0x00a3 /* U+00A3 POUND SIGN */,
|
||||
XK_currency: 0x00a4 /* U+00A4 CURRENCY SIGN */,
|
||||
XK_yen: 0x00a5 /* U+00A5 YEN SIGN */,
|
||||
XK_brokenbar: 0x00a6 /* U+00A6 BROKEN BAR */,
|
||||
XK_section: 0x00a7 /* U+00A7 SECTION SIGN */,
|
||||
XK_diaeresis: 0x00a8 /* U+00A8 DIAERESIS */,
|
||||
XK_copyright: 0x00a9 /* U+00A9 COPYRIGHT SIGN */,
|
||||
XK_ordfeminine: 0x00aa /* U+00AA FEMININE ORDINAL INDICATOR */,
|
||||
XK_guillemotleft: 0x00ab /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */,
|
||||
XK_notsign: 0x00ac /* U+00AC NOT SIGN */,
|
||||
XK_hyphen: 0x00ad /* U+00AD SOFT HYPHEN */,
|
||||
XK_registered: 0x00ae /* U+00AE REGISTERED SIGN */,
|
||||
XK_macron: 0x00af /* U+00AF MACRON */,
|
||||
XK_degree: 0x00b0 /* U+00B0 DEGREE SIGN */,
|
||||
XK_plusminus: 0x00b1 /* U+00B1 PLUS-MINUS SIGN */,
|
||||
XK_twosuperior: 0x00b2 /* U+00B2 SUPERSCRIPT TWO */,
|
||||
XK_threesuperior: 0x00b3 /* U+00B3 SUPERSCRIPT THREE */,
|
||||
XK_acute: 0x00b4 /* U+00B4 ACUTE ACCENT */,
|
||||
XK_mu: 0x00b5 /* U+00B5 MICRO SIGN */,
|
||||
XK_paragraph: 0x00b6 /* U+00B6 PILCROW SIGN */,
|
||||
XK_periodcentered: 0x00b7 /* U+00B7 MIDDLE DOT */,
|
||||
XK_cedilla: 0x00b8 /* U+00B8 CEDILLA */,
|
||||
XK_onesuperior: 0x00b9 /* U+00B9 SUPERSCRIPT ONE */,
|
||||
XK_masculine: 0x00ba /* U+00BA MASCULINE ORDINAL INDICATOR */,
|
||||
XK_guillemotright: 0x00bb /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */,
|
||||
XK_onequarter: 0x00bc /* U+00BC VULGAR FRACTION ONE QUARTER */,
|
||||
XK_onehalf: 0x00bd /* U+00BD VULGAR FRACTION ONE HALF */,
|
||||
XK_threequarters: 0x00be /* U+00BE VULGAR FRACTION THREE QUARTERS */,
|
||||
XK_questiondown: 0x00bf /* U+00BF INVERTED QUESTION MARK */,
|
||||
XK_Agrave: 0x00c0 /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */,
|
||||
XK_Aacute: 0x00c1 /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */,
|
||||
XK_Acircumflex: 0x00c2 /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */,
|
||||
XK_Atilde: 0x00c3 /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */,
|
||||
XK_Adiaeresis: 0x00c4 /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */,
|
||||
XK_Aring: 0x00c5 /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */,
|
||||
XK_AE: 0x00c6 /* U+00C6 LATIN CAPITAL LETTER AE */,
|
||||
XK_Ccedilla: 0x00c7 /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */,
|
||||
XK_Egrave: 0x00c8 /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */,
|
||||
XK_Eacute: 0x00c9 /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */,
|
||||
XK_Ecircumflex: 0x00ca /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */,
|
||||
XK_Ediaeresis: 0x00cb /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */,
|
||||
XK_Igrave: 0x00cc /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */,
|
||||
XK_Iacute: 0x00cd /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */,
|
||||
XK_Icircumflex: 0x00ce /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */,
|
||||
XK_Idiaeresis: 0x00cf /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */,
|
||||
XK_ETH: 0x00d0 /* U+00D0 LATIN CAPITAL LETTER ETH */,
|
||||
XK_Eth: 0x00d0 /* deprecated */,
|
||||
XK_Ntilde: 0x00d1 /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */,
|
||||
XK_Ograve: 0x00d2 /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */,
|
||||
XK_Oacute: 0x00d3 /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */,
|
||||
XK_Ocircumflex: 0x00d4 /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */,
|
||||
XK_Otilde: 0x00d5 /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */,
|
||||
XK_Odiaeresis: 0x00d6 /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */,
|
||||
XK_multiply: 0x00d7 /* U+00D7 MULTIPLICATION SIGN */,
|
||||
XK_Oslash: 0x00d8 /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */,
|
||||
XK_Ooblique: 0x00d8 /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */,
|
||||
XK_Ugrave: 0x00d9 /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */,
|
||||
XK_Uacute: 0x00da /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */,
|
||||
XK_Ucircumflex: 0x00db /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */,
|
||||
XK_Udiaeresis: 0x00dc /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */,
|
||||
XK_Yacute: 0x00dd /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */,
|
||||
XK_THORN: 0x00de /* U+00DE LATIN CAPITAL LETTER THORN */,
|
||||
XK_Thorn: 0x00de /* deprecated */,
|
||||
XK_ssharp: 0x00df /* U+00DF LATIN SMALL LETTER SHARP S */,
|
||||
XK_agrave: 0x00e0 /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */,
|
||||
XK_aacute: 0x00e1 /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */,
|
||||
XK_acircumflex: 0x00e2 /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */,
|
||||
XK_atilde: 0x00e3 /* U+00E3 LATIN SMALL LETTER A WITH TILDE */,
|
||||
XK_adiaeresis: 0x00e4 /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */,
|
||||
XK_aring: 0x00e5 /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */,
|
||||
XK_ae: 0x00e6 /* U+00E6 LATIN SMALL LETTER AE */,
|
||||
XK_ccedilla: 0x00e7 /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */,
|
||||
XK_egrave: 0x00e8 /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */,
|
||||
XK_eacute: 0x00e9 /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */,
|
||||
XK_ecircumflex: 0x00ea /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */,
|
||||
XK_ediaeresis: 0x00eb /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */,
|
||||
XK_igrave: 0x00ec /* U+00EC LATIN SMALL LETTER I WITH GRAVE */,
|
||||
XK_iacute: 0x00ed /* U+00ED LATIN SMALL LETTER I WITH ACUTE */,
|
||||
XK_icircumflex: 0x00ee /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */,
|
||||
XK_idiaeresis: 0x00ef /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */,
|
||||
XK_eth: 0x00f0 /* U+00F0 LATIN SMALL LETTER ETH */,
|
||||
XK_ntilde: 0x00f1 /* U+00F1 LATIN SMALL LETTER N WITH TILDE */,
|
||||
XK_ograve: 0x00f2 /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */,
|
||||
XK_oacute: 0x00f3 /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */,
|
||||
XK_ocircumflex: 0x00f4 /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */,
|
||||
XK_otilde: 0x00f5 /* U+00F5 LATIN SMALL LETTER O WITH TILDE */,
|
||||
XK_odiaeresis: 0x00f6 /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */,
|
||||
XK_division: 0x00f7 /* U+00F7 DIVISION SIGN */,
|
||||
XK_oslash: 0x00f8 /* U+00F8 LATIN SMALL LETTER O WITH STROKE */,
|
||||
XK_ooblique: 0x00f8 /* U+00F8 LATIN SMALL LETTER O WITH STROKE */,
|
||||
XK_ugrave: 0x00f9 /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */,
|
||||
XK_uacute: 0x00fa /* U+00FA LATIN SMALL LETTER U WITH ACUTE */,
|
||||
XK_ucircumflex: 0x00fb /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */,
|
||||
XK_udiaeresis: 0x00fc /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */,
|
||||
XK_yacute: 0x00fd /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */,
|
||||
XK_thorn: 0x00fe /* U+00FE LATIN SMALL LETTER THORN */,
|
||||
XK_ydiaeresis: 0x00ff /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */,
|
||||
|
||||
/*
|
||||
* Korean
|
||||
* Byte 3 = 0x0e
|
||||
*/
|
||||
|
||||
XK_Hangul: 0xff31 /* Hangul start/stop(toggle) */,
|
||||
XK_Hangul_Hanja: 0xff34 /* Start Hangul->Hanja Conversion */,
|
||||
XK_Hangul_Jeonja: 0xff38 /* Jeonja mode */,
|
||||
|
||||
/*
|
||||
* XFree86 vendor specific keysyms.
|
||||
*
|
||||
* The XFree86 keysym range is 0x10080001 - 0x1008FFFF.
|
||||
*/
|
||||
|
||||
XF86XK_ModeLock: 0x1008ff01,
|
||||
XF86XK_MonBrightnessUp: 0x1008ff02,
|
||||
XF86XK_MonBrightnessDown: 0x1008ff03,
|
||||
XF86XK_KbdLightOnOff: 0x1008ff04,
|
||||
XF86XK_KbdBrightnessUp: 0x1008ff05,
|
||||
XF86XK_KbdBrightnessDown: 0x1008ff06,
|
||||
XF86XK_Standby: 0x1008ff10,
|
||||
XF86XK_AudioLowerVolume: 0x1008ff11,
|
||||
XF86XK_AudioMute: 0x1008ff12,
|
||||
XF86XK_AudioRaiseVolume: 0x1008ff13,
|
||||
XF86XK_AudioPlay: 0x1008ff14,
|
||||
XF86XK_AudioStop: 0x1008ff15,
|
||||
XF86XK_AudioPrev: 0x1008ff16,
|
||||
XF86XK_AudioNext: 0x1008ff17,
|
||||
XF86XK_HomePage: 0x1008ff18,
|
||||
XF86XK_Mail: 0x1008ff19,
|
||||
XF86XK_Start: 0x1008ff1a,
|
||||
XF86XK_Search: 0x1008ff1b,
|
||||
XF86XK_AudioRecord: 0x1008ff1c,
|
||||
XF86XK_Calculator: 0x1008ff1d,
|
||||
XF86XK_Memo: 0x1008ff1e,
|
||||
XF86XK_ToDoList: 0x1008ff1f,
|
||||
XF86XK_Calendar: 0x1008ff20,
|
||||
XF86XK_PowerDown: 0x1008ff21,
|
||||
XF86XK_ContrastAdjust: 0x1008ff22,
|
||||
XF86XK_RockerUp: 0x1008ff23,
|
||||
XF86XK_RockerDown: 0x1008ff24,
|
||||
XF86XK_RockerEnter: 0x1008ff25,
|
||||
XF86XK_Back: 0x1008ff26,
|
||||
XF86XK_Forward: 0x1008ff27,
|
||||
XF86XK_Stop: 0x1008ff28,
|
||||
XF86XK_Refresh: 0x1008ff29,
|
||||
XF86XK_PowerOff: 0x1008ff2a,
|
||||
XF86XK_WakeUp: 0x1008ff2b,
|
||||
XF86XK_Eject: 0x1008ff2c,
|
||||
XF86XK_ScreenSaver: 0x1008ff2d,
|
||||
XF86XK_WWW: 0x1008ff2e,
|
||||
XF86XK_Sleep: 0x1008ff2f,
|
||||
XF86XK_Favorites: 0x1008ff30,
|
||||
XF86XK_AudioPause: 0x1008ff31,
|
||||
XF86XK_AudioMedia: 0x1008ff32,
|
||||
XF86XK_MyComputer: 0x1008ff33,
|
||||
XF86XK_VendorHome: 0x1008ff34,
|
||||
XF86XK_LightBulb: 0x1008ff35,
|
||||
XF86XK_Shop: 0x1008ff36,
|
||||
XF86XK_History: 0x1008ff37,
|
||||
XF86XK_OpenURL: 0x1008ff38,
|
||||
XF86XK_AddFavorite: 0x1008ff39,
|
||||
XF86XK_HotLinks: 0x1008ff3a,
|
||||
XF86XK_BrightnessAdjust: 0x1008ff3b,
|
||||
XF86XK_Finance: 0x1008ff3c,
|
||||
XF86XK_Community: 0x1008ff3d,
|
||||
XF86XK_AudioRewind: 0x1008ff3e,
|
||||
XF86XK_BackForward: 0x1008ff3f,
|
||||
XF86XK_Launch0: 0x1008ff40,
|
||||
XF86XK_Launch1: 0x1008ff41,
|
||||
XF86XK_Launch2: 0x1008ff42,
|
||||
XF86XK_Launch3: 0x1008ff43,
|
||||
XF86XK_Launch4: 0x1008ff44,
|
||||
XF86XK_Launch5: 0x1008ff45,
|
||||
XF86XK_Launch6: 0x1008ff46,
|
||||
XF86XK_Launch7: 0x1008ff47,
|
||||
XF86XK_Launch8: 0x1008ff48,
|
||||
XF86XK_Launch9: 0x1008ff49,
|
||||
XF86XK_LaunchA: 0x1008ff4a,
|
||||
XF86XK_LaunchB: 0x1008ff4b,
|
||||
XF86XK_LaunchC: 0x1008ff4c,
|
||||
XF86XK_LaunchD: 0x1008ff4d,
|
||||
XF86XK_LaunchE: 0x1008ff4e,
|
||||
XF86XK_LaunchF: 0x1008ff4f,
|
||||
XF86XK_ApplicationLeft: 0x1008ff50,
|
||||
XF86XK_ApplicationRight: 0x1008ff51,
|
||||
XF86XK_Book: 0x1008ff52,
|
||||
XF86XK_CD: 0x1008ff53,
|
||||
XF86XK_Calculater: 0x1008ff54,
|
||||
XF86XK_Clear: 0x1008ff55,
|
||||
XF86XK_Close: 0x1008ff56,
|
||||
XF86XK_Copy: 0x1008ff57,
|
||||
XF86XK_Cut: 0x1008ff58,
|
||||
XF86XK_Display: 0x1008ff59,
|
||||
XF86XK_DOS: 0x1008ff5a,
|
||||
XF86XK_Documents: 0x1008ff5b,
|
||||
XF86XK_Excel: 0x1008ff5c,
|
||||
XF86XK_Explorer: 0x1008ff5d,
|
||||
XF86XK_Game: 0x1008ff5e,
|
||||
XF86XK_Go: 0x1008ff5f,
|
||||
XF86XK_iTouch: 0x1008ff60,
|
||||
XF86XK_LogOff: 0x1008ff61,
|
||||
XF86XK_Market: 0x1008ff62,
|
||||
XF86XK_Meeting: 0x1008ff63,
|
||||
XF86XK_MenuKB: 0x1008ff65,
|
||||
XF86XK_MenuPB: 0x1008ff66,
|
||||
XF86XK_MySites: 0x1008ff67,
|
||||
XF86XK_New: 0x1008ff68,
|
||||
XF86XK_News: 0x1008ff69,
|
||||
XF86XK_OfficeHome: 0x1008ff6a,
|
||||
XF86XK_Open: 0x1008ff6b,
|
||||
XF86XK_Option: 0x1008ff6c,
|
||||
XF86XK_Paste: 0x1008ff6d,
|
||||
XF86XK_Phone: 0x1008ff6e,
|
||||
XF86XK_Q: 0x1008ff70,
|
||||
XF86XK_Reply: 0x1008ff72,
|
||||
XF86XK_Reload: 0x1008ff73,
|
||||
XF86XK_RotateWindows: 0x1008ff74,
|
||||
XF86XK_RotationPB: 0x1008ff75,
|
||||
XF86XK_RotationKB: 0x1008ff76,
|
||||
XF86XK_Save: 0x1008ff77,
|
||||
XF86XK_ScrollUp: 0x1008ff78,
|
||||
XF86XK_ScrollDown: 0x1008ff79,
|
||||
XF86XK_ScrollClick: 0x1008ff7a,
|
||||
XF86XK_Send: 0x1008ff7b,
|
||||
XF86XK_Spell: 0x1008ff7c,
|
||||
XF86XK_SplitScreen: 0x1008ff7d,
|
||||
XF86XK_Support: 0x1008ff7e,
|
||||
XF86XK_TaskPane: 0x1008ff7f,
|
||||
XF86XK_Terminal: 0x1008ff80,
|
||||
XF86XK_Tools: 0x1008ff81,
|
||||
XF86XK_Travel: 0x1008ff82,
|
||||
XF86XK_UserPB: 0x1008ff84,
|
||||
XF86XK_User1KB: 0x1008ff85,
|
||||
XF86XK_User2KB: 0x1008ff86,
|
||||
XF86XK_Video: 0x1008ff87,
|
||||
XF86XK_WheelButton: 0x1008ff88,
|
||||
XF86XK_Word: 0x1008ff89,
|
||||
XF86XK_Xfer: 0x1008ff8a,
|
||||
XF86XK_ZoomIn: 0x1008ff8b,
|
||||
XF86XK_ZoomOut: 0x1008ff8c,
|
||||
XF86XK_Away: 0x1008ff8d,
|
||||
XF86XK_Messenger: 0x1008ff8e,
|
||||
XF86XK_WebCam: 0x1008ff8f,
|
||||
XF86XK_MailForward: 0x1008ff90,
|
||||
XF86XK_Pictures: 0x1008ff91,
|
||||
XF86XK_Music: 0x1008ff92,
|
||||
XF86XK_Battery: 0x1008ff93,
|
||||
XF86XK_Bluetooth: 0x1008ff94,
|
||||
XF86XK_WLAN: 0x1008ff95,
|
||||
XF86XK_UWB: 0x1008ff96,
|
||||
XF86XK_AudioForward: 0x1008ff97,
|
||||
XF86XK_AudioRepeat: 0x1008ff98,
|
||||
XF86XK_AudioRandomPlay: 0x1008ff99,
|
||||
XF86XK_Subtitle: 0x1008ff9a,
|
||||
XF86XK_AudioCycleTrack: 0x1008ff9b,
|
||||
XF86XK_CycleAngle: 0x1008ff9c,
|
||||
XF86XK_FrameBack: 0x1008ff9d,
|
||||
XF86XK_FrameForward: 0x1008ff9e,
|
||||
XF86XK_Time: 0x1008ff9f,
|
||||
XF86XK_Select: 0x1008ffa0,
|
||||
XF86XK_View: 0x1008ffa1,
|
||||
XF86XK_TopMenu: 0x1008ffa2,
|
||||
XF86XK_Red: 0x1008ffa3,
|
||||
XF86XK_Green: 0x1008ffa4,
|
||||
XF86XK_Yellow: 0x1008ffa5,
|
||||
XF86XK_Blue: 0x1008ffa6,
|
||||
XF86XK_Suspend: 0x1008ffa7,
|
||||
XF86XK_Hibernate: 0x1008ffa8,
|
||||
XF86XK_TouchpadToggle: 0x1008ffa9,
|
||||
XF86XK_TouchpadOn: 0x1008ffb0,
|
||||
XF86XK_TouchpadOff: 0x1008ffb1,
|
||||
XF86XK_AudioMicMute: 0x1008ffb2,
|
||||
XF86XK_Switch_VT_1: 0x1008fe01,
|
||||
XF86XK_Switch_VT_2: 0x1008fe02,
|
||||
XF86XK_Switch_VT_3: 0x1008fe03,
|
||||
XF86XK_Switch_VT_4: 0x1008fe04,
|
||||
XF86XK_Switch_VT_5: 0x1008fe05,
|
||||
XF86XK_Switch_VT_6: 0x1008fe06,
|
||||
XF86XK_Switch_VT_7: 0x1008fe07,
|
||||
XF86XK_Switch_VT_8: 0x1008fe08,
|
||||
XF86XK_Switch_VT_9: 0x1008fe09,
|
||||
XF86XK_Switch_VT_10: 0x1008fe0a,
|
||||
XF86XK_Switch_VT_11: 0x1008fe0b,
|
||||
XF86XK_Switch_VT_12: 0x1008fe0c,
|
||||
XF86XK_Ungrab: 0x1008fe20,
|
||||
XF86XK_ClearGrab: 0x1008fe21,
|
||||
XF86XK_Next_VMode: 0x1008fe22,
|
||||
XF86XK_Prev_VMode: 0x1008fe23,
|
||||
XF86XK_LogWindowTree: 0x1008fe24,
|
||||
XF86XK_LogGrabInfo: 0x1008fe25,
|
||||
};
|
||||
@ -0,0 +1,688 @@
|
||||
/*
|
||||
* Mapping from Unicode codepoints to X11/RFB keysyms
|
||||
*
|
||||
* This file was automatically generated from keysymdef.h
|
||||
* DO NOT EDIT!
|
||||
*/
|
||||
|
||||
/* Functions at the bottom */
|
||||
|
||||
const codepoints = {
|
||||
0x0100: 0x03c0, // XK_Amacron
|
||||
0x0101: 0x03e0, // XK_amacron
|
||||
0x0102: 0x01c3, // XK_Abreve
|
||||
0x0103: 0x01e3, // XK_abreve
|
||||
0x0104: 0x01a1, // XK_Aogonek
|
||||
0x0105: 0x01b1, // XK_aogonek
|
||||
0x0106: 0x01c6, // XK_Cacute
|
||||
0x0107: 0x01e6, // XK_cacute
|
||||
0x0108: 0x02c6, // XK_Ccircumflex
|
||||
0x0109: 0x02e6, // XK_ccircumflex
|
||||
0x010a: 0x02c5, // XK_Cabovedot
|
||||
0x010b: 0x02e5, // XK_cabovedot
|
||||
0x010c: 0x01c8, // XK_Ccaron
|
||||
0x010d: 0x01e8, // XK_ccaron
|
||||
0x010e: 0x01cf, // XK_Dcaron
|
||||
0x010f: 0x01ef, // XK_dcaron
|
||||
0x0110: 0x01d0, // XK_Dstroke
|
||||
0x0111: 0x01f0, // XK_dstroke
|
||||
0x0112: 0x03aa, // XK_Emacron
|
||||
0x0113: 0x03ba, // XK_emacron
|
||||
0x0116: 0x03cc, // XK_Eabovedot
|
||||
0x0117: 0x03ec, // XK_eabovedot
|
||||
0x0118: 0x01ca, // XK_Eogonek
|
||||
0x0119: 0x01ea, // XK_eogonek
|
||||
0x011a: 0x01cc, // XK_Ecaron
|
||||
0x011b: 0x01ec, // XK_ecaron
|
||||
0x011c: 0x02d8, // XK_Gcircumflex
|
||||
0x011d: 0x02f8, // XK_gcircumflex
|
||||
0x011e: 0x02ab, // XK_Gbreve
|
||||
0x011f: 0x02bb, // XK_gbreve
|
||||
0x0120: 0x02d5, // XK_Gabovedot
|
||||
0x0121: 0x02f5, // XK_gabovedot
|
||||
0x0122: 0x03ab, // XK_Gcedilla
|
||||
0x0123: 0x03bb, // XK_gcedilla
|
||||
0x0124: 0x02a6, // XK_Hcircumflex
|
||||
0x0125: 0x02b6, // XK_hcircumflex
|
||||
0x0126: 0x02a1, // XK_Hstroke
|
||||
0x0127: 0x02b1, // XK_hstroke
|
||||
0x0128: 0x03a5, // XK_Itilde
|
||||
0x0129: 0x03b5, // XK_itilde
|
||||
0x012a: 0x03cf, // XK_Imacron
|
||||
0x012b: 0x03ef, // XK_imacron
|
||||
0x012e: 0x03c7, // XK_Iogonek
|
||||
0x012f: 0x03e7, // XK_iogonek
|
||||
0x0130: 0x02a9, // XK_Iabovedot
|
||||
0x0131: 0x02b9, // XK_idotless
|
||||
0x0134: 0x02ac, // XK_Jcircumflex
|
||||
0x0135: 0x02bc, // XK_jcircumflex
|
||||
0x0136: 0x03d3, // XK_Kcedilla
|
||||
0x0137: 0x03f3, // XK_kcedilla
|
||||
0x0138: 0x03a2, // XK_kra
|
||||
0x0139: 0x01c5, // XK_Lacute
|
||||
0x013a: 0x01e5, // XK_lacute
|
||||
0x013b: 0x03a6, // XK_Lcedilla
|
||||
0x013c: 0x03b6, // XK_lcedilla
|
||||
0x013d: 0x01a5, // XK_Lcaron
|
||||
0x013e: 0x01b5, // XK_lcaron
|
||||
0x0141: 0x01a3, // XK_Lstroke
|
||||
0x0142: 0x01b3, // XK_lstroke
|
||||
0x0143: 0x01d1, // XK_Nacute
|
||||
0x0144: 0x01f1, // XK_nacute
|
||||
0x0145: 0x03d1, // XK_Ncedilla
|
||||
0x0146: 0x03f1, // XK_ncedilla
|
||||
0x0147: 0x01d2, // XK_Ncaron
|
||||
0x0148: 0x01f2, // XK_ncaron
|
||||
0x014a: 0x03bd, // XK_ENG
|
||||
0x014b: 0x03bf, // XK_eng
|
||||
0x014c: 0x03d2, // XK_Omacron
|
||||
0x014d: 0x03f2, // XK_omacron
|
||||
0x0150: 0x01d5, // XK_Odoubleacute
|
||||
0x0151: 0x01f5, // XK_odoubleacute
|
||||
0x0152: 0x13bc, // XK_OE
|
||||
0x0153: 0x13bd, // XK_oe
|
||||
0x0154: 0x01c0, // XK_Racute
|
||||
0x0155: 0x01e0, // XK_racute
|
||||
0x0156: 0x03a3, // XK_Rcedilla
|
||||
0x0157: 0x03b3, // XK_rcedilla
|
||||
0x0158: 0x01d8, // XK_Rcaron
|
||||
0x0159: 0x01f8, // XK_rcaron
|
||||
0x015a: 0x01a6, // XK_Sacute
|
||||
0x015b: 0x01b6, // XK_sacute
|
||||
0x015c: 0x02de, // XK_Scircumflex
|
||||
0x015d: 0x02fe, // XK_scircumflex
|
||||
0x015e: 0x01aa, // XK_Scedilla
|
||||
0x015f: 0x01ba, // XK_scedilla
|
||||
0x0160: 0x01a9, // XK_Scaron
|
||||
0x0161: 0x01b9, // XK_scaron
|
||||
0x0162: 0x01de, // XK_Tcedilla
|
||||
0x0163: 0x01fe, // XK_tcedilla
|
||||
0x0164: 0x01ab, // XK_Tcaron
|
||||
0x0165: 0x01bb, // XK_tcaron
|
||||
0x0166: 0x03ac, // XK_Tslash
|
||||
0x0167: 0x03bc, // XK_tslash
|
||||
0x0168: 0x03dd, // XK_Utilde
|
||||
0x0169: 0x03fd, // XK_utilde
|
||||
0x016a: 0x03de, // XK_Umacron
|
||||
0x016b: 0x03fe, // XK_umacron
|
||||
0x016c: 0x02dd, // XK_Ubreve
|
||||
0x016d: 0x02fd, // XK_ubreve
|
||||
0x016e: 0x01d9, // XK_Uring
|
||||
0x016f: 0x01f9, // XK_uring
|
||||
0x0170: 0x01db, // XK_Udoubleacute
|
||||
0x0171: 0x01fb, // XK_udoubleacute
|
||||
0x0172: 0x03d9, // XK_Uogonek
|
||||
0x0173: 0x03f9, // XK_uogonek
|
||||
0x0178: 0x13be, // XK_Ydiaeresis
|
||||
0x0179: 0x01ac, // XK_Zacute
|
||||
0x017a: 0x01bc, // XK_zacute
|
||||
0x017b: 0x01af, // XK_Zabovedot
|
||||
0x017c: 0x01bf, // XK_zabovedot
|
||||
0x017d: 0x01ae, // XK_Zcaron
|
||||
0x017e: 0x01be, // XK_zcaron
|
||||
0x0192: 0x08f6, // XK_function
|
||||
0x01d2: 0x10001d1, // XK_Ocaron
|
||||
0x02c7: 0x01b7, // XK_caron
|
||||
0x02d8: 0x01a2, // XK_breve
|
||||
0x02d9: 0x01ff, // XK_abovedot
|
||||
0x02db: 0x01b2, // XK_ogonek
|
||||
0x02dd: 0x01bd, // XK_doubleacute
|
||||
0x0385: 0x07ae, // XK_Greek_accentdieresis
|
||||
0x0386: 0x07a1, // XK_Greek_ALPHAaccent
|
||||
0x0388: 0x07a2, // XK_Greek_EPSILONaccent
|
||||
0x0389: 0x07a3, // XK_Greek_ETAaccent
|
||||
0x038a: 0x07a4, // XK_Greek_IOTAaccent
|
||||
0x038c: 0x07a7, // XK_Greek_OMICRONaccent
|
||||
0x038e: 0x07a8, // XK_Greek_UPSILONaccent
|
||||
0x038f: 0x07ab, // XK_Greek_OMEGAaccent
|
||||
0x0390: 0x07b6, // XK_Greek_iotaaccentdieresis
|
||||
0x0391: 0x07c1, // XK_Greek_ALPHA
|
||||
0x0392: 0x07c2, // XK_Greek_BETA
|
||||
0x0393: 0x07c3, // XK_Greek_GAMMA
|
||||
0x0394: 0x07c4, // XK_Greek_DELTA
|
||||
0x0395: 0x07c5, // XK_Greek_EPSILON
|
||||
0x0396: 0x07c6, // XK_Greek_ZETA
|
||||
0x0397: 0x07c7, // XK_Greek_ETA
|
||||
0x0398: 0x07c8, // XK_Greek_THETA
|
||||
0x0399: 0x07c9, // XK_Greek_IOTA
|
||||
0x039a: 0x07ca, // XK_Greek_KAPPA
|
||||
0x039b: 0x07cb, // XK_Greek_LAMDA
|
||||
0x039c: 0x07cc, // XK_Greek_MU
|
||||
0x039d: 0x07cd, // XK_Greek_NU
|
||||
0x039e: 0x07ce, // XK_Greek_XI
|
||||
0x039f: 0x07cf, // XK_Greek_OMICRON
|
||||
0x03a0: 0x07d0, // XK_Greek_PI
|
||||
0x03a1: 0x07d1, // XK_Greek_RHO
|
||||
0x03a3: 0x07d2, // XK_Greek_SIGMA
|
||||
0x03a4: 0x07d4, // XK_Greek_TAU
|
||||
0x03a5: 0x07d5, // XK_Greek_UPSILON
|
||||
0x03a6: 0x07d6, // XK_Greek_PHI
|
||||
0x03a7: 0x07d7, // XK_Greek_CHI
|
||||
0x03a8: 0x07d8, // XK_Greek_PSI
|
||||
0x03a9: 0x07d9, // XK_Greek_OMEGA
|
||||
0x03aa: 0x07a5, // XK_Greek_IOTAdieresis
|
||||
0x03ab: 0x07a9, // XK_Greek_UPSILONdieresis
|
||||
0x03ac: 0x07b1, // XK_Greek_alphaaccent
|
||||
0x03ad: 0x07b2, // XK_Greek_epsilonaccent
|
||||
0x03ae: 0x07b3, // XK_Greek_etaaccent
|
||||
0x03af: 0x07b4, // XK_Greek_iotaaccent
|
||||
0x03b0: 0x07ba, // XK_Greek_upsilonaccentdieresis
|
||||
0x03b1: 0x07e1, // XK_Greek_alpha
|
||||
0x03b2: 0x07e2, // XK_Greek_beta
|
||||
0x03b3: 0x07e3, // XK_Greek_gamma
|
||||
0x03b4: 0x07e4, // XK_Greek_delta
|
||||
0x03b5: 0x07e5, // XK_Greek_epsilon
|
||||
0x03b6: 0x07e6, // XK_Greek_zeta
|
||||
0x03b7: 0x07e7, // XK_Greek_eta
|
||||
0x03b8: 0x07e8, // XK_Greek_theta
|
||||
0x03b9: 0x07e9, // XK_Greek_iota
|
||||
0x03ba: 0x07ea, // XK_Greek_kappa
|
||||
0x03bb: 0x07eb, // XK_Greek_lamda
|
||||
0x03bc: 0x07ec, // XK_Greek_mu
|
||||
0x03bd: 0x07ed, // XK_Greek_nu
|
||||
0x03be: 0x07ee, // XK_Greek_xi
|
||||
0x03bf: 0x07ef, // XK_Greek_omicron
|
||||
0x03c0: 0x07f0, // XK_Greek_pi
|
||||
0x03c1: 0x07f1, // XK_Greek_rho
|
||||
0x03c2: 0x07f3, // XK_Greek_finalsmallsigma
|
||||
0x03c3: 0x07f2, // XK_Greek_sigma
|
||||
0x03c4: 0x07f4, // XK_Greek_tau
|
||||
0x03c5: 0x07f5, // XK_Greek_upsilon
|
||||
0x03c6: 0x07f6, // XK_Greek_phi
|
||||
0x03c7: 0x07f7, // XK_Greek_chi
|
||||
0x03c8: 0x07f8, // XK_Greek_psi
|
||||
0x03c9: 0x07f9, // XK_Greek_omega
|
||||
0x03ca: 0x07b5, // XK_Greek_iotadieresis
|
||||
0x03cb: 0x07b9, // XK_Greek_upsilondieresis
|
||||
0x03cc: 0x07b7, // XK_Greek_omicronaccent
|
||||
0x03cd: 0x07b8, // XK_Greek_upsilonaccent
|
||||
0x03ce: 0x07bb, // XK_Greek_omegaaccent
|
||||
0x0401: 0x06b3, // XK_Cyrillic_IO
|
||||
0x0402: 0x06b1, // XK_Serbian_DJE
|
||||
0x0403: 0x06b2, // XK_Macedonia_GJE
|
||||
0x0404: 0x06b4, // XK_Ukrainian_IE
|
||||
0x0405: 0x06b5, // XK_Macedonia_DSE
|
||||
0x0406: 0x06b6, // XK_Ukrainian_I
|
||||
0x0407: 0x06b7, // XK_Ukrainian_YI
|
||||
0x0408: 0x06b8, // XK_Cyrillic_JE
|
||||
0x0409: 0x06b9, // XK_Cyrillic_LJE
|
||||
0x040a: 0x06ba, // XK_Cyrillic_NJE
|
||||
0x040b: 0x06bb, // XK_Serbian_TSHE
|
||||
0x040c: 0x06bc, // XK_Macedonia_KJE
|
||||
0x040e: 0x06be, // XK_Byelorussian_SHORTU
|
||||
0x040f: 0x06bf, // XK_Cyrillic_DZHE
|
||||
0x0410: 0x06e1, // XK_Cyrillic_A
|
||||
0x0411: 0x06e2, // XK_Cyrillic_BE
|
||||
0x0412: 0x06f7, // XK_Cyrillic_VE
|
||||
0x0413: 0x06e7, // XK_Cyrillic_GHE
|
||||
0x0414: 0x06e4, // XK_Cyrillic_DE
|
||||
0x0415: 0x06e5, // XK_Cyrillic_IE
|
||||
0x0416: 0x06f6, // XK_Cyrillic_ZHE
|
||||
0x0417: 0x06fa, // XK_Cyrillic_ZE
|
||||
0x0418: 0x06e9, // XK_Cyrillic_I
|
||||
0x0419: 0x06ea, // XK_Cyrillic_SHORTI
|
||||
0x041a: 0x06eb, // XK_Cyrillic_KA
|
||||
0x041b: 0x06ec, // XK_Cyrillic_EL
|
||||
0x041c: 0x06ed, // XK_Cyrillic_EM
|
||||
0x041d: 0x06ee, // XK_Cyrillic_EN
|
||||
0x041e: 0x06ef, // XK_Cyrillic_O
|
||||
0x041f: 0x06f0, // XK_Cyrillic_PE
|
||||
0x0420: 0x06f2, // XK_Cyrillic_ER
|
||||
0x0421: 0x06f3, // XK_Cyrillic_ES
|
||||
0x0422: 0x06f4, // XK_Cyrillic_TE
|
||||
0x0423: 0x06f5, // XK_Cyrillic_U
|
||||
0x0424: 0x06e6, // XK_Cyrillic_EF
|
||||
0x0425: 0x06e8, // XK_Cyrillic_HA
|
||||
0x0426: 0x06e3, // XK_Cyrillic_TSE
|
||||
0x0427: 0x06fe, // XK_Cyrillic_CHE
|
||||
0x0428: 0x06fb, // XK_Cyrillic_SHA
|
||||
0x0429: 0x06fd, // XK_Cyrillic_SHCHA
|
||||
0x042a: 0x06ff, // XK_Cyrillic_HARDSIGN
|
||||
0x042b: 0x06f9, // XK_Cyrillic_YERU
|
||||
0x042c: 0x06f8, // XK_Cyrillic_SOFTSIGN
|
||||
0x042d: 0x06fc, // XK_Cyrillic_E
|
||||
0x042e: 0x06e0, // XK_Cyrillic_YU
|
||||
0x042f: 0x06f1, // XK_Cyrillic_YA
|
||||
0x0430: 0x06c1, // XK_Cyrillic_a
|
||||
0x0431: 0x06c2, // XK_Cyrillic_be
|
||||
0x0432: 0x06d7, // XK_Cyrillic_ve
|
||||
0x0433: 0x06c7, // XK_Cyrillic_ghe
|
||||
0x0434: 0x06c4, // XK_Cyrillic_de
|
||||
0x0435: 0x06c5, // XK_Cyrillic_ie
|
||||
0x0436: 0x06d6, // XK_Cyrillic_zhe
|
||||
0x0437: 0x06da, // XK_Cyrillic_ze
|
||||
0x0438: 0x06c9, // XK_Cyrillic_i
|
||||
0x0439: 0x06ca, // XK_Cyrillic_shorti
|
||||
0x043a: 0x06cb, // XK_Cyrillic_ka
|
||||
0x043b: 0x06cc, // XK_Cyrillic_el
|
||||
0x043c: 0x06cd, // XK_Cyrillic_em
|
||||
0x043d: 0x06ce, // XK_Cyrillic_en
|
||||
0x043e: 0x06cf, // XK_Cyrillic_o
|
||||
0x043f: 0x06d0, // XK_Cyrillic_pe
|
||||
0x0440: 0x06d2, // XK_Cyrillic_er
|
||||
0x0441: 0x06d3, // XK_Cyrillic_es
|
||||
0x0442: 0x06d4, // XK_Cyrillic_te
|
||||
0x0443: 0x06d5, // XK_Cyrillic_u
|
||||
0x0444: 0x06c6, // XK_Cyrillic_ef
|
||||
0x0445: 0x06c8, // XK_Cyrillic_ha
|
||||
0x0446: 0x06c3, // XK_Cyrillic_tse
|
||||
0x0447: 0x06de, // XK_Cyrillic_che
|
||||
0x0448: 0x06db, // XK_Cyrillic_sha
|
||||
0x0449: 0x06dd, // XK_Cyrillic_shcha
|
||||
0x044a: 0x06df, // XK_Cyrillic_hardsign
|
||||
0x044b: 0x06d9, // XK_Cyrillic_yeru
|
||||
0x044c: 0x06d8, // XK_Cyrillic_softsign
|
||||
0x044d: 0x06dc, // XK_Cyrillic_e
|
||||
0x044e: 0x06c0, // XK_Cyrillic_yu
|
||||
0x044f: 0x06d1, // XK_Cyrillic_ya
|
||||
0x0451: 0x06a3, // XK_Cyrillic_io
|
||||
0x0452: 0x06a1, // XK_Serbian_dje
|
||||
0x0453: 0x06a2, // XK_Macedonia_gje
|
||||
0x0454: 0x06a4, // XK_Ukrainian_ie
|
||||
0x0455: 0x06a5, // XK_Macedonia_dse
|
||||
0x0456: 0x06a6, // XK_Ukrainian_i
|
||||
0x0457: 0x06a7, // XK_Ukrainian_yi
|
||||
0x0458: 0x06a8, // XK_Cyrillic_je
|
||||
0x0459: 0x06a9, // XK_Cyrillic_lje
|
||||
0x045a: 0x06aa, // XK_Cyrillic_nje
|
||||
0x045b: 0x06ab, // XK_Serbian_tshe
|
||||
0x045c: 0x06ac, // XK_Macedonia_kje
|
||||
0x045e: 0x06ae, // XK_Byelorussian_shortu
|
||||
0x045f: 0x06af, // XK_Cyrillic_dzhe
|
||||
0x0490: 0x06bd, // XK_Ukrainian_GHE_WITH_UPTURN
|
||||
0x0491: 0x06ad, // XK_Ukrainian_ghe_with_upturn
|
||||
0x05d0: 0x0ce0, // XK_hebrew_aleph
|
||||
0x05d1: 0x0ce1, // XK_hebrew_bet
|
||||
0x05d2: 0x0ce2, // XK_hebrew_gimel
|
||||
0x05d3: 0x0ce3, // XK_hebrew_dalet
|
||||
0x05d4: 0x0ce4, // XK_hebrew_he
|
||||
0x05d5: 0x0ce5, // XK_hebrew_waw
|
||||
0x05d6: 0x0ce6, // XK_hebrew_zain
|
||||
0x05d7: 0x0ce7, // XK_hebrew_chet
|
||||
0x05d8: 0x0ce8, // XK_hebrew_tet
|
||||
0x05d9: 0x0ce9, // XK_hebrew_yod
|
||||
0x05da: 0x0cea, // XK_hebrew_finalkaph
|
||||
0x05db: 0x0ceb, // XK_hebrew_kaph
|
||||
0x05dc: 0x0cec, // XK_hebrew_lamed
|
||||
0x05dd: 0x0ced, // XK_hebrew_finalmem
|
||||
0x05de: 0x0cee, // XK_hebrew_mem
|
||||
0x05df: 0x0cef, // XK_hebrew_finalnun
|
||||
0x05e0: 0x0cf0, // XK_hebrew_nun
|
||||
0x05e1: 0x0cf1, // XK_hebrew_samech
|
||||
0x05e2: 0x0cf2, // XK_hebrew_ayin
|
||||
0x05e3: 0x0cf3, // XK_hebrew_finalpe
|
||||
0x05e4: 0x0cf4, // XK_hebrew_pe
|
||||
0x05e5: 0x0cf5, // XK_hebrew_finalzade
|
||||
0x05e6: 0x0cf6, // XK_hebrew_zade
|
||||
0x05e7: 0x0cf7, // XK_hebrew_qoph
|
||||
0x05e8: 0x0cf8, // XK_hebrew_resh
|
||||
0x05e9: 0x0cf9, // XK_hebrew_shin
|
||||
0x05ea: 0x0cfa, // XK_hebrew_taw
|
||||
0x060c: 0x05ac, // XK_Arabic_comma
|
||||
0x061b: 0x05bb, // XK_Arabic_semicolon
|
||||
0x061f: 0x05bf, // XK_Arabic_question_mark
|
||||
0x0621: 0x05c1, // XK_Arabic_hamza
|
||||
0x0622: 0x05c2, // XK_Arabic_maddaonalef
|
||||
0x0623: 0x05c3, // XK_Arabic_hamzaonalef
|
||||
0x0624: 0x05c4, // XK_Arabic_hamzaonwaw
|
||||
0x0625: 0x05c5, // XK_Arabic_hamzaunderalef
|
||||
0x0626: 0x05c6, // XK_Arabic_hamzaonyeh
|
||||
0x0627: 0x05c7, // XK_Arabic_alef
|
||||
0x0628: 0x05c8, // XK_Arabic_beh
|
||||
0x0629: 0x05c9, // XK_Arabic_tehmarbuta
|
||||
0x062a: 0x05ca, // XK_Arabic_teh
|
||||
0x062b: 0x05cb, // XK_Arabic_theh
|
||||
0x062c: 0x05cc, // XK_Arabic_jeem
|
||||
0x062d: 0x05cd, // XK_Arabic_hah
|
||||
0x062e: 0x05ce, // XK_Arabic_khah
|
||||
0x062f: 0x05cf, // XK_Arabic_dal
|
||||
0x0630: 0x05d0, // XK_Arabic_thal
|
||||
0x0631: 0x05d1, // XK_Arabic_ra
|
||||
0x0632: 0x05d2, // XK_Arabic_zain
|
||||
0x0633: 0x05d3, // XK_Arabic_seen
|
||||
0x0634: 0x05d4, // XK_Arabic_sheen
|
||||
0x0635: 0x05d5, // XK_Arabic_sad
|
||||
0x0636: 0x05d6, // XK_Arabic_dad
|
||||
0x0637: 0x05d7, // XK_Arabic_tah
|
||||
0x0638: 0x05d8, // XK_Arabic_zah
|
||||
0x0639: 0x05d9, // XK_Arabic_ain
|
||||
0x063a: 0x05da, // XK_Arabic_ghain
|
||||
0x0640: 0x05e0, // XK_Arabic_tatweel
|
||||
0x0641: 0x05e1, // XK_Arabic_feh
|
||||
0x0642: 0x05e2, // XK_Arabic_qaf
|
||||
0x0643: 0x05e3, // XK_Arabic_kaf
|
||||
0x0644: 0x05e4, // XK_Arabic_lam
|
||||
0x0645: 0x05e5, // XK_Arabic_meem
|
||||
0x0646: 0x05e6, // XK_Arabic_noon
|
||||
0x0647: 0x05e7, // XK_Arabic_ha
|
||||
0x0648: 0x05e8, // XK_Arabic_waw
|
||||
0x0649: 0x05e9, // XK_Arabic_alefmaksura
|
||||
0x064a: 0x05ea, // XK_Arabic_yeh
|
||||
0x064b: 0x05eb, // XK_Arabic_fathatan
|
||||
0x064c: 0x05ec, // XK_Arabic_dammatan
|
||||
0x064d: 0x05ed, // XK_Arabic_kasratan
|
||||
0x064e: 0x05ee, // XK_Arabic_fatha
|
||||
0x064f: 0x05ef, // XK_Arabic_damma
|
||||
0x0650: 0x05f0, // XK_Arabic_kasra
|
||||
0x0651: 0x05f1, // XK_Arabic_shadda
|
||||
0x0652: 0x05f2, // XK_Arabic_sukun
|
||||
0x0e01: 0x0da1, // XK_Thai_kokai
|
||||
0x0e02: 0x0da2, // XK_Thai_khokhai
|
||||
0x0e03: 0x0da3, // XK_Thai_khokhuat
|
||||
0x0e04: 0x0da4, // XK_Thai_khokhwai
|
||||
0x0e05: 0x0da5, // XK_Thai_khokhon
|
||||
0x0e06: 0x0da6, // XK_Thai_khorakhang
|
||||
0x0e07: 0x0da7, // XK_Thai_ngongu
|
||||
0x0e08: 0x0da8, // XK_Thai_chochan
|
||||
0x0e09: 0x0da9, // XK_Thai_choching
|
||||
0x0e0a: 0x0daa, // XK_Thai_chochang
|
||||
0x0e0b: 0x0dab, // XK_Thai_soso
|
||||
0x0e0c: 0x0dac, // XK_Thai_chochoe
|
||||
0x0e0d: 0x0dad, // XK_Thai_yoying
|
||||
0x0e0e: 0x0dae, // XK_Thai_dochada
|
||||
0x0e0f: 0x0daf, // XK_Thai_topatak
|
||||
0x0e10: 0x0db0, // XK_Thai_thothan
|
||||
0x0e11: 0x0db1, // XK_Thai_thonangmontho
|
||||
0x0e12: 0x0db2, // XK_Thai_thophuthao
|
||||
0x0e13: 0x0db3, // XK_Thai_nonen
|
||||
0x0e14: 0x0db4, // XK_Thai_dodek
|
||||
0x0e15: 0x0db5, // XK_Thai_totao
|
||||
0x0e16: 0x0db6, // XK_Thai_thothung
|
||||
0x0e17: 0x0db7, // XK_Thai_thothahan
|
||||
0x0e18: 0x0db8, // XK_Thai_thothong
|
||||
0x0e19: 0x0db9, // XK_Thai_nonu
|
||||
0x0e1a: 0x0dba, // XK_Thai_bobaimai
|
||||
0x0e1b: 0x0dbb, // XK_Thai_popla
|
||||
0x0e1c: 0x0dbc, // XK_Thai_phophung
|
||||
0x0e1d: 0x0dbd, // XK_Thai_fofa
|
||||
0x0e1e: 0x0dbe, // XK_Thai_phophan
|
||||
0x0e1f: 0x0dbf, // XK_Thai_fofan
|
||||
0x0e20: 0x0dc0, // XK_Thai_phosamphao
|
||||
0x0e21: 0x0dc1, // XK_Thai_moma
|
||||
0x0e22: 0x0dc2, // XK_Thai_yoyak
|
||||
0x0e23: 0x0dc3, // XK_Thai_rorua
|
||||
0x0e24: 0x0dc4, // XK_Thai_ru
|
||||
0x0e25: 0x0dc5, // XK_Thai_loling
|
||||
0x0e26: 0x0dc6, // XK_Thai_lu
|
||||
0x0e27: 0x0dc7, // XK_Thai_wowaen
|
||||
0x0e28: 0x0dc8, // XK_Thai_sosala
|
||||
0x0e29: 0x0dc9, // XK_Thai_sorusi
|
||||
0x0e2a: 0x0dca, // XK_Thai_sosua
|
||||
0x0e2b: 0x0dcb, // XK_Thai_hohip
|
||||
0x0e2c: 0x0dcc, // XK_Thai_lochula
|
||||
0x0e2d: 0x0dcd, // XK_Thai_oang
|
||||
0x0e2e: 0x0dce, // XK_Thai_honokhuk
|
||||
0x0e2f: 0x0dcf, // XK_Thai_paiyannoi
|
||||
0x0e30: 0x0dd0, // XK_Thai_saraa
|
||||
0x0e31: 0x0dd1, // XK_Thai_maihanakat
|
||||
0x0e32: 0x0dd2, // XK_Thai_saraaa
|
||||
0x0e33: 0x0dd3, // XK_Thai_saraam
|
||||
0x0e34: 0x0dd4, // XK_Thai_sarai
|
||||
0x0e35: 0x0dd5, // XK_Thai_saraii
|
||||
0x0e36: 0x0dd6, // XK_Thai_saraue
|
||||
0x0e37: 0x0dd7, // XK_Thai_sarauee
|
||||
0x0e38: 0x0dd8, // XK_Thai_sarau
|
||||
0x0e39: 0x0dd9, // XK_Thai_sarauu
|
||||
0x0e3a: 0x0dda, // XK_Thai_phinthu
|
||||
0x0e3f: 0x0ddf, // XK_Thai_baht
|
||||
0x0e40: 0x0de0, // XK_Thai_sarae
|
||||
0x0e41: 0x0de1, // XK_Thai_saraae
|
||||
0x0e42: 0x0de2, // XK_Thai_sarao
|
||||
0x0e43: 0x0de3, // XK_Thai_saraaimaimuan
|
||||
0x0e44: 0x0de4, // XK_Thai_saraaimaimalai
|
||||
0x0e45: 0x0de5, // XK_Thai_lakkhangyao
|
||||
0x0e46: 0x0de6, // XK_Thai_maiyamok
|
||||
0x0e47: 0x0de7, // XK_Thai_maitaikhu
|
||||
0x0e48: 0x0de8, // XK_Thai_maiek
|
||||
0x0e49: 0x0de9, // XK_Thai_maitho
|
||||
0x0e4a: 0x0dea, // XK_Thai_maitri
|
||||
0x0e4b: 0x0deb, // XK_Thai_maichattawa
|
||||
0x0e4c: 0x0dec, // XK_Thai_thanthakhat
|
||||
0x0e4d: 0x0ded, // XK_Thai_nikhahit
|
||||
0x0e50: 0x0df0, // XK_Thai_leksun
|
||||
0x0e51: 0x0df1, // XK_Thai_leknung
|
||||
0x0e52: 0x0df2, // XK_Thai_leksong
|
||||
0x0e53: 0x0df3, // XK_Thai_leksam
|
||||
0x0e54: 0x0df4, // XK_Thai_leksi
|
||||
0x0e55: 0x0df5, // XK_Thai_lekha
|
||||
0x0e56: 0x0df6, // XK_Thai_lekhok
|
||||
0x0e57: 0x0df7, // XK_Thai_lekchet
|
||||
0x0e58: 0x0df8, // XK_Thai_lekpaet
|
||||
0x0e59: 0x0df9, // XK_Thai_lekkao
|
||||
0x2002: 0x0aa2, // XK_enspace
|
||||
0x2003: 0x0aa1, // XK_emspace
|
||||
0x2004: 0x0aa3, // XK_em3space
|
||||
0x2005: 0x0aa4, // XK_em4space
|
||||
0x2007: 0x0aa5, // XK_digitspace
|
||||
0x2008: 0x0aa6, // XK_punctspace
|
||||
0x2009: 0x0aa7, // XK_thinspace
|
||||
0x200a: 0x0aa8, // XK_hairspace
|
||||
0x2012: 0x0abb, // XK_figdash
|
||||
0x2013: 0x0aaa, // XK_endash
|
||||
0x2014: 0x0aa9, // XK_emdash
|
||||
0x2015: 0x07af, // XK_Greek_horizbar
|
||||
0x2017: 0x0cdf, // XK_hebrew_doublelowline
|
||||
0x2018: 0x0ad0, // XK_leftsinglequotemark
|
||||
0x2019: 0x0ad1, // XK_rightsinglequotemark
|
||||
0x201a: 0x0afd, // XK_singlelowquotemark
|
||||
0x201c: 0x0ad2, // XK_leftdoublequotemark
|
||||
0x201d: 0x0ad3, // XK_rightdoublequotemark
|
||||
0x201e: 0x0afe, // XK_doublelowquotemark
|
||||
0x2020: 0x0af1, // XK_dagger
|
||||
0x2021: 0x0af2, // XK_doubledagger
|
||||
0x2022: 0x0ae6, // XK_enfilledcircbullet
|
||||
0x2025: 0x0aaf, // XK_doubbaselinedot
|
||||
0x2026: 0x0aae, // XK_ellipsis
|
||||
0x2030: 0x0ad5, // XK_permille
|
||||
0x2032: 0x0ad6, // XK_minutes
|
||||
0x2033: 0x0ad7, // XK_seconds
|
||||
0x2038: 0x0afc, // XK_caret
|
||||
0x203e: 0x047e, // XK_overline
|
||||
0x20a9: 0x0eff, // XK_Korean_Won
|
||||
0x20ac: 0x20ac, // XK_EuroSign
|
||||
0x2105: 0x0ab8, // XK_careof
|
||||
0x2116: 0x06b0, // XK_numerosign
|
||||
0x2117: 0x0afb, // XK_phonographcopyright
|
||||
0x211e: 0x0ad4, // XK_prescription
|
||||
0x2122: 0x0ac9, // XK_trademark
|
||||
0x2153: 0x0ab0, // XK_onethird
|
||||
0x2154: 0x0ab1, // XK_twothirds
|
||||
0x2155: 0x0ab2, // XK_onefifth
|
||||
0x2156: 0x0ab3, // XK_twofifths
|
||||
0x2157: 0x0ab4, // XK_threefifths
|
||||
0x2158: 0x0ab5, // XK_fourfifths
|
||||
0x2159: 0x0ab6, // XK_onesixth
|
||||
0x215a: 0x0ab7, // XK_fivesixths
|
||||
0x215b: 0x0ac3, // XK_oneeighth
|
||||
0x215c: 0x0ac4, // XK_threeeighths
|
||||
0x215d: 0x0ac5, // XK_fiveeighths
|
||||
0x215e: 0x0ac6, // XK_seveneighths
|
||||
0x2190: 0x08fb, // XK_leftarrow
|
||||
0x2191: 0x08fc, // XK_uparrow
|
||||
0x2192: 0x08fd, // XK_rightarrow
|
||||
0x2193: 0x08fe, // XK_downarrow
|
||||
0x21d2: 0x08ce, // XK_implies
|
||||
0x21d4: 0x08cd, // XK_ifonlyif
|
||||
0x2202: 0x08ef, // XK_partialderivative
|
||||
0x2207: 0x08c5, // XK_nabla
|
||||
0x2218: 0x0bca, // XK_jot
|
||||
0x221a: 0x08d6, // XK_radical
|
||||
0x221d: 0x08c1, // XK_variation
|
||||
0x221e: 0x08c2, // XK_infinity
|
||||
0x2227: 0x08de, // XK_logicaland
|
||||
0x2228: 0x08df, // XK_logicalor
|
||||
0x2229: 0x08dc, // XK_intersection
|
||||
0x222a: 0x08dd, // XK_union
|
||||
0x222b: 0x08bf, // XK_integral
|
||||
0x2234: 0x08c0, // XK_therefore
|
||||
0x223c: 0x08c8, // XK_approximate
|
||||
0x2243: 0x08c9, // XK_similarequal
|
||||
0x2245: 0x1002248, // XK_approxeq
|
||||
0x2260: 0x08bd, // XK_notequal
|
||||
0x2261: 0x08cf, // XK_identical
|
||||
0x2264: 0x08bc, // XK_lessthanequal
|
||||
0x2265: 0x08be, // XK_greaterthanequal
|
||||
0x2282: 0x08da, // XK_includedin
|
||||
0x2283: 0x08db, // XK_includes
|
||||
0x22a2: 0x0bfc, // XK_righttack
|
||||
0x22a3: 0x0bdc, // XK_lefttack
|
||||
0x22a4: 0x0bc2, // XK_downtack
|
||||
0x22a5: 0x0bce, // XK_uptack
|
||||
0x2308: 0x0bd3, // XK_upstile
|
||||
0x230a: 0x0bc4, // XK_downstile
|
||||
0x2315: 0x0afa, // XK_telephonerecorder
|
||||
0x2320: 0x08a4, // XK_topintegral
|
||||
0x2321: 0x08a5, // XK_botintegral
|
||||
0x2395: 0x0bcc, // XK_quad
|
||||
0x239b: 0x08ab, // XK_topleftparens
|
||||
0x239d: 0x08ac, // XK_botleftparens
|
||||
0x239e: 0x08ad, // XK_toprightparens
|
||||
0x23a0: 0x08ae, // XK_botrightparens
|
||||
0x23a1: 0x08a7, // XK_topleftsqbracket
|
||||
0x23a3: 0x08a8, // XK_botleftsqbracket
|
||||
0x23a4: 0x08a9, // XK_toprightsqbracket
|
||||
0x23a6: 0x08aa, // XK_botrightsqbracket
|
||||
0x23a8: 0x08af, // XK_leftmiddlecurlybrace
|
||||
0x23ac: 0x08b0, // XK_rightmiddlecurlybrace
|
||||
0x23b7: 0x08a1, // XK_leftradical
|
||||
0x23ba: 0x09ef, // XK_horizlinescan1
|
||||
0x23bb: 0x09f0, // XK_horizlinescan3
|
||||
0x23bc: 0x09f2, // XK_horizlinescan7
|
||||
0x23bd: 0x09f3, // XK_horizlinescan9
|
||||
0x2409: 0x09e2, // XK_ht
|
||||
0x240a: 0x09e5, // XK_lf
|
||||
0x240b: 0x09e9, // XK_vt
|
||||
0x240c: 0x09e3, // XK_ff
|
||||
0x240d: 0x09e4, // XK_cr
|
||||
0x2423: 0x0aac, // XK_signifblank
|
||||
0x2424: 0x09e8, // XK_nl
|
||||
0x2500: 0x08a3, // XK_horizconnector
|
||||
0x2502: 0x08a6, // XK_vertconnector
|
||||
0x250c: 0x08a2, // XK_topleftradical
|
||||
0x2510: 0x09eb, // XK_uprightcorner
|
||||
0x2514: 0x09ed, // XK_lowleftcorner
|
||||
0x2518: 0x09ea, // XK_lowrightcorner
|
||||
0x251c: 0x09f4, // XK_leftt
|
||||
0x2524: 0x09f5, // XK_rightt
|
||||
0x252c: 0x09f7, // XK_topt
|
||||
0x2534: 0x09f6, // XK_bott
|
||||
0x253c: 0x09ee, // XK_crossinglines
|
||||
0x2592: 0x09e1, // XK_checkerboard
|
||||
0x25aa: 0x0ae7, // XK_enfilledsqbullet
|
||||
0x25ab: 0x0ae1, // XK_enopensquarebullet
|
||||
0x25ac: 0x0adb, // XK_filledrectbullet
|
||||
0x25ad: 0x0ae2, // XK_openrectbullet
|
||||
0x25ae: 0x0adf, // XK_emfilledrect
|
||||
0x25af: 0x0acf, // XK_emopenrectangle
|
||||
0x25b2: 0x0ae8, // XK_filledtribulletup
|
||||
0x25b3: 0x0ae3, // XK_opentribulletup
|
||||
0x25b6: 0x0add, // XK_filledrighttribullet
|
||||
0x25b7: 0x0acd, // XK_rightopentriangle
|
||||
0x25bc: 0x0ae9, // XK_filledtribulletdown
|
||||
0x25bd: 0x0ae4, // XK_opentribulletdown
|
||||
0x25c0: 0x0adc, // XK_filledlefttribullet
|
||||
0x25c1: 0x0acc, // XK_leftopentriangle
|
||||
0x25c6: 0x09e0, // XK_soliddiamond
|
||||
0x25cb: 0x0ace, // XK_emopencircle
|
||||
0x25cf: 0x0ade, // XK_emfilledcircle
|
||||
0x25e6: 0x0ae0, // XK_enopencircbullet
|
||||
0x2606: 0x0ae5, // XK_openstar
|
||||
0x260e: 0x0af9, // XK_telephone
|
||||
0x2613: 0x0aca, // XK_signaturemark
|
||||
0x261c: 0x0aea, // XK_leftpointer
|
||||
0x261e: 0x0aeb, // XK_rightpointer
|
||||
0x2640: 0x0af8, // XK_femalesymbol
|
||||
0x2642: 0x0af7, // XK_malesymbol
|
||||
0x2663: 0x0aec, // XK_club
|
||||
0x2665: 0x0aee, // XK_heart
|
||||
0x2666: 0x0aed, // XK_diamond
|
||||
0x266d: 0x0af6, // XK_musicalflat
|
||||
0x266f: 0x0af5, // XK_musicalsharp
|
||||
0x2713: 0x0af3, // XK_checkmark
|
||||
0x2717: 0x0af4, // XK_ballotcross
|
||||
0x271d: 0x0ad9, // XK_latincross
|
||||
0x2720: 0x0af0, // XK_maltesecross
|
||||
0x27e8: 0x0abc, // XK_leftanglebracket
|
||||
0x27e9: 0x0abe, // XK_rightanglebracket
|
||||
0x3001: 0x04a4, // XK_kana_comma
|
||||
0x3002: 0x04a1, // XK_kana_fullstop
|
||||
0x300c: 0x04a2, // XK_kana_openingbracket
|
||||
0x300d: 0x04a3, // XK_kana_closingbracket
|
||||
0x309b: 0x04de, // XK_voicedsound
|
||||
0x309c: 0x04df, // XK_semivoicedsound
|
||||
0x30a1: 0x04a7, // XK_kana_a
|
||||
0x30a2: 0x04b1, // XK_kana_A
|
||||
0x30a3: 0x04a8, // XK_kana_i
|
||||
0x30a4: 0x04b2, // XK_kana_I
|
||||
0x30a5: 0x04a9, // XK_kana_u
|
||||
0x30a6: 0x04b3, // XK_kana_U
|
||||
0x30a7: 0x04aa, // XK_kana_e
|
||||
0x30a8: 0x04b4, // XK_kana_E
|
||||
0x30a9: 0x04ab, // XK_kana_o
|
||||
0x30aa: 0x04b5, // XK_kana_O
|
||||
0x30ab: 0x04b6, // XK_kana_KA
|
||||
0x30ad: 0x04b7, // XK_kana_KI
|
||||
0x30af: 0x04b8, // XK_kana_KU
|
||||
0x30b1: 0x04b9, // XK_kana_KE
|
||||
0x30b3: 0x04ba, // XK_kana_KO
|
||||
0x30b5: 0x04bb, // XK_kana_SA
|
||||
0x30b7: 0x04bc, // XK_kana_SHI
|
||||
0x30b9: 0x04bd, // XK_kana_SU
|
||||
0x30bb: 0x04be, // XK_kana_SE
|
||||
0x30bd: 0x04bf, // XK_kana_SO
|
||||
0x30bf: 0x04c0, // XK_kana_TA
|
||||
0x30c1: 0x04c1, // XK_kana_CHI
|
||||
0x30c3: 0x04af, // XK_kana_tsu
|
||||
0x30c4: 0x04c2, // XK_kana_TSU
|
||||
0x30c6: 0x04c3, // XK_kana_TE
|
||||
0x30c8: 0x04c4, // XK_kana_TO
|
||||
0x30ca: 0x04c5, // XK_kana_NA
|
||||
0x30cb: 0x04c6, // XK_kana_NI
|
||||
0x30cc: 0x04c7, // XK_kana_NU
|
||||
0x30cd: 0x04c8, // XK_kana_NE
|
||||
0x30ce: 0x04c9, // XK_kana_NO
|
||||
0x30cf: 0x04ca, // XK_kana_HA
|
||||
0x30d2: 0x04cb, // XK_kana_HI
|
||||
0x30d5: 0x04cc, // XK_kana_FU
|
||||
0x30d8: 0x04cd, // XK_kana_HE
|
||||
0x30db: 0x04ce, // XK_kana_HO
|
||||
0x30de: 0x04cf, // XK_kana_MA
|
||||
0x30df: 0x04d0, // XK_kana_MI
|
||||
0x30e0: 0x04d1, // XK_kana_MU
|
||||
0x30e1: 0x04d2, // XK_kana_ME
|
||||
0x30e2: 0x04d3, // XK_kana_MO
|
||||
0x30e3: 0x04ac, // XK_kana_ya
|
||||
0x30e4: 0x04d4, // XK_kana_YA
|
||||
0x30e5: 0x04ad, // XK_kana_yu
|
||||
0x30e6: 0x04d5, // XK_kana_YU
|
||||
0x30e7: 0x04ae, // XK_kana_yo
|
||||
0x30e8: 0x04d6, // XK_kana_YO
|
||||
0x30e9: 0x04d7, // XK_kana_RA
|
||||
0x30ea: 0x04d8, // XK_kana_RI
|
||||
0x30eb: 0x04d9, // XK_kana_RU
|
||||
0x30ec: 0x04da, // XK_kana_RE
|
||||
0x30ed: 0x04db, // XK_kana_RO
|
||||
0x30ef: 0x04dc, // XK_kana_WA
|
||||
0x30f2: 0x04a6, // XK_kana_WO
|
||||
0x30f3: 0x04dd, // XK_kana_N
|
||||
0x30fb: 0x04a5, // XK_kana_conjunctive
|
||||
0x30fc: 0x04b0, // XK_prolongedsound
|
||||
};
|
||||
|
||||
export default {
|
||||
lookup(u) {
|
||||
// Latin-1 is one-to-one mapping
|
||||
if (u >= 0x20 && u <= 0xff) {
|
||||
return u;
|
||||
}
|
||||
|
||||
// Lookup table (fairly random)
|
||||
const keysym = codepoints[u];
|
||||
if (keysym !== undefined) {
|
||||
return keysym;
|
||||
}
|
||||
|
||||
// General mapping as final fallback
|
||||
return 0x01000000 | u;
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,215 @@
|
||||
import KeyTable from './keysym.js';
|
||||
import keysyms from './keysymdef.js';
|
||||
import vkeys from './vkeys.js';
|
||||
import fixedkeys from './fixedkeys.js';
|
||||
import DOMKeyTable from './domkeytable.js';
|
||||
import * as browser from '../util/browser.js';
|
||||
|
||||
// Get 'KeyboardEvent.code', handling legacy browsers
|
||||
export function getKeycode(evt) {
|
||||
// Are we getting proper key identifiers?
|
||||
// (unfortunately Firefox and Chrome are crappy here and gives
|
||||
// us an empty string on some platforms, rather than leaving it
|
||||
// undefined)
|
||||
if (evt.code) {
|
||||
// Mozilla isn't fully in sync with the spec yet
|
||||
switch (evt.code) {
|
||||
case 'OSLeft':
|
||||
return 'MetaLeft';
|
||||
case 'OSRight':
|
||||
return 'MetaRight';
|
||||
}
|
||||
|
||||
return evt.code;
|
||||
}
|
||||
|
||||
// The de-facto standard is to use Windows Virtual-Key codes
|
||||
// in the 'keyCode' field for non-printable characters
|
||||
if (evt.keyCode in vkeys) {
|
||||
let code = vkeys[evt.keyCode];
|
||||
|
||||
// macOS has messed up this code for some reason
|
||||
if (browser.isMac() && code === 'ContextMenu') {
|
||||
code = 'MetaRight';
|
||||
}
|
||||
|
||||
// The keyCode doesn't distinguish between left and right
|
||||
// for the standard modifiers
|
||||
if (evt.location === 2) {
|
||||
switch (code) {
|
||||
case 'ShiftLeft':
|
||||
return 'ShiftRight';
|
||||
case 'ControlLeft':
|
||||
return 'ControlRight';
|
||||
case 'AltLeft':
|
||||
return 'AltRight';
|
||||
}
|
||||
}
|
||||
|
||||
// Nor a bunch of the numpad keys
|
||||
if (evt.location === 3) {
|
||||
switch (code) {
|
||||
case 'Delete':
|
||||
return 'NumpadDecimal';
|
||||
case 'Insert':
|
||||
return 'Numpad0';
|
||||
case 'End':
|
||||
return 'Numpad1';
|
||||
case 'ArrowDown':
|
||||
return 'Numpad2';
|
||||
case 'PageDown':
|
||||
return 'Numpad3';
|
||||
case 'ArrowLeft':
|
||||
return 'Numpad4';
|
||||
case 'ArrowRight':
|
||||
return 'Numpad6';
|
||||
case 'Home':
|
||||
return 'Numpad7';
|
||||
case 'ArrowUp':
|
||||
return 'Numpad8';
|
||||
case 'PageUp':
|
||||
return 'Numpad9';
|
||||
case 'Enter':
|
||||
return 'NumpadEnter';
|
||||
}
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
return 'Unidentified';
|
||||
}
|
||||
|
||||
// Get 'KeyboardEvent.key', handling legacy browsers
|
||||
export function getKey(evt) {
|
||||
// Are we getting a proper key value?
|
||||
if (evt.key !== undefined) {
|
||||
// Mozilla isn't fully in sync with the spec yet
|
||||
switch (evt.key) {
|
||||
case 'OS':
|
||||
return 'Meta';
|
||||
case 'LaunchMyComputer':
|
||||
return 'LaunchApplication1';
|
||||
case 'LaunchCalculator':
|
||||
return 'LaunchApplication2';
|
||||
}
|
||||
|
||||
// iOS leaks some OS names
|
||||
switch (evt.key) {
|
||||
case 'UIKeyInputUpArrow':
|
||||
return 'ArrowUp';
|
||||
case 'UIKeyInputDownArrow':
|
||||
return 'ArrowDown';
|
||||
case 'UIKeyInputLeftArrow':
|
||||
return 'ArrowLeft';
|
||||
case 'UIKeyInputRightArrow':
|
||||
return 'ArrowRight';
|
||||
case 'UIKeyInputEscape':
|
||||
return 'Escape';
|
||||
}
|
||||
|
||||
// Broken behaviour in Chrome
|
||||
if (evt.key === '\x00' && evt.code === 'NumpadDecimal') {
|
||||
return 'Delete';
|
||||
}
|
||||
|
||||
return evt.key;
|
||||
}
|
||||
|
||||
// Try to deduce it based on the physical key
|
||||
const code = getKeycode(evt);
|
||||
if (code in fixedkeys) {
|
||||
return fixedkeys[code];
|
||||
}
|
||||
|
||||
// If that failed, then see if we have a printable character
|
||||
if (evt.charCode) {
|
||||
return String.fromCharCode(evt.charCode);
|
||||
}
|
||||
|
||||
// At this point we have nothing left to go on
|
||||
return 'Unidentified';
|
||||
}
|
||||
|
||||
// Get the most reliable keysym value we can get from a key event
|
||||
export function getKeysym(evt) {
|
||||
const key = getKey(evt);
|
||||
|
||||
if (key === 'Unidentified') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// First look up special keys
|
||||
if (key in DOMKeyTable) {
|
||||
let location = evt.location;
|
||||
|
||||
// Safari screws up location for the right cmd key
|
||||
if (key === 'Meta' && location === 0) {
|
||||
location = 2;
|
||||
}
|
||||
|
||||
// And for Clear
|
||||
if (key === 'Clear' && location === 3) {
|
||||
let code = getKeycode(evt);
|
||||
if (code === 'NumLock') {
|
||||
location = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (location === undefined || location > 3) {
|
||||
location = 0;
|
||||
}
|
||||
|
||||
// The original Meta key now gets confused with the Windows key
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1020141
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1232918
|
||||
if (key === 'Meta') {
|
||||
let code = getKeycode(evt);
|
||||
if (code === 'AltLeft') {
|
||||
return KeyTable.XK_Meta_L;
|
||||
} else if (code === 'AltRight') {
|
||||
return KeyTable.XK_Meta_R;
|
||||
}
|
||||
}
|
||||
|
||||
// macOS has Clear instead of NumLock, but the remote system is
|
||||
// probably not macOS, so lying here is probably best...
|
||||
if (key === 'Clear') {
|
||||
let code = getKeycode(evt);
|
||||
if (code === 'NumLock') {
|
||||
return KeyTable.XK_Num_Lock;
|
||||
}
|
||||
}
|
||||
|
||||
// Windows sends alternating symbols for some keys when using a
|
||||
// Japanese layout. We have no way of synchronising with the IM
|
||||
// running on the remote system, so we send some combined keysym
|
||||
// instead and hope for the best.
|
||||
if (browser.isWindows()) {
|
||||
switch (key) {
|
||||
case 'Zenkaku':
|
||||
case 'Hankaku':
|
||||
return KeyTable.XK_Zenkaku_Hankaku;
|
||||
case 'Romaji':
|
||||
case 'KanaMode':
|
||||
return KeyTable.XK_Romaji;
|
||||
}
|
||||
}
|
||||
|
||||
return DOMKeyTable[key][location];
|
||||
}
|
||||
|
||||
// Now we need to look at the Unicode symbol instead
|
||||
|
||||
// Special key? (FIXME: Should have been caught earlier)
|
||||
if (key.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const codepoint = key.charCodeAt();
|
||||
if (codepoint) {
|
||||
return keysyms.lookup(codepoint);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2018 The noVNC Authors
|
||||
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
/*
|
||||
* Mapping between Microsoft® Windows® Virtual-Key codes and
|
||||
* HTML key codes.
|
||||
*/
|
||||
|
||||
export default {
|
||||
0x08: 'Backspace',
|
||||
0x09: 'Tab',
|
||||
0x0a: 'NumpadClear',
|
||||
0x0d: 'Enter',
|
||||
0x10: 'ShiftLeft',
|
||||
0x11: 'ControlLeft',
|
||||
0x12: 'AltLeft',
|
||||
0x13: 'Pause',
|
||||
0x14: 'CapsLock',
|
||||
0x15: 'Lang1',
|
||||
0x19: 'Lang2',
|
||||
0x1b: 'Escape',
|
||||
0x1c: 'Convert',
|
||||
0x1d: 'NonConvert',
|
||||
0x20: 'Space',
|
||||
0x21: 'PageUp',
|
||||
0x22: 'PageDown',
|
||||
0x23: 'End',
|
||||
0x24: 'Home',
|
||||
0x25: 'ArrowLeft',
|
||||
0x26: 'ArrowUp',
|
||||
0x27: 'ArrowRight',
|
||||
0x28: 'ArrowDown',
|
||||
0x29: 'Select',
|
||||
0x2c: 'PrintScreen',
|
||||
0x2d: 'Insert',
|
||||
0x2e: 'Delete',
|
||||
0x2f: 'Help',
|
||||
0x30: 'Digit0',
|
||||
0x31: 'Digit1',
|
||||
0x32: 'Digit2',
|
||||
0x33: 'Digit3',
|
||||
0x34: 'Digit4',
|
||||
0x35: 'Digit5',
|
||||
0x36: 'Digit6',
|
||||
0x37: 'Digit7',
|
||||
0x38: 'Digit8',
|
||||
0x39: 'Digit9',
|
||||
0x5b: 'MetaLeft',
|
||||
0x5c: 'MetaRight',
|
||||
0x5d: 'ContextMenu',
|
||||
0x5f: 'Sleep',
|
||||
0x60: 'Numpad0',
|
||||
0x61: 'Numpad1',
|
||||
0x62: 'Numpad2',
|
||||
0x63: 'Numpad3',
|
||||
0x64: 'Numpad4',
|
||||
0x65: 'Numpad5',
|
||||
0x66: 'Numpad6',
|
||||
0x67: 'Numpad7',
|
||||
0x68: 'Numpad8',
|
||||
0x69: 'Numpad9',
|
||||
0x6a: 'NumpadMultiply',
|
||||
0x6b: 'NumpadAdd',
|
||||
0x6c: 'NumpadDecimal',
|
||||
0x6d: 'NumpadSubtract',
|
||||
0x6e: 'NumpadDecimal', // Duplicate, because buggy on Windows
|
||||
0x6f: 'NumpadDivide',
|
||||
0x70: 'F1',
|
||||
0x71: 'F2',
|
||||
0x72: 'F3',
|
||||
0x73: 'F4',
|
||||
0x74: 'F5',
|
||||
0x75: 'F6',
|
||||
0x76: 'F7',
|
||||
0x77: 'F8',
|
||||
0x78: 'F9',
|
||||
0x79: 'F10',
|
||||
0x7a: 'F11',
|
||||
0x7b: 'F12',
|
||||
0x7c: 'F13',
|
||||
0x7d: 'F14',
|
||||
0x7e: 'F15',
|
||||
0x7f: 'F16',
|
||||
0x80: 'F17',
|
||||
0x81: 'F18',
|
||||
0x82: 'F19',
|
||||
0x83: 'F20',
|
||||
0x84: 'F21',
|
||||
0x85: 'F22',
|
||||
0x86: 'F23',
|
||||
0x87: 'F24',
|
||||
0x90: 'NumLock',
|
||||
0x91: 'ScrollLock',
|
||||
0xa6: 'BrowserBack',
|
||||
0xa7: 'BrowserForward',
|
||||
0xa8: 'BrowserRefresh',
|
||||
0xa9: 'BrowserStop',
|
||||
0xaa: 'BrowserSearch',
|
||||
0xab: 'BrowserFavorites',
|
||||
0xac: 'BrowserHome',
|
||||
0xad: 'AudioVolumeMute',
|
||||
0xae: 'AudioVolumeDown',
|
||||
0xaf: 'AudioVolumeUp',
|
||||
0xb0: 'MediaTrackNext',
|
||||
0xb1: 'MediaTrackPrevious',
|
||||
0xb2: 'MediaStop',
|
||||
0xb3: 'MediaPlayPause',
|
||||
0xb4: 'LaunchMail',
|
||||
0xb5: 'MediaSelect',
|
||||
0xb6: 'LaunchApp1',
|
||||
0xb7: 'LaunchApp2',
|
||||
0xe1: 'AltRight', // Only when it is AltGraph
|
||||
};
|
||||
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* This file is auto-generated from keymaps.csv
|
||||
* Database checksum sha256(76d68c10e97d37fe2ea459e210125ae41796253fb217e900bf2983ade13a7920)
|
||||
* To re-generate, run:
|
||||
* keymap-gen code-map --lang=js keymaps.csv html atset1
|
||||
*/
|
||||
export default {
|
||||
Again: 0xe005 /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */,
|
||||
AltLeft: 0x38 /* html:AltLeft (AltLeft) -> linux:56 (KEY_LEFTALT) -> atset1:56 */,
|
||||
AltRight: 0xe038 /* html:AltRight (AltRight) -> linux:100 (KEY_RIGHTALT) -> atset1:57400 */,
|
||||
ArrowDown: 0xe050 /* html:ArrowDown (ArrowDown) -> linux:108 (KEY_DOWN) -> atset1:57424 */,
|
||||
ArrowLeft: 0xe04b /* html:ArrowLeft (ArrowLeft) -> linux:105 (KEY_LEFT) -> atset1:57419 */,
|
||||
ArrowRight: 0xe04d /* html:ArrowRight (ArrowRight) -> linux:106 (KEY_RIGHT) -> atset1:57421 */,
|
||||
ArrowUp: 0xe048 /* html:ArrowUp (ArrowUp) -> linux:103 (KEY_UP) -> atset1:57416 */,
|
||||
AudioVolumeDown: 0xe02e /* html:AudioVolumeDown (AudioVolumeDown) -> linux:114 (KEY_VOLUMEDOWN) -> atset1:57390 */,
|
||||
AudioVolumeMute: 0xe020 /* html:AudioVolumeMute (AudioVolumeMute) -> linux:113 (KEY_MUTE) -> atset1:57376 */,
|
||||
AudioVolumeUp: 0xe030 /* html:AudioVolumeUp (AudioVolumeUp) -> linux:115 (KEY_VOLUMEUP) -> atset1:57392 */,
|
||||
Backquote: 0x29 /* html:Backquote (Backquote) -> linux:41 (KEY_GRAVE) -> atset1:41 */,
|
||||
Backslash: 0x2b /* html:Backslash (Backslash) -> linux:43 (KEY_BACKSLASH) -> atset1:43 */,
|
||||
Backspace: 0xe /* html:Backspace (Backspace) -> linux:14 (KEY_BACKSPACE) -> atset1:14 */,
|
||||
BracketLeft: 0x1a /* html:BracketLeft (BracketLeft) -> linux:26 (KEY_LEFTBRACE) -> atset1:26 */,
|
||||
BracketRight: 0x1b /* html:BracketRight (BracketRight) -> linux:27 (KEY_RIGHTBRACE) -> atset1:27 */,
|
||||
BrowserBack: 0xe06a /* html:BrowserBack (BrowserBack) -> linux:158 (KEY_BACK) -> atset1:57450 */,
|
||||
BrowserFavorites: 0xe066 /* html:BrowserFavorites (BrowserFavorites) -> linux:156 (KEY_BOOKMARKS) -> atset1:57446 */,
|
||||
BrowserForward: 0xe069 /* html:BrowserForward (BrowserForward) -> linux:159 (KEY_FORWARD) -> atset1:57449 */,
|
||||
BrowserHome: 0xe032 /* html:BrowserHome (BrowserHome) -> linux:172 (KEY_HOMEPAGE) -> atset1:57394 */,
|
||||
BrowserRefresh: 0xe067 /* html:BrowserRefresh (BrowserRefresh) -> linux:173 (KEY_REFRESH) -> atset1:57447 */,
|
||||
BrowserSearch: 0xe065 /* html:BrowserSearch (BrowserSearch) -> linux:217 (KEY_SEARCH) -> atset1:57445 */,
|
||||
BrowserStop: 0xe068 /* html:BrowserStop (BrowserStop) -> linux:128 (KEY_STOP) -> atset1:57448 */,
|
||||
CapsLock: 0x3a /* html:CapsLock (CapsLock) -> linux:58 (KEY_CAPSLOCK) -> atset1:58 */,
|
||||
Comma: 0x33 /* html:Comma (Comma) -> linux:51 (KEY_COMMA) -> atset1:51 */,
|
||||
ContextMenu: 0xe05d /* html:ContextMenu (ContextMenu) -> linux:127 (KEY_COMPOSE) -> atset1:57437 */,
|
||||
ControlLeft: 0x1d /* html:ControlLeft (ControlLeft) -> linux:29 (KEY_LEFTCTRL) -> atset1:29 */,
|
||||
ControlRight: 0xe01d /* html:ControlRight (ControlRight) -> linux:97 (KEY_RIGHTCTRL) -> atset1:57373 */,
|
||||
Convert: 0x79 /* html:Convert (Convert) -> linux:92 (KEY_HENKAN) -> atset1:121 */,
|
||||
Copy: 0xe078 /* html:Copy (Copy) -> linux:133 (KEY_COPY) -> atset1:57464 */,
|
||||
Cut: 0xe03c /* html:Cut (Cut) -> linux:137 (KEY_CUT) -> atset1:57404 */,
|
||||
Delete: 0xe053 /* html:Delete (Delete) -> linux:111 (KEY_DELETE) -> atset1:57427 */,
|
||||
Digit0: 0xb /* html:Digit0 (Digit0) -> linux:11 (KEY_0) -> atset1:11 */,
|
||||
Digit1: 0x2 /* html:Digit1 (Digit1) -> linux:2 (KEY_1) -> atset1:2 */,
|
||||
Digit2: 0x3 /* html:Digit2 (Digit2) -> linux:3 (KEY_2) -> atset1:3 */,
|
||||
Digit3: 0x4 /* html:Digit3 (Digit3) -> linux:4 (KEY_3) -> atset1:4 */,
|
||||
Digit4: 0x5 /* html:Digit4 (Digit4) -> linux:5 (KEY_4) -> atset1:5 */,
|
||||
Digit5: 0x6 /* html:Digit5 (Digit5) -> linux:6 (KEY_5) -> atset1:6 */,
|
||||
Digit6: 0x7 /* html:Digit6 (Digit6) -> linux:7 (KEY_6) -> atset1:7 */,
|
||||
Digit7: 0x8 /* html:Digit7 (Digit7) -> linux:8 (KEY_7) -> atset1:8 */,
|
||||
Digit8: 0x9 /* html:Digit8 (Digit8) -> linux:9 (KEY_8) -> atset1:9 */,
|
||||
Digit9: 0xa /* html:Digit9 (Digit9) -> linux:10 (KEY_9) -> atset1:10 */,
|
||||
Eject: 0xe07d /* html:Eject (Eject) -> linux:162 (KEY_EJECTCLOSECD) -> atset1:57469 */,
|
||||
End: 0xe04f /* html:End (End) -> linux:107 (KEY_END) -> atset1:57423 */,
|
||||
Enter: 0x1c /* html:Enter (Enter) -> linux:28 (KEY_ENTER) -> atset1:28 */,
|
||||
Equal: 0xd /* html:Equal (Equal) -> linux:13 (KEY_EQUAL) -> atset1:13 */,
|
||||
Escape: 0x1 /* html:Escape (Escape) -> linux:1 (KEY_ESC) -> atset1:1 */,
|
||||
F1: 0x3b /* html:F1 (F1) -> linux:59 (KEY_F1) -> atset1:59 */,
|
||||
F10: 0x44 /* html:F10 (F10) -> linux:68 (KEY_F10) -> atset1:68 */,
|
||||
F11: 0x57 /* html:F11 (F11) -> linux:87 (KEY_F11) -> atset1:87 */,
|
||||
F12: 0x58 /* html:F12 (F12) -> linux:88 (KEY_F12) -> atset1:88 */,
|
||||
F13: 0x5d /* html:F13 (F13) -> linux:183 (KEY_F13) -> atset1:93 */,
|
||||
F14: 0x5e /* html:F14 (F14) -> linux:184 (KEY_F14) -> atset1:94 */,
|
||||
F15: 0x5f /* html:F15 (F15) -> linux:185 (KEY_F15) -> atset1:95 */,
|
||||
F16: 0x55 /* html:F16 (F16) -> linux:186 (KEY_F16) -> atset1:85 */,
|
||||
F17: 0xe003 /* html:F17 (F17) -> linux:187 (KEY_F17) -> atset1:57347 */,
|
||||
F18: 0xe077 /* html:F18 (F18) -> linux:188 (KEY_F18) -> atset1:57463 */,
|
||||
F19: 0xe004 /* html:F19 (F19) -> linux:189 (KEY_F19) -> atset1:57348 */,
|
||||
F2: 0x3c /* html:F2 (F2) -> linux:60 (KEY_F2) -> atset1:60 */,
|
||||
F20: 0x5a /* html:F20 (F20) -> linux:190 (KEY_F20) -> atset1:90 */,
|
||||
F21: 0x74 /* html:F21 (F21) -> linux:191 (KEY_F21) -> atset1:116 */,
|
||||
F22: 0xe079 /* html:F22 (F22) -> linux:192 (KEY_F22) -> atset1:57465 */,
|
||||
F23: 0x6d /* html:F23 (F23) -> linux:193 (KEY_F23) -> atset1:109 */,
|
||||
F24: 0x6f /* html:F24 (F24) -> linux:194 (KEY_F24) -> atset1:111 */,
|
||||
F3: 0x3d /* html:F3 (F3) -> linux:61 (KEY_F3) -> atset1:61 */,
|
||||
F4: 0x3e /* html:F4 (F4) -> linux:62 (KEY_F4) -> atset1:62 */,
|
||||
F5: 0x3f /* html:F5 (F5) -> linux:63 (KEY_F5) -> atset1:63 */,
|
||||
F6: 0x40 /* html:F6 (F6) -> linux:64 (KEY_F6) -> atset1:64 */,
|
||||
F7: 0x41 /* html:F7 (F7) -> linux:65 (KEY_F7) -> atset1:65 */,
|
||||
F8: 0x42 /* html:F8 (F8) -> linux:66 (KEY_F8) -> atset1:66 */,
|
||||
F9: 0x43 /* html:F9 (F9) -> linux:67 (KEY_F9) -> atset1:67 */,
|
||||
Find: 0xe041 /* html:Find (Find) -> linux:136 (KEY_FIND) -> atset1:57409 */,
|
||||
Help: 0xe075 /* html:Help (Help) -> linux:138 (KEY_HELP) -> atset1:57461 */,
|
||||
Hiragana: 0x77 /* html:Hiragana (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */,
|
||||
Home: 0xe047 /* html:Home (Home) -> linux:102 (KEY_HOME) -> atset1:57415 */,
|
||||
Insert: 0xe052 /* html:Insert (Insert) -> linux:110 (KEY_INSERT) -> atset1:57426 */,
|
||||
IntlBackslash: 0x56 /* html:IntlBackslash (IntlBackslash) -> linux:86 (KEY_102ND) -> atset1:86 */,
|
||||
IntlRo: 0x73 /* html:IntlRo (IntlRo) -> linux:89 (KEY_RO) -> atset1:115 */,
|
||||
IntlYen: 0x7d /* html:IntlYen (IntlYen) -> linux:124 (KEY_YEN) -> atset1:125 */,
|
||||
KanaMode: 0x70 /* html:KanaMode (KanaMode) -> linux:93 (KEY_KATAKANAHIRAGANA) -> atset1:112 */,
|
||||
Katakana: 0x78 /* html:Katakana (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */,
|
||||
KeyA: 0x1e /* html:KeyA (KeyA) -> linux:30 (KEY_A) -> atset1:30 */,
|
||||
KeyB: 0x30 /* html:KeyB (KeyB) -> linux:48 (KEY_B) -> atset1:48 */,
|
||||
KeyC: 0x2e /* html:KeyC (KeyC) -> linux:46 (KEY_C) -> atset1:46 */,
|
||||
KeyD: 0x20 /* html:KeyD (KeyD) -> linux:32 (KEY_D) -> atset1:32 */,
|
||||
KeyE: 0x12 /* html:KeyE (KeyE) -> linux:18 (KEY_E) -> atset1:18 */,
|
||||
KeyF: 0x21 /* html:KeyF (KeyF) -> linux:33 (KEY_F) -> atset1:33 */,
|
||||
KeyG: 0x22 /* html:KeyG (KeyG) -> linux:34 (KEY_G) -> atset1:34 */,
|
||||
KeyH: 0x23 /* html:KeyH (KeyH) -> linux:35 (KEY_H) -> atset1:35 */,
|
||||
KeyI: 0x17 /* html:KeyI (KeyI) -> linux:23 (KEY_I) -> atset1:23 */,
|
||||
KeyJ: 0x24 /* html:KeyJ (KeyJ) -> linux:36 (KEY_J) -> atset1:36 */,
|
||||
KeyK: 0x25 /* html:KeyK (KeyK) -> linux:37 (KEY_K) -> atset1:37 */,
|
||||
KeyL: 0x26 /* html:KeyL (KeyL) -> linux:38 (KEY_L) -> atset1:38 */,
|
||||
KeyM: 0x32 /* html:KeyM (KeyM) -> linux:50 (KEY_M) -> atset1:50 */,
|
||||
KeyN: 0x31 /* html:KeyN (KeyN) -> linux:49 (KEY_N) -> atset1:49 */,
|
||||
KeyO: 0x18 /* html:KeyO (KeyO) -> linux:24 (KEY_O) -> atset1:24 */,
|
||||
KeyP: 0x19 /* html:KeyP (KeyP) -> linux:25 (KEY_P) -> atset1:25 */,
|
||||
KeyQ: 0x10 /* html:KeyQ (KeyQ) -> linux:16 (KEY_Q) -> atset1:16 */,
|
||||
KeyR: 0x13 /* html:KeyR (KeyR) -> linux:19 (KEY_R) -> atset1:19 */,
|
||||
KeyS: 0x1f /* html:KeyS (KeyS) -> linux:31 (KEY_S) -> atset1:31 */,
|
||||
KeyT: 0x14 /* html:KeyT (KeyT) -> linux:20 (KEY_T) -> atset1:20 */,
|
||||
KeyU: 0x16 /* html:KeyU (KeyU) -> linux:22 (KEY_U) -> atset1:22 */,
|
||||
KeyV: 0x2f /* html:KeyV (KeyV) -> linux:47 (KEY_V) -> atset1:47 */,
|
||||
KeyW: 0x11 /* html:KeyW (KeyW) -> linux:17 (KEY_W) -> atset1:17 */,
|
||||
KeyX: 0x2d /* html:KeyX (KeyX) -> linux:45 (KEY_X) -> atset1:45 */,
|
||||
KeyY: 0x15 /* html:KeyY (KeyY) -> linux:21 (KEY_Y) -> atset1:21 */,
|
||||
KeyZ: 0x2c /* html:KeyZ (KeyZ) -> linux:44 (KEY_Z) -> atset1:44 */,
|
||||
Lang1: 0x72 /* html:Lang1 (Lang1) -> linux:122 (KEY_HANGEUL) -> atset1:114 */,
|
||||
Lang2: 0x71 /* html:Lang2 (Lang2) -> linux:123 (KEY_HANJA) -> atset1:113 */,
|
||||
Lang3: 0x78 /* html:Lang3 (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */,
|
||||
Lang4: 0x77 /* html:Lang4 (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */,
|
||||
Lang5: 0x76 /* html:Lang5 (Lang5) -> linux:85 (KEY_ZENKAKUHANKAKU) -> atset1:118 */,
|
||||
LaunchApp1: 0xe06b /* html:LaunchApp1 (LaunchApp1) -> linux:157 (KEY_COMPUTER) -> atset1:57451 */,
|
||||
LaunchApp2: 0xe021 /* html:LaunchApp2 (LaunchApp2) -> linux:140 (KEY_CALC) -> atset1:57377 */,
|
||||
LaunchMail: 0xe06c /* html:LaunchMail (LaunchMail) -> linux:155 (KEY_MAIL) -> atset1:57452 */,
|
||||
MediaPlayPause: 0xe022 /* html:MediaPlayPause (MediaPlayPause) -> linux:164 (KEY_PLAYPAUSE) -> atset1:57378 */,
|
||||
MediaSelect: 0xe06d /* html:MediaSelect (MediaSelect) -> linux:226 (KEY_MEDIA) -> atset1:57453 */,
|
||||
MediaStop: 0xe024 /* html:MediaStop (MediaStop) -> linux:166 (KEY_STOPCD) -> atset1:57380 */,
|
||||
MediaTrackNext: 0xe019 /* html:MediaTrackNext (MediaTrackNext) -> linux:163 (KEY_NEXTSONG) -> atset1:57369 */,
|
||||
MediaTrackPrevious: 0xe010 /* html:MediaTrackPrevious (MediaTrackPrevious) -> linux:165 (KEY_PREVIOUSSONG) -> atset1:57360 */,
|
||||
MetaLeft: 0xe05b /* html:MetaLeft (MetaLeft) -> linux:125 (KEY_LEFTMETA) -> atset1:57435 */,
|
||||
MetaRight: 0xe05c /* html:MetaRight (MetaRight) -> linux:126 (KEY_RIGHTMETA) -> atset1:57436 */,
|
||||
Minus: 0xc /* html:Minus (Minus) -> linux:12 (KEY_MINUS) -> atset1:12 */,
|
||||
NonConvert: 0x7b /* html:NonConvert (NonConvert) -> linux:94 (KEY_MUHENKAN) -> atset1:123 */,
|
||||
NumLock: 0x45 /* html:NumLock (NumLock) -> linux:69 (KEY_NUMLOCK) -> atset1:69 */,
|
||||
Numpad0: 0x52 /* html:Numpad0 (Numpad0) -> linux:82 (KEY_KP0) -> atset1:82 */,
|
||||
Numpad1: 0x4f /* html:Numpad1 (Numpad1) -> linux:79 (KEY_KP1) -> atset1:79 */,
|
||||
Numpad2: 0x50 /* html:Numpad2 (Numpad2) -> linux:80 (KEY_KP2) -> atset1:80 */,
|
||||
Numpad3: 0x51 /* html:Numpad3 (Numpad3) -> linux:81 (KEY_KP3) -> atset1:81 */,
|
||||
Numpad4: 0x4b /* html:Numpad4 (Numpad4) -> linux:75 (KEY_KP4) -> atset1:75 */,
|
||||
Numpad5: 0x4c /* html:Numpad5 (Numpad5) -> linux:76 (KEY_KP5) -> atset1:76 */,
|
||||
Numpad6: 0x4d /* html:Numpad6 (Numpad6) -> linux:77 (KEY_KP6) -> atset1:77 */,
|
||||
Numpad7: 0x47 /* html:Numpad7 (Numpad7) -> linux:71 (KEY_KP7) -> atset1:71 */,
|
||||
Numpad8: 0x48 /* html:Numpad8 (Numpad8) -> linux:72 (KEY_KP8) -> atset1:72 */,
|
||||
Numpad9: 0x49 /* html:Numpad9 (Numpad9) -> linux:73 (KEY_KP9) -> atset1:73 */,
|
||||
NumpadAdd: 0x4e /* html:NumpadAdd (NumpadAdd) -> linux:78 (KEY_KPPLUS) -> atset1:78 */,
|
||||
NumpadComma: 0x7e /* html:NumpadComma (NumpadComma) -> linux:121 (KEY_KPCOMMA) -> atset1:126 */,
|
||||
NumpadDecimal: 0x53 /* html:NumpadDecimal (NumpadDecimal) -> linux:83 (KEY_KPDOT) -> atset1:83 */,
|
||||
NumpadDivide: 0xe035 /* html:NumpadDivide (NumpadDivide) -> linux:98 (KEY_KPSLASH) -> atset1:57397 */,
|
||||
NumpadEnter: 0xe01c /* html:NumpadEnter (NumpadEnter) -> linux:96 (KEY_KPENTER) -> atset1:57372 */,
|
||||
NumpadEqual: 0x59 /* html:NumpadEqual (NumpadEqual) -> linux:117 (KEY_KPEQUAL) -> atset1:89 */,
|
||||
NumpadMultiply: 0x37 /* html:NumpadMultiply (NumpadMultiply) -> linux:55 (KEY_KPASTERISK) -> atset1:55 */,
|
||||
NumpadParenLeft: 0xe076 /* html:NumpadParenLeft (NumpadParenLeft) -> linux:179 (KEY_KPLEFTPAREN) -> atset1:57462 */,
|
||||
NumpadParenRight: 0xe07b /* html:NumpadParenRight (NumpadParenRight) -> linux:180 (KEY_KPRIGHTPAREN) -> atset1:57467 */,
|
||||
NumpadSubtract: 0x4a /* html:NumpadSubtract (NumpadSubtract) -> linux:74 (KEY_KPMINUS) -> atset1:74 */,
|
||||
Open: 0x64 /* html:Open (Open) -> linux:134 (KEY_OPEN) -> atset1:100 */,
|
||||
PageDown: 0xe051 /* html:PageDown (PageDown) -> linux:109 (KEY_PAGEDOWN) -> atset1:57425 */,
|
||||
PageUp: 0xe049 /* html:PageUp (PageUp) -> linux:104 (KEY_PAGEUP) -> atset1:57417 */,
|
||||
Paste: 0x65 /* html:Paste (Paste) -> linux:135 (KEY_PASTE) -> atset1:101 */,
|
||||
Pause: 0xe046 /* html:Pause (Pause) -> linux:119 (KEY_PAUSE) -> atset1:57414 */,
|
||||
Period: 0x34 /* html:Period (Period) -> linux:52 (KEY_DOT) -> atset1:52 */,
|
||||
Power: 0xe05e /* html:Power (Power) -> linux:116 (KEY_POWER) -> atset1:57438 */,
|
||||
PrintScreen: 0x54 /* html:PrintScreen (PrintScreen) -> linux:99 (KEY_SYSRQ) -> atset1:84 */,
|
||||
Props: 0xe006 /* html:Props (Props) -> linux:130 (KEY_PROPS) -> atset1:57350 */,
|
||||
Quote: 0x28 /* html:Quote (Quote) -> linux:40 (KEY_APOSTROPHE) -> atset1:40 */,
|
||||
ScrollLock: 0x46 /* html:ScrollLock (ScrollLock) -> linux:70 (KEY_SCROLLLOCK) -> atset1:70 */,
|
||||
Semicolon: 0x27 /* html:Semicolon (Semicolon) -> linux:39 (KEY_SEMICOLON) -> atset1:39 */,
|
||||
ShiftLeft: 0x2a /* html:ShiftLeft (ShiftLeft) -> linux:42 (KEY_LEFTSHIFT) -> atset1:42 */,
|
||||
ShiftRight: 0x36 /* html:ShiftRight (ShiftRight) -> linux:54 (KEY_RIGHTSHIFT) -> atset1:54 */,
|
||||
Slash: 0x35 /* html:Slash (Slash) -> linux:53 (KEY_SLASH) -> atset1:53 */,
|
||||
Sleep: 0xe05f /* html:Sleep (Sleep) -> linux:142 (KEY_SLEEP) -> atset1:57439 */,
|
||||
Space: 0x39 /* html:Space (Space) -> linux:57 (KEY_SPACE) -> atset1:57 */,
|
||||
Suspend: 0xe025 /* html:Suspend (Suspend) -> linux:205 (KEY_SUSPEND) -> atset1:57381 */,
|
||||
Tab: 0xf /* html:Tab (Tab) -> linux:15 (KEY_TAB) -> atset1:15 */,
|
||||
Undo: 0xe007 /* html:Undo (Undo) -> linux:131 (KEY_UNDO) -> atset1:57351 */,
|
||||
WakeUp: 0xe063 /* html:WakeUp (WakeUp) -> linux:143 (KEY_WAKEUP) -> atset1:57443 */,
|
||||
};
|
||||
585
gns3server/static/web-ui/assets/vnc-console/novnc/core/ra2.js
Normal file
@ -0,0 +1,585 @@
|
||||
import Base64 from './base64.js';
|
||||
import { encodeUTF8 } from './util/strings.js';
|
||||
import EventTargetMixin from './util/eventtarget.js';
|
||||
|
||||
export class AESEAXCipher {
|
||||
constructor() {
|
||||
this._rawKey = null;
|
||||
this._ctrKey = null;
|
||||
this._cbcKey = null;
|
||||
this._zeroBlock = new Uint8Array(16);
|
||||
this._prefixBlock0 = this._zeroBlock;
|
||||
this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
|
||||
this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
|
||||
}
|
||||
|
||||
async _encryptBlock(block) {
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-CBC',
|
||||
iv: this._zeroBlock,
|
||||
},
|
||||
this._cbcKey,
|
||||
block
|
||||
);
|
||||
return new Uint8Array(encrypted).slice(0, 16);
|
||||
}
|
||||
|
||||
async _initCMAC() {
|
||||
const k1 = await this._encryptBlock(this._zeroBlock);
|
||||
const k2 = new Uint8Array(16);
|
||||
const v = k1[0] >>> 6;
|
||||
for (let i = 0; i < 15; i++) {
|
||||
k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
|
||||
k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
|
||||
}
|
||||
const lut = [0x0, 0x87, 0x0e, 0x89];
|
||||
k2[14] ^= v >>> 1;
|
||||
k2[15] = (k1[15] << 2) ^ lut[v];
|
||||
k1[15] = (k1[15] << 1) ^ lut[v >> 1];
|
||||
this._k1 = k1;
|
||||
this._k2 = k2;
|
||||
}
|
||||
|
||||
async _encryptCTR(data, counter) {
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-CTR',
|
||||
counter: counter,
|
||||
length: 128,
|
||||
},
|
||||
this._ctrKey,
|
||||
data
|
||||
);
|
||||
return new Uint8Array(encrypted);
|
||||
}
|
||||
|
||||
async _decryptCTR(data, counter) {
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-CTR',
|
||||
counter: counter,
|
||||
length: 128,
|
||||
},
|
||||
this._ctrKey,
|
||||
data
|
||||
);
|
||||
return new Uint8Array(decrypted);
|
||||
}
|
||||
|
||||
async _computeCMAC(data, prefixBlock) {
|
||||
if (prefixBlock.length !== 16) {
|
||||
return null;
|
||||
}
|
||||
const n = Math.floor(data.length / 16);
|
||||
const m = Math.ceil(data.length / 16);
|
||||
const r = data.length - n * 16;
|
||||
const cbcData = new Uint8Array((m + 1) * 16);
|
||||
cbcData.set(prefixBlock);
|
||||
cbcData.set(data, 16);
|
||||
if (r === 0) {
|
||||
for (let i = 0; i < 16; i++) {
|
||||
cbcData[n * 16 + i] ^= this._k1[i];
|
||||
}
|
||||
} else {
|
||||
cbcData[(n + 1) * 16 + r] = 0x80;
|
||||
for (let i = 0; i < 16; i++) {
|
||||
cbcData[(n + 1) * 16 + i] ^= this._k2[i];
|
||||
}
|
||||
}
|
||||
let cbcEncrypted = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-CBC',
|
||||
iv: this._zeroBlock,
|
||||
},
|
||||
this._cbcKey,
|
||||
cbcData
|
||||
);
|
||||
|
||||
cbcEncrypted = new Uint8Array(cbcEncrypted);
|
||||
const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
|
||||
return mac;
|
||||
}
|
||||
|
||||
async setKey(key) {
|
||||
this._rawKey = key;
|
||||
this._ctrKey = await window.crypto.subtle.importKey('raw', key, { name: 'AES-CTR' }, false, ['encrypt', 'decrypt']);
|
||||
this._cbcKey = await window.crypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt']);
|
||||
await this._initCMAC();
|
||||
}
|
||||
|
||||
async encrypt(message, associatedData, nonce) {
|
||||
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
||||
const encrypted = await this._encryptCTR(message, nCMAC);
|
||||
const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1);
|
||||
const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
mac[i] ^= nCMAC[i] ^ adCMAC[i];
|
||||
}
|
||||
const res = new Uint8Array(16 + encrypted.length);
|
||||
res.set(encrypted);
|
||||
res.set(mac, encrypted.length);
|
||||
return res;
|
||||
}
|
||||
|
||||
async decrypt(encrypted, associatedData, nonce, mac) {
|
||||
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
||||
const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1);
|
||||
const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
|
||||
}
|
||||
if (computedMac.length !== mac.length) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < mac.length; i++) {
|
||||
if (computedMac[i] !== mac[i]) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const res = await this._decryptCTR(encrypted, nCMAC);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export class RA2Cipher {
|
||||
constructor() {
|
||||
this._cipher = new AESEAXCipher();
|
||||
this._counter = new Uint8Array(16);
|
||||
}
|
||||
|
||||
async setKey(key) {
|
||||
await this._cipher.setKey(key);
|
||||
}
|
||||
|
||||
async makeMessage(message) {
|
||||
const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
|
||||
const encrypted = await this._cipher.encrypt(message, ad, this._counter);
|
||||
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
||||
const res = new Uint8Array(message.length + 2 + 16);
|
||||
res.set(ad);
|
||||
res.set(encrypted, 2);
|
||||
return res;
|
||||
}
|
||||
|
||||
async receiveMessage(length, encrypted, mac) {
|
||||
const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
|
||||
const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac);
|
||||
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export class RSACipher {
|
||||
constructor(keyLength) {
|
||||
this._key = null;
|
||||
this._keyLength = keyLength;
|
||||
this._keyBytes = Math.ceil(keyLength / 8);
|
||||
this._n = null;
|
||||
this._e = null;
|
||||
this._d = null;
|
||||
this._nBigInt = null;
|
||||
this._eBigInt = null;
|
||||
this._dBigInt = null;
|
||||
}
|
||||
|
||||
_base64urlDecode(data) {
|
||||
data = data.replace(/-/g, '+').replace(/_/g, '/');
|
||||
data = data.padEnd(Math.ceil(data.length / 4) * 4, '=');
|
||||
return Base64.decode(data);
|
||||
}
|
||||
|
||||
_u8ArrayToBigInt(arr) {
|
||||
let hex = '0x';
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
hex += arr[i].toString(16).padStart(2, '0');
|
||||
}
|
||||
return BigInt(hex);
|
||||
}
|
||||
|
||||
_padArray(arr, length) {
|
||||
const res = new Uint8Array(length);
|
||||
res.set(arr, length - arr.length);
|
||||
return res;
|
||||
}
|
||||
|
||||
_bigIntToU8Array(bigint, padLength = 0) {
|
||||
let hex = bigint.toString(16);
|
||||
if (padLength === 0) {
|
||||
padLength = Math.ceil(hex.length / 2) * 2;
|
||||
}
|
||||
hex = hex.padStart(padLength * 2, '0');
|
||||
const length = hex.length / 2;
|
||||
const arr = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
_modPow(b, e, m) {
|
||||
if (m === 1n) {
|
||||
return 0;
|
||||
}
|
||||
let r = 1n;
|
||||
b = b % m;
|
||||
while (e > 0) {
|
||||
if (e % 2n === 1n) {
|
||||
r = (r * b) % m;
|
||||
}
|
||||
e = e / 2n;
|
||||
b = (b * b) % m;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
async generateKey() {
|
||||
this._key = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
modulusLength: this._keyLength,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||
hash: { name: 'SHA-256' },
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
const privateKey = await window.crypto.subtle.exportKey('jwk', this._key.privateKey);
|
||||
this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
|
||||
this._nBigInt = this._u8ArrayToBigInt(this._n);
|
||||
this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
|
||||
this._eBigInt = this._u8ArrayToBigInt(this._e);
|
||||
this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
|
||||
this._dBigInt = this._u8ArrayToBigInt(this._d);
|
||||
}
|
||||
|
||||
setPublicKey(n, e) {
|
||||
if (n.length !== this._keyBytes || e.length !== this._keyBytes) {
|
||||
return;
|
||||
}
|
||||
this._n = new Uint8Array(this._keyBytes);
|
||||
this._e = new Uint8Array(this._keyBytes);
|
||||
this._n.set(n);
|
||||
this._e.set(e);
|
||||
this._nBigInt = this._u8ArrayToBigInt(this._n);
|
||||
this._eBigInt = this._u8ArrayToBigInt(this._e);
|
||||
}
|
||||
|
||||
encrypt(message) {
|
||||
if (message.length > this._keyBytes - 11) {
|
||||
return null;
|
||||
}
|
||||
const ps = new Uint8Array(this._keyBytes - message.length - 3);
|
||||
window.crypto.getRandomValues(ps);
|
||||
for (let i = 0; i < ps.length; i++) {
|
||||
ps[i] = Math.floor((ps[i] * 254) / 255 + 1);
|
||||
}
|
||||
const em = new Uint8Array(this._keyBytes);
|
||||
em[1] = 0x02;
|
||||
em.set(ps, 2);
|
||||
em.set(message, ps.length + 3);
|
||||
const emBigInt = this._u8ArrayToBigInt(em);
|
||||
const c = this._modPow(emBigInt, this._eBigInt, this._nBigInt);
|
||||
return this._bigIntToU8Array(c, this._keyBytes);
|
||||
}
|
||||
|
||||
decrypt(message) {
|
||||
if (message.length !== this._keyBytes) {
|
||||
return null;
|
||||
}
|
||||
const msgBigInt = this._u8ArrayToBigInt(message);
|
||||
const emBigInt = this._modPow(msgBigInt, this._dBigInt, this._nBigInt);
|
||||
const em = this._bigIntToU8Array(emBigInt, this._keyBytes);
|
||||
if (em[0] !== 0x00 || em[1] !== 0x02) {
|
||||
return null;
|
||||
}
|
||||
let i = 2;
|
||||
for (; i < em.length; i++) {
|
||||
if (em[i] === 0x00) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i === em.length) {
|
||||
return null;
|
||||
}
|
||||
return em.slice(i + 1, em.length);
|
||||
}
|
||||
|
||||
get keyLength() {
|
||||
return this._keyLength;
|
||||
}
|
||||
|
||||
get n() {
|
||||
return this._n;
|
||||
}
|
||||
|
||||
get e() {
|
||||
return this._e;
|
||||
}
|
||||
|
||||
get d() {
|
||||
return this._d;
|
||||
}
|
||||
}
|
||||
|
||||
export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||
constructor(sock, getCredentials) {
|
||||
super();
|
||||
this._hasStarted = false;
|
||||
this._checkSock = null;
|
||||
this._checkCredentials = null;
|
||||
this._approveServerResolve = null;
|
||||
this._sockReject = null;
|
||||
this._credentialsReject = null;
|
||||
this._approveServerReject = null;
|
||||
this._sock = sock;
|
||||
this._getCredentials = getCredentials;
|
||||
}
|
||||
|
||||
_waitSockAsync(len) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hasData = () => !this._sock.rQwait('RA2', len);
|
||||
if (hasData()) {
|
||||
resolve();
|
||||
} else {
|
||||
this._checkSock = () => {
|
||||
if (hasData()) {
|
||||
resolve();
|
||||
this._checkSock = null;
|
||||
this._sockReject = null;
|
||||
}
|
||||
};
|
||||
this._sockReject = reject;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_waitApproveKeyAsync() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._approveServerResolve = resolve;
|
||||
this._approveServerReject = reject;
|
||||
});
|
||||
}
|
||||
|
||||
_waitCredentialsAsync(subtype) {
|
||||
const hasCredentials = () => {
|
||||
if (
|
||||
subtype === 1 &&
|
||||
this._getCredentials().username !== undefined &&
|
||||
this._getCredentials().password !== undefined
|
||||
) {
|
||||
return true;
|
||||
} else if (subtype === 2 && this._getCredentials().password !== undefined) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
if (hasCredentials()) {
|
||||
resolve();
|
||||
} else {
|
||||
this._checkCredentials = () => {
|
||||
if (hasCredentials()) {
|
||||
resolve();
|
||||
this._checkCredentials = null;
|
||||
this._credentialsReject = null;
|
||||
}
|
||||
};
|
||||
this._credentialsReject = reject;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkInternalEvents() {
|
||||
if (this._checkSock !== null) {
|
||||
this._checkSock();
|
||||
}
|
||||
if (this._checkCredentials !== null) {
|
||||
this._checkCredentials();
|
||||
}
|
||||
}
|
||||
|
||||
approveServer() {
|
||||
if (this._approveServerResolve !== null) {
|
||||
this._approveServerResolve();
|
||||
this._approveServerResolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this._sockReject !== null) {
|
||||
this._sockReject(new Error('disconnect normally'));
|
||||
this._sockReject = null;
|
||||
}
|
||||
if (this._credentialsReject !== null) {
|
||||
this._credentialsReject(new Error('disconnect normally'));
|
||||
this._credentialsReject = null;
|
||||
}
|
||||
if (this._approveServerReject !== null) {
|
||||
this._approveServerReject(new Error('disconnect normally'));
|
||||
this._approveServerReject = null;
|
||||
}
|
||||
}
|
||||
|
||||
async negotiateRA2neAuthAsync() {
|
||||
this._hasStarted = true;
|
||||
// 1: Receive server public key
|
||||
await this._waitSockAsync(4);
|
||||
const serverKeyLengthBuffer = this._sock.rQslice(0, 4);
|
||||
const serverKeyLength = this._sock.rQshift32();
|
||||
if (serverKeyLength < 1024) {
|
||||
throw new Error('RA2: server public key is too short: ' + serverKeyLength);
|
||||
} else if (serverKeyLength > 8192) {
|
||||
throw new Error('RA2: server public key is too long: ' + serverKeyLength);
|
||||
}
|
||||
const serverKeyBytes = Math.ceil(serverKeyLength / 8);
|
||||
await this._waitSockAsync(serverKeyBytes * 2);
|
||||
const serverN = this._sock.rQshiftBytes(serverKeyBytes);
|
||||
const serverE = this._sock.rQshiftBytes(serverKeyBytes);
|
||||
const serverRSACipher = new RSACipher(serverKeyLength);
|
||||
serverRSACipher.setPublicKey(serverN, serverE);
|
||||
const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
|
||||
serverPublickey.set(serverKeyLengthBuffer);
|
||||
serverPublickey.set(serverN, 4);
|
||||
serverPublickey.set(serverE, 4 + serverKeyBytes);
|
||||
|
||||
// verify server public key
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('serververification', {
|
||||
detail: { type: 'RSA', publickey: serverPublickey },
|
||||
})
|
||||
);
|
||||
await this._waitApproveKeyAsync();
|
||||
|
||||
// 2: Send client public key
|
||||
const clientKeyLength = 2048;
|
||||
const clientKeyBytes = Math.ceil(clientKeyLength / 8);
|
||||
const clientRSACipher = new RSACipher(clientKeyLength);
|
||||
await clientRSACipher.generateKey();
|
||||
const clientN = clientRSACipher.n;
|
||||
const clientE = clientRSACipher.e;
|
||||
const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
|
||||
clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
|
||||
clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
|
||||
clientPublicKey[2] = (clientKeyLength & 0xff00) >>> 8;
|
||||
clientPublicKey[3] = clientKeyLength & 0xff;
|
||||
clientPublicKey.set(clientN, 4);
|
||||
clientPublicKey.set(clientE, 4 + clientKeyBytes);
|
||||
this._sock.send(clientPublicKey);
|
||||
|
||||
// 3: Send client random
|
||||
const clientRandom = new Uint8Array(16);
|
||||
window.crypto.getRandomValues(clientRandom);
|
||||
const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom);
|
||||
const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
|
||||
clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
|
||||
clientRandomMessage[1] = serverKeyBytes & 0xff;
|
||||
clientRandomMessage.set(clientEncryptedRandom, 2);
|
||||
this._sock.send(clientRandomMessage);
|
||||
|
||||
// 4: Receive server random
|
||||
await this._waitSockAsync(2);
|
||||
if (this._sock.rQshift16() !== clientKeyBytes) {
|
||||
throw new Error('RA2: wrong encrypted message length');
|
||||
}
|
||||
const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
|
||||
const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom);
|
||||
if (serverRandom === null || serverRandom.length !== 16) {
|
||||
throw new Error('RA2: corrupted server encrypted random');
|
||||
}
|
||||
|
||||
// 5: Compute session keys and set ciphers
|
||||
let clientSessionKey = new Uint8Array(32);
|
||||
let serverSessionKey = new Uint8Array(32);
|
||||
clientSessionKey.set(serverRandom);
|
||||
clientSessionKey.set(clientRandom, 16);
|
||||
serverSessionKey.set(clientRandom);
|
||||
serverSessionKey.set(serverRandom, 16);
|
||||
clientSessionKey = await window.crypto.subtle.digest('SHA-1', clientSessionKey);
|
||||
clientSessionKey = new Uint8Array(clientSessionKey).slice(0, 16);
|
||||
serverSessionKey = await window.crypto.subtle.digest('SHA-1', serverSessionKey);
|
||||
serverSessionKey = new Uint8Array(serverSessionKey).slice(0, 16);
|
||||
const clientCipher = new RA2Cipher();
|
||||
await clientCipher.setKey(clientSessionKey);
|
||||
const serverCipher = new RA2Cipher();
|
||||
await serverCipher.setKey(serverSessionKey);
|
||||
|
||||
// 6: Compute and exchange hashes
|
||||
let serverHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
|
||||
let clientHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
|
||||
serverHash.set(serverPublickey);
|
||||
serverHash.set(clientPublicKey, 4 + serverKeyBytes * 2);
|
||||
clientHash.set(clientPublicKey);
|
||||
clientHash.set(serverPublickey, 4 + clientKeyBytes * 2);
|
||||
serverHash = await window.crypto.subtle.digest('SHA-1', serverHash);
|
||||
clientHash = await window.crypto.subtle.digest('SHA-1', clientHash);
|
||||
serverHash = new Uint8Array(serverHash);
|
||||
clientHash = new Uint8Array(clientHash);
|
||||
this._sock.send(await clientCipher.makeMessage(clientHash));
|
||||
await this._waitSockAsync(2 + 20 + 16);
|
||||
if (this._sock.rQshift16() !== 20) {
|
||||
throw new Error('RA2: wrong server hash');
|
||||
}
|
||||
const serverHashReceived = await serverCipher.receiveMessage(
|
||||
20,
|
||||
this._sock.rQshiftBytes(20),
|
||||
this._sock.rQshiftBytes(16)
|
||||
);
|
||||
if (serverHashReceived === null) {
|
||||
throw new Error('RA2: failed to authenticate the message');
|
||||
}
|
||||
for (let i = 0; i < 20; i++) {
|
||||
if (serverHashReceived[i] !== serverHash[i]) {
|
||||
throw new Error('RA2: wrong server hash');
|
||||
}
|
||||
}
|
||||
|
||||
// 7: Receive subtype
|
||||
await this._waitSockAsync(2 + 1 + 16);
|
||||
if (this._sock.rQshift16() !== 1) {
|
||||
throw new Error('RA2: wrong subtype');
|
||||
}
|
||||
let subtype = await serverCipher.receiveMessage(1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16));
|
||||
if (subtype === null) {
|
||||
throw new Error('RA2: failed to authenticate the message');
|
||||
}
|
||||
subtype = subtype[0];
|
||||
if (subtype === 1) {
|
||||
if (this._getCredentials().username === undefined || this._getCredentials().password === undefined) {
|
||||
this.dispatchEvent(new CustomEvent('credentialsrequired', { detail: { types: ['username', 'password'] } }));
|
||||
}
|
||||
} else if (subtype === 2) {
|
||||
if (this._getCredentials().password === undefined) {
|
||||
this.dispatchEvent(new CustomEvent('credentialsrequired', { detail: { types: ['password'] } }));
|
||||
}
|
||||
} else {
|
||||
throw new Error('RA2: wrong subtype');
|
||||
}
|
||||
await this._waitCredentialsAsync(subtype);
|
||||
let username;
|
||||
if (subtype === 1) {
|
||||
username = encodeUTF8(this._getCredentials().username).slice(0, 255);
|
||||
} else {
|
||||
username = '';
|
||||
}
|
||||
const password = encodeUTF8(this._getCredentials().password).slice(0, 255);
|
||||
const credentials = new Uint8Array(username.length + password.length + 2);
|
||||
credentials[0] = username.length;
|
||||
credentials[username.length + 1] = password.length;
|
||||
for (let i = 0; i < username.length; i++) {
|
||||
credentials[i + 1] = username.charCodeAt(i);
|
||||
}
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
credentials[username.length + 2 + i] = password.charCodeAt(i);
|
||||
}
|
||||
this._sock.send(await clientCipher.makeMessage(credentials));
|
||||
}
|
||||
|
||||
get hasStarted() {
|
||||
return this._hasStarted;
|
||||
}
|
||||
|
||||
set hasStarted(s) {
|
||||
this._hasStarted = s;
|
||||
}
|
||||
}
|
||||
3431
gns3server/static/web-ui/assets/vnc-console/novnc/core/rfb.js
Normal file
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
* Browser feature support detection
|
||||
*/
|
||||
|
||||
import * as Log from './logging.js';
|
||||
|
||||
// Touch detection
|
||||
export let isTouchDevice =
|
||||
'ontouchstart' in document.documentElement ||
|
||||
// requried for Chrome debugger
|
||||
document.ontouchstart !== undefined ||
|
||||
// required for MS Surface
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0;
|
||||
window.addEventListener(
|
||||
'touchstart',
|
||||
function onFirstTouch() {
|
||||
isTouchDevice = true;
|
||||
window.removeEventListener('touchstart', onFirstTouch, false);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// The goal is to find a certain physical width, the devicePixelRatio
|
||||
// brings us a bit closer but is not optimal.
|
||||
export let dragThreshold = 10 * (window.devicePixelRatio || 1);
|
||||
|
||||
let _supportsCursorURIs = false;
|
||||
|
||||
try {
|
||||
const target = document.createElement('canvas');
|
||||
target.style.cursor =
|
||||
'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default';
|
||||
|
||||
if (target.style.cursor.indexOf('url') === 0) {
|
||||
Log.Info('Data URI scheme cursor supported');
|
||||
_supportsCursorURIs = true;
|
||||
} else {
|
||||
Log.Warn('Data URI scheme cursor not supported');
|
||||
}
|
||||
} catch (exc) {
|
||||
Log.Error('Data URI scheme cursor test exception: ' + exc);
|
||||
}
|
||||
|
||||
export const supportsCursorURIs = _supportsCursorURIs;
|
||||
|
||||
let _hasScrollbarGutter = true;
|
||||
try {
|
||||
// Create invisible container
|
||||
const container = document.createElement('div');
|
||||
container.style.visibility = 'hidden';
|
||||
container.style.overflow = 'scroll'; // forcing scrollbars
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Create a div and place it in the container
|
||||
const child = document.createElement('div');
|
||||
container.appendChild(child);
|
||||
|
||||
// Calculate the difference between the container's full width
|
||||
// and the child's width - the difference is the scrollbars
|
||||
const scrollbarWidth = container.offsetWidth - child.offsetWidth;
|
||||
|
||||
// Clean up
|
||||
container.parentNode.removeChild(container);
|
||||
|
||||
_hasScrollbarGutter = scrollbarWidth != 0;
|
||||
} catch (exc) {
|
||||
Log.Error('Scrollbar test exception: ' + exc);
|
||||
}
|
||||
export const hasScrollbarGutter = _hasScrollbarGutter;
|
||||
|
||||
/*
|
||||
* The functions for detection of platforms and browsers below are exported
|
||||
* but the use of these should be minimized as much as possible.
|
||||
*
|
||||
* It's better to use feature detection than platform detection.
|
||||
*/
|
||||
|
||||
/* OS */
|
||||
|
||||
export function isMac() {
|
||||
return !!/mac/i.exec(navigator.platform);
|
||||
}
|
||||
|
||||
export function isWindows() {
|
||||
return !!/win/i.exec(navigator.platform);
|
||||
}
|
||||
|
||||
export function isIOS() {
|
||||
return (
|
||||
!!/ipad/i.exec(navigator.platform) || !!/iphone/i.exec(navigator.platform) || !!/ipod/i.exec(navigator.platform)
|
||||
);
|
||||
}
|
||||
|
||||
export function isAndroid() {
|
||||
/* Android sets navigator.platform to Linux :/ */
|
||||
return !!navigator.userAgent.match('Android ');
|
||||
}
|
||||
|
||||
export function isChromeOS() {
|
||||
/* ChromeOS sets navigator.platform to Linux :/ */
|
||||
return !!navigator.userAgent.match(' CrOS ');
|
||||
}
|
||||
|
||||
/* Browser */
|
||||
|
||||
export function isSafari() {
|
||||
return (
|
||||
!!navigator.userAgent.match('Safari/...') &&
|
||||
!navigator.userAgent.match('Chrome/...') &&
|
||||
!navigator.userAgent.match('Chromium/...') &&
|
||||
!navigator.userAgent.match('Epiphany/...')
|
||||
);
|
||||
}
|
||||
|
||||
export function isFirefox() {
|
||||
return !!navigator.userAgent.match('Firefox/...') && !navigator.userAgent.match('Seamonkey/...');
|
||||
}
|
||||
|
||||
export function isChrome() {
|
||||
return (
|
||||
!!navigator.userAgent.match('Chrome/...') &&
|
||||
!navigator.userAgent.match('Chromium/...') &&
|
||||
!navigator.userAgent.match('Edg/...') &&
|
||||
!navigator.userAgent.match('OPR/...')
|
||||
);
|
||||
}
|
||||
|
||||
export function isChromium() {
|
||||
return !!navigator.userAgent.match('Chromium/...');
|
||||
}
|
||||
|
||||
export function isOpera() {
|
||||
return !!navigator.userAgent.match('OPR/...');
|
||||
}
|
||||
|
||||
export function isEdge() {
|
||||
return !!navigator.userAgent.match('Edg/...');
|
||||
}
|
||||
|
||||
/* Engine */
|
||||
|
||||
export function isGecko() {
|
||||
return !!navigator.userAgent.match('Gecko/...');
|
||||
}
|
||||
|
||||
export function isWebKit() {
|
||||
return !!navigator.userAgent.match('AppleWebKit/...') && !navigator.userAgent.match('Chrome/...');
|
||||
}
|
||||
|
||||
export function isBlink() {
|
||||
return !!navigator.userAgent.match('Chrome/...');
|
||||
}
|
||||
@ -0,0 +1,245 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
import { supportsCursorURIs, isTouchDevice } from './browser.js';
|
||||
|
||||
const useFallback = !supportsCursorURIs || isTouchDevice;
|
||||
|
||||
export default class Cursor {
|
||||
constructor() {
|
||||
this._target = null;
|
||||
|
||||
this._canvas = document.createElement('canvas');
|
||||
|
||||
if (useFallback) {
|
||||
this._canvas.style.position = 'fixed';
|
||||
this._canvas.style.zIndex = '65535';
|
||||
this._canvas.style.pointerEvents = 'none';
|
||||
// Safari on iOS can select the cursor image
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=249223
|
||||
this._canvas.style.userSelect = 'none';
|
||||
this._canvas.style.WebkitUserSelect = 'none';
|
||||
// Can't use "display" because of Firefox bug #1445997
|
||||
this._canvas.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
this._position = { x: 0, y: 0 };
|
||||
this._hotSpot = { x: 0, y: 0 };
|
||||
|
||||
this._eventHandlers = {
|
||||
mouseover: this._handleMouseOver.bind(this),
|
||||
mouseleave: this._handleMouseLeave.bind(this),
|
||||
mousemove: this._handleMouseMove.bind(this),
|
||||
mouseup: this._handleMouseUp.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
attach(target) {
|
||||
if (this._target) {
|
||||
this.detach();
|
||||
}
|
||||
|
||||
this._target = target;
|
||||
|
||||
if (useFallback) {
|
||||
document.body.appendChild(this._canvas);
|
||||
|
||||
const options = { capture: true, passive: true };
|
||||
this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
|
||||
this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
|
||||
this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
|
||||
this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
|
||||
}
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
detach() {
|
||||
if (!this._target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (useFallback) {
|
||||
const options = { capture: true, passive: true };
|
||||
this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
|
||||
this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
|
||||
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
|
||||
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
|
||||
|
||||
document.body.removeChild(this._canvas);
|
||||
}
|
||||
|
||||
this._target = null;
|
||||
}
|
||||
|
||||
change(rgba, hotx, hoty, w, h) {
|
||||
if (w === 0 || h === 0) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this._position.x = this._position.x + this._hotSpot.x - hotx;
|
||||
this._position.y = this._position.y + this._hotSpot.y - hoty;
|
||||
this._hotSpot.x = hotx;
|
||||
this._hotSpot.y = hoty;
|
||||
|
||||
let ctx = this._canvas.getContext('2d');
|
||||
|
||||
this._canvas.width = w;
|
||||
this._canvas.height = h;
|
||||
|
||||
let img = new ImageData(new Uint8ClampedArray(rgba), w, h);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.putImageData(img, 0, 0);
|
||||
|
||||
if (useFallback) {
|
||||
this._updatePosition();
|
||||
} else {
|
||||
let url = this._canvas.toDataURL();
|
||||
this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._target.style.cursor = 'none';
|
||||
this._canvas.width = 0;
|
||||
this._canvas.height = 0;
|
||||
this._position.x = this._position.x + this._hotSpot.x;
|
||||
this._position.y = this._position.y + this._hotSpot.y;
|
||||
this._hotSpot.x = 0;
|
||||
this._hotSpot.y = 0;
|
||||
}
|
||||
|
||||
// Mouse events might be emulated, this allows
|
||||
// moving the cursor in such cases
|
||||
move(clientX, clientY) {
|
||||
if (!useFallback) {
|
||||
return;
|
||||
}
|
||||
// clientX/clientY are relative the _visual viewport_,
|
||||
// but our position is relative the _layout viewport_,
|
||||
// so try to compensate when we can
|
||||
if (window.visualViewport) {
|
||||
this._position.x = clientX + window.visualViewport.offsetLeft;
|
||||
this._position.y = clientY + window.visualViewport.offsetTop;
|
||||
} else {
|
||||
this._position.x = clientX;
|
||||
this._position.y = clientY;
|
||||
}
|
||||
this._updatePosition();
|
||||
let target = document.elementFromPoint(clientX, clientY);
|
||||
this._updateVisibility(target);
|
||||
}
|
||||
|
||||
_handleMouseOver(event) {
|
||||
// This event could be because we're entering the target, or
|
||||
// moving around amongst its sub elements. Let the move handler
|
||||
// sort things out.
|
||||
this._handleMouseMove(event);
|
||||
}
|
||||
|
||||
_handleMouseLeave(event) {
|
||||
// Check if we should show the cursor on the element we are leaving to
|
||||
this._updateVisibility(event.relatedTarget);
|
||||
}
|
||||
|
||||
_handleMouseMove(event) {
|
||||
this._updateVisibility(event.target);
|
||||
|
||||
this._position.x = event.clientX - this._hotSpot.x;
|
||||
this._position.y = event.clientY - this._hotSpot.y;
|
||||
|
||||
this._updatePosition();
|
||||
}
|
||||
|
||||
_handleMouseUp(event) {
|
||||
// We might get this event because of a drag operation that
|
||||
// moved outside of the target. Check what's under the cursor
|
||||
// now and adjust visibility based on that.
|
||||
let target = document.elementFromPoint(event.clientX, event.clientY);
|
||||
this._updateVisibility(target);
|
||||
|
||||
// Captures end with a mouseup but we can't know the event order of
|
||||
// mouseup vs releaseCapture.
|
||||
//
|
||||
// In the cases when releaseCapture comes first, the code above is
|
||||
// enough.
|
||||
//
|
||||
// In the cases when the mouseup comes first, we need wait for the
|
||||
// browser to flush all events and then check again if the cursor
|
||||
// should be visible.
|
||||
if (this._captureIsActive()) {
|
||||
window.setTimeout(() => {
|
||||
// We might have detached at this point
|
||||
if (!this._target) {
|
||||
return;
|
||||
}
|
||||
// Refresh the target from elementFromPoint since queued events
|
||||
// might have altered the DOM
|
||||
target = document.elementFromPoint(event.clientX, event.clientY);
|
||||
this._updateVisibility(target);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
_showCursor() {
|
||||
if (this._canvas.style.visibility === 'hidden') {
|
||||
this._canvas.style.visibility = '';
|
||||
}
|
||||
}
|
||||
|
||||
_hideCursor() {
|
||||
if (this._canvas.style.visibility !== 'hidden') {
|
||||
this._canvas.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
// Should we currently display the cursor?
|
||||
// (i.e. are we over the target, or a child of the target without a
|
||||
// different cursor set)
|
||||
_shouldShowCursor(target) {
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
// Easy case
|
||||
if (target === this._target) {
|
||||
return true;
|
||||
}
|
||||
// Other part of the DOM?
|
||||
if (!this._target.contains(target)) {
|
||||
return false;
|
||||
}
|
||||
// Has the child its own cursor?
|
||||
// FIXME: How can we tell that a sub element has an
|
||||
// explicit "cursor: none;"?
|
||||
if (window.getComputedStyle(target).cursor !== 'none') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_updateVisibility(target) {
|
||||
// When the cursor target has capture we want to show the cursor.
|
||||
// So, if a capture is active - look at the captured element instead.
|
||||
if (this._captureIsActive()) {
|
||||
target = document.captureElement;
|
||||
}
|
||||
if (this._shouldShowCursor(target)) {
|
||||
this._showCursor();
|
||||
} else {
|
||||
this._hideCursor();
|
||||
}
|
||||
}
|
||||
|
||||
_updatePosition() {
|
||||
this._canvas.style.left = this._position.x + 'px';
|
||||
this._canvas.style.top = this._position.y + 'px';
|
||||
}
|
||||
|
||||
_captureIsActive() {
|
||||
return document.captureElement && document.documentElement.contains(document.captureElement);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2020 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
/*
|
||||
* HTML element utility functions
|
||||
*/
|
||||
|
||||
export function clientToElement(x, y, elem) {
|
||||
const bounds = elem.getBoundingClientRect();
|
||||
let pos = { x: 0, y: 0 };
|
||||
// Clip to target bounds
|
||||
if (x < bounds.left) {
|
||||
pos.x = 0;
|
||||
} else if (x >= bounds.right) {
|
||||
pos.x = bounds.width - 1;
|
||||
} else {
|
||||
pos.x = x - bounds.left;
|
||||
}
|
||||
if (y < bounds.top) {
|
||||
pos.y = 0;
|
||||
} else if (y >= bounds.bottom) {
|
||||
pos.y = bounds.height - 1;
|
||||
} else {
|
||||
pos.y = y - bounds.top;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2018 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Cross-browser event and position routines
|
||||
*/
|
||||
|
||||
export function getPointerEvent(e) {
|
||||
return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e;
|
||||
}
|
||||
|
||||
export function stopEvent(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Emulate Element.setCapture() when not supported
|
||||
let _captureRecursion = false;
|
||||
let _elementForUnflushedEvents = null;
|
||||
document.captureElement = null;
|
||||
function _captureProxy(e) {
|
||||
// Recursion protection as we'll see our own event
|
||||
if (_captureRecursion) return;
|
||||
|
||||
// Clone the event as we cannot dispatch an already dispatched event
|
||||
const newEv = new e.constructor(e.type, e);
|
||||
|
||||
_captureRecursion = true;
|
||||
if (document.captureElement) {
|
||||
document.captureElement.dispatchEvent(newEv);
|
||||
} else {
|
||||
_elementForUnflushedEvents.dispatchEvent(newEv);
|
||||
}
|
||||
_captureRecursion = false;
|
||||
|
||||
// Avoid double events
|
||||
e.stopPropagation();
|
||||
|
||||
// Respect the wishes of the redirected event handlers
|
||||
if (newEv.defaultPrevented) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Implicitly release the capture on button release
|
||||
if (e.type === 'mouseup') {
|
||||
releaseCapture();
|
||||
}
|
||||
}
|
||||
|
||||
// Follow cursor style of target element
|
||||
function _capturedElemChanged() {
|
||||
const proxyElem = document.getElementById('noVNC_mouse_capture_elem');
|
||||
proxyElem.style.cursor = window.getComputedStyle(document.captureElement).cursor;
|
||||
}
|
||||
|
||||
const _captureObserver = new MutationObserver(_capturedElemChanged);
|
||||
|
||||
export function setCapture(target) {
|
||||
if (target.setCapture) {
|
||||
target.setCapture();
|
||||
document.captureElement = target;
|
||||
} else {
|
||||
// Release any existing capture in case this method is
|
||||
// called multiple times without coordination
|
||||
releaseCapture();
|
||||
|
||||
let proxyElem = document.getElementById('noVNC_mouse_capture_elem');
|
||||
|
||||
if (proxyElem === null) {
|
||||
proxyElem = document.createElement('div');
|
||||
proxyElem.id = 'noVNC_mouse_capture_elem';
|
||||
proxyElem.style.position = 'fixed';
|
||||
proxyElem.style.top = '0px';
|
||||
proxyElem.style.left = '0px';
|
||||
proxyElem.style.width = '100%';
|
||||
proxyElem.style.height = '100%';
|
||||
proxyElem.style.zIndex = 10000;
|
||||
proxyElem.style.display = 'none';
|
||||
document.body.appendChild(proxyElem);
|
||||
|
||||
// This is to make sure callers don't get confused by having
|
||||
// our blocking element as the target
|
||||
proxyElem.addEventListener('contextmenu', _captureProxy);
|
||||
|
||||
proxyElem.addEventListener('mousemove', _captureProxy);
|
||||
proxyElem.addEventListener('mouseup', _captureProxy);
|
||||
}
|
||||
|
||||
document.captureElement = target;
|
||||
|
||||
// Track cursor and get initial cursor
|
||||
_captureObserver.observe(target, { attributes: true });
|
||||
_capturedElemChanged();
|
||||
|
||||
proxyElem.style.display = '';
|
||||
|
||||
// We listen to events on window in order to keep tracking if it
|
||||
// happens to leave the viewport
|
||||
window.addEventListener('mousemove', _captureProxy);
|
||||
window.addEventListener('mouseup', _captureProxy);
|
||||
}
|
||||
}
|
||||
|
||||
export function releaseCapture() {
|
||||
if (document.releaseCapture) {
|
||||
document.releaseCapture();
|
||||
document.captureElement = null;
|
||||
} else {
|
||||
if (!document.captureElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// There might be events already queued. The event proxy needs
|
||||
// access to the captured element for these queued events.
|
||||
// E.g. contextmenu (right-click) in Microsoft Edge
|
||||
//
|
||||
// Before removing the capturedElem pointer we save it to a
|
||||
// temporary variable that the unflushed events can use.
|
||||
_elementForUnflushedEvents = document.captureElement;
|
||||
document.captureElement = null;
|
||||
|
||||
_captureObserver.disconnect();
|
||||
|
||||
const proxyElem = document.getElementById('noVNC_mouse_capture_elem');
|
||||
proxyElem.style.display = 'none';
|
||||
|
||||
window.removeEventListener('mousemove', _captureProxy);
|
||||
window.removeEventListener('mouseup', _captureProxy);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
export default class EventTargetMixin {
|
||||
constructor() {
|
||||
this._listeners = new Map();
|
||||
}
|
||||
|
||||
addEventListener(type, callback) {
|
||||
if (!this._listeners.has(type)) {
|
||||
this._listeners.set(type, new Set());
|
||||
}
|
||||
this._listeners.get(type).add(callback);
|
||||
}
|
||||
|
||||
removeEventListener(type, callback) {
|
||||
if (this._listeners.has(type)) {
|
||||
this._listeners.get(type).delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
dispatchEvent(event) {
|
||||
if (!this._listeners.has(event.type)) {
|
||||
return true;
|
||||
}
|
||||
this._listeners.get(event.type).forEach((callback) => callback.call(this, event));
|
||||
return !event.defaultPrevented;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2020 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
export function toUnsigned32bit(toConvert) {
|
||||
return toConvert >>> 0;
|
||||
}
|
||||
|
||||
export function toSigned32bit(toConvert) {
|
||||
return toConvert | 0;
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Logging/debug routines
|
||||
*/
|
||||
|
||||
let _logLevel = 'warn';
|
||||
|
||||
let Debug = () => {};
|
||||
let Info = () => {};
|
||||
let Warn = () => {};
|
||||
let Error = () => {};
|
||||
|
||||
export function initLogging(level) {
|
||||
if (typeof level === 'undefined') {
|
||||
level = _logLevel;
|
||||
} else {
|
||||
_logLevel = level;
|
||||
}
|
||||
|
||||
Debug = Info = Warn = Error = () => {};
|
||||
|
||||
if (typeof window.console !== 'undefined') {
|
||||
/* eslint-disable no-console, no-fallthrough */
|
||||
switch (level) {
|
||||
case 'debug':
|
||||
Debug = console.debug.bind(window.console);
|
||||
case 'info':
|
||||
Info = console.info.bind(window.console);
|
||||
case 'warn':
|
||||
Warn = console.warn.bind(window.console);
|
||||
case 'error':
|
||||
Error = console.error.bind(window.console);
|
||||
case 'none':
|
||||
break;
|
||||
default:
|
||||
throw new window.Error("invalid logging type '" + level + "'");
|
||||
}
|
||||
/* eslint-enable no-console, no-fallthrough */
|
||||
}
|
||||
}
|
||||
|
||||
export function getLogging() {
|
||||
return _logLevel;
|
||||
}
|
||||
|
||||
export { Debug, Error, Info, Warn };
|
||||
|
||||
// Initialize logging level
|
||||
initLogging();
|
||||
@ -0,0 +1,382 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2021 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Performs MD5 hashing on a string of binary characters, returns an array of bytes
|
||||
*/
|
||||
|
||||
export function MD5(d) {
|
||||
let r = M(V(Y(X(d), 8 * d.length)));
|
||||
return r;
|
||||
}
|
||||
|
||||
function M(d) {
|
||||
let f = new Uint8Array(d.length);
|
||||
for (let i = 0; i < d.length; i++) {
|
||||
f[i] = d.charCodeAt(i);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
function X(d) {
|
||||
let r = Array(d.length >> 2);
|
||||
for (let m = 0; m < r.length; m++) r[m] = 0;
|
||||
for (let m = 0; m < 8 * d.length; m += 8) r[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32;
|
||||
return r;
|
||||
}
|
||||
|
||||
function V(d) {
|
||||
let r = '';
|
||||
for (let m = 0; m < 32 * d.length; m += 8) r += String.fromCharCode((d[m >> 5] >>> m % 32) & 255);
|
||||
return r;
|
||||
}
|
||||
|
||||
function Y(d, g) {
|
||||
(d[g >> 5] |= 128 << g % 32), (d[14 + (((g + 64) >>> 9) << 4)] = g);
|
||||
let m = 1732584193,
|
||||
f = -271733879,
|
||||
r = -1732584194,
|
||||
i = 271733878;
|
||||
for (let n = 0; n < d.length; n += 16) {
|
||||
let h = m,
|
||||
t = f,
|
||||
g = r,
|
||||
e = i;
|
||||
(f = ii(
|
||||
(f = ii(
|
||||
(f = ii(
|
||||
(f = ii(
|
||||
(f = hh(
|
||||
(f = hh(
|
||||
(f = hh(
|
||||
(f = hh(
|
||||
(f = gg(
|
||||
(f = gg(
|
||||
(f = gg(
|
||||
(f = gg(
|
||||
(f = ff(
|
||||
(f = ff(
|
||||
(f = ff(
|
||||
(f = ff(
|
||||
f,
|
||||
(r = ff(
|
||||
r,
|
||||
(i = ff(
|
||||
i,
|
||||
(m = ff(m, f, r, i, d[n + 0], 7, -680876936)),
|
||||
f,
|
||||
r,
|
||||
d[n + 1],
|
||||
12,
|
||||
-389564586
|
||||
)),
|
||||
m,
|
||||
f,
|
||||
d[n + 2],
|
||||
17,
|
||||
606105819
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 3],
|
||||
22,
|
||||
-1044525330
|
||||
)),
|
||||
(r = ff(
|
||||
r,
|
||||
(i = ff(
|
||||
i,
|
||||
(m = ff(m, f, r, i, d[n + 4], 7, -176418897)),
|
||||
f,
|
||||
r,
|
||||
d[n + 5],
|
||||
12,
|
||||
1200080426
|
||||
)),
|
||||
m,
|
||||
f,
|
||||
d[n + 6],
|
||||
17,
|
||||
-1473231341
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 7],
|
||||
22,
|
||||
-45705983
|
||||
)),
|
||||
(r = ff(
|
||||
r,
|
||||
(i = ff(
|
||||
i,
|
||||
(m = ff(m, f, r, i, d[n + 8], 7, 1770035416)),
|
||||
f,
|
||||
r,
|
||||
d[n + 9],
|
||||
12,
|
||||
-1958414417
|
||||
)),
|
||||
m,
|
||||
f,
|
||||
d[n + 10],
|
||||
17,
|
||||
-42063
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 11],
|
||||
22,
|
||||
-1990404162
|
||||
)),
|
||||
(r = ff(
|
||||
r,
|
||||
(i = ff(
|
||||
i,
|
||||
(m = ff(m, f, r, i, d[n + 12], 7, 1804603682)),
|
||||
f,
|
||||
r,
|
||||
d[n + 13],
|
||||
12,
|
||||
-40341101
|
||||
)),
|
||||
m,
|
||||
f,
|
||||
d[n + 14],
|
||||
17,
|
||||
-1502002290
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 15],
|
||||
22,
|
||||
1236535329
|
||||
)),
|
||||
(r = gg(
|
||||
r,
|
||||
(i = gg(
|
||||
i,
|
||||
(m = gg(m, f, r, i, d[n + 1], 5, -165796510)),
|
||||
f,
|
||||
r,
|
||||
d[n + 6],
|
||||
9,
|
||||
-1069501632
|
||||
)),
|
||||
m,
|
||||
f,
|
||||
d[n + 11],
|
||||
14,
|
||||
643717713
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 0],
|
||||
20,
|
||||
-373897302
|
||||
)),
|
||||
(r = gg(
|
||||
r,
|
||||
(i = gg(i, (m = gg(m, f, r, i, d[n + 5], 5, -701558691)), f, r, d[n + 10], 9, 38016083)),
|
||||
m,
|
||||
f,
|
||||
d[n + 15],
|
||||
14,
|
||||
-660478335
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 4],
|
||||
20,
|
||||
-405537848
|
||||
)),
|
||||
(r = gg(
|
||||
r,
|
||||
(i = gg(i, (m = gg(m, f, r, i, d[n + 9], 5, 568446438)), f, r, d[n + 14], 9, -1019803690)),
|
||||
m,
|
||||
f,
|
||||
d[n + 3],
|
||||
14,
|
||||
-187363961
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 8],
|
||||
20,
|
||||
1163531501
|
||||
)),
|
||||
(r = gg(
|
||||
r,
|
||||
(i = gg(i, (m = gg(m, f, r, i, d[n + 13], 5, -1444681467)), f, r, d[n + 2], 9, -51403784)),
|
||||
m,
|
||||
f,
|
||||
d[n + 7],
|
||||
14,
|
||||
1735328473
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 12],
|
||||
20,
|
||||
-1926607734
|
||||
)),
|
||||
(r = hh(
|
||||
r,
|
||||
(i = hh(i, (m = hh(m, f, r, i, d[n + 5], 4, -378558)), f, r, d[n + 8], 11, -2022574463)),
|
||||
m,
|
||||
f,
|
||||
d[n + 11],
|
||||
16,
|
||||
1839030562
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 14],
|
||||
23,
|
||||
-35309556
|
||||
)),
|
||||
(r = hh(
|
||||
r,
|
||||
(i = hh(i, (m = hh(m, f, r, i, d[n + 1], 4, -1530992060)), f, r, d[n + 4], 11, 1272893353)),
|
||||
m,
|
||||
f,
|
||||
d[n + 7],
|
||||
16,
|
||||
-155497632
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 10],
|
||||
23,
|
||||
-1094730640
|
||||
)),
|
||||
(r = hh(
|
||||
r,
|
||||
(i = hh(i, (m = hh(m, f, r, i, d[n + 13], 4, 681279174)), f, r, d[n + 0], 11, -358537222)),
|
||||
m,
|
||||
f,
|
||||
d[n + 3],
|
||||
16,
|
||||
-722521979
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 6],
|
||||
23,
|
||||
76029189
|
||||
)),
|
||||
(r = hh(
|
||||
r,
|
||||
(i = hh(i, (m = hh(m, f, r, i, d[n + 9], 4, -640364487)), f, r, d[n + 12], 11, -421815835)),
|
||||
m,
|
||||
f,
|
||||
d[n + 15],
|
||||
16,
|
||||
530742520
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 2],
|
||||
23,
|
||||
-995338651
|
||||
)),
|
||||
(r = ii(
|
||||
r,
|
||||
(i = ii(i, (m = ii(m, f, r, i, d[n + 0], 6, -198630844)), f, r, d[n + 7], 10, 1126891415)),
|
||||
m,
|
||||
f,
|
||||
d[n + 14],
|
||||
15,
|
||||
-1416354905
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 5],
|
||||
21,
|
||||
-57434055
|
||||
)),
|
||||
(r = ii(
|
||||
r,
|
||||
(i = ii(i, (m = ii(m, f, r, i, d[n + 12], 6, 1700485571)), f, r, d[n + 3], 10, -1894986606)),
|
||||
m,
|
||||
f,
|
||||
d[n + 10],
|
||||
15,
|
||||
-1051523
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 1],
|
||||
21,
|
||||
-2054922799
|
||||
)),
|
||||
(r = ii(
|
||||
r,
|
||||
(i = ii(i, (m = ii(m, f, r, i, d[n + 8], 6, 1873313359)), f, r, d[n + 15], 10, -30611744)),
|
||||
m,
|
||||
f,
|
||||
d[n + 6],
|
||||
15,
|
||||
-1560198380
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 13],
|
||||
21,
|
||||
1309151649
|
||||
)),
|
||||
(r = ii(
|
||||
r,
|
||||
(i = ii(i, (m = ii(m, f, r, i, d[n + 4], 6, -145523070)), f, r, d[n + 11], 10, -1120210379)),
|
||||
m,
|
||||
f,
|
||||
d[n + 2],
|
||||
15,
|
||||
718787259
|
||||
)),
|
||||
i,
|
||||
m,
|
||||
d[n + 9],
|
||||
21,
|
||||
-343485551
|
||||
)),
|
||||
(m = add(m, h)),
|
||||
(f = add(f, t)),
|
||||
(r = add(r, g)),
|
||||
(i = add(i, e));
|
||||
}
|
||||
return Array(m, f, r, i);
|
||||
}
|
||||
|
||||
function cmn(d, g, m, f, r, i) {
|
||||
return add(rol(add(add(g, d), add(f, i)), r), m);
|
||||
}
|
||||
|
||||
function ff(d, g, m, f, r, i, n) {
|
||||
return cmn((g & m) | (~g & f), d, g, r, i, n);
|
||||
}
|
||||
|
||||
function gg(d, g, m, f, r, i, n) {
|
||||
return cmn((g & f) | (m & ~f), d, g, r, i, n);
|
||||
}
|
||||
|
||||
function hh(d, g, m, f, r, i, n) {
|
||||
return cmn(g ^ m ^ f, d, g, r, i, n);
|
||||
}
|
||||
|
||||
function ii(d, g, m, f, r, i, n) {
|
||||
return cmn(m ^ (g | ~f), d, g, r, i, n);
|
||||
}
|
||||
|
||||
function add(d, g) {
|
||||
let m = (65535 & d) + (65535 & g);
|
||||
return (((d >> 16) + (g >> 16) + (m >> 16)) << 16) | (65535 & m);
|
||||
}
|
||||
|
||||
function rol(d, g) {
|
||||
return (d << g) | (d >>> (32 - g));
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
// Decode from UTF-8
|
||||
export function decodeUTF8(utf8string, allowLatin1 = false) {
|
||||
try {
|
||||
return decodeURIComponent(escape(utf8string));
|
||||
} catch (e) {
|
||||
if (e instanceof URIError) {
|
||||
if (allowLatin1) {
|
||||
// If we allow Latin1 we can ignore any decoding fails
|
||||
// and in these cases return the original string
|
||||
return utf8string;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Encode to UTF-8
|
||||
export function encodeUTF8(DOMString) {
|
||||
return unescape(encodeURIComponent(DOMString));
|
||||
}
|
||||
@ -0,0 +1,351 @@
|
||||
/*
|
||||
* Websock: high-performance buffering wrapper
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* Websock is similar to the standard WebSocket / RTCDataChannel object
|
||||
* but with extra buffer handling.
|
||||
*
|
||||
* Websock has built-in receive queue buffering; the message event
|
||||
* does not contain actual data but is simply a notification that
|
||||
* there is new data available. Several rQ* methods are available to
|
||||
* read binary data off of the receive queue.
|
||||
*/
|
||||
|
||||
import * as Log from './util/logging.js';
|
||||
|
||||
// this has performance issues in some versions Chromium, and
|
||||
// doesn't gain a tremendous amount of performance increase in Firefox
|
||||
// at the moment. It may be valuable to turn it on in the future.
|
||||
const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB
|
||||
|
||||
// Constants pulled from RTCDataChannelState enum
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState#RTCDataChannelState_enum
|
||||
const DataChannel = {
|
||||
CONNECTING: 'connecting',
|
||||
OPEN: 'open',
|
||||
CLOSING: 'closing',
|
||||
CLOSED: 'closed',
|
||||
};
|
||||
|
||||
const ReadyStates = {
|
||||
CONNECTING: [WebSocket.CONNECTING, DataChannel.CONNECTING],
|
||||
OPEN: [WebSocket.OPEN, DataChannel.OPEN],
|
||||
CLOSING: [WebSocket.CLOSING, DataChannel.CLOSING],
|
||||
CLOSED: [WebSocket.CLOSED, DataChannel.CLOSED],
|
||||
};
|
||||
|
||||
// Properties a raw channel must have, WebSocket and RTCDataChannel are two examples
|
||||
const rawChannelProps = ['send', 'close', 'binaryType', 'onerror', 'onmessage', 'onopen', 'protocol', 'readyState'];
|
||||
|
||||
export default class Websock {
|
||||
constructor() {
|
||||
this._websocket = null; // WebSocket or RTCDataChannel object
|
||||
|
||||
this._rQi = 0; // Receive queue index
|
||||
this._rQlen = 0; // Next write position in the receive queue
|
||||
this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB)
|
||||
// called in init: this._rQ = new Uint8Array(this._rQbufferSize);
|
||||
this._rQ = null; // Receive queue
|
||||
|
||||
this._sQbufferSize = 1024 * 10; // 10 KiB
|
||||
// called in init: this._sQ = new Uint8Array(this._sQbufferSize);
|
||||
this._sQlen = 0;
|
||||
this._sQ = null; // Send queue
|
||||
|
||||
this._eventHandlers = {
|
||||
message: () => {},
|
||||
open: () => {},
|
||||
close: () => {},
|
||||
error: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
get readyState() {
|
||||
let subState;
|
||||
|
||||
if (this._websocket === null) {
|
||||
return 'unused';
|
||||
}
|
||||
|
||||
subState = this._websocket.readyState;
|
||||
|
||||
if (ReadyStates.CONNECTING.includes(subState)) {
|
||||
return 'connecting';
|
||||
} else if (ReadyStates.OPEN.includes(subState)) {
|
||||
return 'open';
|
||||
} else if (ReadyStates.CLOSING.includes(subState)) {
|
||||
return 'closing';
|
||||
} else if (ReadyStates.CLOSED.includes(subState)) {
|
||||
return 'closed';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
get sQ() {
|
||||
return this._sQ;
|
||||
}
|
||||
|
||||
get rQ() {
|
||||
return this._rQ;
|
||||
}
|
||||
|
||||
get rQi() {
|
||||
return this._rQi;
|
||||
}
|
||||
|
||||
set rQi(val) {
|
||||
this._rQi = val;
|
||||
}
|
||||
|
||||
// Receive Queue
|
||||
get rQlen() {
|
||||
return this._rQlen - this._rQi;
|
||||
}
|
||||
|
||||
rQpeek8() {
|
||||
return this._rQ[this._rQi];
|
||||
}
|
||||
|
||||
rQskipBytes(bytes) {
|
||||
this._rQi += bytes;
|
||||
}
|
||||
|
||||
rQshift8() {
|
||||
return this._rQshift(1);
|
||||
}
|
||||
|
||||
rQshift16() {
|
||||
return this._rQshift(2);
|
||||
}
|
||||
|
||||
rQshift32() {
|
||||
return this._rQshift(4);
|
||||
}
|
||||
|
||||
// TODO(directxman12): test performance with these vs a DataView
|
||||
_rQshift(bytes) {
|
||||
let res = 0;
|
||||
for (let byte = bytes - 1; byte >= 0; byte--) {
|
||||
res += this._rQ[this._rQi++] << (byte * 8);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
rQshiftStr(len) {
|
||||
if (typeof len === 'undefined') {
|
||||
len = this.rQlen;
|
||||
}
|
||||
let str = '';
|
||||
// Handle large arrays in steps to avoid long strings on the stack
|
||||
for (let i = 0; i < len; i += 4096) {
|
||||
let part = this.rQshiftBytes(Math.min(4096, len - i));
|
||||
str += String.fromCharCode.apply(null, part);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
rQshiftBytes(len) {
|
||||
if (typeof len === 'undefined') {
|
||||
len = this.rQlen;
|
||||
}
|
||||
this._rQi += len;
|
||||
return new Uint8Array(this._rQ.buffer, this._rQi - len, len);
|
||||
}
|
||||
|
||||
rQshiftTo(target, len) {
|
||||
if (len === undefined) {
|
||||
len = this.rQlen;
|
||||
}
|
||||
// TODO: make this just use set with views when using a ArrayBuffer to store the rQ
|
||||
target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
|
||||
this._rQi += len;
|
||||
}
|
||||
|
||||
rQslice(start, end = this.rQlen) {
|
||||
return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start);
|
||||
}
|
||||
|
||||
// Check to see if we must wait for 'num' bytes (default to FBU.bytes)
|
||||
// to be available in the receive queue. Return true if we need to
|
||||
// wait (and possibly print a debug message), otherwise false.
|
||||
rQwait(msg, num, goback) {
|
||||
if (this.rQlen < num) {
|
||||
if (goback) {
|
||||
if (this._rQi < goback) {
|
||||
throw new Error('rQwait cannot backup ' + goback + ' bytes');
|
||||
}
|
||||
this._rQi -= goback;
|
||||
}
|
||||
return true; // true means need more data
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send Queue
|
||||
|
||||
flush() {
|
||||
if (this._sQlen > 0 && this.readyState === 'open') {
|
||||
this._websocket.send(this._encodeMessage());
|
||||
this._sQlen = 0;
|
||||
}
|
||||
}
|
||||
|
||||
send(arr) {
|
||||
this._sQ.set(arr, this._sQlen);
|
||||
this._sQlen += arr.length;
|
||||
this.flush();
|
||||
}
|
||||
|
||||
sendString(str) {
|
||||
this.send(str.split('').map((chr) => chr.charCodeAt(0)));
|
||||
}
|
||||
|
||||
// Event Handlers
|
||||
off(evt) {
|
||||
this._eventHandlers[evt] = () => {};
|
||||
}
|
||||
|
||||
on(evt, handler) {
|
||||
this._eventHandlers[evt] = handler;
|
||||
}
|
||||
|
||||
_allocateBuffers() {
|
||||
this._rQ = new Uint8Array(this._rQbufferSize);
|
||||
this._sQ = new Uint8Array(this._sQbufferSize);
|
||||
}
|
||||
|
||||
init() {
|
||||
this._allocateBuffers();
|
||||
this._rQi = 0;
|
||||
this._websocket = null;
|
||||
}
|
||||
|
||||
open(uri, protocols) {
|
||||
this.attach(new WebSocket(uri, protocols));
|
||||
}
|
||||
|
||||
attach(rawChannel) {
|
||||
this.init();
|
||||
|
||||
// Must get object and class methods to be compatible with the tests.
|
||||
const channelProps = [...Object.keys(rawChannel), ...Object.getOwnPropertyNames(Object.getPrototypeOf(rawChannel))];
|
||||
for (let i = 0; i < rawChannelProps.length; i++) {
|
||||
const prop = rawChannelProps[i];
|
||||
if (channelProps.indexOf(prop) < 0) {
|
||||
throw new Error('Raw channel missing property: ' + prop);
|
||||
}
|
||||
}
|
||||
|
||||
this._websocket = rawChannel;
|
||||
this._websocket.binaryType = 'arraybuffer';
|
||||
this._websocket.onmessage = this._recvMessage.bind(this);
|
||||
|
||||
this._websocket.onopen = () => {
|
||||
Log.Debug('>> WebSock.onopen');
|
||||
if (this._websocket.protocol) {
|
||||
Log.Info('Server choose sub-protocol: ' + this._websocket.protocol);
|
||||
}
|
||||
|
||||
this._eventHandlers.open();
|
||||
Log.Debug('<< WebSock.onopen');
|
||||
};
|
||||
|
||||
this._websocket.onclose = (e) => {
|
||||
Log.Debug('>> WebSock.onclose');
|
||||
this._eventHandlers.close(e);
|
||||
Log.Debug('<< WebSock.onclose');
|
||||
};
|
||||
|
||||
this._websocket.onerror = (e) => {
|
||||
Log.Debug('>> WebSock.onerror: ' + e);
|
||||
this._eventHandlers.error(e);
|
||||
Log.Debug('<< WebSock.onerror: ' + e);
|
||||
};
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this._websocket) {
|
||||
if (this.readyState === 'connecting' || this.readyState === 'open') {
|
||||
Log.Info('Closing WebSocket connection');
|
||||
this._websocket.close();
|
||||
}
|
||||
|
||||
this._websocket.onmessage = () => {};
|
||||
}
|
||||
}
|
||||
|
||||
// private methods
|
||||
_encodeMessage() {
|
||||
// Put in a binary arraybuffer
|
||||
// according to the spec, you can send ArrayBufferViews with the send method
|
||||
return new Uint8Array(this._sQ.buffer, 0, this._sQlen);
|
||||
}
|
||||
|
||||
// We want to move all the unread data to the start of the queue,
|
||||
// e.g. compacting.
|
||||
// The function also expands the receive que if needed, and for
|
||||
// performance reasons we combine these two actions to avoid
|
||||
// unneccessary copying.
|
||||
_expandCompactRQ(minFit) {
|
||||
// if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
|
||||
// instead of resizing
|
||||
const requiredBufferSize = (this._rQlen - this._rQi + minFit) * 8;
|
||||
const resizeNeeded = this._rQbufferSize < requiredBufferSize;
|
||||
|
||||
if (resizeNeeded) {
|
||||
// Make sure we always *at least* double the buffer size, and have at least space for 8x
|
||||
// the current amount of data
|
||||
this._rQbufferSize = Math.max(this._rQbufferSize * 2, requiredBufferSize);
|
||||
}
|
||||
|
||||
// we don't want to grow unboundedly
|
||||
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
|
||||
this._rQbufferSize = MAX_RQ_GROW_SIZE;
|
||||
if (this._rQbufferSize - this.rQlen < minFit) {
|
||||
throw new Error(
|
||||
'Receive Queue buffer exceeded ' + MAX_RQ_GROW_SIZE + ' bytes, and the new message could not fit'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (resizeNeeded) {
|
||||
const oldRQbuffer = this._rQ.buffer;
|
||||
this._rQ = new Uint8Array(this._rQbufferSize);
|
||||
this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi));
|
||||
} else {
|
||||
this._rQ.copyWithin(0, this._rQi, this._rQlen);
|
||||
}
|
||||
|
||||
this._rQlen = this._rQlen - this._rQi;
|
||||
this._rQi = 0;
|
||||
}
|
||||
|
||||
// push arraybuffer values onto the end of the receive que
|
||||
_DecodeMessage(data) {
|
||||
const u8 = new Uint8Array(data);
|
||||
if (u8.length > this._rQbufferSize - this._rQlen) {
|
||||
this._expandCompactRQ(u8.length);
|
||||
}
|
||||
this._rQ.set(u8, this._rQlen);
|
||||
this._rQlen += u8.length;
|
||||
}
|
||||
|
||||
_recvMessage(e) {
|
||||
this._DecodeMessage(e.data);
|
||||
if (this.rQlen > 0) {
|
||||
this._eventHandlers.message();
|
||||
if (this._rQlen == this._rQi) {
|
||||
// All data has now been processed, this means we
|
||||
// can reset the receive queue.
|
||||
this._rQlen = 0;
|
||||
this._rQi = 0;
|
||||
}
|
||||
} else {
|
||||
Log.Debug('Ignoring empty message');
|
||||
}
|
||||
}
|
||||
}
|
||||
21
gns3server/static/web-ui/assets/vnc-console/novnc/vendor/pako/LICENSE
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (C) 2014-2016 by Vitaly Puzrin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
6
gns3server/static/web-ui/assets/vnc-console/novnc/vendor/pako/README.md
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
This is an ES6-modules-compatible version of
|
||||
https://github.com/nodeca/pako, based on pako version 1.0.3.
|
||||
|
||||
It's more-or-less a direct translation of the original, with unused parts
|
||||
removed, and the dynamic support for non-typed arrays removed (since ES6
|
||||
modules don't work well with dynamic exports).
|
||||
1767
gns3server/static/web-ui/assets/vnc-console/vnc-controller.js
Normal file
372
gns3server/static/web-ui/assets/xpra-html5/LICENSE
Normal file
@ -0,0 +1,372 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale,
|
||||
having made, import, or transfer of either its Contributions or
|
||||
its Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under,
|
||||
and making use of, this License. For legal entities, "You"
|
||||
includes any entity that controls, is controlled by, or is under
|
||||
common control with You. For purposes of this definition,
|
||||
"control" means (a) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of
|
||||
the outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
---------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 3.3) or under the terms of a Secondary License (if
|
||||
allowed under Section 3.3), and You may not additional grants that
|
||||
convert any of the rights granted to You under this License to a
|
||||
subsequent license with different terms.
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
The Sections below define the conditions under which Your license to
|
||||
use Covered Software will be terminated. These Sections also define
|
||||
how the license is reinstated if it is terminated, and how the
|
||||
Contributor may limit Your ability to make further Modifications to
|
||||
the Covered Software. Except as expressly stated in this License, no
|
||||
other conditions or restrictions are placed on Your rights to use
|
||||
Covered Software.
|
||||
|
||||
2.8. Distribution
|
||||
|
||||
You may distribute Covered Software, provided that You:
|
||||
|
||||
(a) comply with all conditions in Section 3; and
|
||||
|
||||
(b) make all Your Contributions available under this License; and
|
||||
|
||||
(c) include a copy of this License (including the copyright notice
|
||||
and license grant) with every copy of the Covered Software You
|
||||
distribute; and
|
||||
|
||||
(d) ensure that all Your Modifications to Covered Software are
|
||||
available under this License; and
|
||||
|
||||
(e) ensure that all Your Contributions to Covered Software are
|
||||
available under this License; and
|
||||
|
||||
(f) ensure that all Your Modifications to the Covered Software in
|
||||
Executable Form are made available under this License, either:
|
||||
|
||||
(i) by providing the Source Code of the Executable Form, or
|
||||
|
||||
(ii) by making the Source Code of the Executable Form available
|
||||
under the terms of this License, at a location no more remote
|
||||
than the location from which You made the Executable Form
|
||||
available, and for at least twelve (12) months.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Code
|
||||
|
||||
You may distribute Covered Software in Source Code form, provided that
|
||||
You:
|
||||
|
||||
(a) include a copy of this License (including the copyright notice
|
||||
and license grant) with every copy of the Covered Software You
|
||||
distribute; and
|
||||
|
||||
(b) ensure that all Your Contributions are available under this
|
||||
License; and
|
||||
|
||||
(c) include a NOTICE file with all Your Contributions, and
|
||||
ensure that such NOTICE includes details of how You can be
|
||||
contacted.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
You may distribute Covered Software in Executable form, provided that
|
||||
You:
|
||||
|
||||
(a) ensure that all Your Contributions are available under this
|
||||
License; and
|
||||
|
||||
(b) include a copy of this License (including the copyright notice
|
||||
and license grant) with every copy of the Covered Software You
|
||||
distribute; and
|
||||
|
||||
(c) ensure that all Your Modifications to the Covered Software in
|
||||
Executable Form are made available under this License, either:
|
||||
|
||||
(i) by providing the Source Code of the Executable Form, or
|
||||
|
||||
(ii) by making the Source Code of the Executable Form available
|
||||
under the terms of this License, at a location no more remote
|
||||
than the location from which You made the Executable Form
|
||||
available, and for at least twelve (12) months; and
|
||||
|
||||
(d) ensure that all Your Contributions to the Covered Software in
|
||||
Executable Form are made available under this License; and
|
||||
|
||||
(e) ensure that all Your Modifications to the Covered Software in
|
||||
Executable Form are made available under this License; and
|
||||
|
||||
(f) if the Executable Form is made available, include a NOTICE file
|
||||
with all Your Contributions, and ensure that such NOTICE includes
|
||||
details of how You can be contacted.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may distribute a Larger Work, provided that You:
|
||||
|
||||
(a) comply with all conditions in Section 3 with respect to the
|
||||
Covered Software contained in the Larger Work; and
|
||||
|
||||
(b) ensure that all Your Contributions to the Larger Work are
|
||||
available under this License.
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You must retain all copyright, patent, trademark, and attribution
|
||||
notices in Covered Software, except:
|
||||
|
||||
(a) to the extent necessary to comply with the notice requirements
|
||||
of this License; or
|
||||
|
||||
(b) if the original notice is altered, then You must add a notice
|
||||
indicating that You have altered the copyright, patent, trademark,
|
||||
and attribution notices.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and charge a fee for, acceptance of support,
|
||||
warranty, indemnity, or other liability obligations and/or rights
|
||||
consistent with this License. However, in accepting such obligations,
|
||||
You may act only on Your own behalf and on Your sole responsibility,
|
||||
not on behalf of any other Contributor, and only if You agree to
|
||||
indemnify, defend, and hold each Contributor harmless for any
|
||||
liability incurred by, or claims asserted against, such Contributor
|
||||
by reason of your accepting any such warranty or other liability.
|
||||
|
||||
4. Inability to Comply
|
||||
----------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License due to statute or judicial regulation, then You cannot:
|
||||
|
||||
(a) distribute the Covered Software; or
|
||||
|
||||
(b) grant any rights to it; or
|
||||
|
||||
(c) remove any of the above conditions from this License.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. Termination
|
||||
|
||||
You may terminate this License at any time by notifying all
|
||||
Contributors. This License and the rights granted hereunder will
|
||||
terminate automatically if You fail to comply with terms herein and
|
||||
fail to cure such breach within 30 days of becoming aware of the
|
||||
breach. Provisions which, by their nature, must remain in effect beyond
|
||||
the termination of this License shall survive.
|
||||
|
||||
5.2. Reinstatement
|
||||
|
||||
If You initiate litigation against any entity by asserting patent
|
||||
infringement claims (including declaratory judgment, counterclaim,
|
||||
or cross-claim) against that entity regarding any Contributions of
|
||||
that entity, then any patent licenses granted to You under this
|
||||
License for that Contribution shall terminate. However, if You
|
||||
settle such litigation and pay all litigation costs, then any
|
||||
patent licenses granted to You under this License for that
|
||||
Contribution shall be reinstated.
|
||||
|
||||
5.3. Limitation on Liability
|
||||
|
||||
Except as required by applicable law or agreed to in writing, a
|
||||
Contributor is not liable to You for damages, including any direct,
|
||||
indirect, special, incidental, or consequential damages of any
|
||||
character arising as a result of this License or out of the use or
|
||||
inability to use the Covered Software (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or
|
||||
malfunction, or any and all other commercial damages or losses),
|
||||
even if such Contributor has been advised of the possibility of such
|
||||
damages.
|
||||
|
||||
6. Miscellaneous
|
||||
----------------
|
||||
|
||||
6.1. US Government Users
|
||||
|
||||
The Covered Software is a "commercial item" as defined in 48 C.F.R.
|
||||
2.101 (Oct. 1995), consisting of "commercial computer software" and
|
||||
"commercial computer software documentation," as such terms are used
|
||||
in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212
|
||||
and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all
|
||||
U.S. Government End Users acquire Covered Software with only those
|
||||
rights set forth herein.
|
||||
|
||||
6.2. Attribution
|
||||
|
||||
This License may not be modified without the express written
|
||||
permission of the parties to this License, except as allowed in
|
||||
Section 3.3.
|
||||
|
||||
7. Versions of the License
|
||||
---------------------------
|
||||
|
||||
7.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in
|
||||
Section 7.3, no one other than the license steward has the right
|
||||
to modify or publish new versions of this License.
|
||||
|
||||
7.2. Effective Date
|
||||
|
||||
This License Version 2.0 was first published on January 2016.
|
||||
|
||||
7.3. Other Versions
|
||||
|
||||
You may distribute under the terms of this License, or under the
|
||||
terms of a subsequent version of this License (see Section 3.3), or
|
||||
under the terms of a Secondary License (if permitted under Section
|
||||
3.3).
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
69
gns3server/static/web-ui/assets/xpra-html5/README.md
Normal file
@ -0,0 +1,69 @@
|
||||
# xpra-html5
|
||||
|
||||
HTML5 client for Xpra remote desktop server.
|
||||
|
||||
## Version
|
||||
|
||||
This is based on **xpra-html5 version 19** from the official Xpra project.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Mozilla Public License Version 2.0 (MPL-2.0).
|
||||
|
||||
See the [LICENSE](LICENSE) file for the full license text.
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright (c) 2013 Antoine Martin <antoine@xpra.org>
|
||||
Copyright (c) 2014 Joshua Higgins <josh@kxes.net>
|
||||
|
||||
## Source
|
||||
|
||||
This is a modified version of the xpra-html5 client from the official Xpra project:
|
||||
- Official repository: https://github.com/Xpra-org/xpra-html5
|
||||
- Project website: https://xpra.org/
|
||||
- Version: 19
|
||||
|
||||
## Modifications from v19
|
||||
|
||||
This version has been modified for integration with GNS3 Web UI:
|
||||
|
||||
### Client.js
|
||||
No modifications - using original v19 code.
|
||||
|
||||
### Protocol.js
|
||||
- **Line 191**: Added documentation comment clarifying that the 'binary' subprotocol is required by xpra protocol
|
||||
- Code remains unchanged from v19
|
||||
|
||||
### index.html
|
||||
- **Lines 927-933**: Simplified connection close callback
|
||||
- **Before**: Redirected to connect.html with extensive parameter passing
|
||||
- **After**: Only logs the close reason, no redirection
|
||||
- **Impact**: Simplified error handling for embedded GNS3 Web UI usage
|
||||
- Removed 82 lines of redirection logic
|
||||
|
||||
### TypeScript Service (xpra-console.service.ts)
|
||||
- **buildXpraConsolePageUrl()**: Improved WebSocket URL parsing
|
||||
- **Before**: Manual parsing and concatenation of path and token
|
||||
- **After**: Using URL API to correctly extract pathname and search
|
||||
- **Impact**: More reliable handling of query parameters and tokens
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
- **Total files modified**: 3
|
||||
- **Lines added**: 11
|
||||
- **Lines removed**: 95
|
||||
- **Net change**: -84 lines (simplified)
|
||||
|
||||
All modifications maintain compliance with MPL-2.0 license requirements.
|
||||
|
||||
## License Compliance
|
||||
|
||||
This project complies with the MPL-2.0 license requirements:
|
||||
- All source code modifications are made available under MPL-2.0
|
||||
- Original copyright notices are preserved
|
||||
- A copy of the MPL-2.0 license is included
|
||||
- Modifications are clearly documented
|
||||
|
||||
For more information about MPL-2.0, see:
|
||||
https://www.mozilla.org/en-US/MPL/2.0/FAQ/
|
||||
88
gns3server/static/web-ui/assets/xpra-html5/background.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
192
gns3server/static/web-ui/assets/xpra-html5/clipboard.html
Normal file
@ -0,0 +1,192 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<!--
|
||||
Copyright (c) 2024 Antoine Martin <antoine@xpra.org>
|
||||
Licensed under MPL 2.0
|
||||
-->
|
||||
|
||||
<title>Xpra HTML5 Clipboard Test Page</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<form action="./index.html">
|
||||
|
||||
<div>
|
||||
<label for="input">Input</label>
|
||||
<input title="Input" type="text" class="form-control" id="input" placeholder="Input" maxlength="256" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<input type="checkbox" id="autofocus" />
|
||||
<br />
|
||||
<label for="pasteboard">Pasteboard:</label><br />
|
||||
<textarea id="pasteboard" readonly></textarea>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<label for="contents">Contents:</label><br />
|
||||
<pre id="contents"></pre>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<label for="info">Events:</label><br />
|
||||
<pre id="info"></pre>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="button" id="readText">readText()</button>
|
||||
<br />
|
||||
<button type="button" id="read">read()</button>
|
||||
<input type="text" id="format" value="text/html" />
|
||||
|
||||
<br />
|
||||
<button type="button" id="exec-copy">execCommand("copy")</button>
|
||||
<br />
|
||||
<button type="button" id="exec-paste">execCommand("paste")</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const input = document.getElementById("input");
|
||||
|
||||
const info = document.getElementById("info");
|
||||
const lines = [];
|
||||
|
||||
function update_info(newtext) {
|
||||
// console.log(newtext);
|
||||
while (lines.length > 10) {
|
||||
lines.shift();
|
||||
}
|
||||
lines.push(newtext);
|
||||
info.innerText = lines.join("\n");
|
||||
}
|
||||
|
||||
const autofocus = document.getElementById("autofocus");
|
||||
const pasteboard = document.getElementById("pasteboard");
|
||||
|
||||
pasteboard.onblur = function() {
|
||||
if (autofocus.checked) {
|
||||
pasteboard.focus();
|
||||
}
|
||||
}
|
||||
|
||||
autofocus.onchange = function() {
|
||||
pasteboard.autofocus = autofocus.checked;
|
||||
if (autofocus.checked) {
|
||||
pasteboard.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const contents = document.getElementById("contents");
|
||||
|
||||
if (navigator.clipboard) {
|
||||
update_info("`navigator.clipboard` found");
|
||||
if (navigator.clipboard.clipboardData) {
|
||||
update_info("`navigator.clipboard.clipboardData` found");
|
||||
} else {
|
||||
update_info("No `navigator.clipboard.clipboardData`");
|
||||
}
|
||||
} else {
|
||||
update_info("Error: `navigator.clipboard` is missing!");
|
||||
}
|
||||
|
||||
function read_clipboard_data(format) {
|
||||
update_info("requesting " + format);
|
||||
navigator.clipboard.read().then((data) => {
|
||||
update_info("got " + format + " clipboard data: " + data);
|
||||
for (const item of data) {
|
||||
for (const type of item.types) {
|
||||
const item_data = item.getType(type).then((item_data) => {
|
||||
update_info("got " + type + "=" + item_data);
|
||||
const fileReader = new FileReader();
|
||||
fileReader.addEventListener("load", (event) => {
|
||||
update_info("loaded " + type + "=" + event + " using " + event.target);
|
||||
update_info("result=" + event.target.result);
|
||||
},
|
||||
(error) => {
|
||||
update_info("failed to load " + type + " clipboard data: " + error);
|
||||
});
|
||||
fileReader.readAsText(item_data);
|
||||
},
|
||||
(error) => {
|
||||
update_info("failed to get " + type + " clipboard data: " + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
contents.innerText = data;
|
||||
},
|
||||
(error) => {
|
||||
update_info("failed to read " + format + ": " + error);
|
||||
});
|
||||
}
|
||||
if (navigator.clipboard.read) {
|
||||
read_clipboard_data("text/html");
|
||||
read_clipboard_data("text/plain");
|
||||
} else {
|
||||
update_info("missing `navigator.clipboard.read`");
|
||||
}
|
||||
|
||||
function read_clipboard_text() {
|
||||
update_info("requesting contents via readText()");
|
||||
navigator.clipboard.readText().then((text) => {
|
||||
update_info("readText() clipboard data: '" + text + "'");
|
||||
contents.innerHTML = text;
|
||||
// const clipboard_buffer = unescape(encodeURIComponent(text));
|
||||
},
|
||||
(error) => {
|
||||
update_info("failed to readText(): " + error);
|
||||
});
|
||||
}
|
||||
read_clipboard_text();
|
||||
|
||||
const read = document.getElementById("read");
|
||||
const readText = document.getElementById("readText");
|
||||
const format = document.getElementById("format");
|
||||
|
||||
function show_event(e) {
|
||||
// console.log("show_event:", e);
|
||||
// console.log("event clipboard data:", (e.originalEvent || e).clipboardData);
|
||||
}
|
||||
|
||||
read.onclick = function(e) {
|
||||
show_event(e);
|
||||
const fmt = format.value;
|
||||
read_clipboard_data(fmt);
|
||||
}
|
||||
readText.onclick = function(e) {
|
||||
show_event(e);
|
||||
read_clipboard_text();
|
||||
}
|
||||
|
||||
const exec_copy = document.getElementById("exec-copy");
|
||||
exec_copy.onclick = function(e) {
|
||||
input.select();
|
||||
const copy = document.execCommand("copy");
|
||||
update_info("copy=" + copy);
|
||||
}
|
||||
|
||||
const exec_paste = document.getElementById("exec-paste");
|
||||
exec_paste.onclick = function(e) {
|
||||
input.select();
|
||||
const paste = document.execCommand("paste");
|
||||
update_info("paste=" + paste);
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1620
gns3server/static/web-ui/assets/xpra-html5/connect.html
Normal file
174
gns3server/static/web-ui/assets/xpra-html5/crypto.html
Normal file
612
gns3server/static/web-ui/assets/xpra-html5/css/client.css
Normal file
@ -0,0 +1,612 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background-color: #e8e8e8;
|
||||
background-image: url(../background.svg), linear-gradient(135deg, #e8e8e8 0%, #f5f5f5 50%, #e8e8e8 100%);
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 700px 700px, cover;
|
||||
}
|
||||
|
||||
div.window canvas {
|
||||
touch-action: none;
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
|
||||
div.window canvas.gpu-trigger {
|
||||
transform: transale3d(0, 0, 0);
|
||||
/* Refer to client.js */
|
||||
}
|
||||
|
||||
body.desktop {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
div#top_bar {
|
||||
background-color: #444;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
div#top_bar img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
div#connect_form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div#disconnect_form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background-color: black;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0.5;
|
||||
z-index: 10;
|
||||
text-align: center;
|
||||
padding-top: 3em;
|
||||
}
|
||||
|
||||
#progress-label {
|
||||
font-size: 1.4em;
|
||||
color: white;
|
||||
line-height: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
#progress-details {
|
||||
font-size: 1.2em;
|
||||
color: white;
|
||||
line-height: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#pasteboard {
|
||||
position: fixed;
|
||||
top: -40px;
|
||||
width: 100px;
|
||||
height: 30px;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
.undecorated {
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.window {
|
||||
border: 1px;
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
/* required for radius clip */
|
||||
}
|
||||
|
||||
.window.override-redirect {}
|
||||
|
||||
.window.tray {
|
||||
position: relative;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.window.desktop {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.window-NORMAL {}
|
||||
|
||||
.window.border {
|
||||
border-color: grey;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 6px 6px 3px 3px;
|
||||
}
|
||||
|
||||
.windowicon {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.windowtitle {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 5;
|
||||
padding-right: 48;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.windowbuttons {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding-right: 5px;
|
||||
cursor: pointer;
|
||||
padding-top: 7px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.above {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.below {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
div.windowinfocus {
|
||||
z-index: 5000;
|
||||
}
|
||||
|
||||
.windowhead {
|
||||
height: 30px;
|
||||
color: #000000;
|
||||
background-color: #a0a0a0;
|
||||
border-bottom: 1px solid #c0c0c0;
|
||||
}
|
||||
|
||||
div.windowinfocus div.windowhead {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
.spinneroverlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
background-color: black;
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.spinnermiddle {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-resizable-helper {
|
||||
border: 2px dotted #00f;
|
||||
z-index: 30000 !important;
|
||||
}
|
||||
|
||||
.ui-resizable-handle {
|
||||
position: absolute;
|
||||
font-size: 0.1px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ui-resizable-disabled .ui-resizable-handle,
|
||||
.ui-resizable-autohide .ui-resizable-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-resizable-n {
|
||||
cursor: n-resize;
|
||||
height: 7px;
|
||||
width: 100%;
|
||||
top: -5px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ui-resizable-s {
|
||||
cursor: s-resize;
|
||||
height: 7px;
|
||||
width: 100%;
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ui-resizable-e {
|
||||
cursor: e-resize;
|
||||
width: 7px;
|
||||
right: -5px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ui-resizable-w {
|
||||
cursor: w-resize;
|
||||
width: 7px;
|
||||
left: -5px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ui-resizable-se {
|
||||
cursor: se-resize;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
}
|
||||
|
||||
.ui-resizable-sw {
|
||||
cursor: sw-resize;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
left: -5px;
|
||||
bottom: -5px;
|
||||
}
|
||||
|
||||
.ui-resizable-nw {
|
||||
cursor: nw-resize;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
left: -5px;
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
.ui-resizable-ne {
|
||||
cursor: ne-resize;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
position: absolute;
|
||||
left: auto;
|
||||
right: 0;
|
||||
top: 96px;
|
||||
bottom: auto;
|
||||
width: 600px;
|
||||
height: auto;
|
||||
z-index: 10001;
|
||||
transition: height 0.5s, opacity 0.5s, top 0.5s, box-shadow 0.5s, bottom 0.5s;
|
||||
overflow: hidden;
|
||||
padding: 40px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.notifications .alert {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.notification_icon {
|
||||
max-width: 128px;
|
||||
max-height: 128px;
|
||||
float: left;
|
||||
padding: 0.4rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.notification_buttons {
|
||||
clear: both;
|
||||
margin: 10px 0 0 auto;
|
||||
font-weight: 500;
|
||||
font-size: 1.2rem;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.notification_button {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
background-color: #f8f8f8;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.notification_button:hover {
|
||||
background-color: #000;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.notifications {
|
||||
bottom: 0;
|
||||
box-shadow: none;
|
||||
padding-top: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen {
|
||||
.notifications .alert {
|
||||
box-shadow: 0 1rem 2rem rgba(21, 36, 48, 0.75);
|
||||
background-color: white;
|
||||
overflow: visible;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
opacity: 40;
|
||||
margin-top: -10rem;
|
||||
position: relative;
|
||||
transform: translateY(10rem);
|
||||
transition: left 0.5s, margin 0.5s, opacity 0.5s, transform 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.notifications .alert {
|
||||
opacity: 0;
|
||||
margin-top: 0;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.notifications .alert span {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0 3rem;
|
||||
font-weight: 300;
|
||||
font-size: 0.9rem;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1rem;
|
||||
max-height: 5rem;
|
||||
}
|
||||
|
||||
.notifications .alert span.title {
|
||||
margin: 1rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
|
||||
.notifications .alert span.message {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.notifications .alert span a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.notifications .alert .dismiss {
|
||||
width: 2rem;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1em;
|
||||
transform: scale(1, 1) translateY(-50%);
|
||||
font-size: 2rem;
|
||||
transition: margin 0.5s, opacity 0.5s, transform 0.5s;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
font-weight: 300;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
#about,
|
||||
#sessioninfo,
|
||||
#bugreport {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background-color: #eee;
|
||||
border: 2px solid #ddd;
|
||||
display: none;
|
||||
z-index: 1000000;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#about,
|
||||
#sessioninfo,
|
||||
#bugreport {
|
||||
position: fixed;
|
||||
margin: auto;
|
||||
width: 640px;
|
||||
height: fit-content;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -320px;
|
||||
margin-top: -140px;
|
||||
}
|
||||
|
||||
#bugreport {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#shadow_pointer {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 1px;
|
||||
z-index: 100000;
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
table#sessiondata {
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
table#sessiondata th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table#sessiondata td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#upload {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-icons {
|
||||
position: relative;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.action-events {
|
||||
position: relative;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#action-link {
|
||||
display: none;
|
||||
color: #99f;
|
||||
}
|
||||
|
||||
.clock_block {
|
||||
position: relative;
|
||||
float: left;
|
||||
padding-left: 100px;
|
||||
}
|
||||
|
||||
#clock_text {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.audio {
|
||||
float: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.simple-keyboard {
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
#login-overlay {
|
||||
position: fixed;
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 20;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#login-box {
|
||||
height: fit-content;
|
||||
width: 480px;
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
background: #d8d8d8;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#login-innerbox {
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
z-index: 3;
|
||||
background: #e8e8e8;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#login-header {
|
||||
font-size: 2em;
|
||||
clear: both;
|
||||
display: inline-block;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
#login-innerbox input {
|
||||
font-size: 1.5em;
|
||||
clear: both;
|
||||
display: inline-block;
|
||||
margin: 0.5em;
|
||||
padding: 2px;
|
||||
border: 2px solid #101010;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#password-box {
|
||||
background: url("../icons/authentication.png") no-repeat;
|
||||
margin-left: 2em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
#login-buttons {
|
||||
margin: 2em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
margin: 1em;
|
||||
padding: 10px 32px;
|
||||
font-size: 18px;
|
||||
line-height: 1.3333333;
|
||||
color: #fff;
|
||||
background-color: #5cb85c;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-image: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.login-button:focus,
|
||||
.login-button:hover {
|
||||
background-color: #449d44;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
.notifications .alert.visible {
|
||||
margin-top: 0;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.notifications .alert.hidden {
|
||||
overflow: hidden;
|
||||
margin-top: -2.125rem;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.notifications .alert.hidden {
|
||||
transform: translateY(0) translateX(-10rem);
|
||||
}
|
||||
}
|
||||
306
gns3server/static/web-ui/assets/xpra-html5/css/connect.css
Normal file
@ -0,0 +1,306 @@
|
||||
/* Screenreader-only */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-signin-heading {
|
||||
font-weight: 500;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
/* Version number */
|
||||
h5 {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Forms for ports, passwords, etc... */
|
||||
.form-control::placeholder {
|
||||
color: #999;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
|
||||
color: #555;
|
||||
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
|
||||
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
}
|
||||
|
||||
/* Checkbox labels */
|
||||
#ssl-span,
|
||||
#webtransport-span,
|
||||
#encryption-span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Radio button labels */
|
||||
label {
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Start session text boxes */
|
||||
#action_connect_group,
|
||||
#action_start_group,
|
||||
#action_start_desktop_group,
|
||||
#action_shadow_group {
|
||||
max-height: 34px;
|
||||
}
|
||||
|
||||
#action_connect_group>.form-control,
|
||||
#action_start_group>.form-control,
|
||||
#action_start_desktop_group>.form-control,
|
||||
#action_shadow_group>.form-control {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.list-style-none.action {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.panel-group .list-style-none.action {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* Connect buttons */
|
||||
.form-group {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 1px solid transparent;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 10px 16px;
|
||||
|
||||
border-radius: 6px;
|
||||
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
color: white;
|
||||
background-color: #5cb85c;
|
||||
border-color: #4cac4c;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
color: white;
|
||||
background-color: #449d44;
|
||||
border-color: #398439;
|
||||
}
|
||||
|
||||
/* Advanced options */
|
||||
.panel.panel-default {
|
||||
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.panel-group .panel {
|
||||
margin-bottom: 0;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#expandopts {
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
padding: 10px 15px;
|
||||
|
||||
border-color: #ddd;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
position: relative;
|
||||
top: -16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
display: block;
|
||||
list-style-type: none;
|
||||
|
||||
line-height: 1.4;
|
||||
padding: 0;
|
||||
margin-bottom: -1px;
|
||||
|
||||
background-color: white;
|
||||
|
||||
border: 1px solid #ddd;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.list-group-item>select {
|
||||
padding: 4px 1px 4px 5px;
|
||||
}
|
||||
|
||||
.list-group-item>.form-control {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Background */
|
||||
div.bg-image {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-image: url(../background.jpg);
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
opacity: 0.3;
|
||||
z-index: -1000;
|
||||
}
|
||||
|
||||
div.container {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.form-signin {
|
||||
max-width: 530px;
|
||||
padding: 15px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-or-heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-signin h5 {
|
||||
padding-left: 8em;
|
||||
}
|
||||
|
||||
.form-signin .form-signin-heading,
|
||||
.form-signin .checkbox {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-signin .checkbox,
|
||||
.form-signin .radiobox {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.form-signin .form-control {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
max-width: 16em;
|
||||
}
|
||||
|
||||
.form-signin .form-control.command {
|
||||
margin-left: 1.6em;
|
||||
}
|
||||
|
||||
.form-signin .form-control:focus {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.form-signin .panel-group {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.form-signin .security-box {
|
||||
padding-left: 5em;
|
||||
}
|
||||
|
||||
.form-signin .security-box #key {
|
||||
font-size: 12px;
|
||||
padding: 0 10px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.form-signin .list-group-item {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.form-signin .list-style-none {
|
||||
list-style-type: none;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form-signin .action input {
|
||||
font-size: 12px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.form-signin img.command-icon {
|
||||
object-fit: scale-down;
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
|
||||
.form-signin span.warning {
|
||||
color: red;
|
||||
font-size: smaller;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.form-signin span.disabled {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.form-signin img.field-icon {
|
||||
margin-left: -32px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
font-family: "bootstrap-icons";
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
}
|
||||
|
||||
.form-signin span#server-info {
|
||||
padding-left: 20px;
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
}
|
||||
36
gns3server/static/web-ui/assets/xpra-html5/css/icon.css
Normal file
@ -0,0 +1,36 @@
|
||||
@font-face {
|
||||
font-family: "materialicons";
|
||||
src: url(../icons/materialicons-regular.woff) format("woff");
|
||||
}
|
||||
|
||||
.Icon,
|
||||
*[data-icon]:before {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
|
||||
font-family: "materialicons";
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
|
||||
color: black;
|
||||
|
||||
content: attr(data-icon);
|
||||
|
||||
/* This is important for IE, which has it off by default */
|
||||
font-feature-settings: "liga" 1;
|
||||
}
|
||||
|
||||
.Icon.-small {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.icon-toggled:before {
|
||||
color: #777777;
|
||||
}
|
||||
|
||||
.icon-paused:before {
|
||||
color: gold;
|
||||
}
|
||||
197
gns3server/static/web-ui/assets/xpra-html5/css/menu-skin.css
Normal file
@ -0,0 +1,197 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Mark Harkin, 2016 Dylan Hicks (aka. dylanh333)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
#float_menu_button {
|
||||
background: white;
|
||||
height: 50px;
|
||||
width: 12px;
|
||||
border-radius: 0 10px 10px 0;
|
||||
|
||||
position: relative;
|
||||
left: 30px;
|
||||
top: -125px;
|
||||
}
|
||||
|
||||
#float_menu_arrow {
|
||||
color: black;
|
||||
border-color: black;
|
||||
|
||||
position: relative;
|
||||
right: 14px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
#float_menu {
|
||||
display: none;
|
||||
position: fixed;
|
||||
height: 30px;
|
||||
padding: 0 20px 0 0;
|
||||
z-index: 100000;
|
||||
background: white;
|
||||
|
||||
border-color: grey;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.menu-divleft {
|
||||
float: left;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.menu-divright {
|
||||
float: right;
|
||||
clear: right;
|
||||
}
|
||||
|
||||
.menu-content-left {
|
||||
white-space: nowrap;
|
||||
float: left;
|
||||
width: 0;
|
||||
height: 24px;
|
||||
padding: 3px 2px;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.menu-content-right {
|
||||
white-space: nowrap;
|
||||
float: right;
|
||||
width: 0;
|
||||
height: 16px;
|
||||
padding: 7px 2px;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.windowlist-li {
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.Menu {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.Menu.-vertical {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
/* Foreground */
|
||||
.Menu,
|
||||
.Menu li.-hasSubmenu>a:after {
|
||||
color: black;
|
||||
border-color: black;
|
||||
/* chevron colour */
|
||||
}
|
||||
|
||||
/* Background */
|
||||
.Menu,
|
||||
.Menu li ul {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Border */
|
||||
.Menu,
|
||||
.Menu li ul {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.Menu li ul,
|
||||
.Menu.-floating {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.Menu.-horizontal>li>ul {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.Menu:not(.-alignRight) li>ul,
|
||||
.Menu.-floating:not(.-alignRight) {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
.Menu.-alignRight li>ul,
|
||||
.Menu.-floating.-alignRight {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
/* Drop shadow on Menus and header */
|
||||
.Menu li ul,
|
||||
.Menu.-floating {
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2),
|
||||
0 3px 1px 0 rgba(0, 0, 0, 0.2), 0 6px 12px 0 rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
#window_preview {
|
||||
display: none;
|
||||
width: 65%;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 80000;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
box-shadow: 0 0 25px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 2px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.slick-track {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.slick-slide {
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
.slick-current {
|
||||
box-shadow: 0 0 20px rgba(255, 255, 255, 65%);
|
||||
border-radius: 2px;
|
||||
border: 3px solid rgba(255, 255, 255, 50%);
|
||||
}
|
||||
|
||||
.window-preview-item-container {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.window-preview-item-img {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.window-preview-item-text {
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: "auto";
|
||||
padding: 4px;
|
||||
}
|
||||
309
gns3server/static/web-ui/assets/xpra-html5/css/menu.css
Normal file
@ -0,0 +1,309 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Mark Harkin, 2016 Dylan Hicks (aka. dylanh333)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
.Menu {
|
||||
display: none;
|
||||
position: relative;
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
/* Three basic menu layouts */
|
||||
/* This also enforces that menu is a ul, and that
|
||||
* layout modifier is specified
|
||||
*/
|
||||
ul.Menu.-horizontal,
|
||||
ul.Menu.-vertical {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul.Menu.-floating {
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Menu and menu-item layout */
|
||||
.Menu,
|
||||
.Menu li,
|
||||
.Menu li>ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Menu li {
|
||||
display: block;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.Menu.-horizontal>li {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.Menu li>* {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Menu li>ul {
|
||||
position: absolute;
|
||||
min-width: 100%;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.Menu.-horizontal.-alignRight li>ul {
|
||||
left: auto;
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
.Menu.-horizontal.-alignRight>li>ul {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.Menu.-horizontal>li>ul {
|
||||
top: auto;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
/* Menu behaviour */
|
||||
.Menu li>ul,
|
||||
.Menu.-floating {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.Menu li>ul.-visible,
|
||||
ul.Menu.-floating.-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Menu animation */
|
||||
.Menu li>ul,
|
||||
.Menu.-horizontal.-alignRight li>ul,
|
||||
.Menu.-floating {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
transform-origin: left top;
|
||||
}
|
||||
|
||||
.Menu.-alignRight li>ul,
|
||||
.Menu.-floating.-alignRight {
|
||||
transform-origin: right top;
|
||||
}
|
||||
|
||||
.Menu li>ul.-animating,
|
||||
.Menu.-floating.-animating {
|
||||
opacity: 0 !important;
|
||||
transform: scale(0.96) translateX(-16px);
|
||||
}
|
||||
|
||||
.Menu li>ul.-animating {
|
||||
z-index: -1 !important;
|
||||
}
|
||||
|
||||
.Menu.-horizontal>li>ul.-animating {
|
||||
transform: scale(0.96) translateY(-16px);
|
||||
}
|
||||
|
||||
.Menu.-alignRight li>ul.-animating,
|
||||
.Menu.-floating.-alignRight.-animating {
|
||||
transform: scale(0.96) translateX(16px);
|
||||
}
|
||||
|
||||
.Menu.-horizontal.-alignRight>li>ul.-animating {
|
||||
transform: scale(0.96) translateY(-16px);
|
||||
}
|
||||
|
||||
/* Menu item icons */
|
||||
.Menu *[data-icon]:before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.Menu .Icon,
|
||||
.Menu *[data-icon]:before {
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.Menu .Icon {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.Menu *:empty[data-icon] {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Submenu chevrons */
|
||||
.Menu li.-hasSubmenu>a:after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
right: 8px;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
margin: auto 0;
|
||||
|
||||
transform: rotate(45deg);
|
||||
border-width: 1px;
|
||||
border-color: black;
|
||||
border-style: solid solid none none;
|
||||
|
||||
content: "";
|
||||
}
|
||||
|
||||
.Menu.-horizontal>li.-hasSubmenu>a:after {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
bottom: 4px;
|
||||
top: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
|
||||
border-style: none solid solid none;
|
||||
}
|
||||
|
||||
.Menu li.-hasSubmenu.-noChevron>a:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
** Configurable values
|
||||
*/
|
||||
/* Height of navbar, and menu items */
|
||||
/* All of these must be of equal value */
|
||||
.Menu {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.Menu.-horizontal,
|
||||
.Menu li,
|
||||
.Menu li> :first-child {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.Menu *[data-icon]:before,
|
||||
.Menu .Icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.Menu *[data-icon] {
|
||||
min-width: 30px;
|
||||
min-height: 30px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
/* Default font settings for menu */
|
||||
.Menu {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Icon font sizes */
|
||||
.Menu *[data-icon]:before,
|
||||
.Menu .Icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* For submenus */
|
||||
.Menu>li ul .Icon,
|
||||
.Menu.-floating .Icon,
|
||||
.Menu>li ul *[data-icon]:before,
|
||||
.Menu.-floating *[data-icon]:before {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Colouring of menus */
|
||||
/* Foreground */
|
||||
.Menu,
|
||||
.Menu li.-hasSubmenu>a:after {
|
||||
color: black;
|
||||
border-color: black;
|
||||
/* chevron colour */
|
||||
}
|
||||
|
||||
/* Background */
|
||||
.Menu ul,
|
||||
.Menu.-floating {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Padding for each menu item */
|
||||
.Menu li>* {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* Indent of chevron */
|
||||
.Menu li li.-hasSubmenu>a:after,
|
||||
.Menu:not(.-horizontal)>li.-hasSubmenu>a:after {
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.Menu li li.-hasSubmenu>a,
|
||||
.Menu:not(.-horizontal)>li.-hasSubmenu>a {
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
/* Minimum width of dropdown menus */
|
||||
.Menu.-horizontal li>ul,
|
||||
.Menu.-floating {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Animation speed of dropdown menus */
|
||||
.Menu li>ul,
|
||||
.Menu.-floating {
|
||||
transition: width 0.1s, height 0.1s, transform 0.1s, opacity 0.1s;
|
||||
}
|
||||
|
||||
/* Styling of hyperlink text */
|
||||
.Menu li>a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Animation speed of :hover shading */
|
||||
.Menu li>a:first-child {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
/* Colour of :hover shading */
|
||||
.Menu li:hover>a:first-child,
|
||||
.Menu li.-active>a:first-child {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/*
|
||||
** End configurable values
|
||||
*/
|
||||
@ -0,0 +1,122 @@
|
||||
/*!
|
||||
*
|
||||
* simple-keyboard v2.32.56
|
||||
* https://github.com/hodgef/simple-keyboard
|
||||
*
|
||||
* Copyright (c) Francisco Hodge (https://github.com/hodgef)
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
.hg-theme-default {
|
||||
width: 100%;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button span {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hg-theme-default button.hg-button {
|
||||
border-width: 0;
|
||||
outline: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.hg-theme-default {
|
||||
font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue",
|
||||
Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
background-color: #ececec;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button {
|
||||
display: inline-block;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-row:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-row .hg-button-container,
|
||||
.hg-theme-default .hg-row .hg-button:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-row>div:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-row .hg-button-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button {
|
||||
box-shadow: 0 0 3px -1px rgba(0, 0, 0, 0.3);
|
||||
height: 40px;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
padding: 5px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #b5b5b5;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button.hg-activeButton {
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
.hg-theme-default.hg-layout-numeric .hg-button {
|
||||
width: 33.3%;
|
||||
height: 60px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button.hg-button-numpadadd,
|
||||
.hg-theme-default .hg-button.hg-button-numpadenter {
|
||||
height: 85px;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button.hg-button-numpad0 {
|
||||
width: 105px;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button.hg-button-com {
|
||||
max-width: 85px;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button.hg-standardBtn.hg-button-at {
|
||||
max-width: 45px;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button.hg-selectedButton {
|
||||
background: rgba(5, 25, 70, 0.53);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button.hg-standardBtn[data-skbtn=".com"] {
|
||||
max-width: 82px;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button.hg-standardBtn[data-skbtn="@"] {
|
||||
max-width: 60px;
|
||||
}
|
||||
109
gns3server/static/web-ui/assets/xpra-html5/css/slick.css
Normal file
@ -0,0 +1,109 @@
|
||||
/* Slider */
|
||||
.slick-slider {
|
||||
position: relative;
|
||||
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
-webkit-touch-callout: none;
|
||||
-ms-touch-action: pan-y;
|
||||
touch-action: pan-y;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.slick-list {
|
||||
position: relative;
|
||||
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.slick-list:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.slick-list.dragging {
|
||||
cursor: hand;
|
||||
}
|
||||
|
||||
.slick-slider .slick-track,
|
||||
.slick-slider .slick-list {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.slick-track {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.slick-track:before,
|
||||
.slick-track:after {
|
||||
display: table;
|
||||
|
||||
content: "";
|
||||
}
|
||||
|
||||
.slick-track:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.slick-loading .slick-track {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.slick-slide {
|
||||
display: none;
|
||||
float: left;
|
||||
|
||||
height: 100%;
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .slick-slide {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.slick-slide img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.slick-slide.slick-loading img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slick-slide.dragging img {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slick-initialized .slick-slide {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.slick-loading .slick-slide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.slick-vertical .slick-slide {
|
||||
display: block;
|
||||
|
||||
height: auto;
|
||||
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.slick-arrow.slick-hidden {
|
||||
display: none;
|
||||
}
|
||||
41
gns3server/static/web-ui/assets/xpra-html5/css/spinner.css
Normal file
@ -0,0 +1,41 @@
|
||||
.spinner {
|
||||
margin: 60px auto;
|
||||
font-size: 10px;
|
||||
position: relative;
|
||||
text-indent: -9999em;
|
||||
border-top: 1.1em solid rgba(255, 255, 255, 0.2);
|
||||
border-right: 1.1em solid rgba(255, 255, 255, 0.2);
|
||||
border-bottom: 1.1em solid rgba(255, 255, 255, 0.2);
|
||||
border-left: 1.1em solid #ffffff;
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation: load8 1.1s infinite linear;
|
||||
animation: load8 1.1s infinite linear;
|
||||
}
|
||||
|
||||
.spinner,
|
||||
.spinner:after {
|
||||
border-radius: 50%;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
}
|
||||
|
||||
@-webkit-keyframes load8 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes load8 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
# Xpra HTML5 default settings
|
||||
#
|
||||
# ie:
|
||||
# port = 10000
|
||||
# keyboard_layout = gb
|
||||
# encoding = auto
|
||||
# bandwidth_limit = 10000000
|
||||
# debug_keyboard = true
|
||||
|
||||
blocked-hosts = xpra.org,www.xpra.org
|
||||
|
||||
min-quality = 10
|
||||
min-speed = 50
|
||||
|
||||
# Configure xpra HTML5 client defaults - disable unused features
|
||||
file_transfer=false
|
||||
printing=false
|
||||
sound=false
|
||||
audio=false
|
||||
sound_button=false
|
||||
fullscreen_button=false
|
||||
keyboard_button=false
|
||||
cursor_lock_button=false
|
||||
keyboard=false
|
||||
mediasource=false
|
||||
aurora=false
|
||||
http-stream=false
|
||||
|
||||
# Menu toolbar position: top-left, top, top-right, novnc
|
||||
toolbar_position=top-right
|
||||
|
||||
# hide some menus:
|
||||
# xpramenu=false
|
||||
# cursor_lock_button=false
|
||||
82
gns3server/static/web-ui/assets/xpra-html5/digest.html
Normal file
@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<!--
|
||||
Copyright (c) 2024 Antoine Martin <antoine@xpra.org>
|
||||
Licensed under MPL 2.0
|
||||
-->
|
||||
|
||||
<title>Xpra HTML5 AES Test Page</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<script type="text/javascript" src="js/Utilities.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div>
|
||||
<pre id="info"></pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const info = document.getElementById("info");
|
||||
const lines = [];
|
||||
|
||||
function show(newtext) {
|
||||
// console.log(newtext);
|
||||
lines.push(newtext);
|
||||
info.innerText = lines.join("\n");
|
||||
}
|
||||
|
||||
function show_digest(digest_name, v1, v2, expected) {
|
||||
Utilities.gendigest(digest_name, v1, v2)
|
||||
.then(digest => {
|
||||
show("got response: " + Utilities.convertToHex(digest));
|
||||
if (expected != undefined && digest != expected) {
|
||||
show("does not match expected value: " + expected);
|
||||
}
|
||||
show("");
|
||||
})
|
||||
.catch(err => {
|
||||
// console.error("digest error: ", err);
|
||||
show("failed to generate " + digest_name + ": " + err)
|
||||
show("");
|
||||
});
|
||||
}
|
||||
|
||||
show_digest("xor", "hello", "foo00", Utilities.hexToString("0e0a035c5f"));
|
||||
|
||||
show_digest("test-invalid", "", "");
|
||||
|
||||
let password = Utilities.hexToString("4a87629131cdd27f0baa7bdbfa7f53ca7d11fc954937c2f0c025be326c7d8f742624daf578a146d1c7f10e3b0b158a725d6073af111ef517f7f12694fd1ab7cd");
|
||||
let salt = Utilities.hexToString("0aa4ecbebd55488d6a976628616c9170032083a0385feefcde7cd0ecb82ed87c");
|
||||
// show("password=" + password);
|
||||
// show("salt=" + password);
|
||||
show("hex(password)=" + Utilities.convertToHex(password));
|
||||
show("hex(salt)=" + Utilities.convertToHex(password));
|
||||
|
||||
show_digest("hmac+sha512", salt, password);
|
||||
|
||||
// similar to login obfuscation:
|
||||
password = "foobar12";
|
||||
salt = Utilities.hexToString("a54ca3f2777c2ce00090389e328fc9a2249cc1cfe99c84125f6d1d92abc113212bc96c4907f8f9492cf43dd4c22a889c4d170cc7ac62f41b0dcf28d488588f27");
|
||||
Utilities.gendigest("xor", password, salt)
|
||||
.then(value => {
|
||||
show("xored password:" + value);
|
||||
Utilities.gendigest("xor", value, salt)
|
||||
.then(restored => {
|
||||
show("unxored password:" + restored);
|
||||
if (restored != password) {
|
||||
show("error! xor is not symmetrical!")
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
gns3server/static/web-ui/assets/xpra-html5/favicon.ico
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
gns3server/static/web-ui/assets/xpra-html5/favicon.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
BIN
gns3server/static/web-ui/assets/xpra-html5/icons/close.png
Normal file
|
After Width: | Height: | Size: 333 B |
|
After Width: | Height: | Size: 1.2 KiB |
BIN
gns3server/static/web-ui/assets/xpra-html5/icons/empty.png
Normal file
|
After Width: | Height: | Size: 356 B |
BIN
gns3server/static/web-ui/assets/xpra-html5/icons/eye-slash.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
gns3server/static/web-ui/assets/xpra-html5/icons/eye.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
gns3server/static/web-ui/assets/xpra-html5/icons/fullscreen.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
gns3server/static/web-ui/assets/xpra-html5/icons/maximize.png
Normal file
|
After Width: | Height: | Size: 291 B |
BIN
gns3server/static/web-ui/assets/xpra-html5/icons/minimize.png
Normal file
|
After Width: | Height: | Size: 199 B |
BIN
gns3server/static/web-ui/assets/xpra-html5/icons/noicon.png
Normal file
|
After Width: | Height: | Size: 133 B |
|
After Width: | Height: | Size: 543 B |
BIN
gns3server/static/web-ui/assets/xpra-html5/icons/xpra-logo.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
1600
gns3server/static/web-ui/assets/xpra-html5/index.html
Normal file
4991
gns3server/static/web-ui/assets/xpra-html5/js/Client.js
Normal file
130
gns3server/static/web-ui/assets/xpra-html5/js/Constants.js
Normal file
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* This file is part of Xpra.
|
||||
* Copyright (C) 2016 Antoine Martin <antoine@xpra.org>
|
||||
* Licensed under MPL 2.0, see:
|
||||
* http://www.mozilla.org/MPL/2.0/
|
||||
*
|
||||
*/
|
||||
|
||||
const DEFAULT_BOX_COLORS = {
|
||||
png: "yellow",
|
||||
h264: "blue",
|
||||
vp8: "green",
|
||||
rgb24: "orange",
|
||||
rgb32: "red",
|
||||
jpeg: "purple",
|
||||
webp: "pink",
|
||||
"png/P": "indigo",
|
||||
"png/L": "teal",
|
||||
h265: "khaki",
|
||||
vp9: "lavender",
|
||||
mpeg4: "black",
|
||||
scroll: "brown",
|
||||
mpeg1: "olive",
|
||||
avif: "cyan",
|
||||
};
|
||||
|
||||
const MOVERESIZE_SIZE_TOPLEFT = 0;
|
||||
const MOVERESIZE_SIZE_TOP = 1;
|
||||
const MOVERESIZE_SIZE_TOPRIGHT = 2;
|
||||
const MOVERESIZE_SIZE_RIGHT = 3;
|
||||
const MOVERESIZE_SIZE_BOTTOMRIGHT = 4;
|
||||
const MOVERESIZE_SIZE_BOTTOM = 5;
|
||||
const MOVERESIZE_SIZE_BOTTOMLEFT = 6;
|
||||
const MOVERESIZE_SIZE_LEFT = 7;
|
||||
const MOVERESIZE_MOVE = 8;
|
||||
const MOVERESIZE_SIZE_KEYBOARD = 9;
|
||||
const MOVERESIZE_MOVE_KEYBOARD = 10;
|
||||
const MOVERESIZE_CANCEL = 11;
|
||||
const MOVERESIZE_DIRECTION_STRING = {
|
||||
0: "SIZE_TOPLEFT",
|
||||
1: "SIZE_TOP",
|
||||
2: "SIZE_TOPRIGHT",
|
||||
3: "SIZE_RIGHT",
|
||||
4: "SIZE_BOTTOMRIGHT",
|
||||
5: "SIZE_BOTTOM",
|
||||
6: "SIZE_BOTTOMLEFT",
|
||||
7: "SIZE_LEFT",
|
||||
8: "MOVE",
|
||||
9: "SIZE_KEYBOARD",
|
||||
10: "MOVE_KEYBOARD",
|
||||
11: "CANCEL",
|
||||
};
|
||||
const MOVERESIZE_DIRECTION_JS_NAME = {
|
||||
0: "nw",
|
||||
1: "n",
|
||||
2: "ne",
|
||||
3: "e",
|
||||
4: "se",
|
||||
5: "s",
|
||||
6: "sw",
|
||||
7: "w",
|
||||
};
|
||||
|
||||
const PACKET_TYPES = {
|
||||
control: "control",
|
||||
ack_file_chunk: "ack-file-chunk",
|
||||
bell: "bell",
|
||||
buffer_refresh: "buffer-refresh",
|
||||
button_action: "button-action",
|
||||
challenge: "challenge",
|
||||
clipboard_request: "clipboard-request",
|
||||
clipboard_token: "clipboard-token",
|
||||
clipboard_contents_none: "clipboard-contents-none",
|
||||
clipboard_contents: "clipboard-contents",
|
||||
close: "close",
|
||||
close_window: "close-window",
|
||||
configure_override_redirect: "configure-override-redirect",
|
||||
configure_window: "configure-window",
|
||||
connection_data: "connection-data",
|
||||
cursor: "cursor",
|
||||
damage_sequence: "damage-sequence",
|
||||
desktop_size: "desktop_size",
|
||||
configure_display: "configure-display",
|
||||
disconnect: "disconnect",
|
||||
draw: "draw",
|
||||
encodings: "encodings",
|
||||
eos: "eos",
|
||||
error: "error",
|
||||
focus: "focus",
|
||||
hello: "hello",
|
||||
info_request: "info-request",
|
||||
info_response: "info-response",
|
||||
initiate_moveresize: "initiate-moveresize",
|
||||
key_action: "key-action",
|
||||
layout_changed: "layout-changed",
|
||||
keymap_changed: "keymap-changed",
|
||||
logging: "logging",
|
||||
lost_window: "lost-window",
|
||||
map_window: "map-window",
|
||||
new_override_redirect: "new-override-redirect",
|
||||
new_tray: "new-tray",
|
||||
new_window: "new-window",
|
||||
notification_action: "notification-action",
|
||||
notification_close: "notification-close",
|
||||
notify_close: "notify_close",
|
||||
notify_show: "notify_show",
|
||||
open: "open",
|
||||
open_url: "open-url",
|
||||
ping: "ping",
|
||||
ping_echo: "ping_echo",
|
||||
pointer_position: "pointer-position",
|
||||
printers: "printers",
|
||||
raise_window: "raise-window",
|
||||
resume: "resume",
|
||||
send_file: "send-file",
|
||||
send_file_chunk: "send-file-chunk",
|
||||
set_clipboard_enabled: "set-clipboard-enabled",
|
||||
setting_change: "setting-change",
|
||||
sound_control: "sound-control",
|
||||
sound_data: "sound-data",
|
||||
startup_complete: "startup-complete",
|
||||
start_command: "start-command",
|
||||
suspend: "suspend",
|
||||
unmap_window: "unmap-window",
|
||||
wheel_motion: "wheel-motion",
|
||||
window_icon: "window-icon",
|
||||
window_metadata: "window-metadata",
|
||||
window_move_resize: "window-move-resize",
|
||||
window_resized: "window-resized",
|
||||
};
|
||||
347
gns3server/static/web-ui/assets/xpra-html5/js/DecodeWorker.js
Normal file
@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Copyright (c) 2021 Antoine Martin <antoine@xpra.org>
|
||||
*/
|
||||
|
||||
importScripts("./lib/lz4.js");
|
||||
importScripts("./RgbHelpers.js");
|
||||
|
||||
const on_hold = new Map();
|
||||
|
||||
let zerocopy = true;
|
||||
|
||||
function decode_eos(wid) {}
|
||||
|
||||
function decode_draw_packet(packet, start) {
|
||||
const wid = packet[1];
|
||||
const width = packet[4];
|
||||
const height = packet[5];
|
||||
const coding = packet[6];
|
||||
const packet_sequence = packet[8];
|
||||
|
||||
function send_back(raw_buffers) {
|
||||
const wid_hold = on_hold.get(wid);
|
||||
if (wid_hold) {
|
||||
//find the highest sequence number which is still lower than this packet
|
||||
let seq_holding = 0;
|
||||
for (const seq of wid_hold.keys()) {
|
||||
if (seq > seq_holding && seq < packet_sequence) {
|
||||
seq_holding = seq;
|
||||
}
|
||||
}
|
||||
if (seq_holding) {
|
||||
const held = wid_hold.get(seq_holding);
|
||||
if (held) {
|
||||
held.push([packet, raw_buffers]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
do_send_back(packet, raw_buffers);
|
||||
}
|
||||
|
||||
function do_send_back(p, raw_buffers) {
|
||||
self.postMessage({
|
||||
draw: p,
|
||||
start
|
||||
}, raw_buffers);
|
||||
}
|
||||
|
||||
function decode_error(message) {
|
||||
self.postMessage({
|
||||
error: `${message}`,
|
||||
packet,
|
||||
start
|
||||
});
|
||||
}
|
||||
|
||||
function hold() {
|
||||
//we're loading asynchronously
|
||||
//so ensure that any packet sequence arriving after this one will be put on hold
|
||||
//until we have finished decoding this one:
|
||||
let wid_hold = on_hold.get(wid);
|
||||
if (!wid_hold) {
|
||||
wid_hold = new Map();
|
||||
on_hold.set(wid, wid_hold);
|
||||
}
|
||||
wid_hold.set(packet_sequence, []);
|
||||
return wid_hold;
|
||||
}
|
||||
|
||||
function release() {
|
||||
const wid_hold = on_hold.get(wid);
|
||||
if (!wid_hold) {
|
||||
//could have been cancelled by EOS
|
||||
return;
|
||||
}
|
||||
//release any packets held back by this image:
|
||||
const held = wid_hold.get(packet_sequence);
|
||||
if (!held) {
|
||||
//could have been cancelled by EOS
|
||||
return;
|
||||
}
|
||||
let index;
|
||||
for (index = 0; index < held.length; index++) {
|
||||
const held_packet = held[index][0];
|
||||
const held_raw_buffers = held[index][1];
|
||||
do_send_back(held_packet, held_raw_buffers);
|
||||
}
|
||||
wid_hold.delete(packet_sequence);
|
||||
if (wid_hold.size === 0 && on_hold.has(wid)) {
|
||||
//this was the last held sequence for this window
|
||||
on_hold.delete(wid);
|
||||
}
|
||||
}
|
||||
|
||||
function send_rgb32_back(data, actual_width, actual_height, options) {
|
||||
const img = new ImageData(
|
||||
new Uint8ClampedArray(data.buffer),
|
||||
actual_width,
|
||||
actual_height
|
||||
);
|
||||
hold();
|
||||
createImageBitmap(img, 0, 0, actual_width, actual_height, options).then(
|
||||
function(bitmap) {
|
||||
packet[6] = "bitmap:rgb32";
|
||||
packet[7] = bitmap;
|
||||
send_back([bitmap]);
|
||||
release();
|
||||
},
|
||||
function(error) {
|
||||
decode_error(
|
||||
`failed to create ${actual_width}x${actual_height} rgb32 bitmap from buffer ${data}: ${error}`
|
||||
);
|
||||
release();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let options = {};
|
||||
if (packet.length > 10) options = packet[10];
|
||||
const bitmap_options = {
|
||||
premultiplyAlpha: "none",
|
||||
};
|
||||
const scaled_size = options["scaled_size"];
|
||||
if (scaled_size) {
|
||||
delete options["scaled-size"];
|
||||
bitmap_options["resizeWidth"] = width;
|
||||
bitmap_options["resizeHeight"] = height;
|
||||
bitmap_options["resizeQuality"] = "medium";
|
||||
}
|
||||
try {
|
||||
if (coding === "rgb24" || coding === "rgb32") {
|
||||
const data = decode_rgb(packet);
|
||||
send_rgb32_back(data, width, height, bitmap_options);
|
||||
} else if (
|
||||
coding.startsWith("png") ||
|
||||
coding === "jpeg" ||
|
||||
coding === "webp" ||
|
||||
coding === "avif"
|
||||
) {
|
||||
const data = packet[7];
|
||||
if (!data.buffer) {
|
||||
decode_error(`missing pixel data buffer: ${typeof data}`);
|
||||
release();
|
||||
return;
|
||||
}
|
||||
let buffer = data;
|
||||
if (zerocopy) {
|
||||
buffer = data.buffer;
|
||||
}
|
||||
const blob = new Blob([buffer], {
|
||||
type: `image/${coding}`
|
||||
});
|
||||
hold();
|
||||
createImageBitmap(blob, bitmap_options).then(
|
||||
function(bitmap) {
|
||||
packet[6] = `bitmap:${coding}`;
|
||||
packet[7] = bitmap;
|
||||
send_back([bitmap]);
|
||||
release();
|
||||
},
|
||||
function(error) {
|
||||
console.info(`decode worker failed to create ${coding} image bitmap: ${error}`);
|
||||
console.info(`using ${blob} + ${JSON.stringify(bitmap_options)} from data=${data}`);
|
||||
console.info(`data from ${buffer.constructor.name} of length ${data.length}`);
|
||||
// maybe the regular paint function will succeed?
|
||||
console.info("sending it back for decoding directly in the client");
|
||||
send_back([]);
|
||||
release();
|
||||
if (zerocopy) {
|
||||
console.warn("turning off zerocopy");
|
||||
zerocopy = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
//pass-through:
|
||||
send_back([]);
|
||||
}
|
||||
} catch (error) {
|
||||
decode_error(
|
||||
`error processing ${coding} packet ${packet_sequence}: ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function check_image_decode(
|
||||
format,
|
||||
image_bytes,
|
||||
success_callback,
|
||||
fail_callback
|
||||
) {
|
||||
if (console) {
|
||||
console.info(
|
||||
"checking",
|
||||
format,
|
||||
`with test image: ${image_bytes.length} bytes`
|
||||
);
|
||||
}
|
||||
try {
|
||||
const timer = setTimeout(function() {
|
||||
fail_callback(format, `timeout, no ${format} picture decoded`);
|
||||
}, 2000);
|
||||
const data = new Uint8Array(image_bytes);
|
||||
const blob = new Blob([data], {
|
||||
type: `image/${format}`
|
||||
});
|
||||
createImageBitmap(blob, {
|
||||
premultiplyAlpha: "none",
|
||||
}).then(
|
||||
function() {
|
||||
clearTimeout(timer);
|
||||
success_callback(format);
|
||||
},
|
||||
function(error) {
|
||||
clearTimeout(timer);
|
||||
fail_callback(format, `${error}`);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
fail_callback(format, `${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
onmessage = function(e) {
|
||||
const data = e.data;
|
||||
switch (data.cmd) {
|
||||
case "check": {
|
||||
const encodings = data.encodings;
|
||||
if (console) {
|
||||
console.info("decode worker checking:", encodings);
|
||||
}
|
||||
const CHECKS = {
|
||||
png: [
|
||||
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0,
|
||||
1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68,
|
||||
65, 84, 120, 218, 99, 252, 207, 192, 80, 15, 0, 4, 133, 1, 128, 132,
|
||||
169, 140, 33, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
|
||||
],
|
||||
webp: [
|
||||
82, 73, 70, 70, 58, 0, 0, 0, 87, 69, 66, 80, 86, 80, 56, 32, 46, 0, 0,
|
||||
0, 178, 2, 0, 157, 1, 42, 2, 0, 2, 0, 46, 105, 52, 154, 77, 34, 34,
|
||||
34, 34, 34, 0, 104, 75, 40, 0, 5, 206, 150, 90, 0, 0, 254, 247, 159,
|
||||
127, 253, 15, 63, 198, 192, 255, 242, 240, 96, 0, 0,
|
||||
],
|
||||
jpeg: [
|
||||
255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 1, 1, 0, 96, 0, 96,
|
||||
0, 0, 255, 219, 0, 67, 0, 8, 6, 6, 7, 6, 5, 8, 7, 7, 7, 9, 9, 8, 10,
|
||||
12, 20, 13, 12, 11, 11, 12, 25, 18, 19, 15, 20, 29, 26, 31, 30, 29,
|
||||
26, 28, 28, 32, 36, 46, 39, 32, 34, 44, 35, 28, 28, 40, 55, 41, 44,
|
||||
48, 49, 52, 52, 52, 31, 39, 57, 61, 56, 50, 60, 46, 51, 52, 50, 255,
|
||||
219, 0, 67, 1, 9, 9, 9, 12, 11, 12, 24, 13, 13, 24, 50, 33, 28, 33,
|
||||
50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50,
|
||||
50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50,
|
||||
50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 255,
|
||||
192, 0, 17, 8, 0, 1, 0, 1, 3, 1, 34, 0, 2, 17, 1, 3, 17, 1, 255, 196,
|
||||
0, 31, 0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3,
|
||||
4, 5, 6, 7, 8, 9, 10, 11, 255, 196, 0, 181, 16, 0, 2, 1, 3, 3, 2, 4,
|
||||
3, 5, 5, 4, 4, 0, 0, 1, 125, 1, 2, 3, 0, 4, 17, 5, 18, 33, 49, 65, 6,
|
||||
19, 81, 97, 7, 34, 113, 20, 50, 129, 145, 161, 8, 35, 66, 177, 193,
|
||||
21, 82, 209, 240, 36, 51, 98, 114, 130, 9, 10, 22, 23, 24, 25, 26, 37,
|
||||
38, 39, 40, 41, 42, 52, 53, 54, 55, 56, 57, 58, 67, 68, 69, 70, 71,
|
||||
72, 73, 74, 83, 84, 85, 86, 87, 88, 89, 90, 99, 100, 101, 102, 103,
|
||||
104, 105, 106, 115, 116, 117, 118, 119, 120, 121, 122, 131, 132, 133,
|
||||
134, 135, 136, 137, 138, 146, 147, 148, 149, 150, 151, 152, 153, 154,
|
||||
162, 163, 164, 165, 166, 167, 168, 169, 170, 178, 179, 180, 181, 182,
|
||||
183, 184, 185, 186, 194, 195, 196, 197, 198, 199, 200, 201, 202, 210,
|
||||
211, 212, 213, 214, 215, 216, 217, 218, 225, 226, 227, 228, 229, 230,
|
||||
231, 232, 233, 234, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250,
|
||||
255, 196, 0, 31, 1, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 255, 196, 0, 181, 17, 0, 2, 1, 2,
|
||||
4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 119, 0, 1, 2, 3, 17, 4, 5, 33, 49, 6,
|
||||
18, 65, 81, 7, 97, 113, 19, 34, 50, 129, 8, 20, 66, 145, 161, 177,
|
||||
193, 9, 35, 51, 82, 240, 21, 98, 114, 209, 10, 22, 36, 52, 225, 37,
|
||||
241, 23, 24, 25, 26, 38, 39, 40, 41, 42, 53, 54, 55, 56, 57, 58, 67,
|
||||
68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, 87, 88, 89, 90, 99, 100,
|
||||
101, 102, 103, 104, 105, 106, 115, 116, 117, 118, 119, 120, 121, 122,
|
||||
130, 131, 132, 133, 134, 135, 136, 137, 138, 146, 147, 148, 149, 150,
|
||||
151, 152, 153, 154, 162, 163, 164, 165, 166, 167, 168, 169, 170, 178,
|
||||
179, 180, 181, 182, 183, 184, 185, 186, 194, 195, 196, 197, 198, 199,
|
||||
200, 201, 202, 210, 211, 212, 213, 214, 215, 216, 217, 218, 226, 227,
|
||||
228, 229, 230, 231, 232, 233, 234, 242, 243, 244, 245, 246, 247, 248,
|
||||
249, 250, 255, 218, 0, 12, 3, 1, 0, 2, 17, 3, 17, 0, 63, 0, 247, 250,
|
||||
40, 162, 128, 63, 255, 217,
|
||||
],
|
||||
};
|
||||
const errors = [];
|
||||
const formats = ["rgb24", "rgb32"];
|
||||
const done = (format) => {
|
||||
delete CHECKS[format];
|
||||
if (Object.keys(CHECKS).length === 0) {
|
||||
if (errors.length === 0) {
|
||||
self.postMessage({
|
||||
result: true,
|
||||
formats
|
||||
});
|
||||
} else {
|
||||
self.postMessage({
|
||||
result: false,
|
||||
errors
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
const success = (format) => {
|
||||
//only enable this format if the client requested it:
|
||||
if (encodings.includes(format)) {
|
||||
formats.push(format);
|
||||
}
|
||||
done(format);
|
||||
};
|
||||
const failure = (format, message) => {
|
||||
//only record an error if the client actually asked us to verify this format
|
||||
if (encodings.includes(format)) {
|
||||
errors.push(message);
|
||||
if (console.warn) {
|
||||
console.warn(
|
||||
`Warning: decode worker error on '${format}': ${message}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.info(`decode worker failure on '${format}': ${message}`);
|
||||
}
|
||||
done(format);
|
||||
};
|
||||
for (const format in CHECKS) {
|
||||
const image_bytes = CHECKS[format];
|
||||
check_image_decode(format, image_bytes, success, failure);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "eos":
|
||||
decode_eos(data.wid);
|
||||
break;
|
||||
case "remove":
|
||||
decode_eos(data.wid);
|
||||
on_hold.delete(data.wid);
|
||||
break;
|
||||
case "decode":
|
||||
decode_draw_packet(data.packet, data.start);
|
||||
break;
|
||||
case "close":
|
||||
on_hold.clear();
|
||||
break;
|
||||
default:
|
||||
console.error(`decode worker got unknown message: ${data.cmd}`);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* This file is part of Xpra.
|
||||
* Copyright (C) 2021 Tijs van der Zwaan <tijzwa@vpo.nl>
|
||||
* Copyright (c) 2021 Antoine Martin <antoine@xpra.org>
|
||||
* Licensed under MPL 2.0, see:
|
||||
* http://www.mozilla.org/MPL/2.0/
|
||||
*
|
||||
*/
|
||||
|
||||
class XpraImageDecoder {
|
||||
async convertToBitmap(packet) {
|
||||
const width = packet[4];
|
||||
const height = packet[5];
|
||||
const coding = packet[6];
|
||||
if (coding.startsWith("rgb")) {
|
||||
const data = decode_rgb(packet);
|
||||
const bitmap = await createImageBitmap(new ImageData(new Uint8ClampedArray(data.buffer), width, height), 0, 0, width, height);
|
||||
packet[6] = `bitmap:${coding}`;
|
||||
packet[7] = bitmap;
|
||||
} else {
|
||||
const paint_coding = coding.split("/")[0]; //ie: "png/P" -> "png"
|
||||
const options = packet[10];
|
||||
const bitmap_options = {
|
||||
premultiplyAlpha: "none",
|
||||
};
|
||||
if ("scaled_size" in options) {
|
||||
bitmap_options.resizeWidth = width;
|
||||
bitmap_options.resizeHeight = height;
|
||||
bitmap_options.resizeQuality = options["scaling-quality"] || "medium";
|
||||
}
|
||||
|
||||
const blob = new Blob([packet[7].buffer], {
|
||||
type: `image/${paint_coding}`,
|
||||
});
|
||||
const bitmap = await createImageBitmap(blob, bitmap_options);
|
||||
packet[6] = `bitmap:${coding}`;
|
||||
packet[7] = bitmap;
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
1776
gns3server/static/web-ui/assets/xpra-html5/js/Keycodes.js
Normal file
322
gns3server/static/web-ui/assets/xpra-html5/js/MediaSourceUtil.js
Normal file
@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Copyright (c) 2016 Antoine Martin <antoine@xpra.org>
|
||||
* Licensed under MPL 2.0
|
||||
*
|
||||
*/
|
||||
|
||||
const MediaSourceConstants = {
|
||||
CODEC_DESCRIPTION: {
|
||||
mp4a: "mpeg4: aac",
|
||||
"aac+mpeg4": "mpeg4: aac",
|
||||
mp3: "mp3",
|
||||
"mp3+id3v2": "mp3",
|
||||
"mp3+mpeg4": "mpeg4: mp3",
|
||||
wav: "wav",
|
||||
wave: "wave",
|
||||
flac: "flac",
|
||||
opus: "opus",
|
||||
vorbis: "vorbis",
|
||||
"opus+mka": "webm: opus",
|
||||
"opus+ogg": "ogg: opus",
|
||||
"vorbis+mka": "webm: vorbis",
|
||||
"vorbis+ogg": "ogg: vorbis",
|
||||
"speex+ogg": "ogg: speex",
|
||||
"flac+ogg": "ogg: flac",
|
||||
},
|
||||
|
||||
CODEC_STRING: {
|
||||
"aac+mpeg4": 'audio/mp4; codecs="mp4a.40.2"',
|
||||
mp3: "audio/mpeg",
|
||||
"mp3+mpeg4": 'audio/mp4; codecs="mp3"',
|
||||
ogg: "audio/ogg",
|
||||
wav: "audio/wav",
|
||||
flac: "audio/flac",
|
||||
"opus+mka": 'audio/webm; codecs="opus"',
|
||||
"vorbis+mka": 'audio/webm; codecs="vorbis"',
|
||||
"vorbis+ogg": 'audio/ogg; codecs="vorbis"',
|
||||
"speex+ogg": 'audio/ogg; codecs="speex"',
|
||||
"flac+ogg": 'audio/ogg; codecs="flac"',
|
||||
"opus+ogg": 'audio/ogg; codecs="opus"',
|
||||
},
|
||||
|
||||
PREFERRED_CODEC_ORDER: [
|
||||
"opus+mka",
|
||||
"vorbis+mka",
|
||||
"opus+ogg",
|
||||
"vorbis+ogg",
|
||||
"opus",
|
||||
"vorbis",
|
||||
"speex+ogg",
|
||||
"flac+ogg",
|
||||
"aac+mpeg4",
|
||||
"mp3+mpeg4",
|
||||
"mp3",
|
||||
"mp3+id3v2",
|
||||
"flac",
|
||||
"wav",
|
||||
"wave",
|
||||
],
|
||||
|
||||
H264_PROFILE_CODE: {
|
||||
baseline: "42C0",
|
||||
main: "4D40",
|
||||
high: "6400",
|
||||
extended: "58A0",
|
||||
},
|
||||
|
||||
H264_LEVEL_CODE: {
|
||||
"3.0": "1E",
|
||||
3.1: "1F",
|
||||
4.1: "29",
|
||||
5.1: "33",
|
||||
},
|
||||
|
||||
READY_STATE: {
|
||||
0: "NOTHING",
|
||||
1: "METADATA",
|
||||
2: "CURRENT DATA",
|
||||
3: "FUTURE DATA",
|
||||
4: "ENOUGH DATA",
|
||||
},
|
||||
|
||||
NETWORK_STATE: {
|
||||
0: "EMPTY",
|
||||
1: "IDLE",
|
||||
2: "LOADING",
|
||||
3: "NO_SOURCE",
|
||||
},
|
||||
|
||||
ERROR_CODE: {
|
||||
1: "ABORTED: fetching process aborted by user",
|
||||
2: "NETWORK: error occurred when downloading",
|
||||
3: "DECODE: error occurred when decoding",
|
||||
4: "SRC_NOT_SUPPORTED",
|
||||
},
|
||||
|
||||
AURORA_CODECS: {
|
||||
wav: "lpcm",
|
||||
"mp3+id3v2": "mp3",
|
||||
flac: "flac",
|
||||
"aac+mpeg4": "mp4a",
|
||||
},
|
||||
};
|
||||
|
||||
const MediaSourceUtil = {
|
||||
getMediaSourceClass() {
|
||||
return window.MediaSource || window.WebKitMediaSource;
|
||||
},
|
||||
|
||||
getMediaSource() {
|
||||
const ms = MediaSourceUtil.getMediaSourceClass();
|
||||
if (!ms) {
|
||||
throw new Error("no MediaSource support!");
|
||||
}
|
||||
return new ms();
|
||||
},
|
||||
|
||||
getAuroraAudioCodecs() {
|
||||
const codecs_supported = new Map();
|
||||
const codecs_failed = new Map();
|
||||
if (AV && AV.Decoder && AV.Decoder.find) {
|
||||
for (const codec_option in MediaSourceConstants.AURORA_CODECS) {
|
||||
const codec_string = MediaSourceConstants.AURORA_CODECS[codec_option];
|
||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||
const decoder = AV.Decoder.find(codec_string);
|
||||
if (decoder) {
|
||||
codecs_supported[codec_option] = codec_string;
|
||||
} else {
|
||||
codecs_failed[codec_option] = codec_string;
|
||||
}
|
||||
}
|
||||
}
|
||||
Utilities.log("audio aurora codecs supported:", codecs_supported);
|
||||
Utilities.log("audio aurora codecs not available:", codecs_failed);
|
||||
return codecs_supported;
|
||||
},
|
||||
|
||||
getMediaSourceAudioCodecs(ignore_blacklist) {
|
||||
const media_source_class = MediaSourceUtil.getMediaSourceClass();
|
||||
if (!media_source_class) {
|
||||
Utilities.log("audio forwarding: no media source API support");
|
||||
return [];
|
||||
}
|
||||
const codecs_supported = [];
|
||||
const codecs_failed = {};
|
||||
for (const codec_option in MediaSourceConstants.CODEC_STRING) {
|
||||
const codec_string = MediaSourceConstants.CODEC_STRING[codec_option];
|
||||
try {
|
||||
if (!media_source_class.isTypeSupported(codec_string)) {
|
||||
codecs_failed[codec_option] = codec_string;
|
||||
//add whitelisting here?
|
||||
continue;
|
||||
}
|
||||
let blacklist = [];
|
||||
if (Utilities.isFirefox() || Utilities.isSafari()) {
|
||||
blacklist += ["opus+mka", "vorbis+mka"];
|
||||
if (Utilities.isSafari()) {
|
||||
//this crashes Safari!
|
||||
blacklist += ["wav"];
|
||||
}
|
||||
} else if (Utilities.isChrome()) {
|
||||
blacklist = ["aac+mpeg4"];
|
||||
if (Utilities.isMacOS()) {
|
||||
blacklist += ["opus+mka"];
|
||||
}
|
||||
}
|
||||
if (blacklist.includes(codec_option)) {
|
||||
if (ignore_blacklist) {
|
||||
Utilities.log("blacklist overruled!");
|
||||
} else {
|
||||
codecs_failed[codec_option] = codec_string;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
codecs_supported[codec_option] = codec_string;
|
||||
} catch (error) {
|
||||
Utilities.error(`audio error probing codec '${codec_string}' / '${codec_string}': ${error}`);
|
||||
codecs_failed[codec_option] = codec_string;
|
||||
}
|
||||
}
|
||||
Utilities.log("audio codec MediaSource supported:", codecs_supported);
|
||||
Utilities.log("audio codec MediaSource not available:", codecs_failed);
|
||||
return codecs_supported;
|
||||
},
|
||||
|
||||
getSupportedAudioCodecs() {
|
||||
const codecs_supported = MediaSourceUtil.getMediaSourceAudioCodecs();
|
||||
const aurora_codecs = MediaSourceUtil.getAuroraAudioCodecs();
|
||||
for (const codec_option in aurora_codecs) {
|
||||
if (codec_option in codecs_supported) {
|
||||
//we already have native MediaSource support!
|
||||
continue;
|
||||
}
|
||||
codecs_supported[codec_option] = aurora_codecs[codec_option];
|
||||
}
|
||||
return codecs_supported;
|
||||
},
|
||||
|
||||
getDefaultAudioCodec(codecs) {
|
||||
if (!codecs) {
|
||||
return null;
|
||||
}
|
||||
const codec_options = Object.keys(codecs);
|
||||
for (let codec_option of MediaSourceConstants.PREFERRED_CODEC_ORDER) {
|
||||
if (codec_options.includes(codec_option)) {
|
||||
return codec_option;
|
||||
}
|
||||
}
|
||||
return Object.keys(codecs)[0];
|
||||
},
|
||||
|
||||
addMediaSourceEventDebugListeners(media_source, source_type) {
|
||||
function debug_source_event(event) {
|
||||
let message = `${source_type} source ${event}`;
|
||||
try {
|
||||
message += `: ${media_source.readyState}`;
|
||||
} catch {
|
||||
//don't care
|
||||
}
|
||||
console.debug(message);
|
||||
}
|
||||
media_source.addEventListener("sourceopen", function(e) {
|
||||
debug_source_event("open");
|
||||
});
|
||||
media_source.addEventListener("sourceended", function(e) {
|
||||
debug_source_event("ended");
|
||||
});
|
||||
media_source.addEventListener("sourceclose", function(e) {
|
||||
debug_source_event("close");
|
||||
});
|
||||
media_source.addEventListener("error", function(e) {
|
||||
debug_source_event("error");
|
||||
});
|
||||
},
|
||||
|
||||
addMediaElementEventDebugListeners(media_element, element_type) {
|
||||
function debug_me_event(event) {
|
||||
console.debug(`${element_type} ${event}`);
|
||||
}
|
||||
media_element.addEventListener("waiting", function() {
|
||||
debug_me_event("waiting");
|
||||
});
|
||||
media_element.addEventListener("stalled", function() {
|
||||
debug_me_event("stalled");
|
||||
});
|
||||
media_element.addEventListener("playing", function() {
|
||||
debug_me_event("playing");
|
||||
});
|
||||
media_element.addEventListener("loadstart", function() {
|
||||
debug_me_event("loadstart");
|
||||
});
|
||||
media_element.addEventListener("loadedmetadata", function() {
|
||||
debug_me_event("loadedmetadata");
|
||||
});
|
||||
media_element.addEventListener("loadeddata", function() {
|
||||
debug_me_event("loadeddata");
|
||||
});
|
||||
media_element.addEventListener("error", function() {
|
||||
debug_me_event("error");
|
||||
});
|
||||
media_element.addEventListener("canplay", function() {
|
||||
debug_me_event("canplay");
|
||||
});
|
||||
media_element.addEventListener("play", function() {
|
||||
debug_me_event("play");
|
||||
});
|
||||
},
|
||||
|
||||
addSourceBufferEventDebugListeners(asb, element_type) {
|
||||
function debug_buffer_event(event) {
|
||||
const message = `${element_type} buffer ${event}`;
|
||||
console.debug(message);
|
||||
}
|
||||
asb.addEventListener("updatestart", function(e) {
|
||||
debug_buffer_event("updatestart");
|
||||
});
|
||||
asb.addEventListener("updateend", function(e) {
|
||||
debug_buffer_event("updateend");
|
||||
});
|
||||
asb.addEventListener("error", function(e) {
|
||||
debug_buffer_event("error");
|
||||
});
|
||||
asb.addEventListener("abort", function(e) {
|
||||
debug_buffer_event("abort");
|
||||
});
|
||||
},
|
||||
|
||||
get_supported_codecs(mediasource, aurora, ignore_audio_blacklist) {
|
||||
const codecs_supported = {};
|
||||
if (mediasource) {
|
||||
const mediasource_codecs = MediaSourceUtil.getMediaSourceAudioCodecs(ignore_audio_blacklist);
|
||||
for (const codec_option in mediasource_codecs) {
|
||||
codecs_supported[`mediasource:${codec_option}`] =
|
||||
MediaSourceConstants.CODEC_DESCRIPTION[codec_option];
|
||||
}
|
||||
}
|
||||
if (aurora) {
|
||||
const aurora_codecs = MediaSourceUtil.getAuroraAudioCodecs();
|
||||
for (const codec_option in aurora_codecs) {
|
||||
if (codec_option in codecs_supported) {
|
||||
//we already have native MediaSource support!
|
||||
continue;
|
||||
}
|
||||
codecs_supported[`aurora:${codec_option}`] = `legacy: ${MediaSourceConstants.CODEC_DESCRIPTION[codec_option]}`;
|
||||
}
|
||||
}
|
||||
return codecs_supported;
|
||||
},
|
||||
|
||||
get_best_codec(codecs_supported) {
|
||||
let best_codec = null;
|
||||
let best_distance = MediaSourceConstants.PREFERRED_CODEC_ORDER.length;
|
||||
for (const codec_option in codecs_supported) {
|
||||
const cs = codec_option.split(":")[1];
|
||||
const distance = MediaSourceConstants.PREFERRED_CODEC_ORDER.indexOf(cs);
|
||||
if (distance >= 0 && distance < best_distance) {
|
||||
best_codec = codec_option;
|
||||
best_distance = distance;
|
||||
}
|
||||
}
|
||||
return best_codec;
|
||||
},
|
||||
};
|
||||
143
gns3server/static/web-ui/assets/xpra-html5/js/Menu.js
Normal file
@ -0,0 +1,143 @@
|
||||
/**
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Mark Harkin, 2016 Dylan Hicks (aka. dylanh333)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
(function() {
|
||||
function $(selector, context) {
|
||||
context = context || document;
|
||||
return context["querySelectorAll"](selector);
|
||||
}
|
||||
|
||||
function forEach(collection, iterator) {
|
||||
for (const key in Object.keys(collection)) {
|
||||
iterator(collection[key]);
|
||||
}
|
||||
}
|
||||
|
||||
const ACTIVE_CLASS_NAME = "-active";
|
||||
const ANIMATING_CLASS_NAME = "-animating";
|
||||
const HAS_SUBMENU_CLASS_NAME = "-hasSubmenu";
|
||||
const HIDE_CLASS_NAME = "-hide";
|
||||
const MENU_CLASS_NAME = "Menu";
|
||||
const VISIBLE_CLASS_NAME = "-visible";
|
||||
|
||||
function showMenu() {
|
||||
const menu = this;
|
||||
const ul = $("ul", menu)[0];
|
||||
//hack to hide the menu from javascript
|
||||
if (!ul) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ul.classList.contains(HIDE_CLASS_NAME)) {
|
||||
ul.classList.remove(HIDE_CLASS_NAME);
|
||||
ul.parentElement.classList.remove(ACTIVE_CLASS_NAME);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ul.classList.contains(VISIBLE_CLASS_NAME)) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.classList.add(ACTIVE_CLASS_NAME);
|
||||
ul.classList.add(ANIMATING_CLASS_NAME);
|
||||
ul.classList.add(VISIBLE_CLASS_NAME);
|
||||
setTimeout(function() {
|
||||
ul.classList.remove(ANIMATING_CLASS_NAME);
|
||||
}, 25);
|
||||
}
|
||||
|
||||
function hideMenu() {
|
||||
const menu = this;
|
||||
const ul = $("ul", menu)[0];
|
||||
|
||||
if (!ul || !ul.classList.contains(VISIBLE_CLASS_NAME)) return;
|
||||
|
||||
menu.classList.remove(ACTIVE_CLASS_NAME);
|
||||
ul.classList.add(ANIMATING_CLASS_NAME);
|
||||
setTimeout(function() {
|
||||
ul.classList.remove(VISIBLE_CLASS_NAME);
|
||||
ul.classList.remove(ANIMATING_CLASS_NAME);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function hideAllInactiveMenus() {
|
||||
const menu = this;
|
||||
forEach(
|
||||
$(
|
||||
`li.${HAS_SUBMENU_CLASS_NAME}.${ACTIVE_CLASS_NAME}:not(:hover)`,
|
||||
menu.parent
|
||||
),
|
||||
function(e) {
|
||||
e.hideMenu && e.hideMenu();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function hideAllMenus() {
|
||||
const menu = this;
|
||||
forEach($(`li.${HAS_SUBMENU_CLASS_NAME}`, menu.parent), function(e) {
|
||||
e.hideMenu && e.hideMenu();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("load", function() {
|
||||
forEach(
|
||||
$(`.${MENU_CLASS_NAME} li.${HAS_SUBMENU_CLASS_NAME}`),
|
||||
function(e) {
|
||||
e.showMenu = showMenu;
|
||||
e.hideMenu = hideMenu;
|
||||
}
|
||||
);
|
||||
|
||||
forEach(
|
||||
$(`.${MENU_CLASS_NAME} > li.${HAS_SUBMENU_CLASS_NAME}`),
|
||||
function(e) {
|
||||
e.addEventListener("click", showMenu);
|
||||
}
|
||||
);
|
||||
|
||||
forEach(
|
||||
$(`.${MENU_CLASS_NAME} > li.${HAS_SUBMENU_CLASS_NAME} li`),
|
||||
function(e) {
|
||||
e.addEventListener("mouseenter", hideAllInactiveMenus);
|
||||
}
|
||||
);
|
||||
|
||||
forEach(
|
||||
$(
|
||||
`.${MENU_CLASS_NAME} > li.${HAS_SUBMENU_CLASS_NAME} li.${HAS_SUBMENU_CLASS_NAME}`
|
||||
),
|
||||
function(e) {
|
||||
e.addEventListener("mouseenter", showMenu);
|
||||
}
|
||||
);
|
||||
|
||||
forEach($("a"), function(e) {
|
||||
e.addEventListener("click", hideAllMenus);
|
||||
});
|
||||
|
||||
document.addEventListener("click", hideAllInactiveMenus);
|
||||
});
|
||||
})();
|
||||
173
gns3server/static/web-ui/assets/xpra-html5/js/MenuCustom.js
Normal file
@ -0,0 +1,173 @@
|
||||
/**
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Mark Harkin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
const MENU_CONTENT_LEFT_CLASS_NAME = "menu-content-left";
|
||||
const MENU_CONTENT_RIGHT_CLASS_NAME = "menu-content-right";
|
||||
|
||||
function noWindowList() {
|
||||
const open_windows = document.querySelector("#open_windows");
|
||||
if (open_windows) {
|
||||
open_windows.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function addWindowListItem(win, wid, title) {
|
||||
const li = document.createElement("li");
|
||||
li.className = "windowlist-li";
|
||||
li.id = `windowlistitem${wid}`;
|
||||
|
||||
const a = document.createElement("a");
|
||||
|
||||
a.id = `windowlistitemlink${wid}`;
|
||||
a.addEventListener("mouseover", function(e) {
|
||||
if (e.ctrlKey) {
|
||||
client._window_set_focus(win);
|
||||
}
|
||||
});
|
||||
a.addEventListener("click", function(e) {
|
||||
// Skip handling minimize, maximize, close events.
|
||||
if ($(e.target).hasClass(MENU_CONTENT_RIGHT_CLASS_NAME)) return;
|
||||
if (win.minimized) {
|
||||
win.toggle_minimized();
|
||||
} else {
|
||||
client.set_focus(win);
|
||||
}
|
||||
this.parentElement.parentElement.className = "-hide";
|
||||
});
|
||||
|
||||
function hideWindowList() {
|
||||
document.querySelector("#open_windows_list").className = "";
|
||||
}
|
||||
|
||||
const divLeft = document.createElement("div");
|
||||
divLeft.id = `windowlistdivleft${wid}`;
|
||||
divLeft.className = "menu-divleft";
|
||||
const img = new Image();
|
||||
img.id = `windowlistitemicon${wid}`;
|
||||
img.src = "favicon.png";
|
||||
img.className = MENU_CONTENT_LEFT_CLASS_NAME;
|
||||
divLeft.append(img);
|
||||
|
||||
const titleDiv = document.createElement("div");
|
||||
titleDiv.append(document.createTextNode(title));
|
||||
titleDiv.id = `windowlistitemtitle${wid}`;
|
||||
titleDiv.className = MENU_CONTENT_LEFT_CLASS_NAME;
|
||||
divLeft.append(titleDiv);
|
||||
|
||||
const divRight = document.createElement("div");
|
||||
divRight.className = "menu-divright";
|
||||
|
||||
const img2 = new Image();
|
||||
img2.id = `windowlistitemclose${wid}`;
|
||||
img2.src = "icons/close.png";
|
||||
img2.title = "Close";
|
||||
img2.className = MENU_CONTENT_RIGHT_CLASS_NAME;
|
||||
img2.addEventListener("click", function(e) {
|
||||
client.send_close_window(win);
|
||||
e.stopPropagation();
|
||||
hideWindowList();
|
||||
});
|
||||
const img3 = new Image();
|
||||
img3.id = `windowlistitemmax${wid}`;
|
||||
img3.src = "icons/maximize.png";
|
||||
img3.title = "Maximize";
|
||||
img3.addEventListener("click", function(e) {
|
||||
win.toggle_maximized();
|
||||
e.stopPropagation();
|
||||
hideWindowList();
|
||||
});
|
||||
img3.className = MENU_CONTENT_RIGHT_CLASS_NAME;
|
||||
const img4 = new Image();
|
||||
img4.id = `windowlistitemmin${wid}`;
|
||||
img4.src = "icons/minimize.png";
|
||||
img4.title = "Minimize";
|
||||
img4.addEventListener("click", function(e) {
|
||||
win.toggle_minimized();
|
||||
e.stopPropagation();
|
||||
hideWindowList();
|
||||
});
|
||||
img4.className = MENU_CONTENT_RIGHT_CLASS_NAME;
|
||||
|
||||
divRight.append(img2);
|
||||
divRight.append(img3);
|
||||
divRight.append(img4);
|
||||
a.append(divLeft);
|
||||
a.append(divRight);
|
||||
li.append(a);
|
||||
|
||||
document.querySelector("#open_windows_list").append(li);
|
||||
}
|
||||
|
||||
function removeWindowListItem(itemId) {
|
||||
const element = document.querySelector(`#windowlistitem${itemId}`);
|
||||
if (element && element.parentNode) {
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
$(function() {
|
||||
const float_menu = $("#float_menu");
|
||||
float_menu.draggable({
|
||||
cancel: ".noDrag",
|
||||
containment: "window",
|
||||
scroll: false,
|
||||
});
|
||||
float_menu.on("dragstart", function(event_, ui) {
|
||||
client.mouse_grabbed = true;
|
||||
});
|
||||
float_menu.on("dragstop", function(event_, ui) {
|
||||
client.mouse_grabbed = false;
|
||||
client.toolbar_position = "custom";
|
||||
client.reconfigure_all_trays();
|
||||
|
||||
if (float_menu.children(".Menu").hasClass("-vertical")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the float menu is in the upper half of the screen, the menu_list and open_windows_list will open downwards, and vice versa.
|
||||
const float_menu_top_css = float_menu.css("top")
|
||||
const float_menu_top = float_menu_top_css.substring(0, float_menu_top_css.length - 2);
|
||||
|
||||
if (Number(float_menu_top) > client.desktop_height / 2) {
|
||||
$("#menu_list").css("bottom", "30px");
|
||||
$("#open_windows_list").css({
|
||||
"bottom": "30px",
|
||||
"border-top-left-radius": "3px",
|
||||
"border-top-right-radius": "3px",
|
||||
"border-bottom-left-radius": "0px",
|
||||
"border-bottom-right-radius": "0px"
|
||||
});
|
||||
} else {
|
||||
$("#menu_list").css("bottom", "unset");
|
||||
$("#open_windows_list").css({
|
||||
"bottom": "unset",
|
||||
"border-top-left-radius": "0px",
|
||||
"border-top-right-radius": "0px",
|
||||
"border-bottom-left-radius": "3px",
|
||||
"border-bottom-right-radius": "3px"
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
168
gns3server/static/web-ui/assets/xpra-html5/js/Notifications.js
Normal file
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* This file is part of Xpra.
|
||||
* Copyright (C) 2016 Antoine Martin <antoine@xpra.org>
|
||||
* Copyright (c) 2016 Spikes, Inc.
|
||||
* Licensed under MPL 2.0, see:
|
||||
* http://www.mozilla.org/MPL/2.0/
|
||||
*
|
||||
*/
|
||||
|
||||
$(function() {
|
||||
window.notification_timers = {};
|
||||
|
||||
const NOTIFICATION_SELECTOR = ".notifications";
|
||||
|
||||
window.doNotification = function(
|
||||
type,
|
||||
nid,
|
||||
title,
|
||||
message,
|
||||
timeout,
|
||||
icon,
|
||||
actions,
|
||||
hints,
|
||||
onAction,
|
||||
onClose
|
||||
) {
|
||||
console.debug(
|
||||
"doNotification",
|
||||
type,
|
||||
nid,
|
||||
title,
|
||||
message,
|
||||
timeout,
|
||||
icon,
|
||||
actions,
|
||||
hints,
|
||||
onAction,
|
||||
onClose
|
||||
);
|
||||
const nID = `notification${nid}`;
|
||||
const a = $(
|
||||
`<div id="${nID}" class="alert ${type}">` +
|
||||
`<img class="notification_icon" id="notification_icon${nID}">` +
|
||||
`<span class="title">${title}</span>` +
|
||||
`<span class="message">${message}</span>` +
|
||||
`<div class="dismiss">×</div>` +
|
||||
`</div>`
|
||||
);
|
||||
const notifications_elements = $(NOTIFICATION_SELECTOR);
|
||||
notifications_elements.prepend(a);
|
||||
if (actions) {
|
||||
const notification_buttons = $(
|
||||
'<div class="notification_buttons"></div>'
|
||||
);
|
||||
a.append(notification_buttons);
|
||||
for (let index = 0; index < actions.length; index += 2) {
|
||||
const action_id = actions[index];
|
||||
const action_label = actions[index + 1];
|
||||
const notification_button = window._notification_button(
|
||||
nid,
|
||||
action_id,
|
||||
action_label,
|
||||
onAction,
|
||||
onClose
|
||||
);
|
||||
notification_buttons.append(notification_button);
|
||||
}
|
||||
}
|
||||
notifications_elements.prepend(a);
|
||||
if (icon) {
|
||||
const encoding = icon[0];
|
||||
const img_data = icon[3];
|
||||
if (encoding == "png") {
|
||||
const source = `data:image/${encoding};base64,${Utilities.ArrayBufferToBase64(
|
||||
img_data
|
||||
)}`;
|
||||
$(`#notification_icon${nID}`).attr("src", source);
|
||||
}
|
||||
}
|
||||
|
||||
a.on("click", ".dismiss", function() {
|
||||
window.cancelNotificationTimer(nid);
|
||||
a.removeClass("visible").addClass("hidden");
|
||||
a.on(
|
||||
"transitionend webkitTransitionEnd",
|
||||
$.debounce(250, function() {
|
||||
a.trigger("dismissed");
|
||||
a.remove();
|
||||
})
|
||||
);
|
||||
if (onClose) {
|
||||
onClose(nid, 3, "user clicked dismiss");
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
a.addClass("visible");
|
||||
}, 1);
|
||||
|
||||
if (timeout) {
|
||||
a.data("timeLeft", timeout);
|
||||
window.notification_timers[nid] = setInterval(function() {
|
||||
const tleft = a.data("timeLeft") - 1;
|
||||
if (a.data("timeLeft") === 0) {
|
||||
a.find(".dismiss").trigger("click");
|
||||
if (onClose) {
|
||||
onClose(nid, 1, "timeout");
|
||||
}
|
||||
} else {
|
||||
a.find("sec").text(tleft);
|
||||
a.data("timeLeft", tleft);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
return a;
|
||||
};
|
||||
|
||||
window._notification_button = function(
|
||||
nid,
|
||||
action_id,
|
||||
action_label,
|
||||
onAction,
|
||||
onClose
|
||||
) {
|
||||
const notification_button = $(
|
||||
`<div class="notification_button" id=notification"${action_id}">${action_label}</div>`
|
||||
);
|
||||
notification_button.on("click", function() {
|
||||
window.closeNotification(nid);
|
||||
if (onAction) {
|
||||
onAction(nid, action_id);
|
||||
}
|
||||
if (onClose) {
|
||||
onClose(nid, 3, "user clicked action");
|
||||
}
|
||||
});
|
||||
return notification_button;
|
||||
};
|
||||
|
||||
window.cancelNotificationTimer = function(nid) {
|
||||
const timer = window.notification_timers[nid];
|
||||
if (timer) {
|
||||
window.clearInterval(timer);
|
||||
delete window.notification_timers[nid];
|
||||
}
|
||||
};
|
||||
window.cancelNotificationTimers = function() {
|
||||
for (const nid in window.notification_timers) {
|
||||
window.cancelNotificationTimer(nid);
|
||||
}
|
||||
};
|
||||
|
||||
window.closeNotification = function(nid) {
|
||||
window.cancelNotificationTimer(nid);
|
||||
const nID = `notification${nid}`;
|
||||
$(NOTIFICATION_SELECTOR).find(`#${nID}`).find(".dismiss").trigger("click");
|
||||
};
|
||||
|
||||
window.clearNotifications = function() {
|
||||
window.cancelNotificationTimers();
|
||||
$(NOTIFICATION_SELECTOR).find(".dismiss").trigger("click");
|
||||
};
|
||||
|
||||
window.removeNotifications = function() {
|
||||
window.cancelNotificationTimers();
|
||||
$(NOTIFICATION_SELECTOR).empty();
|
||||
};
|
||||
});
|
||||
@ -0,0 +1,341 @@
|
||||
/*
|
||||
* This file is part of Xpra.
|
||||
* Copyright (C) 2021 Tijs van der Zwaan <tijzwa@vpo.nl>
|
||||
* Copyright (c) 2022 Antoine Martin <antoine@xpra.org>
|
||||
* Licensed under MPL 2.0, see:
|
||||
* http://www.mozilla.org/MPL/2.0/
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Worker for offscreen decoding.
|
||||
*/
|
||||
|
||||
importScripts("./lib/lz4.js");
|
||||
importScripts("./VideoDecoder.js");
|
||||
importScripts("./ImageDecoder.js");
|
||||
importScripts("./RgbHelpers.js");
|
||||
importScripts("./Constants.js");
|
||||
|
||||
// WindowDecoder for each window we have control over:
|
||||
const window_decoders = new Map();
|
||||
|
||||
// You can change this delay to test decode worker initialization timeouts:
|
||||
const ACK_DELAY = 0;
|
||||
|
||||
const image_coding = [
|
||||
"rgb",
|
||||
"rgb32",
|
||||
"rgb24",
|
||||
"jpeg",
|
||||
"png",
|
||||
"png/P",
|
||||
"png/L",
|
||||
"webp",
|
||||
"avif",
|
||||
];
|
||||
const video_coding = [];
|
||||
if (XpraVideoDecoderLoader.hasNativeDecoder()) {
|
||||
// We can support native H264 & VP8 decoding
|
||||
video_coding.push("h264", "vp8");
|
||||
} else {
|
||||
console.warn("Offscreen decoding is available for images only");
|
||||
console.warn("Please consider using Google Chrome 94+ in a secure (SSL or localhost) context for h264 offscreen decoding support.");
|
||||
}
|
||||
|
||||
const all_encodings = new Set([
|
||||
"void",
|
||||
...image_coding,
|
||||
...video_coding,
|
||||
]);
|
||||
|
||||
function send_decode_error(packet, error) {
|
||||
packet[7] = null;
|
||||
self.postMessage({
|
||||
error: `${error}`,
|
||||
packet
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
class WindowDecoder {
|
||||
constructor(wid, canvas, debug) {
|
||||
this.wid = wid;
|
||||
this.canvas = canvas;
|
||||
this.context = this.canvas.getContext("2d");
|
||||
this.debug = debug;
|
||||
|
||||
this.image_decoder = new XpraImageDecoder();
|
||||
this.video_decoder = new XpraVideoDecoder();
|
||||
|
||||
this.decode_queue = [];
|
||||
this.decode_queue_draining = false;
|
||||
}
|
||||
|
||||
decode_error(packet, error) {
|
||||
const coding = packet[6];
|
||||
const packet_sequence = packet[8];
|
||||
const message = `failed to decode '${coding}' draw packet sequence ${packet_sequence}: ${error}`;
|
||||
console.error(message);
|
||||
packet[7] = null;
|
||||
send_decode_error(packet, message);
|
||||
}
|
||||
|
||||
queue_draw_packet(packet) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.decode_queue.push(packet);
|
||||
if (!this.decode_queue_draining) {
|
||||
this.process_decode_queue();
|
||||
}
|
||||
}
|
||||
|
||||
process_decode_queue() {
|
||||
this.decode_queue_draining = true;
|
||||
const packet = this.decode_queue.shift();
|
||||
this.process_packet(packet).then(
|
||||
() => {
|
||||
if (this.decode_queue.length > 0) {
|
||||
// Next
|
||||
this.process_decode_queue();
|
||||
} else {
|
||||
this.decode_queue_draining = false;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
send_decode_error(packet, error);
|
||||
this.decode_queue_draining = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async process_packet(packet) {
|
||||
let coding = packet[6];
|
||||
const start = performance.now();
|
||||
if (coding === "eos" && this.video_decoder) {
|
||||
this.video_decoder._close();
|
||||
return;
|
||||
} else if (coding === "scroll" || coding === "void") {
|
||||
// Nothing to do
|
||||
} else if (image_coding.includes(coding)) {
|
||||
await this.image_decoder.convertToBitmap(packet);
|
||||
} else if (video_coding.includes(coding)) {
|
||||
if (!this.video_decoder.initialized) {
|
||||
this.video_decoder.init(coding);
|
||||
}
|
||||
packet = await this.video_decoder.queue_frame(packet).catch((error) => {
|
||||
this.decode_error(packet, error);
|
||||
});
|
||||
} else {
|
||||
this.decode_error(packet, `unsupported encoding: '${coding}'`);
|
||||
}
|
||||
|
||||
// Hold throttle packages for 500 ms to prevent flooding of the VideoDecoder
|
||||
if (packet[6] === "throttle") {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
// Fake packet to send back
|
||||
const options = packet[10] || {};
|
||||
const decode_time = Math.round(1000 * (performance.now() - start));
|
||||
options["decode_time"] = Math.max(0, decode_time);
|
||||
// Copy without data
|
||||
const clonepacket = packet.map((x, i) => {
|
||||
if (i !== 7) {
|
||||
return x;
|
||||
}
|
||||
});
|
||||
clonepacket[6] = "offscreen-painted";
|
||||
clonepacket[10] = options;
|
||||
|
||||
// Tell the server we are done with this packet
|
||||
self.postMessage({
|
||||
draw: clonepacket,
|
||||
start
|
||||
});
|
||||
|
||||
// Paint the packet on screen refresh (if we can use requestAnimationFrame in the worker)
|
||||
if (packet[6] === "throttle") {
|
||||
return;
|
||||
}
|
||||
|
||||
const wid = packet[1];
|
||||
const x = packet[2];
|
||||
const y = packet[3];
|
||||
const w = packet[4];
|
||||
const h = packet[5];
|
||||
coding = packet[6];
|
||||
const image = packet[7];
|
||||
this.paint_packet(wid, coding, image, x, y, w, h);
|
||||
}
|
||||
|
||||
paint_packet(wid, coding, image, x, y, width, height) {
|
||||
let painted = false;
|
||||
try {
|
||||
// Paint the packet on screen refresh (if we can use requestAnimationFrame in the worker)
|
||||
if (typeof requestAnimationFrame == "function") {
|
||||
requestAnimationFrame(() => {
|
||||
this.do_paint_packet(wid, coding, image, x, y, width, height);
|
||||
});
|
||||
painted = true;
|
||||
}
|
||||
} catch {
|
||||
// If requestAnimationFrame is a function but it failed somehow (ie forbidden in worker in the current browser) we fall back
|
||||
console.error("requestAnimationFrame error for paint packet");
|
||||
painted = false;
|
||||
} finally {
|
||||
if (!painted) {
|
||||
// Paint right away
|
||||
this.do_paint_packet(wid, coding, image, x, y, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do_paint_packet(wid, coding, image, x, y, width, height) {
|
||||
// Update the coding propery
|
||||
if (!this.canvas) {
|
||||
return;
|
||||
}
|
||||
let context = this.context;
|
||||
if (coding.startsWith("bitmap")) {
|
||||
// Bitmap paint
|
||||
context.imageSmoothingEnabled = false;
|
||||
context.clearRect(x, y, width, height);
|
||||
context.drawImage(image, 0, 0, width, height, x, y, width, height);
|
||||
this.paint_box(coding, context, x, y, width, height);
|
||||
} else if (coding === "scroll") {
|
||||
let canvas = this.canvas;
|
||||
context.imageSmoothingEnabled = false;
|
||||
for (let index = 0, stop = image.length; index < stop; ++index) {
|
||||
const scroll_data = image[index];
|
||||
const sx = scroll_data[0];
|
||||
const sy = scroll_data[1];
|
||||
const sw = scroll_data[2];
|
||||
const sh = scroll_data[3];
|
||||
const xdelta = scroll_data[4];
|
||||
const ydelta = scroll_data[5];
|
||||
context.drawImage(canvas,
|
||||
sx, sy, sw, sh,
|
||||
sx + xdelta, sy + ydelta, sw, sh,
|
||||
);
|
||||
this.paint_box(coding, context, sx, sy, sw, sh);
|
||||
}
|
||||
} else if (coding.startsWith("frame")) {
|
||||
context.drawImage(image, 0, 0, width, height, x, y, width, height);
|
||||
image.close();
|
||||
this.paint_box(coding, context, x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
paint_box(coding, context, px, py, pw, ph) {
|
||||
if (!this.debug) {
|
||||
return;
|
||||
}
|
||||
const source_encoding = coding.split(":")[1] || ""; //ie: "rgb24"
|
||||
const box_color = DEFAULT_BOX_COLORS[source_encoding];
|
||||
if (box_color) {
|
||||
context.strokeStyle = box_color;
|
||||
context.lineWidth = 2;
|
||||
context.strokeRect(px, py, pw, ph);
|
||||
}
|
||||
}
|
||||
|
||||
update_geometry(w, h) {
|
||||
this.canvas.width = w;
|
||||
this.canvas.height = h;
|
||||
}
|
||||
|
||||
redraw() {
|
||||
console.info("REDRAW requested");
|
||||
}
|
||||
|
||||
eos() {
|
||||
// Add eos packet to queue to prevent closing the decoder before all packets are proceeded
|
||||
const packet = [];
|
||||
packet[6] = "eos";
|
||||
this.decode_queue.push(packet);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.eos();
|
||||
this.canvas = null;
|
||||
this.decode_queue = [];
|
||||
this.decode_queue_draining = true;
|
||||
}
|
||||
}
|
||||
|
||||
onmessage = function(e) {
|
||||
const data = e.data;
|
||||
let wd = null;
|
||||
switch (data.cmd) {
|
||||
case "check": {
|
||||
// Check if we support the given encodings.
|
||||
const encodings = [...data.encodings];
|
||||
const common = encodings.filter((value) => all_encodings.has(value));
|
||||
function ack() {
|
||||
self.postMessage({
|
||||
result: true,
|
||||
formats: common
|
||||
});
|
||||
}
|
||||
setTimeout(ack, ACK_DELAY);
|
||||
break;
|
||||
}
|
||||
case "eos":
|
||||
wd = window_decoders.get(data.wid);
|
||||
if (wd) {
|
||||
wd.eos();
|
||||
}
|
||||
break;
|
||||
case "remove":
|
||||
wd = window_decoders.get(data.wid);
|
||||
if (wd) {
|
||||
wd.close();
|
||||
window_decoders.delete(data.wid);
|
||||
}
|
||||
break;
|
||||
case "decode": {
|
||||
const packet = data.packet;
|
||||
const wid = packet[1];
|
||||
wd = window_decoders.get(wid);
|
||||
if (wd) {
|
||||
wd.queue_draw_packet(packet);
|
||||
} else {
|
||||
send_decode_error(packet,
|
||||
`no window decoder found for wid ${wid}, only:${[...window_decoders.keys(),].join(",")}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "redraw":
|
||||
wd = window_decoders.get(data.wid);
|
||||
if (wd) {
|
||||
wd.redraw();
|
||||
}
|
||||
break;
|
||||
case "canvas":
|
||||
// console.log("canvas transfer for window", data.wid, ":", data.canvas, data.debug);
|
||||
if (data.canvas) {
|
||||
window_decoders.set(
|
||||
data.wid,
|
||||
new WindowDecoder(data.wid, data.canvas, data.debug)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "canvas-geo":
|
||||
wd = window_decoders.get(data.wid);
|
||||
if (wd) {
|
||||
wd.update_geometry(data.w, data.h);
|
||||
}
|
||||
break;
|
||||
case "close":
|
||||
for (const decoder of window_decoders.values()) {
|
||||
decoder.close();
|
||||
}
|
||||
window_decoders.clear();
|
||||
break;
|
||||
default:
|
||||
console.error(`Offscreen decode worker got unknown message: ${data.cmd}`);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of Xpra.
|
||||
* Copyright (C) 2021 Tijs van der Zwaan <tijzwa@vpo.nl>
|
||||
* Copyright (c) 2022 Antoine Martin <antoine@xpra.org>
|
||||
* Licensed under MPL 2.0, see:
|
||||
* http://www.mozilla.org/MPL/2.0/
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Helper for offscreen decoding and painting.
|
||||
*/
|
||||
|
||||
const XpraOffscreenWorker = {
|
||||
// OffscreenCanvas supported since v.16.4
|
||||
isSafariVersionSupported() {
|
||||
const match = navigator.userAgent.match(/version\/(\d+\.\d+)/i);
|
||||
if (match && match[1]) {
|
||||
const version = parseFloat(match[1]);
|
||||
// Safari is buggy, see #227
|
||||
return version >= 999.9;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// OffscreenCanvas supported since v.105 (with fixed added for 107/108)
|
||||
isFirefoxVersionSupported() {
|
||||
const match = navigator.userAgent.match(/firefox\/(\d+)/i);
|
||||
if (match && match[1]) {
|
||||
const version = parseInt(match[1], 10);
|
||||
return version >= 108;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isAvailable(ssl) {
|
||||
if (Utilities.isSafari() && !this.isSafariVersionSupported()) {
|
||||
// console.log("offscreen canvas is not supported with this version of Safari");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Utilities.isWebkit() && !Utilities.isChrome()) {
|
||||
// console.log("offscreen canvas is not supported with this webkit browser");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Utilities.isFirefox() && !this.isFirefoxVersionSupported()) {
|
||||
// console.log("offscreen canvas is not supported with this version of Firefox");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Utilities.isChrome() && !ssl) {
|
||||
// console.log("offscreen canvas requires https with Chrome");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof OffscreenCanvas !== "undefined") {
|
||||
//we also need the direct constructor:
|
||||
try {
|
||||
new OffscreenCanvas(256, 256);
|
||||
// console.log("offscreen canvas is available with", navigator.userAgent);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn("unable to instantiate an offscreen canvas:", error);
|
||||
}
|
||||
}
|
||||
console.warn(
|
||||
"Offscreen decoding is not available. Please consider using " +
|
||||
"Google Chrome, Firefox >= 108 for better performance."
|
||||
);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
746
gns3server/static/web-ui/assets/xpra-html5/js/Protocol.js
Normal file
@ -0,0 +1,746 @@
|
||||
/*
|
||||
* Copyright (c) 2013 Antoine Martin <antoine@xpra.org>
|
||||
* Copyright (c) 2016 David Brushinski <dbrushinski@spikes.com>
|
||||
* Copyright (c) 2014 Joshua Higgins <josh@kxes.net>
|
||||
* Copyright (c) 2015 Spikes, Inc.
|
||||
* Portions based on websock.js by Joel Martin
|
||||
* Copyright (C) 2012 Joel Martin
|
||||
*
|
||||
* Licensed under MPL 2.0
|
||||
*
|
||||
* xpra wire protocol with worker support
|
||||
*
|
||||
* requires:
|
||||
* lz4.js
|
||||
* brotli_decode.js
|
||||
*/
|
||||
|
||||
const CONNECT_TIMEOUT = 15_000;
|
||||
|
||||
/*
|
||||
A stub class to facilitate communication with the protocol when
|
||||
it is loaded in a worker
|
||||
*/
|
||||
class XpraProtocolWorkerHost {
|
||||
constructor() {
|
||||
this.worker = null;
|
||||
this.packet_handler = null;
|
||||
}
|
||||
|
||||
open(uri) {
|
||||
if (this.worker) {
|
||||
//re-use the existing worker:
|
||||
this.worker.postMessage({
|
||||
c: "o",
|
||||
u: uri
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.worker = new Worker("js/Protocol.js");
|
||||
this.worker.addEventListener(
|
||||
"message",
|
||||
(e) => {
|
||||
const data = e.data;
|
||||
switch (data.c) {
|
||||
case "r":
|
||||
this.worker.postMessage({
|
||||
c: "o",
|
||||
u: uri
|
||||
});
|
||||
break;
|
||||
case "p":
|
||||
if (this.packet_handler) {
|
||||
this.packet_handler(data.p);
|
||||
}
|
||||
break;
|
||||
case "l":
|
||||
// console.log(data.t);
|
||||
break;
|
||||
default:
|
||||
console.error("got unknown command from worker");
|
||||
console.error(e.data);
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
close = function() {
|
||||
this.worker.postMessage({
|
||||
c: "c"
|
||||
});
|
||||
};
|
||||
|
||||
terminate = function() {
|
||||
this.worker.postMessage({
|
||||
c: "t"
|
||||
});
|
||||
};
|
||||
|
||||
send = function(packet) {
|
||||
this.worker.postMessage({
|
||||
c: "s",
|
||||
p: packet
|
||||
});
|
||||
};
|
||||
|
||||
set_packet_handler = function(callback) {
|
||||
this.packet_handler = callback;
|
||||
};
|
||||
|
||||
set_cipher_in = function(caps, key) {
|
||||
this.worker.postMessage({
|
||||
c: "z",
|
||||
p: caps,
|
||||
k: key
|
||||
});
|
||||
};
|
||||
|
||||
set_cipher_out = function(caps, key) {
|
||||
this.worker.postMessage({
|
||||
c: "x",
|
||||
p: caps,
|
||||
k: key
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
The main Xpra wire protocol
|
||||
*/
|
||||
class XpraProtocol {
|
||||
constructor() {
|
||||
this.verify_connected_timer = 0;
|
||||
this.is_worker = false;
|
||||
this.packet_handler = null;
|
||||
this.websocket = null;
|
||||
this.raw_packets = [];
|
||||
this.cipher_in_block_size = null;
|
||||
this.cipher_in_params = null;
|
||||
this.cipher_in_key = null;
|
||||
this.cipher_out_params = null;
|
||||
this.cipher_out_key = null;
|
||||
this.rQ = []; // Receive queue
|
||||
this.sQ = []; // Send queue
|
||||
this.mQ = []; // Worker message queue
|
||||
this.header = [];
|
||||
|
||||
//Queue processing via intervals
|
||||
this.process_interval = 0; //milliseconds
|
||||
}
|
||||
|
||||
close_event_str(event) {
|
||||
const code_mappings = {
|
||||
1000: "Normal Closure",
|
||||
1001: "Going Away",
|
||||
1002: "Protocol Error",
|
||||
1003: "Unsupported Data",
|
||||
1004: "(For future)",
|
||||
1005: "No Status Received",
|
||||
1006: "Abnormal Closure",
|
||||
1007: "Invalid frame payload data",
|
||||
1008: "Policy Violation",
|
||||
1009: "Message too big",
|
||||
1010: "Missing Extension",
|
||||
1011: "Internal Error",
|
||||
1012: "Service Restart",
|
||||
1013: "Try Again Later",
|
||||
1014: "Bad Gateway",
|
||||
1015: "TLS Handshake",
|
||||
};
|
||||
let message = "";
|
||||
if (event.code) {
|
||||
try {
|
||||
message +=
|
||||
typeof code_mappings[event.code] !== "undefined" ?
|
||||
`'${code_mappings[event.code]}' (${event.code})` :
|
||||
`${event.code}`;
|
||||
if (event.reason) {
|
||||
message += `: '${event.reason}'`;
|
||||
}
|
||||
} catch (error) {
|
||||
this.error("cannot parse websocket event:", error);
|
||||
message = "unknown reason";
|
||||
}
|
||||
} else {
|
||||
message = "unknown reason (no websocket error code)";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
open(uri) {
|
||||
const me = this;
|
||||
// (re-)init
|
||||
this.raw_packets = [];
|
||||
this.rQ = [];
|
||||
this.sQ = [];
|
||||
this.mQ = [];
|
||||
this.header = [];
|
||||
this.websocket = null;
|
||||
|
||||
function handle(packet) {
|
||||
me.packet_handler(packet);
|
||||
}
|
||||
this.verify_connected_timer = setTimeout(
|
||||
() => handle(["error", "connection timed out", 0]),
|
||||
CONNECT_TIMEOUT
|
||||
);
|
||||
// connect the socket
|
||||
try {
|
||||
// Request 'binary' subprotocol as required by xpra protocol
|
||||
this.websocket = new WebSocket(uri, "binary");
|
||||
} catch (error) {
|
||||
handle(["error", `${error}`, 0]);
|
||||
return;
|
||||
}
|
||||
this.websocket.binaryType = "arraybuffer";
|
||||
this.websocket.addEventListener("open", function() {
|
||||
if (me.verify_connected_timer) {
|
||||
clearTimeout(me.verify_connected_timer);
|
||||
me.verify_connected_timer = 0;
|
||||
}
|
||||
handle(["open"]);
|
||||
});
|
||||
this.websocket.addEventListener("close", (event) =>
|
||||
handle(["close", me.close_event_str(event)])
|
||||
);
|
||||
this.websocket.onerror = (event) =>
|
||||
handle(["error", me.close_event_str(event), event.code || 0]);
|
||||
this.websocket.onmessage = function(e) {
|
||||
// push arraybuffer values onto the end
|
||||
me.rQ.push(new Uint8Array(e.data));
|
||||
setTimeout(function() {
|
||||
me.process_receive_queue();
|
||||
}, this.process_interval);
|
||||
};
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.websocket) {
|
||||
this.websocket.onopen = null;
|
||||
this.websocket.onclose = null;
|
||||
this.websocket.onerror = null;
|
||||
this.websocket.onmessage = null;
|
||||
this.websocket.close();
|
||||
this.websocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
protocol_error(message) {
|
||||
this.error("protocol error:", message);
|
||||
//make sure we stop processing packets and events:
|
||||
this.websocket.onopen = null;
|
||||
this.websocket.onclose = null;
|
||||
this.websocket.onerror = null;
|
||||
this.websocket.onmessage = null;
|
||||
this.header = [];
|
||||
this.rQ = [];
|
||||
//and just tell the client to close (it may still try to re-connect):
|
||||
this.packet_handler(["close", message]);
|
||||
}
|
||||
|
||||
process_receive_queue() {
|
||||
while (this.websocket && this.rQ.length > 0 && this.do_process_receive_queue());
|
||||
}
|
||||
|
||||
error() {
|
||||
console.error.apply(console, arguments);
|
||||
}
|
||||
log() {
|
||||
// console.log.apply(console, arguments);
|
||||
}
|
||||
|
||||
do_process_receive_queue() {
|
||||
/*
|
||||
* process data from this.rQ until we have enough for one packet chunk
|
||||
* then calls this.process_packet_data
|
||||
*/
|
||||
if (this.header.length < 8 && this.rQ.length > 0) {
|
||||
//add from receive queue data to header until we get the 8 bytes we need:
|
||||
while (this.header.length < 8 && this.rQ.length > 0) {
|
||||
const slice = this.rQ[0];
|
||||
const needed = 8 - this.header.length;
|
||||
const n = Math.min(needed, slice.length);
|
||||
this.header.push(...slice.subarray(0, n));
|
||||
if (slice.length > needed) {
|
||||
//replace the slice with what is left over:
|
||||
this.rQ[0] = slice.subarray(n);
|
||||
} else {
|
||||
//this slice has been fully consumed already:
|
||||
this.rQ.shift();
|
||||
}
|
||||
}
|
||||
|
||||
//verify the header format:
|
||||
if (this.header[0] !== 80) {
|
||||
let message = `invalid packet header format: ${this.header[0]}`;
|
||||
if (this.header.length > 1) {
|
||||
let hex = "";
|
||||
for (let p of this.header) {
|
||||
const v = p.toString(16);
|
||||
hex += v.length < 2 ? `0${v}` : v;
|
||||
}
|
||||
message += `: 0x${hex}`;
|
||||
}
|
||||
this.protocol_error(message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.header.length < 8) {
|
||||
//we need more data to continue
|
||||
return true;
|
||||
}
|
||||
|
||||
//ignore 0x8: this flag is unused client-side:
|
||||
let proto_flags = this.header[1] & ~0x8;
|
||||
const encrypted = proto_flags & 0x2;
|
||||
if (encrypted) {
|
||||
proto_flags = proto_flags & ~0x2;
|
||||
}
|
||||
if (proto_flags > 1 && proto_flags !== 0x10) {
|
||||
this.protocol_error(`we can't handle this protocol flag yet: ${proto_flags}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
let packet_size = [4, 5, 6, 7].reduce((accumulator, value) => accumulator * 0x1_00 + this.header[value], 0);
|
||||
|
||||
// add padding if encryption is enabled
|
||||
let padding = 0;
|
||||
if (encrypted && this.cipher_in_block_size > 0) {
|
||||
// PKCS#7 has always at least one byte of padding!
|
||||
padding = this.cipher_in_block_size - packet_size % this.cipher_in_block_size;
|
||||
|
||||
packet_size += padding;
|
||||
}
|
||||
|
||||
// verify that we have enough data for the full payload:
|
||||
let rsize = this.rQ.reduce((accumulator, value) => accumulator + value.length, 0);
|
||||
if (rsize < packet_size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// done parsing the header, the next packet will need a new one:
|
||||
const header = this.header;
|
||||
this.header = [];
|
||||
|
||||
let packet_data;
|
||||
if (this.rQ[0].length === packet_size) {
|
||||
//exact match: the payload is in a buffer already:
|
||||
packet_data = this.rQ.shift();
|
||||
} else {
|
||||
//aggregate all the buffers into "packet_data" until we get exactly "packet_size" bytes:
|
||||
packet_data = new Uint8Array(packet_size);
|
||||
rsize = 0;
|
||||
while (rsize < packet_size) {
|
||||
const slice = this.rQ[0];
|
||||
const needed = packet_size - rsize;
|
||||
if (slice.length > needed) {
|
||||
//add part of this slice:
|
||||
packet_data.set(slice.subarray(0, needed), rsize);
|
||||
rsize += needed;
|
||||
this.rQ[0] = slice.subarray(needed);
|
||||
} else {
|
||||
//add this slice in full:
|
||||
packet_data.set(slice, rsize);
|
||||
rsize += slice.length;
|
||||
this.rQ.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// decrypt if needed
|
||||
if (encrypted) {
|
||||
if (!this.cipher_in_key) {
|
||||
this.protocol_error("encrypted packet received, but no decryption is configured");
|
||||
return false;
|
||||
}
|
||||
// extract the iv:
|
||||
const iv = packet_data.slice(0, 16);
|
||||
const encrypted_data = packet_data.subarray(16);
|
||||
this.cipher_in_params["iv"] = iv;
|
||||
// console.log("decrypt", packet_data.byteLength, "bytes, padding=", padding, JSON.stringify(this.cipher_in_params), packet_data);
|
||||
crypto.subtle.decrypt(this.cipher_in_params, this.cipher_in_key, encrypted_data)
|
||||
.then(decrypted => {
|
||||
// console.log("decrypted", decrypted.byteLength, "bytes, padding=", padding);
|
||||
const expected_length = packet_size - padding - iv.length;
|
||||
if (!decrypted || decrypted.byteLength < expected_length) {
|
||||
this.protocol_error(` expected ${expected_length} bytes, but got ${decrypted.byteLength}`);
|
||||
return false;
|
||||
}
|
||||
if (decrypted.byteLength === packet_size - padding) {
|
||||
packet_data = new Uint8Array(decrypted);
|
||||
} else {
|
||||
packet_data = new Uint8Array(decrypted.slice(0, packet_size - padding));
|
||||
}
|
||||
// console.log("packet data:", packet_data);
|
||||
this.process_packet_data(header, packet_data);
|
||||
})
|
||||
.catch(err => this.protocol_error("failed to decrypt data: " + err));
|
||||
return true;
|
||||
}
|
||||
|
||||
this.process_packet_data(header, packet_data);
|
||||
return true;
|
||||
}
|
||||
|
||||
process_packet_data(header, packet_data) {
|
||||
/*
|
||||
* the packet data has been decrypted (if needed),
|
||||
* decompress it (if needed),
|
||||
* then either store it if it is a chunk,
|
||||
* or decode the packet if we have received all the chunks (chunk no is 0)
|
||||
*/
|
||||
const level = header[2];
|
||||
const index = header[3];
|
||||
// console.log("process packet data, header=", header, packet_data.byteLength, "bytes, index=", index, "level=", level);
|
||||
|
||||
//decompress it if needed:
|
||||
if (level !== 0) {
|
||||
let inflated;
|
||||
if (level & 0x10) {
|
||||
inflated = lz4.decode(packet_data);
|
||||
} else if (level & 0x40) {
|
||||
inflated = new Uint8Array(BrotliDecode(packet_data));
|
||||
} else {
|
||||
throw `unsupported compressor specified: ${level}`;
|
||||
}
|
||||
packet_data = inflated;
|
||||
}
|
||||
|
||||
//save it for later? (partial raw packet)
|
||||
if (index > 0) {
|
||||
if (index >= 20) {
|
||||
this.protocol_error(`invalid packet index: ${index}`);
|
||||
return;
|
||||
}
|
||||
this.raw_packets[index] = packet_data;
|
||||
if (this.raw_packets.length >= 4) {
|
||||
this.protocol_error(`too many raw packets: ${this.raw_packets.length}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//decode raw packet data into objects:
|
||||
let packet = null;
|
||||
try {
|
||||
packet = rdecode(packet_data);
|
||||
for (const index in this.raw_packets) {
|
||||
packet[index] = this.raw_packets[index];
|
||||
}
|
||||
this.raw_packets = {};
|
||||
} catch (error) {
|
||||
//FIXME: maybe we should error out and disconnect here?
|
||||
this.error("error decoding packet", error);
|
||||
this.error(`packet=${packet_data}`);
|
||||
const proto_flags = header[1];
|
||||
this.error(`protocol flags=${proto_flags}`);
|
||||
this.error(` level=${level}`);
|
||||
this.error(` index=${index}`);
|
||||
this.raw_packets = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// call the packet handler
|
||||
if (this.is_worker) {
|
||||
this.mQ[this.mQ.length] = packet;
|
||||
setTimeout(() => this.process_message_queue(), this.process_interval);
|
||||
} else {
|
||||
this.packet_handler(packet);
|
||||
}
|
||||
} catch (error) {
|
||||
//FIXME: maybe we should error out and disconnect here?
|
||||
this.error(`error processing packet ${packet[0]}: ${error}`);
|
||||
this.error(` packet data: ${packet_data}`)
|
||||
}
|
||||
}
|
||||
|
||||
process_send_queue() {
|
||||
while (this.sQ.length > 0 && this.websocket) {
|
||||
const packet = this.sQ.shift();
|
||||
if (!packet) {
|
||||
return;
|
||||
}
|
||||
let bdata = null;
|
||||
try {
|
||||
bdata = rencode(packet);
|
||||
} catch (error) {
|
||||
this.error("Error: failed to encode packet:", packet);
|
||||
this.error(error);
|
||||
continue;
|
||||
}
|
||||
let payload_size = bdata.length;
|
||||
|
||||
if (this.cipher_out_key) {
|
||||
//console("encrypting", packet[0], "packet using", JSON.stringify(this.cipher_out_params), ":", bdata);
|
||||
const iv = Utilities.getSecureRandomBytes(16);
|
||||
this.cipher_out_params["iv"] = iv;
|
||||
crypto.subtle.encrypt(this.cipher_out_params, this.cipher_out_key, bdata)
|
||||
.then(encrypted => {
|
||||
const enc_u8 = new Uint8Array(encrypted);
|
||||
const packet_data = new Uint8Array(iv.byteLength + enc_u8.byteLength);
|
||||
packet_data.set(iv, 0);
|
||||
packet_data.set(enc_u8, iv.byteLength);
|
||||
payload_size += iv.byteLength;
|
||||
this.send_packet(packet_data, payload_size, true);
|
||||
})
|
||||
.catch(err => this.protocol_error("failed to encrypt packet: " + err));
|
||||
return;
|
||||
}
|
||||
this.send_packet(bdata, payload_size, false);
|
||||
}
|
||||
}
|
||||
|
||||
make_packet_header(proto_flags, level, payload_size) {
|
||||
const header = new Uint8Array(8);
|
||||
header[0] = "P".charCodeAt(0);
|
||||
header[1] = proto_flags;
|
||||
header[2] = level;
|
||||
header[3] = 0;
|
||||
//size header:
|
||||
for (let index = 0; index < 4; index++) {
|
||||
header[7 - index] = (payload_size >> (8 * index)) & 0xff;
|
||||
}
|
||||
return header;
|
||||
}
|
||||
|
||||
send_packet(bdata, payload_size, encrypted) {
|
||||
const level = 0;
|
||||
let proto_flags = 0x10;
|
||||
if (encrypted) {
|
||||
proto_flags |= 0x2;
|
||||
}
|
||||
const header = this.make_packet_header(proto_flags, level, payload_size);
|
||||
const actual_size = bdata.byteLength;
|
||||
const packet = new Uint8Array(8 + actual_size);
|
||||
packet.set(header, 0);
|
||||
packet.set(bdata, 8);
|
||||
// put into buffer before send
|
||||
if (this.websocket) {
|
||||
this.websocket.send(packet.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
process_message_queue() {
|
||||
while (this.mQ.length > 0) {
|
||||
const packet = this.mQ.shift();
|
||||
|
||||
if (!packet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const raw_buffers = [];
|
||||
if (packet[0] === "draw" && "buffer" in packet[7]) {
|
||||
raw_buffers.push(packet[7].buffer);
|
||||
} else if (packet[0] === "sound-data" && Object.hasOwn(packet[2], "buffer")) {
|
||||
raw_buffers.push(packet[2].buffer);
|
||||
}
|
||||
postMessage({
|
||||
c: "p",
|
||||
p: packet
|
||||
}, raw_buffers);
|
||||
}
|
||||
}
|
||||
|
||||
send(packet) {
|
||||
this.sQ[this.sQ.length] = packet;
|
||||
setTimeout(() => this.process_send_queue(), this.process_interval);
|
||||
}
|
||||
|
||||
set_packet_handler(callback) {
|
||||
this.packet_handler = callback;
|
||||
}
|
||||
|
||||
set_cipher_in(caps, key) {
|
||||
// console.log("configuring cipher in:", caps);
|
||||
this.setup_cipher(caps, key, "decrypt", (block_size, params, crypto_key) => {
|
||||
// console.log("cipher in configured, params=", JSON.stringify(params));
|
||||
this.cipher_in_block_size = block_size;
|
||||
this.cipher_in_params = params;
|
||||
this.cipher_in_key = crypto_key;
|
||||
});
|
||||
}
|
||||
|
||||
set_cipher_out(caps, key) {
|
||||
// console.log("configuring cipher out:", caps);
|
||||
this.setup_cipher(caps, key, "encrypt", (block_size, params, crypto_key) => {
|
||||
// console.log("cipher out configured, params=", JSON.stringify(params));
|
||||
this.cipher_out_params = params;
|
||||
this.cipher_out_key = crypto_key;
|
||||
});
|
||||
}
|
||||
|
||||
setup_cipher(caps, key, usage, setup_function) {
|
||||
if (!key) {
|
||||
throw "missing encryption key";
|
||||
}
|
||||
|
||||
const cipher = caps["cipher"] || "AES";
|
||||
if (cipher !== "AES") {
|
||||
throw `unsupported encryption specified: '${cipher}'`;
|
||||
}
|
||||
|
||||
const DEFAULT_MODE = "CBC";
|
||||
const mode = caps["mode"] || DEFAULT_MODE;
|
||||
let block_size = 0;
|
||||
if (mode === "CBC") {
|
||||
block_size = 16;
|
||||
} else if (!["GCM", "CTR"].includes(mode)) {
|
||||
throw `unsupported AES mode '${mode}'`;
|
||||
}
|
||||
|
||||
const iv = caps["iv"];
|
||||
if (!iv) {
|
||||
throw "missing IV";
|
||||
}
|
||||
|
||||
const salt = Utilities.u8(caps["key_salt"]);
|
||||
if (!salt) {
|
||||
throw "missing salt";
|
||||
}
|
||||
|
||||
const iterations = caps["key_stretch_iterations"];
|
||||
if (iterations < 1000 || iterations > 1000000) {
|
||||
throw `invalid number of iterations: ${iterations}`;
|
||||
}
|
||||
|
||||
const DEFAULT_KEYSIZE = 32;
|
||||
const key_size = caps["key_size"] || DEFAULT_KEYSIZE;
|
||||
if (![32, 24, 16].includes(key_size)) {
|
||||
throw `invalid key size '${key_size}'`;
|
||||
}
|
||||
|
||||
const DEFAULT_KEYSTRETCH = "PBKDF2";
|
||||
const key_stretch = caps["key_stretch"] || DEFAULT_KEYSTRETCH;
|
||||
if (key_stretch.toUpperCase() !== "PBKDF2") {
|
||||
throw `invalid key stretching function ${key_stretch}`;
|
||||
}
|
||||
|
||||
const DEFAULT_KEY_HASH = "SHA-1";
|
||||
let key_hash = (caps["key_hash"] || DEFAULT_KEY_HASH).toUpperCase();
|
||||
if (key_hash.startsWith("SHA") && !key_hash.startsWith("SHA-")) {
|
||||
key_hash = "SHA-" + key_hash.substring(3);
|
||||
}
|
||||
|
||||
const params = {
|
||||
name: "AES-" + mode, //ie: "AES-CBC"
|
||||
iv: Utilities.u8(iv),
|
||||
}
|
||||
|
||||
// console.log("importing", "PBKDF2", "key", "'" + key + "'");
|
||||
crypto.subtle.importKey("raw", Utilities.u8(key), {
|
||||
name: "PBKDF2"
|
||||
}, false, ["deriveKey", "deriveBits"])
|
||||
.then(imported_key => {
|
||||
// console.log("imported key:", imported_key);
|
||||
// console.log("deriving", key_size * 8, "bits", mode, "key with:", iterations, key_hash);
|
||||
// console.log("salt=", salt);
|
||||
// now stretch it to get the real key:
|
||||
crypto.subtle.deriveKey({
|
||||
name: "PBKDF2",
|
||||
salt: salt,
|
||||
iterations: iterations,
|
||||
hash: {
|
||||
name: key_hash
|
||||
},
|
||||
}, imported_key, {
|
||||
name: "AES-" + mode,
|
||||
length: key_size * 8,
|
||||
}, false, [usage], )
|
||||
.then(crypto_key => {
|
||||
// console.log("derived key for", usage, "usage:", crypto_key);
|
||||
setup_function(block_size, params, crypto_key);
|
||||
})
|
||||
.catch(err => {
|
||||
this.protocol_error("failed to derive AES key: " + err);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.protocol_error("failed to import AES key: " + err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
If we are in a web worker, set up an instance of the protocol
|
||||
*/
|
||||
if (
|
||||
!(
|
||||
typeof window == "object" &&
|
||||
typeof document == "object" &&
|
||||
window.document === document
|
||||
)
|
||||
) {
|
||||
// some required imports
|
||||
// worker imports are relative to worker script path
|
||||
importScripts(
|
||||
"lib/lz4.js",
|
||||
"lib/brotli_decode.js",
|
||||
"lib/rencode.js",
|
||||
"Utilities.js"
|
||||
);
|
||||
// make protocol instance
|
||||
const protocol = new XpraProtocol();
|
||||
protocol.is_worker = true;
|
||||
// we create a custom packet handler which posts packet as a message
|
||||
protocol.set_packet_handler((packet) => {
|
||||
let raw_buffer = [];
|
||||
if (packet[0] === "draw" && Object.hasOwn(packet[7], "buffer")) {
|
||||
//zero-copy the draw buffer
|
||||
raw_buffer = packet[7].buffer;
|
||||
packet[7] = null;
|
||||
} else if (
|
||||
packet[0] === "send-file-chunk" &&
|
||||
Object.hasOwn(packet[3], "buffer")
|
||||
) {
|
||||
//zero-copy the file data buffer
|
||||
raw_buffer = packet[3].buffer;
|
||||
packet[3] = null;
|
||||
}
|
||||
postMessage({
|
||||
c: "p",
|
||||
p: packet
|
||||
}, raw_buffer);
|
||||
}, null);
|
||||
// attach listeners from main thread
|
||||
self.addEventListener(
|
||||
"message",
|
||||
(e) => {
|
||||
const data = e.data;
|
||||
switch (data.c) {
|
||||
case "o":
|
||||
protocol.open(data.u);
|
||||
break;
|
||||
case "s":
|
||||
protocol.send(data.p);
|
||||
break;
|
||||
case "x":
|
||||
protocol.set_cipher_out(data.p, data.k);
|
||||
break;
|
||||
case "z":
|
||||
protocol.set_cipher_in(data.p, data.k);
|
||||
break;
|
||||
case "c":
|
||||
// close the connection
|
||||
protocol.close();
|
||||
break;
|
||||
case "t":
|
||||
// terminate the worker
|
||||
self.close();
|
||||
break;
|
||||
default:
|
||||
postMessage({
|
||||
c: "l",
|
||||
t: "got unknown command from host"
|
||||
});
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
// tell host we are ready
|
||||
postMessage({
|
||||
c: "r"
|
||||
});
|
||||
}
|
||||
74
gns3server/static/web-ui/assets/xpra-html5/js/RgbHelpers.js
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (c) 2021 Antoine Martin <antoine@xpra.org>
|
||||
*/
|
||||
|
||||
//deals with zlib or lz4 pixel compression
|
||||
//as well as converting rgb24 to rb32 and
|
||||
//re-striding the pixel data if needed so that lines are not padded
|
||||
//(that is: the rowstride must be width*4)
|
||||
//this function modifies the packet data directly
|
||||
function decode_rgb(packet) {
|
||||
const width = packet[4];
|
||||
const height = packet[5];
|
||||
const coding = packet[6];
|
||||
const rowstride = packet[9];
|
||||
let data = packet[7];
|
||||
const options = packet[10] || {};
|
||||
if (options["zlib"] > 0) {
|
||||
throw "zlib compression is not supported";
|
||||
}
|
||||
if (options["lz4"] > 0) {
|
||||
data = lz4.decode(data);
|
||||
delete options["lz4"];
|
||||
}
|
||||
if (coding === "rgb24") {
|
||||
packet[9] = width * 4;
|
||||
packet[6] = "rgb32";
|
||||
return rgb24_to_rgb32(data, width, height, rowstride);
|
||||
}
|
||||
//coding=rgb32
|
||||
if (rowstride === width * 4) {
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
//re-striding
|
||||
//might be quicker to copy 32bit at a time using Uint32Array
|
||||
//and then casting the result?
|
||||
const uint = new Uint8Array(width * height * 4);
|
||||
let psrc = 0;
|
||||
let pdst = 0;
|
||||
for (let row_index = 0; row_index < height; row_index++) {
|
||||
psrc = row_index * rowstride;
|
||||
pdst = row_index * width * 4;
|
||||
for (let column_index = 0; column_index < width * 4; column_index++) {
|
||||
uint[pdst++] = data[psrc++];
|
||||
}
|
||||
}
|
||||
return uint;
|
||||
}
|
||||
|
||||
function rgb24_to_rgb32(data, width, height, rowstride) {
|
||||
const uint = new Uint8Array(width * height * 4);
|
||||
let source_index = 0;
|
||||
let target_index = 0;
|
||||
if (rowstride === width * 3) {
|
||||
//faster path, single loop:
|
||||
const source_length = data.length;
|
||||
while (source_index < source_length) {
|
||||
uint[target_index++] = data[source_index++];
|
||||
uint[target_index++] = data[source_index++];
|
||||
uint[target_index++] = data[source_index++];
|
||||
uint[target_index++] = 255;
|
||||
}
|
||||
} else {
|
||||
for (let row_index = 0; row_index < height; row_index++) {
|
||||
source_index = row_index * rowstride;
|
||||
for (let column_index = 0; column_index < width; column_index++) {
|
||||
uint[target_index++] = data[source_index++];
|
||||
uint[target_index++] = data[source_index++];
|
||||
uint[target_index++] = data[source_index++];
|
||||
uint[target_index++] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
return uint;
|
||||
}
|
||||