update noVNC to 1.5.0

Signed-off-by: si458 <simonsmith5521@gmail.com>
This commit is contained in:
si458 2024-09-29 15:26:20 +01:00
parent b20e51561a
commit 1139a37338
34 changed files with 1730 additions and 1036 deletions

View File

@ -1,4 +1,5 @@
{ {
"HTTPS is required for full functionality": "Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα",
"Connecting...": "Συνδέεται...", "Connecting...": "Συνδέεται...",
"Disconnecting...": "Aποσυνδέεται...", "Disconnecting...": "Aποσυνδέεται...",
"Reconnecting...": "Επανασυνδέεται...", "Reconnecting...": "Επανασυνδέεται...",
@ -7,19 +8,15 @@
"Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ", "Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ",
"Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ", "Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ",
"Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε", "Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε",
"Failed to connect to server": "Αποτυχία στη σύνδεση με το διακομιστή",
"Disconnected": "Αποσυνδέθηκε", "Disconnected": "Αποσυνδέθηκε",
"New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ", "New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ",
"New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ", "New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ",
"Password is required": "Απαιτείται ο κωδικός πρόσβασης", "Credentials are required": "Απαιτούνται διαπιστευτήρια",
"noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:", "noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:",
"Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου", "Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου",
"Drag": "Σύρσιμο",
"Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου", "Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου",
"viewport drag": "σύρσιμο θεατού πεδίου",
"Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού",
"No mousebutton": "Χωρίς Πλήκτρο Ποντικιού",
"Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού",
"Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού",
"Right mousebutton": "Δεξί Πλήκτρο Ποντικιού",
"Keyboard": "Πληκτρολόγιο", "Keyboard": "Πληκτρολόγιο",
"Show Keyboard": "Εμφάνιση Πληκτρολογίου", "Show Keyboard": "Εμφάνιση Πληκτρολογίου",
"Extra keys": "Επιπλέον πλήκτρα", "Extra keys": "Επιπλέον πλήκτρα",
@ -28,6 +25,8 @@
"Toggle Ctrl": "Εναλλαγή Ctrl", "Toggle Ctrl": "Εναλλαγή Ctrl",
"Alt": "Alt", "Alt": "Alt",
"Toggle Alt": "Εναλλαγή Alt", "Toggle Alt": "Εναλλαγή Alt",
"Toggle Windows": "Εναλλαγή Παράθυρων",
"Windows": "Παράθυρα",
"Send Tab": "Αποστολή Tab", "Send Tab": "Αποστολή Tab",
"Tab": "Tab", "Tab": "Tab",
"Esc": "Esc", "Esc": "Esc",
@ -41,8 +40,7 @@
"Reboot": "Επανεκκίνηση", "Reboot": "Επανεκκίνηση",
"Reset": "Επαναφορά", "Reset": "Επαναφορά",
"Clipboard": "Πρόχειρο", "Clipboard": "Πρόχειρο",
"Clear": "Καθάρισμα", "Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.",
"Fullscreen": "Πλήρης Οθόνη",
"Settings": "Ρυθμίσεις", "Settings": "Ρυθμίσεις",
"Shared Mode": "Κοινόχρηστη Λειτουργία", "Shared Mode": "Κοινόχρηστη Λειτουργία",
"View Only": "Μόνο Θέαση", "View Only": "Μόνο Θέαση",
@ -52,6 +50,8 @@
"Local Scaling": "Τοπική Κλιμάκωση", "Local Scaling": "Τοπική Κλιμάκωση",
"Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους", "Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους",
"Advanced": "Για προχωρημένους", "Advanced": "Για προχωρημένους",
"Quality:": "Ποιότητα:",
"Compression level:": "Επίπεδο συμπίεσης:",
"Repeater ID:": "Repeater ID:", "Repeater ID:": "Repeater ID:",
"WebSocket": "WebSocket", "WebSocket": "WebSocket",
"Encrypt": "Κρυπτογράφηση", "Encrypt": "Κρυπτογράφηση",
@ -60,10 +60,20 @@
"Path:": "Διαδρομή:", "Path:": "Διαδρομή:",
"Automatic Reconnect": "Αυτόματη επανασύνδεση", "Automatic Reconnect": "Αυτόματη επανασύνδεση",
"Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):", "Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):",
"Show Dot when No Cursor": "Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας",
"Logging:": "Καταγραφή:", "Logging:": "Καταγραφή:",
"Version:": "Έκδοση:",
"Disconnect": "Αποσύνδεση", "Disconnect": "Αποσύνδεση",
"Connect": "Σύνδεση", "Connect": "Σύνδεση",
"Server identity": "Ταυτότητα Διακομιστή",
"The server has provided the following identifying information:": "Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:",
"Fingerprint:": "Δακτυλικό αποτύπωμα:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \"Αποδοχή\". Αλλιώς πιέστε \"Απόρριψη\".",
"Approve": "Αποδοχή",
"Reject": "Απόρριψη",
"Credentials": "Διαπιστευτήρια",
"Username:": "Κωδικός Χρήστη:",
"Password:": "Κωδικός Πρόσβασης:", "Password:": "Κωδικός Πρόσβασης:",
"Cancel": "Ακύρωση", "Send Credentials": "Αποστολή Διαπιστευτηρίων",
"Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas" "Cancel": "Ακύρωση"
} }

View File

@ -1,5 +1,4 @@
{ {
"HTTPS is required for full functionality": "",
"Connecting...": "En cours de connexion...", "Connecting...": "En cours de connexion...",
"Disconnecting...": "Déconnexion en cours...", "Disconnecting...": "Déconnexion en cours...",
"Reconnecting...": "Reconnexion en cours...", "Reconnecting...": "Reconnexion en cours...",
@ -40,7 +39,8 @@
"Reboot": "Redémarrer", "Reboot": "Redémarrer",
"Reset": "Réinitialiser", "Reset": "Réinitialiser",
"Clipboard": "Presse-papiers", "Clipboard": "Presse-papiers",
"Edit clipboard content in the textarea below.": "", "Clear": "Effacer",
"Fullscreen": "Plein écran",
"Settings": "Paramètres", "Settings": "Paramètres",
"Shared Mode": "Mode partagé", "Shared Mode": "Mode partagé",
"View Only": "Afficher uniquement", "View Only": "Afficher uniquement",
@ -65,12 +65,6 @@
"Version:": "Version :", "Version:": "Version :",
"Disconnect": "Déconnecter", "Disconnect": "Déconnecter",
"Connect": "Connecter", "Connect": "Connecter",
"Server identity": "",
"The server has provided the following identifying information:": "",
"Fingerprint:": "",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "",
"Approve": "",
"Reject": "",
"Username:": "Nom d'utilisateur :", "Username:": "Nom d'utilisateur :",
"Password:": "Mot de passe :", "Password:": "Mot de passe :",
"Send Credentials": "Envoyer les identifiants", "Send Credentials": "Envoyer les identifiants",

View File

@ -14,8 +14,6 @@
"Credentials are required": "Le credenziali sono obbligatorie", "Credentials are required": "Le credenziali sono obbligatorie",
"noVNC encountered an error:": "noVNC ha riscontrato un errore:", "noVNC encountered an error:": "noVNC ha riscontrato un errore:",
"Hide/Show the control bar": "Nascondi/Mostra la barra di controllo", "Hide/Show the control bar": "Nascondi/Mostra la barra di controllo",
"Drag": "",
"Move/Drag Viewport": "",
"Keyboard": "Tastiera", "Keyboard": "Tastiera",
"Show Keyboard": "Mostra tastiera", "Show Keyboard": "Mostra tastiera",
"Extra keys": "Tasti Aggiuntivi", "Extra keys": "Tasti Aggiuntivi",
@ -44,7 +42,6 @@
"Settings": "Impostazioni", "Settings": "Impostazioni",
"Shared Mode": "Modalità condivisa", "Shared Mode": "Modalità condivisa",
"View Only": "Sola Visualizzazione", "View Only": "Sola Visualizzazione",
"Clip to Window": "",
"Scaling Mode:": "Modalità di ridimensionamento:", "Scaling Mode:": "Modalità di ridimensionamento:",
"None": "Nessuna", "None": "Nessuna",
"Local Scaling": "Ridimensionamento Locale", "Local Scaling": "Ridimensionamento Locale",
@ -61,7 +58,6 @@
"Automatic Reconnect": "Riconnessione Automatica", "Automatic Reconnect": "Riconnessione Automatica",
"Reconnect Delay (ms):": "Ritardo Riconnessione (ms):", "Reconnect Delay (ms):": "Ritardo Riconnessione (ms):",
"Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore", "Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore",
"Logging:": "",
"Version:": "Versione:", "Version:": "Versione:",
"Disconnect": "Disconnetti", "Disconnect": "Disconnetti",
"Connect": "Connetti", "Connect": "Connetti",

View File

@ -1,4 +1,5 @@
{ {
"HTTPS is required for full functionality": "すべての機能を使用するにはHTTPS接続が必要です",
"Connecting...": "接続しています...", "Connecting...": "接続しています...",
"Disconnecting...": "切断しています...", "Disconnecting...": "切断しています...",
"Reconnecting...": "再接続しています...", "Reconnecting...": "再接続しています...",
@ -21,10 +22,10 @@
"Extra keys": "追加キー", "Extra keys": "追加キー",
"Show Extra Keys": "追加キーを表示", "Show Extra Keys": "追加キーを表示",
"Ctrl": "Ctrl", "Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl キーを切り替え", "Toggle Ctrl": "Ctrl キーをトグル",
"Alt": "Alt", "Alt": "Alt",
"Toggle Alt": "Alt キーを切り替え", "Toggle Alt": "Alt キーをトグル",
"Toggle Windows": "Windows キーを切り替え", "Toggle Windows": "Windows キーをトグル",
"Windows": "Windows", "Windows": "Windows",
"Send Tab": "Tab キーを送信", "Send Tab": "Tab キーを送信",
"Tab": "Tab", "Tab": "Tab",
@ -39,11 +40,11 @@
"Reboot": "再起動", "Reboot": "再起動",
"Reset": "リセット", "Reset": "リセット",
"Clipboard": "クリップボード", "Clipboard": "クリップボード",
"Clear": "クリア", "Edit clipboard content in the textarea below.": "以下の入力欄からクリップボードの内容を編集できます。",
"Fullscreen": "全画面表示", "Full Screen": "全画面表示",
"Settings": "設定", "Settings": "設定",
"Shared Mode": "共有モード", "Shared Mode": "共有モード",
"View Only": "表示のみ", "View Only": "表示専用",
"Clip to Window": "ウィンドウにクリップ", "Clip to Window": "ウィンドウにクリップ",
"Scaling Mode:": "スケーリングモード:", "Scaling Mode:": "スケーリングモード:",
"None": "なし", "None": "なし",
@ -60,11 +61,18 @@
"Path:": "パス:", "Path:": "パス:",
"Automatic Reconnect": "自動再接続", "Automatic Reconnect": "自動再接続",
"Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):", "Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):",
"Show Dot when No Cursor": "カーソルがないときにドットを表示", "Show Dot when No Cursor": "カーソルがないときにドットを表示する",
"Logging:": "ロギング:", "Logging:": "ロギング:",
"Version:": "バージョン:", "Version:": "バージョン:",
"Disconnect": "切断", "Disconnect": "切断",
"Connect": "接続", "Connect": "接続",
"Server identity": "サーバーの識別情報",
"The server has provided the following identifying information:": "サーバーは以下の識別情報を提供しています:",
"Fingerprint:": "フィンガープリント:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してください。",
"Approve": "承認",
"Reject": "拒否",
"Credentials": "資格情報",
"Username:": "ユーザー名:", "Username:": "ユーザー名:",
"Password:": "パスワード:", "Password:": "パスワード:",
"Send Credentials": "資格情報を送信", "Send Credentials": "資格情報を送信",

View File

@ -1,10 +1,11 @@
{ {
"HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet", "Running without HTTPS is not recommended, crashes or other issues are likely.": "Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är troliga.",
"Connecting...": "Ansluter...", "Connecting...": "Ansluter...",
"Disconnecting...": "Kopplar ner...", "Disconnecting...": "Kopplar ner...",
"Reconnecting...": "Återansluter...", "Reconnecting...": "Återansluter...",
"Internal error": "Internt fel", "Internal error": "Internt fel",
"Must set host": "Du måste specifiera en värd", "Must set host": "Du måste specifiera en värd",
"Failed to connect to server: ": "Misslyckades att ansluta till servern: ",
"Connected (encrypted) to ": "Ansluten (krypterat) till ", "Connected (encrypted) to ": "Ansluten (krypterat) till ",
"Connected (unencrypted) to ": "Ansluten (okrypterat) till ", "Connected (unencrypted) to ": "Ansluten (okrypterat) till ",
"Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades", "Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades",

View File

@ -1,69 +1,69 @@
{ {
"Connecting...": "连接中...", "Connecting...": "连接中...",
"Connected (encrypted) to ": "已连接(已加密)到",
"Connected (unencrypted) to ": "已连接(未加密)到",
"Disconnecting...": "正在断开连接...", "Disconnecting...": "正在断开连接...",
"Reconnecting...": "重新连接中...",
"Internal error": "内部错误",
"Must set host": "请提供主机名",
"Connected (encrypted) to ": "已连接到(加密)",
"Connected (unencrypted) to ": "已连接到(未加密)",
"Something went wrong, connection is closed": "发生错误,连接已关闭",
"Failed to connect to server": "无法连接到服务器",
"Disconnected": "已断开连接", "Disconnected": "已断开连接",
"New connection has been rejected with reason: ": "连接被拒绝,原因:", "Must set host": "必须设置主机",
"New connection has been rejected": "连接被拒绝", "Reconnecting...": "重新连接中...",
"Password is required": "请提供密码", "Password is required": "请提供密码",
"Disconnect timeout": "超时断开",
"noVNC encountered an error:": "noVNC 遇到一个错误:", "noVNC encountered an error:": "noVNC 遇到一个错误:",
"Hide/Show the control bar": "显示/隐藏控制栏", "Hide/Show the control bar": "显示/隐藏控制栏",
"Move/Drag Viewport": "拖放显示范围", "Move/Drag Viewport": "移动/拖动窗口",
"viewport drag": "显示范围拖放", "viewport drag": "窗口拖动",
"Active Mouse Button": "启动鼠标按", "Active Mouse Button": "启动鼠标按",
"No mousebutton": "禁用鼠标按", "No mousebutton": "禁用鼠标按",
"Left mousebutton": "鼠标左", "Left mousebutton": "鼠标左",
"Middle mousebutton": "鼠标中", "Middle mousebutton": "鼠标中",
"Right mousebutton": "鼠标右", "Right mousebutton": "鼠标右",
"Keyboard": "键盘", "Keyboard": "键盘",
"Show Keyboard": "显示键盘", "Show Keyboard": "显示键盘",
"Extra keys": "额外按键", "Extra keys": "额外按键",
"Show Extra Keys": "显示额外按键", "Show Extra Keys": "显示额外按键",
"Ctrl": "Ctrl", "Ctrl": "Ctrl",
"Toggle Ctrl": "切换 Ctrl", "Toggle Ctrl": "切换 Ctrl",
"Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。",
"Alt": "Alt", "Alt": "Alt",
"Toggle Alt": "切换 Alt", "Toggle Alt": "切换 Alt",
"Send Tab": "发送 Tab 键", "Send Tab": "发送 Tab 键",
"Tab": "Tab", "Tab": "Tab",
"Esc": "Esc", "Esc": "Esc",
"Send Escape": "发送 Escape 键", "Send Escape": "发送 Escape 键",
"Ctrl+Alt+Del": "Ctrl-Alt-Del", "Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "发送 Ctrl-Alt-Del 键", "Send Ctrl-Alt-Del": "发送 Ctrl+Alt+Del 键",
"Shutdown/Reboot": "关机/重", "Shutdown/Reboot": "关机/重启",
"Shutdown/Reboot...": "关机/重...", "Shutdown/Reboot...": "关机/重启...",
"Power": "电源", "Power": "电源",
"Shutdown": "关机", "Shutdown": "关机",
"Reboot": "重", "Reboot": "重启",
"Reset": "重置", "Reset": "重置",
"Clipboard": "剪贴板", "Clipboard": "剪贴板",
"Clear": "清除", "Clear": "清除",
"Fullscreen": "全屏", "Fullscreen": "全屏",
"Settings": "设置", "Settings": "设置",
"Encrypt": "加密",
"Shared Mode": "分享模式", "Shared Mode": "分享模式",
"View Only": "仅查看", "View Only": "仅查看",
"Clip to Window": "限制/裁切窗口大小", "Clip to Window": "限制/裁切窗口大小",
"Scaling Mode:": "缩放模式:", "Scaling Mode:": "缩放模式:",
"None": "无", "None": "无",
"Local Scaling": "本地缩放", "Local Scaling": "本地缩放",
"Local Downscaling": "降低本地尺寸",
"Remote Resizing": "远程调整大小", "Remote Resizing": "远程调整大小",
"Advanced": "高级", "Advanced": "高级",
"Local Cursor": "本地光标",
"Repeater ID:": "中继站 ID", "Repeater ID:": "中继站 ID",
"WebSocket": "WebSocket", "WebSocket": "WebSocket",
"Encrypt": "加密",
"Host:": "主机:", "Host:": "主机:",
"Port:": "端口:", "Port:": "端口:",
"Path:": "路径:", "Path:": "路径:",
"Automatic Reconnect": "自动重新连接", "Automatic Reconnect": "自动重新连接",
"Reconnect Delay (ms):": "重新连接间隔 (ms)", "Reconnect Delay (ms):": "重新连接间隔 (ms)",
"Logging:": "日志级别:", "Logging:": "日志级别:",
"Disconnect": "断连接", "Disconnect": "连接",
"Connect": "连接", "Connect": "连接",
"Password:": "密码:", "Password:": "密码:",
"Cancel": "取消" "Cancel": "取消",
"Canvas not supported.": "不支持 Canvas。"
} }

View File

@ -16,13 +16,19 @@ export class Localizer {
this.language = 'en'; this.language = 'en';
// Current dictionary of translations // Current dictionary of translations
this.dictionary = undefined; this._dictionary = undefined;
} }
// Configure suitable language based on user preferences // Configure suitable language based on user preferences
setup(supportedLanguages) { async setup(supportedLanguages, baseURL) {
this.language = 'en'; // Default: US English this.language = 'en'; // Default: US English
this._dictionary = undefined;
this._setupLanguage(supportedLanguages);
await this._setupDictionary(baseURL);
}
_setupLanguage(supportedLanguages) {
/* /*
* Navigator.languages only available in Chrome (32+) and FireFox (32+) * Navigator.languages only available in Chrome (32+) and FireFox (32+)
* Fall back to navigator.language for other browsers * Fall back to navigator.language for other browsers
@ -40,12 +46,6 @@ export class Localizer {
.replace("_", "-") .replace("_", "-")
.split("-"); .split("-");
// Built-in default?
if ((userLang[0] === 'en') &&
((userLang[1] === undefined) || (userLang[1] === 'us'))) {
return;
}
// First pass: perfect match // First pass: perfect match
for (let j = 0; j < supportedLanguages.length; j++) { for (let j = 0; j < supportedLanguages.length; j++) {
const supLang = supportedLanguages[j] const supLang = supportedLanguages[j]
@ -64,7 +64,12 @@ export class Localizer {
return; return;
} }
// Second pass: fallback // Second pass: English fallback
if (userLang[0] === 'en') {
return;
}
// Third pass pass: other fallback
for (let j = 0;j < supportedLanguages.length;j++) { for (let j = 0;j < supportedLanguages.length;j++) {
const supLang = supportedLanguages[j] const supLang = supportedLanguages[j]
.toLowerCase() .toLowerCase()
@ -84,10 +89,32 @@ export class Localizer {
} }
} }
async _setupDictionary(baseURL) {
if (baseURL) {
if (!baseURL.endsWith("/")) {
baseURL = baseURL + "/";
}
} else {
baseURL = "";
}
if (this.language === "en") {
return;
}
let response = await fetch(baseURL + this.language + ".json");
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
this._dictionary = await response.json();
}
// Retrieve localised text // Retrieve localised text
get(id) { get(id) {
if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) { if (typeof this._dictionary !== 'undefined' &&
return this.dictionary[id]; this._dictionary[id]) {
return this._dictionary[id];
} else { } else {
return id; return id;
} }

View File

@ -661,7 +661,7 @@ html {
justify-content: center; justify-content: center;
align-content: center; align-content: center;
line-height: 25px; line-height: 1.6;
word-wrap: break-word; word-wrap: break-word;
color: #fff; color: #fff;
@ -887,7 +887,7 @@ html {
.noVNC_logo { .noVNC_logo {
color:yellow; color:yellow;
font-family: 'Orbitron', 'OrbitronTTF', sans-serif; font-family: 'Orbitron', 'OrbitronTTF', sans-serif;
line-height:90%; line-height: 0.9;
text-shadow: 0.1em 0.1em 0 black; text-shadow: 0.1em 0.1em 0 black;
} }
.noVNC_logo span{ .noVNC_logo span{

View File

@ -86,6 +86,9 @@ option {
* Checkboxes * Checkboxes
*/ */
input[type=checkbox] { input[type=checkbox] {
display: inline-flex;
justify-content: center;
align-items: center;
background-color: white; background-color: white;
background-image: unset; background-image: unset;
border: 1px solid dimgrey; border: 1px solid dimgrey;
@ -104,14 +107,11 @@ input[type=checkbox]:checked {
input[type=checkbox]:checked::after { input[type=checkbox]:checked::after {
content: ""; content: "";
display: block; /* width & height doesn't work on inline elements */ display: block; /* width & height doesn't work on inline elements */
position: relative;
top: 0;
left: 3px;
width: 3px; width: 3px;
height: 7px; height: 7px;
border: 1px solid white; border: 1px solid white;
border-width: 0 2px 2px 0; border-width: 0 2px 2px 0;
transform: rotate(40deg); transform: rotate(40deg) translateY(-1px);
} }
/* /*

View File

@ -91,7 +91,7 @@ const UI = {
// insecure context // insecure context
if (!window.isSecureContext) { if (!window.isSecureContext) {
// FIXME: This gets hidden when connecting // FIXME: This gets hidden when connecting
UI.showStatus(_("HTTPS is required for full functionality"), 'error'); UI.showStatus(_("Running without HTTPS is not recommended, crashes or other issues are likely."), 'error');
} }
// Try to fetch version number // Try to fetch version number
@ -1058,11 +1058,18 @@ const UI = {
try {
UI.rfb = new RFB(document.getElementById('noVNC_container'), urlargs.ws,
{ shared: UI.getSetting('shared'),
repeaterID: UI.getSetting('repeaterID'),
credentials: { password: password } });
} catch (exc) {
Log.Error("Failed to connect to server: " + exc);
UI.updateVisualState('disconnected');
UI.showStatus(_("Failed to connect to server: ") + exc, 'error');
return;
}
UI.rfb = new RFB(document.getElementById('noVNC_container'), urlargs.ws,
{ shared: UI.getSetting('shared'),
repeaterID: UI.getSetting('repeaterID'),
credentials: { password: password } });
UI.rfb.addEventListener("connect", UI.connectFinished); UI.rfb.addEventListener("connect", UI.connectFinished);
UI.rfb.addEventListener("disconnect", UI.disconnectFinished); UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
UI.rfb.addEventListener("serververification", UI.serverVerify); UI.rfb.addEventListener("serververification", UI.serverVerify);
@ -1168,6 +1175,7 @@ const UI = {
UI.showStatus(_("Disconnected"), 'normal'); UI.showStatus(_("Disconnected"), 'normal');
} }
document.title = PAGE_TITLE;
UI.openControlbar(); UI.openControlbar();
UI.openConnectPanel(); UI.openConnectPanel();
@ -1780,20 +1788,8 @@ const UI = {
// Set up translations // Set up translations
const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
l10n.setup(LINGUAS); l10n.setup(LINGUAS, "app/locale/")
if (l10n.language === "en" || l10n.dictionary !== undefined) { .catch(err => Log.Error("Failed to load translations: " + err))
UI.prime(); .then(UI.prime);
} else {
fetch('app/locale/' + l10n.language + '.json')
.then((response) => {
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
return response.json();
})
.then((translations) => { l10n.dictionary = translations; })
.catch(err => Log.Error("Failed to load translations: " + err))
.then(UI.prime);
}
export default UI; export default UI;

View File

@ -6,16 +6,16 @@
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
*/ */
import { initLogging as mainInitLogging } from '../core/util/logging.js'; import * as Log from '../core/util/logging.js';
// init log level reading the logging HTTP param // init log level reading the logging HTTP param
export function initLogging(level) { export function initLogging(level) {
"use strict"; "use strict";
if (typeof level !== "undefined") { if (typeof level !== "undefined") {
mainInitLogging(level); Log.initLogging(level);
} else { } else {
const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/); const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/);
mainInitLogging(param || undefined); Log.initLogging(param || undefined);
} }
} }
@ -25,14 +25,14 @@ export function initLogging(level) {
// //
// For privacy (Using a hastag #, the parameters will not be sent to the server) // For privacy (Using a hastag #, the parameters will not be sent to the server)
// the url can be requested in the following way: // the url can be requested in the following way:
// https://www.example.com#myqueryparam=myvalue&password=secreatvalue // https://www.example.com#myqueryparam=myvalue&password=secretvalue
// //
// Even Mixing public and non public parameters will work: // Even Mixing public and non public parameters will work:
// https://www.example.com?nonsecretparam=example.com#password=secreatvalue // https://www.example.com?nonsecretparam=example.com#password=secretvalue
export function getQueryVar(name, defVal) { export function getQueryVar(name, defVal) {
"use strict"; "use strict";
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
match = ''.concat(document.location.href, window.location.hash).match(re); match = document.location.href.match(re);
if (typeof defVal === 'undefined') { defVal = null; } if (typeof defVal === 'undefined') { defVal = null; }
if (match) { if (match) {
@ -146,7 +146,7 @@ export function writeSetting(name, value) {
if (window.chrome && window.chrome.storage) { if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.set(settings); window.chrome.storage.sync.set(settings);
} else { } else {
localStorage.setItem(name, value); localStorageSet(name, value);
} }
} }
@ -156,7 +156,7 @@ export function readSetting(name, defaultValue) {
if ((name in settings) || (window.chrome && window.chrome.storage)) { if ((name in settings) || (window.chrome && window.chrome.storage)) {
value = settings[name]; value = settings[name];
} else { } else {
value = localStorage.getItem(name); value = localStorageGet(name);
settings[name] = value; settings[name] = value;
} }
if (typeof value === "undefined") { if (typeof value === "undefined") {
@ -181,6 +181,70 @@ export function eraseSetting(name) {
if (window.chrome && window.chrome.storage) { if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.remove(name); window.chrome.storage.sync.remove(name);
} else { } else {
localStorage.removeItem(name); localStorageRemove(name);
}
}
let loggedMsgs = [];
function logOnce(msg, level = "warn") {
if (!loggedMsgs.includes(msg)) {
switch (level) {
case "error":
Log.Error(msg);
break;
case "warn":
Log.Warn(msg);
break;
case "debug":
Log.Debug(msg);
break;
default:
Log.Info(msg);
}
loggedMsgs.push(msg);
}
}
let cookiesMsg = "Couldn't access noVNC settings, are cookies disabled?";
function localStorageGet(name) {
let r;
try {
r = localStorage.getItem(name);
} catch (e) {
if (e instanceof DOMException) {
logOnce(cookiesMsg);
logOnce("'localStorage.getItem(" + name + ")' failed: " + e,
"debug");
} else {
throw e;
}
}
return r;
}
function localStorageSet(name, value) {
try {
localStorage.setItem(name, value);
} catch (e) {
if (e instanceof DOMException) {
logOnce(cookiesMsg);
logOnce("'localStorage.setItem(" + name + "," + value +
")' failed: " + e, "debug");
} else {
throw e;
}
}
}
function localStorageRemove(name) {
try {
localStorage.removeItem(name);
} catch (e) {
if (e instanceof DOMException) {
logOnce(cookiesMsg);
logOnce("'localStorage.removeItem(" + name + ")' failed: " + e,
"debug");
} else {
throw e;
}
} }
} }

View File

@ -0,0 +1,178 @@
export class AESECBCipher {
constructor() {
this._key = null;
}
get algorithm() {
return { name: "AES-ECB" };
}
static async importKey(key, _algorithm, extractable, keyUsages) {
const cipher = new AESECBCipher;
await cipher._importKey(key, extractable, keyUsages);
return cipher;
}
async _importKey(key, extractable, keyUsages) {
this._key = await window.crypto.subtle.importKey(
"raw", key, {name: "AES-CBC"}, extractable, keyUsages);
}
async encrypt(_algorithm, plaintext) {
const x = new Uint8Array(plaintext);
if (x.length % 16 !== 0 || this._key === null) {
return null;
}
const n = x.length / 16;
for (let i = 0; i < n; i++) {
const y = new Uint8Array(await window.crypto.subtle.encrypt({
name: "AES-CBC",
iv: new Uint8Array(16),
}, this._key, x.slice(i * 16, i * 16 + 16))).slice(0, 16);
x.set(y, i * 16);
}
return x;
}
}
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]);
}
get algorithm() {
return { name: "AES-EAX" };
}
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;
}
static async importKey(key, _algorithm, _extractable, _keyUsages) {
const cipher = new AESEAXCipher;
await cipher._importKey(key);
return cipher;
}
async _importKey(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"]);
await this._initCMAC();
}
async encrypt(algorithm, message) {
const ad = algorithm.additionalData;
const nonce = algorithm.iv;
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
const encrypted = await this._encryptCTR(message, nCMAC);
const adCMAC = await this._computeCMAC(ad, 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(algorithm, data) {
const encrypted = data.slice(0, data.length - 16);
const ad = algorithm.additionalData;
const nonce = algorithm.iv;
const mac = data.slice(data.length - 16);
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
const adCMAC = await this._computeCMAC(ad, 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;
}
}

View File

@ -0,0 +1,34 @@
export function modPow(b, e, m) {
let r = 1n;
b = b % m;
while (e > 0n) {
if ((e & 1n) === 1n) {
r = (r * b) % m;
}
e = e >> 1n;
b = (b * b) % m;
}
return r;
}
export function bigIntToU8Array(bigint, padLength=0) {
let hex = bigint.toString(16);
if (padLength === 0) {
padLength = Math.ceil(hex.length / 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;
}
export function u8ArrayToBigInt(arr) {
let hex = '0x';
for (let i = 0; i < arr.length; i++) {
hex += arr[i].toString(16).padStart(2, '0');
}
return BigInt(hex);
}

View File

@ -0,0 +1,90 @@
import { AESECBCipher, AESEAXCipher } from "./aes.js";
import { DESCBCCipher, DESECBCipher } from "./des.js";
import { RSACipher } from "./rsa.js";
import { DHCipher } from "./dh.js";
import { MD5 } from "./md5.js";
// A single interface for the cryptographic algorithms not supported by SubtleCrypto.
// Both synchronous and asynchronous implmentations are allowed.
class LegacyCrypto {
constructor() {
this._algorithms = {
"AES-ECB": AESECBCipher,
"AES-EAX": AESEAXCipher,
"DES-ECB": DESECBCipher,
"DES-CBC": DESCBCCipher,
"RSA-PKCS1-v1_5": RSACipher,
"DH": DHCipher,
"MD5": MD5,
};
}
encrypt(algorithm, key, data) {
if (key.algorithm.name !== algorithm.name) {
throw new Error("algorithm does not match");
}
if (typeof key.encrypt !== "function") {
throw new Error("key does not support encryption");
}
return key.encrypt(algorithm, data);
}
decrypt(algorithm, key, data) {
if (key.algorithm.name !== algorithm.name) {
throw new Error("algorithm does not match");
}
if (typeof key.decrypt !== "function") {
throw new Error("key does not support encryption");
}
return key.decrypt(algorithm, data);
}
importKey(format, keyData, algorithm, extractable, keyUsages) {
if (format !== "raw") {
throw new Error("key format is not supported");
}
const alg = this._algorithms[algorithm.name];
if (typeof alg === "undefined" || typeof alg.importKey !== "function") {
throw new Error("algorithm is not supported");
}
return alg.importKey(keyData, algorithm, extractable, keyUsages);
}
generateKey(algorithm, extractable, keyUsages) {
const alg = this._algorithms[algorithm.name];
if (typeof alg === "undefined" || typeof alg.generateKey !== "function") {
throw new Error("algorithm is not supported");
}
return alg.generateKey(algorithm, extractable, keyUsages);
}
exportKey(format, key) {
if (format !== "raw") {
throw new Error("key format is not supported");
}
if (typeof key.exportKey !== "function") {
throw new Error("key does not support exportKey");
}
return key.exportKey();
}
digest(algorithm, data) {
const alg = this._algorithms[algorithm];
if (typeof alg !== "function") {
throw new Error("algorithm is not supported");
}
return alg(data);
}
deriveBits(algorithm, key, length) {
if (key.algorithm.name !== algorithm.name) {
throw new Error("algorithm does not match");
}
if (typeof key.deriveBits !== "function") {
throw new Error("key does not support deriveBits");
}
return key.deriveBits(algorithm, length);
}
}
export default new LegacyCrypto;

View File

@ -0,0 +1,330 @@
/*
* 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 */
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;
}
}
export class DESECBCipher {
constructor() {
this._cipher = null;
}
get algorithm() {
return { name: "DES-ECB" };
}
static importKey(key, _algorithm, _extractable, _keyUsages) {
const cipher = new DESECBCipher;
cipher._importKey(key);
return cipher;
}
_importKey(key, _extractable, _keyUsages) {
this._cipher = new DES(key);
}
encrypt(_algorithm, plaintext) {
const x = new Uint8Array(plaintext);
if (x.length % 8 !== 0 || this._cipher === null) {
return null;
}
const n = x.length / 8;
for (let i = 0; i < n; i++) {
x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8);
}
return x;
}
}
export class DESCBCCipher {
constructor() {
this._cipher = null;
}
get algorithm() {
return { name: "DES-CBC" };
}
static importKey(key, _algorithm, _extractable, _keyUsages) {
const cipher = new DESCBCCipher;
cipher._importKey(key);
return cipher;
}
_importKey(key) {
this._cipher = new DES(key);
}
encrypt(algorithm, plaintext) {
const x = new Uint8Array(plaintext);
let y = new Uint8Array(algorithm.iv);
if (x.length % 8 !== 0 || this._cipher === null) {
return null;
}
const n = x.length / 8;
for (let i = 0; i < n; i++) {
for (let j = 0; j < 8; j++) {
y[j] ^= plaintext[i * 8 + j];
}
y = this._cipher.enc8(y);
x.set(y, i * 8);
}
return x;
}
}

View File

@ -0,0 +1,55 @@
import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
class DHPublicKey {
constructor(key) {
this._key = key;
}
get algorithm() {
return { name: "DH" };
}
exportKey() {
return this._key;
}
}
export class DHCipher {
constructor() {
this._g = null;
this._p = null;
this._gBigInt = null;
this._pBigInt = null;
this._privateKey = null;
}
get algorithm() {
return { name: "DH" };
}
static generateKey(algorithm, _extractable) {
const cipher = new DHCipher;
cipher._generateKey(algorithm);
return { privateKey: cipher, publicKey: new DHPublicKey(cipher._publicKey) };
}
_generateKey(algorithm) {
const g = algorithm.g;
const p = algorithm.p;
this._keyBytes = p.length;
this._gBigInt = u8ArrayToBigInt(g);
this._pBigInt = u8ArrayToBigInt(p);
this._privateKey = window.crypto.getRandomValues(new Uint8Array(this._keyBytes));
this._privateKeyBigInt = u8ArrayToBigInt(this._privateKey);
this._publicKey = bigIntToU8Array(modPow(
this._gBigInt, this._privateKeyBigInt, this._pBigInt), this._keyBytes);
}
deriveBits(algorithm, length) {
const bytes = Math.ceil(length / 8);
const pkey = new Uint8Array(algorithm.public);
const len = bytes > this._keyBytes ? bytes : this._keyBytes;
const secret = modPow(u8ArrayToBigInt(pkey), this._privateKeyBigInt, this._pBigInt);
return bigIntToU8Array(secret, len).slice(0, len);
}
}

View File

@ -0,0 +1,82 @@
/*
* 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 an array of bytes, returns an array of bytes
*/
export async function MD5(d) {
let s = "";
for (let i = 0; i < d.length; i++) {
s += String.fromCharCode(d[i]);
}
return M(V(Y(X(s), 8 * s.length)));
}
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;
}

View File

@ -0,0 +1,132 @@
import Base64 from "../base64.js";
import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
export class RSACipher {
constructor() {
this._keyLength = 0;
this._keyBytes = 0;
this._n = null;
this._e = null;
this._d = null;
this._nBigInt = null;
this._eBigInt = null;
this._dBigInt = null;
this._extractable = false;
}
get algorithm() {
return { name: "RSA-PKCS1-v1_5" };
}
_base64urlDecode(data) {
data = data.replace(/-/g, "+").replace(/_/g, "/");
data = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
return Base64.decode(data);
}
_padArray(arr, length) {
const res = new Uint8Array(length);
res.set(arr, length - arr.length);
return res;
}
static async generateKey(algorithm, extractable, _keyUsages) {
const cipher = new RSACipher;
await cipher._generateKey(algorithm, extractable);
return { privateKey: cipher };
}
async _generateKey(algorithm, extractable) {
this._keyLength = algorithm.modulusLength;
this._keyBytes = Math.ceil(this._keyLength / 8);
const key = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: algorithm.modulusLength,
publicExponent: algorithm.publicExponent,
hash: {name: "SHA-256"},
},
true, ["encrypt", "decrypt"]);
const privateKey = await window.crypto.subtle.exportKey("jwk", key.privateKey);
this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
this._nBigInt = u8ArrayToBigInt(this._n);
this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
this._eBigInt = u8ArrayToBigInt(this._e);
this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
this._dBigInt = u8ArrayToBigInt(this._d);
this._extractable = extractable;
}
static async importKey(key, _algorithm, extractable, keyUsages) {
if (keyUsages.length !== 1 || keyUsages[0] !== "encrypt") {
throw new Error("only support importing RSA public key");
}
const cipher = new RSACipher;
await cipher._importKey(key, extractable);
return cipher;
}
async _importKey(key, extractable) {
const n = key.n;
const e = key.e;
if (n.length !== e.length) {
throw new Error("the sizes of modulus and public exponent do not match");
}
this._keyBytes = n.length;
this._keyLength = this._keyBytes * 8;
this._n = new Uint8Array(this._keyBytes);
this._e = new Uint8Array(this._keyBytes);
this._n.set(n);
this._e.set(e);
this._nBigInt = u8ArrayToBigInt(this._n);
this._eBigInt = u8ArrayToBigInt(this._e);
this._extractable = extractable;
}
async encrypt(_algorithm, 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 = u8ArrayToBigInt(em);
const c = modPow(emBigInt, this._eBigInt, this._nBigInt);
return bigIntToU8Array(c, this._keyBytes);
}
async decrypt(_algorithm, message) {
if (message.length !== this._keyBytes) {
return null;
}
const msgBigInt = u8ArrayToBigInt(message);
const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt);
const em = 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);
}
async exportKey() {
if (!this._extractable) {
throw new Error("key is not extractable");
}
return { n: this._n, e: this._e, d: this._d };
}
}

View File

@ -31,10 +31,7 @@ export default class HextileDecoder {
return false; return false;
} }
let rQ = sock.rQ; let subencoding = sock.rQpeek8();
let rQi = sock.rQi;
let subencoding = rQ[rQi]; // Peek
if (subencoding > 30) { // Raw if (subencoding > 30) { // Raw
throw new Error("Illegal hextile subencoding (subencoding: " + throw new Error("Illegal hextile subencoding (subencoding: " +
subencoding + ")"); subencoding + ")");
@ -65,7 +62,7 @@ export default class HextileDecoder {
return false; return false;
} }
let subrects = rQ[rQi + bytes - 1]; // Peek let subrects = sock.rQpeekBytes(bytes).at(-1);
if (subencoding & 0x10) { // SubrectsColoured if (subencoding & 0x10) { // SubrectsColoured
bytes += subrects * (4 + 2); bytes += subrects * (4 + 2);
} else { } else {
@ -79,7 +76,7 @@ export default class HextileDecoder {
} }
// We know the encoding and have a whole tile // We know the encoding and have a whole tile
rQi++; sock.rQshift8();
if (subencoding === 0) { if (subencoding === 0) {
if (this._lastsubencoding & 0x01) { if (this._lastsubencoding & 0x01) {
// Weird: ignore blanks are RAW // Weird: ignore blanks are RAW
@ -89,42 +86,36 @@ export default class HextileDecoder {
} }
} else if (subencoding & 0x01) { // Raw } else if (subencoding & 0x01) { // Raw
let pixels = tw * th; let pixels = tw * th;
let data = sock.rQshiftBytes(pixels * 4, false);
// Max sure the image is fully opaque // Max sure the image is fully opaque
for (let i = 0;i < pixels;i++) { for (let i = 0;i < pixels;i++) {
rQ[rQi + i * 4 + 3] = 255; data[i * 4 + 3] = 255;
} }
display.blitImage(tx, ty, tw, th, rQ, rQi); display.blitImage(tx, ty, tw, th, data, 0);
rQi += bytes - 1;
} else { } else {
if (subencoding & 0x02) { // Background if (subencoding & 0x02) { // Background
this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; this._background = new Uint8Array(sock.rQshiftBytes(4));
rQi += 4;
} }
if (subencoding & 0x04) { // Foreground if (subencoding & 0x04) { // Foreground
this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; this._foreground = new Uint8Array(sock.rQshiftBytes(4));
rQi += 4;
} }
this._startTile(tx, ty, tw, th, this._background); this._startTile(tx, ty, tw, th, this._background);
if (subencoding & 0x08) { // AnySubrects if (subencoding & 0x08) { // AnySubrects
let subrects = rQ[rQi]; let subrects = sock.rQshift8();
rQi++;
for (let s = 0; s < subrects; s++) { for (let s = 0; s < subrects; s++) {
let color; let color;
if (subencoding & 0x10) { // SubrectsColoured if (subencoding & 0x10) { // SubrectsColoured
color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; color = sock.rQshiftBytes(4);
rQi += 4;
} else { } else {
color = this._foreground; color = this._foreground;
} }
const xy = rQ[rQi]; const xy = sock.rQshift8();
rQi++;
const sx = (xy >> 4); const sx = (xy >> 4);
const sy = (xy & 0x0f); const sy = (xy & 0x0f);
const wh = rQ[rQi]; const wh = sock.rQshift8();
rQi++;
const sw = (wh >> 4) + 1; const sw = (wh >> 4) + 1;
const sh = (wh & 0x0f) + 1; const sh = (wh & 0x0f) + 1;
@ -133,7 +124,6 @@ export default class HextileDecoder {
} }
this._finishTile(display); this._finishTile(display);
} }
sock.rQi = rQi;
this._lastsubencoding = subencoding; this._lastsubencoding = subencoding;
this._tiles--; this._tiles--;
} }

View File

@ -11,131 +11,136 @@ export default class JPEGDecoder {
constructor() { constructor() {
// RealVNC will reuse the quantization tables // RealVNC will reuse the quantization tables
// and Huffman tables, so we need to cache them. // and Huffman tables, so we need to cache them.
this._quantTables = [];
this._huffmanTables = [];
this._cachedQuantTables = []; this._cachedQuantTables = [];
this._cachedHuffmanTables = []; this._cachedHuffmanTables = [];
this._jpegLength = 0;
this._segments = []; this._segments = [];
} }
decodeRect(x, y, width, height, sock, display, depth) { decodeRect(x, y, width, height, sock, display, depth) {
// A rect of JPEG encodings is simply a JPEG file // 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) { while (true) {
let j = i; let segment = this._readSegment(sock);
if (j + 2 > bufferLength) { if (segment === null) {
return false; 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); this._segments.push(segment);
i = j; // End of image?
if (segment[1] === 0xD9) {
break;
}
} }
let huffmanTables = [];
let quantTables = [];
for (let segment of this._segments) {
let type = segment[1];
if (type === 0xC4) {
// Huffman tables
huffmanTables.push(segment);
} else if (type === 0xDB) {
// Quantization tables
quantTables.push(segment);
}
}
const sofIndex = this._segments.findIndex(
x => x[1] == 0xC0 || x[1] == 0xC2
);
if (sofIndex == -1) {
throw new Error("Illegal JPEG image without SOF");
}
if (quantTables.length === 0) {
this._segments.splice(sofIndex+1, 0,
...this._cachedQuantTables);
}
if (huffmanTables.length === 0) {
this._segments.splice(sofIndex+1, 0,
...this._cachedHuffmanTables);
}
let length = 0;
for (let segment of this._segments) {
length += segment.length;
}
let data = new Uint8Array(length);
length = 0;
for (let segment of this._segments) {
data.set(segment, length);
length += segment.length;
}
display.imageRect(x, y, width, height, "image/jpeg", data);
if (huffmanTables.length !== 0) {
this._cachedHuffmanTables = huffmanTables;
}
if (quantTables.length !== 0) {
this._cachedQuantTables = quantTables;
}
this._segments = [];
return true;
}
_readSegment(sock) {
if (sock.rQwait("JPEG", 2)) {
return null;
}
let marker = sock.rQshift8();
if (marker != 0xFF) {
throw new Error("Illegal JPEG marker received (byte: " +
marker + ")");
}
let type = sock.rQshift8();
if (type >= 0xD0 && type <= 0xD9 || type == 0x01) {
// No length after marker
return new Uint8Array([marker, type]);
}
if (sock.rQwait("JPEG", 2, 2)) {
return null;
}
let length = sock.rQshift16();
if (length < 2) {
throw new Error("Illegal JPEG length received (length: " +
length + ")");
}
if (sock.rQwait("JPEG", length-2, 4)) {
return null;
}
let extra = 0;
if (type === 0xDA) {
// start of scan
extra += 2;
while (true) {
if (sock.rQwait("JPEG", length-2+extra, 4)) {
return null;
}
let data = sock.rQpeekBytes(length-2+extra, false);
if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 &&
!(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) {
extra -= 2;
break;
}
extra++;
}
}
let segment = new Uint8Array(2 + length + extra);
segment[0] = marker;
segment[1] = type;
segment[2] = length >> 8;
segment[3] = length;
segment.set(sock.rQshiftBytes(length-2+extra, false), 4);
return segment;
} }
} }

View File

@ -24,41 +24,34 @@ export default class RawDecoder {
const pixelSize = depth == 8 ? 1 : 4; const pixelSize = depth == 8 ? 1 : 4;
const bytesPerLine = width * pixelSize; const bytesPerLine = width * pixelSize;
if (sock.rQwait("RAW", bytesPerLine)) { while (this._lines > 0) {
return false; 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 const curY = y + (height - this._lines);
for (let i = 0; i < pixels; i++) {
data[index + i * 4 + 3] = 255;
}
display.blitImage(x, curY, width, currHeight, data, index); let data = sock.rQshiftBytes(bytesPerLine, false);
sock.rQskipBytes(currHeight * bytesPerLine);
this._lines -= currHeight; // Convert data if needed
if (this._lines > 0) { if (depth == 8) {
return false; const newdata = new Uint8Array(width * 4);
for (let i = 0; i < width; i++) {
newdata[i * 4 + 0] = ((data[i] >> 0) & 0x3) * 255 / 3;
newdata[i * 4 + 1] = ((data[i] >> 2) & 0x3) * 255 / 3;
newdata[i * 4 + 2] = ((data[i] >> 4) & 0x3) * 255 / 3;
newdata[i * 4 + 3] = 255;
}
data = newdata;
}
// Max sure the image is fully opaque
for (let i = 0; i < width; i++) {
data[i * 4 + 3] = 255;
}
display.blitImage(x, curY, width, 1, data, 0);
this._lines--;
} }
return true; return true;

View File

@ -76,12 +76,8 @@ export default class TightDecoder {
return false; return false;
} }
const rQi = sock.rQi; let pixel = sock.rQshiftBytes(3);
const rQ = sock.rQ; display.fillRect(x, y, width, height, pixel, false);
display.fillRect(x, y, width, height,
[rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false);
sock.rQskipBytes(3);
return true; return true;
} }
@ -289,7 +285,73 @@ export default class TightDecoder {
} }
_gradientFilter(streamId, x, y, width, height, sock, display, depth) { _gradientFilter(streamId, x, y, width, height, sock, display, depth) {
throw new Error("Gradient filter not implemented"); // assume the TPIXEL is 3 bytes long
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(4 * width * height);
let rgbxIndex = 0, dataIndex = 0;
let left = new Uint8Array(3);
for (let x = 0; x < width; x++) {
for (let c = 0; c < 3; c++) {
const prediction = left[c];
const value = data[dataIndex++] + prediction;
rgbx[rgbxIndex++] = value;
left[c] = value;
}
rgbx[rgbxIndex++] = 255;
}
let upperIndex = 0;
let upper = new Uint8Array(3),
upperleft = new Uint8Array(3);
for (let y = 1; y < height; y++) {
left.fill(0);
upperleft.fill(0);
for (let x = 0; x < width; x++) {
for (let c = 0; c < 3; c++) {
upper[c] = rgbx[upperIndex++];
let prediction = left[c] + upper[c] - upperleft[c];
if (prediction < 0) {
prediction = 0;
} else if (prediction > 255) {
prediction = 255;
}
const value = data[dataIndex++] + prediction;
rgbx[rgbxIndex++] = value;
upperleft[c] = upper[c];
left[c] = value;
}
rgbx[rgbxIndex++] = 255;
upperIndex++;
}
}
display.blitImage(x, y, width, height, rgbx, 0, false);
return true;
} }
_readData(sock) { _readData(sock) {
@ -316,7 +378,7 @@ export default class TightDecoder {
return null; return null;
} }
let data = sock.rQshiftBytes(this._len); let data = sock.rQshiftBytes(this._len, false);
this._len = 0; this._len = 0;
return data; return data;

View File

@ -32,7 +32,7 @@ export default class ZRLEDecoder {
return false; return false;
} }
const data = sock.rQshiftBytes(this._length); const data = sock.rQshiftBytes(this._length, false);
this._inflator.setInput(data); this._inflator.setInput(data);

View File

@ -7,7 +7,7 @@
*/ */
import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js"; import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
import { Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js"; import { Z_FULL_FLUSH, Z_DEFAULT_COMPRESSION } from "../vendor/pako/lib/zlib/deflate.js";
import ZStream from "../vendor/pako/lib/zlib/zstream.js"; import ZStream from "../vendor/pako/lib/zlib/zstream.js";
export default class Deflator { export default class Deflator {
@ -15,9 +15,8 @@ export default class Deflator {
this.strm = new ZStream(); this.strm = new ZStream();
this.chunkSize = 1024 * 10 * 10; this.chunkSize = 1024 * 10 * 10;
this.outputBuffer = new Uint8Array(this.chunkSize); this.outputBuffer = new Uint8Array(this.chunkSize);
this.windowBits = 5;
deflateInit(this.strm, this.windowBits); deflateInit(this.strm, Z_DEFAULT_COMPRESSION);
} }
deflate(inData) { deflate(inData) {

View File

@ -15,7 +15,7 @@ export default class Display {
this._drawCtx = null; this._drawCtx = null;
this._renderQ = []; // queue drawing actions for in-oder rendering this._renderQ = []; // queue drawing actions for in-oder rendering
this._flushing = false; this._flushPromise = null;
// the full frame buffer (logical canvas) size // the full frame buffer (logical canvas) size
this._fbWidth = 0; this._fbWidth = 0;
@ -61,10 +61,6 @@ export default class Display {
this._scale = 1.0; this._scale = 1.0;
this._clipViewport = false; this._clipViewport = false;
// ===== EVENT HANDLERS =====
this.onflush = () => {}; // A flush request has finished
} }
// ===== PROPERTIES ===== // ===== PROPERTIES =====
@ -306,9 +302,14 @@ export default class Display {
flush() { flush() {
if (this._renderQ.length === 0) { if (this._renderQ.length === 0) {
this.onflush(); return Promise.resolve();
} else { } else {
this._flushing = true; if (this._flushPromise === null) {
this._flushPromise = new Promise((resolve) => {
this._flushResolve = resolve;
});
}
return this._flushPromise;
} }
} }
@ -517,9 +518,11 @@ export default class Display {
} }
} }
if (this._renderQ.length === 0 && this._flushing) { if (this._renderQ.length === 0 &&
this._flushing = false; this._flushPromise !== null) {
this.onflush(); this._flushResolve();
this._flushPromise = null;
this._flushResolve = null;
} }
} }
} }

View File

@ -22,6 +22,7 @@ export const encodings = {
pseudoEncodingLastRect: -224, pseudoEncodingLastRect: -224,
pseudoEncodingCursor: -239, pseudoEncodingCursor: -239,
pseudoEncodingQEMUExtendedKeyEvent: -258, pseudoEncodingQEMUExtendedKeyEvent: -258,
pseudoEncodingQEMULedEvent: -261,
pseudoEncodingDesktopName: -307, pseudoEncodingDesktopName: -307,
pseudoEncodingExtendedDesktopSize: -308, pseudoEncodingExtendedDesktopSize: -308,
pseudoEncodingXvp: -309, pseudoEncodingXvp: -309,

View File

@ -14,9 +14,8 @@ export default class Inflate {
this.strm = new ZStream(); this.strm = new ZStream();
this.chunkSize = 1024 * 10 * 10; this.chunkSize = 1024 * 10 * 10;
this.strm.output = new Uint8Array(this.chunkSize); this.strm.output = new Uint8Array(this.chunkSize);
this.windowBits = 5;
inflateInit(this.strm, this.windowBits); inflateInit(this.strm);
} }
setInput(data) { setInput(data) {

View File

@ -36,7 +36,7 @@ export default class Keyboard {
// ===== PRIVATE METHODS ===== // ===== PRIVATE METHODS =====
_sendKeyEvent(keysym, code, down) { _sendKeyEvent(keysym, code, down, numlock = null, capslock = null) {
if (down) { if (down) {
this._keyDownList[code] = keysym; this._keyDownList[code] = keysym;
} else { } else {
@ -48,8 +48,9 @@ export default class Keyboard {
} }
Log.Debug("onkeyevent " + (down ? "down" : "up") + Log.Debug("onkeyevent " + (down ? "down" : "up") +
", keysym: " + keysym, ", code: " + code); ", keysym: " + keysym, ", code: " + code +
this.onkeyevent(keysym, code, down); ", numlock: " + numlock + ", capslock: " + capslock);
this.onkeyevent(keysym, code, down, numlock, capslock);
} }
_getKeyCode(e) { _getKeyCode(e) {
@ -86,6 +87,14 @@ export default class Keyboard {
_handleKeyDown(e) { _handleKeyDown(e) {
const code = this._getKeyCode(e); const code = this._getKeyCode(e);
let keysym = KeyboardUtil.getKeysym(e); let keysym = KeyboardUtil.getKeysym(e);
let numlock = e.getModifierState('NumLock');
let capslock = e.getModifierState('CapsLock');
// getModifierState for NumLock is not supported on mac and ios and always returns false.
// Set to null to indicate unknown/unsupported instead.
if (browser.isMac() || browser.isIOS()) {
numlock = null;
}
// Windows doesn't have a proper AltGr, but handles it using // Windows doesn't have a proper AltGr, but handles it using
// fake Ctrl+Alt. However the remote end might not be Windows, // fake Ctrl+Alt. However the remote end might not be Windows,
@ -107,7 +116,7 @@ export default class Keyboard {
// key to "AltGraph". // key to "AltGraph".
keysym = KeyTable.XK_ISO_Level3_Shift; keysym = KeyTable.XK_ISO_Level3_Shift;
} else { } else {
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true, numlock, capslock);
} }
} }
@ -118,8 +127,8 @@ export default class Keyboard {
// If it's a virtual keyboard then it should be // If it's a virtual keyboard then it should be
// sufficient to just send press and release right // sufficient to just send press and release right
// after each other // after each other
this._sendKeyEvent(keysym, code, true); this._sendKeyEvent(keysym, code, true, numlock, capslock);
this._sendKeyEvent(keysym, code, false); this._sendKeyEvent(keysym, code, false, numlock, capslock);
} }
stopEvent(e); stopEvent(e);
@ -157,8 +166,8 @@ export default class Keyboard {
// while meta is held down // while meta is held down
if ((browser.isMac() || browser.isIOS()) && if ((browser.isMac() || browser.isIOS()) &&
(e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) { (e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) {
this._sendKeyEvent(keysym, code, true); this._sendKeyEvent(keysym, code, true, numlock, capslock);
this._sendKeyEvent(keysym, code, false); this._sendKeyEvent(keysym, code, false, numlock, capslock);
stopEvent(e); stopEvent(e);
return; return;
} }
@ -168,8 +177,8 @@ export default class Keyboard {
// which toggles on each press, but not on release. So pretend // which toggles on each press, but not on release. So pretend
// it was a quick press and release of the button. // it was a quick press and release of the button.
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock);
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock);
stopEvent(e); stopEvent(e);
return; return;
} }
@ -182,8 +191,8 @@ export default class Keyboard {
KeyTable.XK_Hiragana, KeyTable.XK_Hiragana,
KeyTable.XK_Romaji ]; KeyTable.XK_Romaji ];
if (browser.isWindows() && jpBadKeys.includes(keysym)) { if (browser.isWindows() && jpBadKeys.includes(keysym)) {
this._sendKeyEvent(keysym, code, true); this._sendKeyEvent(keysym, code, true, numlock, capslock);
this._sendKeyEvent(keysym, code, false); this._sendKeyEvent(keysym, code, false, numlock, capslock);
stopEvent(e); stopEvent(e);
return; return;
} }
@ -199,7 +208,7 @@ export default class Keyboard {
return; return;
} }
this._sendKeyEvent(keysym, code, true); this._sendKeyEvent(keysym, code, true, numlock, capslock);
} }
_handleKeyUp(e) { _handleKeyUp(e) {

View File

@ -67,7 +67,7 @@ export function getKeycode(evt) {
// Get 'KeyboardEvent.key', handling legacy browsers // Get 'KeyboardEvent.key', handling legacy browsers
export function getKey(evt) { export function getKey(evt) {
// Are we getting a proper key value? // Are we getting a proper key value?
if (evt.key !== undefined) { if ((evt.key !== undefined) && (evt.key !== 'Unidentified')) {
// Mozilla isn't fully in sync with the spec yet // Mozilla isn't fully in sync with the spec yet
switch (evt.key) { switch (evt.key) {
case 'OS': return 'Meta'; case 'OS': return 'Meta';

View File

@ -1,146 +1,25 @@
import Base64 from './base64.js';
import { encodeUTF8 } from './util/strings.js'; import { encodeUTF8 } from './util/strings.js';
import EventTargetMixin from './util/eventtarget.js'; import EventTargetMixin from './util/eventtarget.js';
import legacyCrypto from './crypto/crypto.js';
export class AESEAXCipher { class RA2Cipher {
constructor() { constructor() {
this._rawKey = null; this._cipher = 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); this._counter = new Uint8Array(16);
} }
async setKey(key) { async setKey(key) {
await this._cipher.setKey(key); this._cipher = await legacyCrypto.importKey(
"raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]);
} }
async makeMessage(message) { async makeMessage(message) {
const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]); const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
const encrypted = await this._cipher.encrypt(message, ad, this._counter); const encrypted = await legacyCrypto.encrypt({
name: "AES-EAX",
iv: this._counter,
additionalData: ad,
}, this._cipher, message);
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
const res = new Uint8Array(message.length + 2 + 16); const res = new Uint8Array(message.length + 2 + 16);
res.set(ad); res.set(ad);
@ -148,164 +27,18 @@ export class RA2Cipher {
return res; return res;
} }
async receiveMessage(length, encrypted, mac) { async receiveMessage(length, encrypted) {
const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]); const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac); const res = await legacyCrypto.decrypt({
name: "AES-EAX",
iv: this._counter,
additionalData: ad,
}, this._cipher, encrypted);
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
return res; 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 { export default class RSAAESAuthenticationState extends EventTargetMixin {
constructor(sock, getCredentials) { constructor(sock, getCredentials) {
super(); super();
@ -406,7 +139,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
this._hasStarted = true; this._hasStarted = true;
// 1: Receive server public key // 1: Receive server public key
await this._waitSockAsync(4); await this._waitSockAsync(4);
const serverKeyLengthBuffer = this._sock.rQslice(0, 4); const serverKeyLengthBuffer = this._sock.rQpeekBytes(4);
const serverKeyLength = this._sock.rQshift32(); const serverKeyLength = this._sock.rQshift32();
if (serverKeyLength < 1024) { if (serverKeyLength < 1024) {
throw new Error("RA2: server public key is too short: " + serverKeyLength); throw new Error("RA2: server public key is too short: " + serverKeyLength);
@ -417,26 +150,31 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
await this._waitSockAsync(serverKeyBytes * 2); await this._waitSockAsync(serverKeyBytes * 2);
const serverN = this._sock.rQshiftBytes(serverKeyBytes); const serverN = this._sock.rQshiftBytes(serverKeyBytes);
const serverE = this._sock.rQshiftBytes(serverKeyBytes); const serverE = this._sock.rQshiftBytes(serverKeyBytes);
const serverRSACipher = new RSACipher(serverKeyLength); const serverRSACipher = await legacyCrypto.importKey(
serverRSACipher.setPublicKey(serverN, serverE); "raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]);
const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2); const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
serverPublickey.set(serverKeyLengthBuffer); serverPublickey.set(serverKeyLengthBuffer);
serverPublickey.set(serverN, 4); serverPublickey.set(serverN, 4);
serverPublickey.set(serverE, 4 + serverKeyBytes); serverPublickey.set(serverE, 4 + serverKeyBytes);
// verify server public key // verify server public key
let approveKey = this._waitApproveKeyAsync();
this.dispatchEvent(new CustomEvent("serververification", { this.dispatchEvent(new CustomEvent("serververification", {
detail: { type: "RSA", publickey: serverPublickey } detail: { type: "RSA", publickey: serverPublickey }
})); }));
await this._waitApproveKeyAsync(); await approveKey;
// 2: Send client public key // 2: Send client public key
const clientKeyLength = 2048; const clientKeyLength = 2048;
const clientKeyBytes = Math.ceil(clientKeyLength / 8); const clientKeyBytes = Math.ceil(clientKeyLength / 8);
const clientRSACipher = new RSACipher(clientKeyLength); const clientRSACipher = (await legacyCrypto.generateKey({
await clientRSACipher.generateKey(); name: "RSA-PKCS1-v1_5",
const clientN = clientRSACipher.n; modulusLength: clientKeyLength,
const clientE = clientRSACipher.e; publicExponent: new Uint8Array([1, 0, 1]),
}, true, ["encrypt"])).privateKey;
const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher);
const clientN = clientExportedRSAKey.n;
const clientE = clientExportedRSAKey.e;
const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2); const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24; clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16; clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
@ -444,17 +182,20 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
clientPublicKey[3] = clientKeyLength & 0xff; clientPublicKey[3] = clientKeyLength & 0xff;
clientPublicKey.set(clientN, 4); clientPublicKey.set(clientN, 4);
clientPublicKey.set(clientE, 4 + clientKeyBytes); clientPublicKey.set(clientE, 4 + clientKeyBytes);
this._sock.send(clientPublicKey); this._sock.sQpushBytes(clientPublicKey);
this._sock.flush();
// 3: Send client random // 3: Send client random
const clientRandom = new Uint8Array(16); const clientRandom = new Uint8Array(16);
window.crypto.getRandomValues(clientRandom); window.crypto.getRandomValues(clientRandom);
const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom); const clientEncryptedRandom = await legacyCrypto.encrypt(
{ name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom);
const clientRandomMessage = new Uint8Array(2 + serverKeyBytes); const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8; clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
clientRandomMessage[1] = serverKeyBytes & 0xff; clientRandomMessage[1] = serverKeyBytes & 0xff;
clientRandomMessage.set(clientEncryptedRandom, 2); clientRandomMessage.set(clientEncryptedRandom, 2);
this._sock.send(clientRandomMessage); this._sock.sQpushBytes(clientRandomMessage);
this._sock.flush();
// 4: Receive server random // 4: Receive server random
await this._waitSockAsync(2); await this._waitSockAsync(2);
@ -462,7 +203,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
throw new Error("RA2: wrong encrypted message length"); throw new Error("RA2: wrong encrypted message length");
} }
const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes); const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom); const serverRandom = await legacyCrypto.decrypt(
{ name: "RSA-PKCS1-v1_5" }, clientRSACipher, serverEncryptedRandom);
if (serverRandom === null || serverRandom.length !== 16) { if (serverRandom === null || serverRandom.length !== 16) {
throw new Error("RA2: corrupted server encrypted random"); throw new Error("RA2: corrupted server encrypted random");
} }
@ -494,13 +236,14 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
clientHash = await window.crypto.subtle.digest("SHA-1", clientHash); clientHash = await window.crypto.subtle.digest("SHA-1", clientHash);
serverHash = new Uint8Array(serverHash); serverHash = new Uint8Array(serverHash);
clientHash = new Uint8Array(clientHash); clientHash = new Uint8Array(clientHash);
this._sock.send(await clientCipher.makeMessage(clientHash)); this._sock.sQpushBytes(await clientCipher.makeMessage(clientHash));
this._sock.flush();
await this._waitSockAsync(2 + 20 + 16); await this._waitSockAsync(2 + 20 + 16);
if (this._sock.rQshift16() !== 20) { if (this._sock.rQshift16() !== 20) {
throw new Error("RA2: wrong server hash"); throw new Error("RA2: wrong server hash");
} }
const serverHashReceived = await serverCipher.receiveMessage( const serverHashReceived = await serverCipher.receiveMessage(
20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16)); 20, this._sock.rQshiftBytes(20 + 16));
if (serverHashReceived === null) { if (serverHashReceived === null) {
throw new Error("RA2: failed to authenticate the message"); throw new Error("RA2: failed to authenticate the message");
} }
@ -516,11 +259,12 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
throw new Error("RA2: wrong subtype"); throw new Error("RA2: wrong subtype");
} }
let subtype = (await serverCipher.receiveMessage( let subtype = (await serverCipher.receiveMessage(
1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16))); 1, this._sock.rQshiftBytes(1 + 16)));
if (subtype === null) { if (subtype === null) {
throw new Error("RA2: failed to authenticate the message"); throw new Error("RA2: failed to authenticate the message");
} }
subtype = subtype[0]; subtype = subtype[0];
let waitCredentials = this._waitCredentialsAsync(subtype);
if (subtype === 1) { if (subtype === 1) {
if (this._getCredentials().username === undefined || if (this._getCredentials().username === undefined ||
this._getCredentials().password === undefined) { this._getCredentials().password === undefined) {
@ -537,7 +281,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
} else { } else {
throw new Error("RA2: wrong subtype"); throw new Error("RA2: wrong subtype");
} }
await this._waitCredentialsAsync(subtype); await waitCredentials;
let username; let username;
if (subtype === 1) { if (subtype === 1) {
username = encodeUTF8(this._getCredentials().username).slice(0, 255); username = encodeUTF8(this._getCredentials().username).slice(0, 255);
@ -554,7 +298,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
for (let i = 0; i < password.length; i++) { for (let i = 0; i < password.length; i++) {
credentials[username.length + 2 + i] = password.charCodeAt(i); credentials[username.length + 2 + i] = password.charCodeAt(i);
} }
this._sock.send(await clientCipher.makeMessage(credentials)); this._sock.sQpushBytes(await clientCipher.makeMessage(credentials));
this._sock.flush();
} }
get hasStarted() { get hasStarted() {
@ -564,4 +309,4 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
set hasStarted(s) { set hasStarted(s) {
this._hasStarted = s; this._hasStarted = s;
} }
} }

View File

@ -21,12 +21,11 @@ import Keyboard from "./input/keyboard.js";
import GestureHandler from "./input/gesturehandler.js"; import GestureHandler from "./input/gesturehandler.js";
import Cursor from "./util/cursor.js"; import Cursor from "./util/cursor.js";
import Websock from "./websock.js"; import Websock from "./websock.js";
import DES from "./des.js";
import KeyTable from "./input/keysym.js"; import KeyTable from "./input/keysym.js";
import XtScancode from "./input/xtscancodes.js"; import XtScancode from "./input/xtscancodes.js";
import { encodings } from "./encodings.js"; import { encodings } from "./encodings.js";
import RSAAESAuthenticationState from "./ra2.js"; import RSAAESAuthenticationState from "./ra2.js";
import { MD5 } from "./util/md5.js"; import legacyCrypto from "./crypto/crypto.js";
import RawDecoder from "./decoders/raw.js"; import RawDecoder from "./decoders/raw.js";
import CopyRectDecoder from "./decoders/copyrect.js"; import CopyRectDecoder from "./decoders/copyrect.js";
@ -258,10 +257,11 @@ export default class RFB extends EventTargetMixin {
Log.Error("Display exception: " + exc); Log.Error("Display exception: " + exc);
throw exc; throw exc;
} }
this._display.onflush = this._onFlush.bind(this);
this._keyboard = new Keyboard(this._canvas); this._keyboard = new Keyboard(this._canvas);
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
this._remoteCapsLock = null; // Null indicates unknown or irrelevant
this._remoteNumLock = null;
this._gestures = new GestureHandler(); this._gestures = new GestureHandler();
@ -960,7 +960,7 @@ export default class RFB extends EventTargetMixin {
} }
_handleMessage() { _handleMessage() {
if (this._sock.rQlen === 0) { if (this._sock.rQwait("message", 1)) {
Log.Warn("handleMessage called on an empty receive queue"); Log.Warn("handleMessage called on an empty receive queue");
return; return;
} }
@ -977,7 +977,7 @@ export default class RFB extends EventTargetMixin {
if (!this._normalMsg()) { if (!this._normalMsg()) {
break; break;
} }
if (this._sock.rQlen === 0) { if (this._sock.rQwait("message", 1)) {
break; break;
} }
} }
@ -995,7 +995,35 @@ export default class RFB extends EventTargetMixin {
} }
} }
_handleKeyEvent(keysym, code, down) { _handleKeyEvent(keysym, code, down, numlock, capslock) {
// If remote state of capslock is known, and it doesn't match the local led state of
// the keyboard, we send a capslock keypress first to bring it into sync.
// If we just pressed CapsLock, or we toggled it remotely due to it being out of sync
// we clear the remote state so that we don't send duplicate or spurious fixes,
// since it may take some time to receive the new remote CapsLock state.
if (code == 'CapsLock' && down) {
this._remoteCapsLock = null;
}
if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) {
Log.Debug("Fixing remote caps lock");
this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true);
this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false);
// We clear the remote capsLock state when we do this to prevent issues with doing this twice
// before we receive an update of the the remote state.
this._remoteCapsLock = null;
}
// Logic for numlock is exactly the same.
if (code == 'NumLock' && down) {
this._remoteNumLock = null;
}
if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) {
Log.Debug("Fixing remote num lock");
this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true);
this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false);
this._remoteNumLock = null;
}
this.sendKey(keysym, code, down); this.sendKey(keysym, code, down);
} }
@ -1383,7 +1411,8 @@ export default class RFB extends EventTargetMixin {
while (repeaterID.length < 250) { while (repeaterID.length < 250) {
repeaterID += "\0"; repeaterID += "\0";
} }
this._sock.sendString(repeaterID); this._sock.sQpushString(repeaterID);
this._sock.flush();
return true; return true;
} }
@ -1393,7 +1422,8 @@ export default class RFB extends EventTargetMixin {
const cversion = "00" + parseInt(this._rfbVersion, 10) + const cversion = "00" + parseInt(this._rfbVersion, 10) +
".00" + ((this._rfbVersion * 10) % 10); ".00" + ((this._rfbVersion * 10) % 10);
this._sock.sendString("RFB " + cversion + "\n"); this._sock.sQpushString("RFB " + cversion + "\n");
this._sock.flush();
Log.Debug('Sent ProtocolVersion: ' + cversion); Log.Debug('Sent ProtocolVersion: ' + cversion);
this._rfbInitState = 'Security'; this._rfbInitState = 'Security';
@ -1445,7 +1475,8 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unsupported security types (types: " + types + ")"); return this._fail("Unsupported security types (types: " + types + ")");
} }
this._sock.send([this._rfbAuthScheme]); this._sock.sQpush8(this._rfbAuthScheme);
this._sock.flush();
} else { } else {
// Server decides // Server decides
if (this._sock.rQwait("security scheme", 4)) { return false; } if (this._sock.rQwait("security scheme", 4)) { return false; }
@ -1507,12 +1538,15 @@ export default class RFB extends EventTargetMixin {
return false; return false;
} }
const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) + this._sock.sQpush8(this._rfbCredentials.username.length);
String.fromCharCode(this._rfbCredentials.target.length) + this._sock.sQpush8(this._rfbCredentials.target.length);
this._rfbCredentials.username + this._sock.sQpushString(this._rfbCredentials.username);
this._rfbCredentials.target; this._sock.sQpushString(this._rfbCredentials.target);
this._sock.sendString(xvpAuthStr);
this._sock.flush();
this._rfbAuthScheme = securityTypeVNCAuth; this._rfbAuthScheme = securityTypeVNCAuth;
return this._negotiateAuthentication(); return this._negotiateAuthentication();
} }
@ -1530,7 +1564,9 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unsupported VeNCrypt version " + major + "." + minor); return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
} }
this._sock.send([0, 2]); this._sock.sQpush8(0);
this._sock.sQpush8(2);
this._sock.flush();
this._rfbVeNCryptState = 1; this._rfbVeNCryptState = 1;
} }
@ -1589,12 +1625,10 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unsupported security types (types: " + subtypes + ")"); return this._fail("Unsupported security types (types: " + subtypes + ")");
} }
this._sock.send([this._rfbAuthScheme >> 24, this._sock.sQpush32(this._rfbAuthScheme);
this._rfbAuthScheme >> 16, this._sock.flush();
this._rfbAuthScheme >> 8,
this._rfbAuthScheme]);
this._rfbVeNCryptState == 4; this._rfbVeNCryptState = 4;
return true; return true;
} }
} }
@ -1611,20 +1645,11 @@ export default class RFB extends EventTargetMixin {
const user = encodeUTF8(this._rfbCredentials.username); const user = encodeUTF8(this._rfbCredentials.username);
const pass = encodeUTF8(this._rfbCredentials.password); const pass = encodeUTF8(this._rfbCredentials.password);
this._sock.send([ this._sock.sQpush32(user.length);
(user.length >> 24) & 0xFF, this._sock.sQpush32(pass.length);
(user.length >> 16) & 0xFF, this._sock.sQpushString(user);
(user.length >> 8) & 0xFF, this._sock.sQpushString(pass);
user.length & 0xFF this._sock.flush();
]);
this._sock.send([
(pass.length >> 24) & 0xFF,
(pass.length >> 16) & 0xFF,
(pass.length >> 8) & 0xFF,
pass.length & 0xFF
]);
this._sock.sendString(user);
this._sock.sendString(pass);
this._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
return true; return true;
@ -1643,7 +1668,8 @@ export default class RFB extends EventTargetMixin {
// TODO(directxman12): make genDES not require an Array // TODO(directxman12): make genDES not require an Array
const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
const response = RFB.genDES(this._rfbCredentials.password, challenge); const response = RFB.genDES(this._rfbCredentials.password, challenge);
this._sock.send(response); this._sock.sQpushBytes(response);
this._sock.flush();
this._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
return true; return true;
} }
@ -1661,8 +1687,9 @@ export default class RFB extends EventTargetMixin {
if (this._rfbCredentials.ardPublicKey != undefined && if (this._rfbCredentials.ardPublicKey != undefined &&
this._rfbCredentials.ardCredentials != undefined) { this._rfbCredentials.ardCredentials != undefined) {
// if the async web crypto is done return the results // if the async web crypto is done return the results
this._sock.send(this._rfbCredentials.ardCredentials); this._sock.sQpushBytes(this._rfbCredentials.ardCredentials);
this._sock.send(this._rfbCredentials.ardPublicKey); this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey);
this._sock.flush();
this._rfbCredentials.ardCredentials = null; this._rfbCredentials.ardCredentials = null;
this._rfbCredentials.ardPublicKey = null; this._rfbCredentials.ardPublicKey = null;
this._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
@ -1681,77 +1708,35 @@ export default class RFB extends EventTargetMixin {
let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength)); let clientKey = legacyCrypto.generateKey(
let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), byte => String.fromCharCode(65+byte%26)).join(''); { name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);
this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding);
return false; return false;
} }
_modPow(base, exponent, modulus) { async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {
const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
const sharedKey = legacyCrypto.deriveBits(
{ name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);
let baseHex = "0x"+Array.from(base, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
let exponentHex = "0x"+Array.from(exponent, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
let modulusHex = "0x"+Array.from(modulus, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
let b = BigInt(baseHex); const credentials = window.crypto.getRandomValues(new Uint8Array(128));
let e = BigInt(exponentHex); for (let i = 0; i < username.length; i++) {
let m = BigInt(modulusHex); credentials[i] = username.charCodeAt(i);
let r = 1n;
b = b % m;
while (e > 0) {
if (e % 2n === 1n) {
r = (r * b) % m;
}
e = e / 2n;
b = (b * b) % m;
} }
let hexResult = r.toString(16); credentials[username.length] = 0;
for (let i = 0; i < password.length; i++) {
while (hexResult.length/2<exponent.length || (hexResult.length%2 != 0)) { credentials[64 + i] = password.charCodeAt(i);
hexResult = "0"+hexResult;
} }
credentials[64 + password.length] = 0;
let bytesResult = []; const key = await legacyCrypto.digest("MD5", sharedKey);
for (let c = 0; c < hexResult.length; c += 2) { const cipher = await legacyCrypto.importKey(
bytesResult.push(parseInt(hexResult.substr(c, 2), 16)); "raw", key, { name: "AES-ECB" }, false, ["encrypt"]);
} const encrypted = await legacyCrypto.encrypt({ name: "AES-ECB" }, cipher, credentials);
return bytesResult;
}
async _aesEcbEncrypt(string, key) {
// perform AES-ECB blocks
let keyString = Array.from(key, byte => String.fromCharCode(byte)).join('');
let aesKey = await window.crypto.subtle.importKey("raw", MD5(keyString), {name: "AES-CBC"}, false, ["encrypt"]);
let data = new Uint8Array(string.length);
for (let i = 0; i < string.length; ++i) {
data[i] = string.charCodeAt(i);
}
let encrypted = new Uint8Array(data.length);
for (let i=0;i<data.length;i+=16) {
let block = data.slice(i, i+16);
let encryptedBlock = await window.crypto.subtle.encrypt({name: "AES-CBC", iv: block},
aesKey, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
);
encrypted.set((new Uint8Array(encryptedBlock)).slice(0, 16), i);
}
return encrypted;
}
async _negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding) {
// calculate the DH keys
let clientPublicKey = this._modPow(generator, clientPrivateKey, prime);
let sharedKey = this._modPow(serverPublicKey, clientPrivateKey, prime);
let username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
let password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
let paddedUsername = username + '\0' + padding.substring(0, 63);
let paddedPassword = password + '\0' + padding.substring(0, 63);
let credentials = paddedUsername.substring(0, 64) + paddedPassword.substring(0, 64);
let encrypted = await this._aesEcbEncrypt(credentials, sharedKey);
this._rfbCredentials.ardCredentials = encrypted; this._rfbCredentials.ardCredentials = encrypted;
this._rfbCredentials.ardPublicKey = clientPublicKey; this._rfbCredentials.ardPublicKey = clientPublicKey;
@ -1768,10 +1753,12 @@ export default class RFB extends EventTargetMixin {
return false; return false;
} }
this._sock.send([0, 0, 0, this._rfbCredentials.username.length]); this._sock.sQpush32(this._rfbCredentials.username.length);
this._sock.send([0, 0, 0, this._rfbCredentials.password.length]); this._sock.sQpush32(this._rfbCredentials.password.length);
this._sock.sendString(this._rfbCredentials.username); this._sock.sQpushString(this._rfbCredentials.username);
this._sock.sendString(this._rfbCredentials.password); this._sock.sQpushString(this._rfbCredentials.password);
this._sock.flush();
this._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
return true; return true;
} }
@ -1809,7 +1796,8 @@ export default class RFB extends EventTargetMixin {
"vendor or signature"); "vendor or signature");
} }
Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]); Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
this._sock.send([0, 0, 0, 0]); // use NOTUNNEL this._sock.sQpush32(0); // use NOTUNNEL
this._sock.flush();
return false; // wait until we receive the sub auth count to continue return false; // wait until we receive the sub auth count to continue
} else { } else {
return this._fail("Server wanted tunnels, but doesn't support " + return this._fail("Server wanted tunnels, but doesn't support " +
@ -1859,7 +1847,8 @@ export default class RFB extends EventTargetMixin {
for (let authType in clientSupportedTypes) { for (let authType in clientSupportedTypes) {
if (serverSupportedTypes.indexOf(authType) != -1) { if (serverSupportedTypes.indexOf(authType) != -1) {
this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); this._sock.sQpush32(clientSupportedTypes[authType]);
this._sock.flush();
Log.Debug("Selected authentication type: " + authType); Log.Debug("Selected authentication type: " + authType);
switch (authType) { switch (authType) {
@ -1905,8 +1894,8 @@ export default class RFB extends EventTargetMixin {
if (e.message !== "disconnect normally") { if (e.message !== "disconnect normally") {
this._fail(e.message); this._fail(e.message);
} }
}).then(() => { })
this.dispatchEvent(new CustomEvent('securityresult')); .then(() => {
this._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
return true; return true;
}).finally(() => { }).finally(() => {
@ -1934,15 +1923,15 @@ export default class RFB extends EventTargetMixin {
const g = this._sock.rQshiftBytes(8); const g = this._sock.rQshiftBytes(8);
const p = this._sock.rQshiftBytes(8); const p = this._sock.rQshiftBytes(8);
const A = this._sock.rQshiftBytes(8); const A = this._sock.rQshiftBytes(8);
const b = window.crypto.getRandomValues(new Uint8Array(8)); const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]);
const B = new Uint8Array(this._modPow(g, b, p)); const B = legacyCrypto.exportKey("raw", dhKey.publicKey);
const secret = new Uint8Array(this._modPow(A, b, p)); const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64);
const des = new DES(secret); const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]);
const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255); const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63); const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
const usernameBytes = new Uint8Array(256); let usernameBytes = new Uint8Array(256);
const passwordBytes = new Uint8Array(64); let passwordBytes = new Uint8Array(64);
window.crypto.getRandomValues(usernameBytes); window.crypto.getRandomValues(usernameBytes);
window.crypto.getRandomValues(passwordBytes); window.crypto.getRandomValues(passwordBytes);
for (let i = 0; i < username.length; i++) { for (let i = 0; i < username.length; i++) {
@ -1953,25 +1942,12 @@ export default class RFB extends EventTargetMixin {
passwordBytes[i] = password.charCodeAt(i); passwordBytes[i] = password.charCodeAt(i);
} }
passwordBytes[password.length] = 0; passwordBytes[password.length] = 0;
let x = new Uint8Array(secret); usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes);
for (let i = 0; i < 32; i++) { passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes);
for (let j = 0; j < 8; j++) { this._sock.sQpushBytes(B);
x[j] ^= usernameBytes[i * 8 + j]; this._sock.sQpushBytes(usernameBytes);
} this._sock.sQpushBytes(passwordBytes);
x = des.enc8(x); this._sock.flush();
usernameBytes.set(x, i * 8);
}
x = new Uint8Array(secret);
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
x[j] ^= passwordBytes[i * 8 + j];
}
x = des.enc8(x);
passwordBytes.set(x, i * 8);
}
this._sock.send(B);
this._sock.send(usernameBytes);
this._sock.send(passwordBytes);
this._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
return true; return true;
} }
@ -1979,7 +1955,11 @@ export default class RFB extends EventTargetMixin {
_negotiateAuthentication() { _negotiateAuthentication() {
switch (this._rfbAuthScheme) { switch (this._rfbAuthScheme) {
case securityTypeNone: case securityTypeNone:
this._rfbInitState = 'SecurityResult'; if (this._rfbVersion >= 3.8) {
this._rfbInitState = 'SecurityResult';
} else {
this._rfbInitState = 'ClientInitialisation';
}
return true; return true;
case securityTypeXVP: case securityTypeXVP:
@ -2016,13 +1996,6 @@ export default class RFB extends EventTargetMixin {
} }
_handleSecurityResult() { _handleSecurityResult() {
// There is no security choice, and hence no security result
// until RFB 3.7
if (this._rfbVersion < 3.7) {
this._rfbInitState = 'ClientInitialisation';
return true;
}
if (this._sock.rQwait('VNC auth response ', 4)) { return false; } if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
const status = this._sock.rQshift32(); const status = this._sock.rQshift32();
@ -2158,6 +2131,7 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.pseudoEncodingDesktopSize); encs.push(encodings.pseudoEncodingDesktopSize);
encs.push(encodings.pseudoEncodingLastRect); encs.push(encodings.pseudoEncodingLastRect);
encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent); encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
encs.push(encodings.pseudoEncodingQEMULedEvent);
encs.push(encodings.pseudoEncodingExtendedDesktopSize); encs.push(encodings.pseudoEncodingExtendedDesktopSize);
encs.push(encodings.pseudoEncodingXvp); encs.push(encodings.pseudoEncodingXvp);
encs.push(encodings.pseudoEncodingFence); encs.push(encodings.pseudoEncodingFence);
@ -2199,7 +2173,8 @@ export default class RFB extends EventTargetMixin {
return this._handleSecurityReason(); return this._handleSecurityReason();
case 'ClientInitialisation': case 'ClientInitialisation':
this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation
this._sock.flush();
this._rfbInitState = 'ServerInitialisation'; this._rfbInitState = 'ServerInitialisation';
return true; return true;
@ -2381,7 +2356,7 @@ export default class RFB extends EventTargetMixin {
textData = textData.slice(0, -1); textData = textData.slice(0, -1);
} }
textData = textData.replace("\r\n", "\n"); textData = textData.replaceAll("\r\n", "\n");
this.dispatchEvent(new CustomEvent( this.dispatchEvent(new CustomEvent(
"clipboard", "clipboard",
@ -2512,19 +2487,11 @@ export default class RFB extends EventTargetMixin {
default: default:
this._fail("Unexpected server message (type " + msgType + ")"); this._fail("Unexpected server message (type " + msgType + ")");
Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); Log.Debug("sock.rQpeekBytes(30): " + this._sock.rQpeekBytes(30));
return true; return true;
} }
} }
_onFlush() {
this._flushing = false;
// Resume processing
if (this._sock.rQlen > 0) {
this._handleMessage();
}
}
_framebufferUpdate() { _framebufferUpdate() {
if (this._FBU.rects === 0) { if (this._FBU.rects === 0) {
if (this._sock.rQwait("FBU header", 3, 1)) { return false; } if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
@ -2535,7 +2502,14 @@ export default class RFB extends EventTargetMixin {
// to avoid building up an excessive queue // to avoid building up an excessive queue
if (this._display.pending()) { if (this._display.pending()) {
this._flushing = true; this._flushing = true;
this._display.flush(); this._display.flush()
.then(() => {
this._flushing = false;
// Resume processing
if (!this._sock.rQwait("message", 1)) {
this._handleMessage();
}
});
return false; return false;
} }
} }
@ -2545,13 +2519,13 @@ export default class RFB extends EventTargetMixin {
if (this._sock.rQwait("rect header", 12)) { return false; } if (this._sock.rQwait("rect header", 12)) { return false; }
/* New FramebufferUpdate */ /* New FramebufferUpdate */
const hdr = this._sock.rQshiftBytes(12); this._FBU.x = this._sock.rQshift16();
this._FBU.x = (hdr[0] << 8) + hdr[1]; this._FBU.y = this._sock.rQshift16();
this._FBU.y = (hdr[2] << 8) + hdr[3]; this._FBU.width = this._sock.rQshift16();
this._FBU.width = (hdr[4] << 8) + hdr[5]; this._FBU.height = this._sock.rQshift16();
this._FBU.height = (hdr[6] << 8) + hdr[7]; this._FBU.encoding = this._sock.rQshift32();
this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + /* Encodings are signed */
(hdr[10] << 8) + hdr[11], 10); this._FBU.encoding >>= 0;
} }
if (!this._handleRect()) { if (!this._handleRect()) {
@ -2593,6 +2567,9 @@ export default class RFB extends EventTargetMixin {
case encodings.pseudoEncodingExtendedDesktopSize: case encodings.pseudoEncodingExtendedDesktopSize:
return this._handleExtendedDesktopSize(); return this._handleExtendedDesktopSize();
case encodings.pseudoEncodingQEMULedEvent:
return this._handleLedEvent();
default: default:
return this._handleDataRect(); return this._handleDataRect();
} }
@ -2770,6 +2747,21 @@ export default class RFB extends EventTargetMixin {
return true; return true;
} }
_handleLedEvent() {
if (this._sock.rQwait("LED Status", 1)) {
return false;
}
let data = this._sock.rQshift8();
// ScrollLock state can be retrieved with data & 1. This is currently not needed.
let numLock = data & 2 ? true : false;
let capsLock = data & 4 ? true : false;
this._remoteCapsLock = capsLock;
this._remoteNumLock = numLock;
return true;
}
_handleExtendedDesktopSize() { _handleExtendedDesktopSize() {
if (this._sock.rQwait("ExtendedDesktopSize", 4)) { if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
return false; return false;
@ -2785,26 +2777,18 @@ export default class RFB extends EventTargetMixin {
const firstUpdate = !this._supportsSetDesktopSize; const firstUpdate = !this._supportsSetDesktopSize;
this._supportsSetDesktopSize = true; this._supportsSetDesktopSize = true;
// Normally we only apply the current resize mode after a
// window resize event. However there is no such trigger on the
// initial connect. And we don't know if the server supports
// resizing until we've gotten here.
if (firstUpdate) {
this._requestRemoteResize();
}
this._sock.rQskipBytes(1); // number-of-screens this._sock.rQskipBytes(1); // number-of-screens
this._sock.rQskipBytes(3); // padding this._sock.rQskipBytes(3); // padding
for (let i = 0; i < numberOfScreens; i += 1) { for (let i = 0; i < numberOfScreens; i += 1) {
// Save the id and flags of the first screen // Save the id and flags of the first screen
if (i === 0) { if (i === 0) {
this._screenID = this._sock.rQshiftBytes(4); // id this._screenID = this._sock.rQshift32(); // id
this._sock.rQskipBytes(2); // x-position this._sock.rQskipBytes(2); // x-position
this._sock.rQskipBytes(2); // y-position this._sock.rQskipBytes(2); // y-position
this._sock.rQskipBytes(2); // width this._sock.rQskipBytes(2); // width
this._sock.rQskipBytes(2); // height this._sock.rQskipBytes(2); // height
this._screenFlags = this._sock.rQshiftBytes(4); // flags this._screenFlags = this._sock.rQshift32(); // flags
} else { } else {
this._sock.rQskipBytes(16); this._sock.rQskipBytes(16);
} }
@ -2842,6 +2826,14 @@ export default class RFB extends EventTargetMixin {
this._resize(this._FBU.width, this._FBU.height); this._resize(this._FBU.width, this._FBU.height);
} }
// Normally we only apply the current resize mode after a
// window resize event. However there is no such trigger on the
// initial connect. And we don't know if the server supports
// resizing until we've gotten here.
if (firstUpdate) {
this._requestRemoteResize();
}
return true; return true;
} }
@ -2937,28 +2929,22 @@ export default class RFB extends EventTargetMixin {
static genDES(password, challenge) { static genDES(password, challenge) {
const passwordChars = password.split('').map(c => c.charCodeAt(0)); const passwordChars = password.split('').map(c => c.charCodeAt(0));
return (new DES(passwordChars)).encrypt(challenge); const key = legacyCrypto.importKey(
"raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]);
return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge);
} }
} }
// Class Methods // Class Methods
RFB.messages = { RFB.messages = {
keyEvent(sock, keysym, down) { keyEvent(sock, keysym, down) {
const buff = sock._sQ; sock.sQpush8(4); // msg-type
const offset = sock._sQlen; sock.sQpush8(down);
buff[offset] = 4; // msg-type sock.sQpush16(0);
buff[offset + 1] = down;
buff[offset + 2] = 0; sock.sQpush32(keysym);
buff[offset + 3] = 0;
buff[offset + 4] = (keysym >> 24);
buff[offset + 5] = (keysym >> 16);
buff[offset + 6] = (keysym >> 8);
buff[offset + 7] = keysym;
sock._sQlen += 8;
sock.flush(); sock.flush();
}, },
@ -2972,46 +2958,28 @@ RFB.messages = {
return xtScanCode; return xtScanCode;
} }
const buff = sock._sQ; sock.sQpush8(255); // msg-type
const offset = sock._sQlen; sock.sQpush8(0); // sub msg-type
buff[offset] = 255; // msg-type sock.sQpush16(down);
buff[offset + 1] = 0; // sub msg-type
buff[offset + 2] = (down >> 8); sock.sQpush32(keysym);
buff[offset + 3] = down;
buff[offset + 4] = (keysym >> 24);
buff[offset + 5] = (keysym >> 16);
buff[offset + 6] = (keysym >> 8);
buff[offset + 7] = keysym;
const RFBkeycode = getRFBkeycode(keycode); const RFBkeycode = getRFBkeycode(keycode);
buff[offset + 8] = (RFBkeycode >> 24); sock.sQpush32(RFBkeycode);
buff[offset + 9] = (RFBkeycode >> 16);
buff[offset + 10] = (RFBkeycode >> 8);
buff[offset + 11] = RFBkeycode;
sock._sQlen += 12;
sock.flush(); sock.flush();
}, },
pointerEvent(sock, x, y, mask) { pointerEvent(sock, x, y, mask) {
const buff = sock._sQ; sock.sQpush8(5); // msg-type
const offset = sock._sQlen;
buff[offset] = 5; // msg-type sock.sQpush8(mask);
buff[offset + 1] = mask; sock.sQpush16(x);
sock.sQpush16(y);
buff[offset + 2] = x >> 8;
buff[offset + 3] = x;
buff[offset + 4] = y >> 8;
buff[offset + 5] = y;
sock._sQlen += 6;
sock.flush(); sock.flush();
}, },
@ -3111,14 +3079,11 @@ RFB.messages = {
}, },
clientCutText(sock, data, extended = false) { clientCutText(sock, data, extended = false) {
const buff = sock._sQ; sock.sQpush8(6); // msg-type
const offset = sock._sQlen;
buff[offset] = 6; // msg-type sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
buff[offset + 1] = 0; // padding sock.sQpush8(0); // padding
buff[offset + 2] = 0; // padding
buff[offset + 3] = 0; // padding
let length; let length;
if (extended) { if (extended) {
@ -3127,121 +3092,63 @@ RFB.messages = {
length = data.length; length = data.length;
} }
buff[offset + 4] = length >> 24; sock.sQpush32(length);
buff[offset + 5] = length >> 16; sock.sQpushBytes(data);
buff[offset + 6] = length >> 8; sock.flush();
buff[offset + 7] = length;
sock._sQlen += 8;
// We have to keep track of from where in the data we begin creating the
// buffer for the flush in the next iteration.
let dataOffset = 0;
let remaining = data.length;
while (remaining > 0) {
let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
for (let i = 0; i < flushSize; i++) {
buff[sock._sQlen + i] = data[dataOffset + i];
}
sock._sQlen += flushSize;
sock.flush();
remaining -= flushSize;
dataOffset += flushSize;
}
}, },
setDesktopSize(sock, width, height, id, flags) { setDesktopSize(sock, width, height, id, flags) {
const buff = sock._sQ; sock.sQpush8(251); // msg-type
const offset = sock._sQlen;
buff[offset] = 251; // msg-type sock.sQpush8(0); // padding
buff[offset + 1] = 0; // padding
buff[offset + 2] = width >> 8; // width
buff[offset + 3] = width;
buff[offset + 4] = height >> 8; // height
buff[offset + 5] = height;
buff[offset + 6] = 1; // number-of-screens sock.sQpush16(width);
buff[offset + 7] = 0; // padding sock.sQpush16(height);
sock.sQpush8(1); // number-of-screens
sock.sQpush8(0); // padding
// screen array // screen array
buff[offset + 8] = id >> 24; // id sock.sQpush32(id);
buff[offset + 9] = id >> 16; sock.sQpush16(0); // x-position
buff[offset + 10] = id >> 8; sock.sQpush16(0); // y-position
buff[offset + 11] = id; sock.sQpush16(width);
buff[offset + 12] = 0; // x-position sock.sQpush16(height);
buff[offset + 13] = 0; sock.sQpush32(flags);
buff[offset + 14] = 0; // y-position
buff[offset + 15] = 0;
buff[offset + 16] = width >> 8; // width
buff[offset + 17] = width;
buff[offset + 18] = height >> 8; // height
buff[offset + 19] = height;
buff[offset + 20] = flags >> 24; // flags
buff[offset + 21] = flags >> 16;
buff[offset + 22] = flags >> 8;
buff[offset + 23] = flags;
sock._sQlen += 24;
sock.flush(); sock.flush();
}, },
clientFence(sock, flags, payload) { clientFence(sock, flags, payload) {
const buff = sock._sQ; sock.sQpush8(248); // msg-type
const offset = sock._sQlen;
buff[offset] = 248; // msg-type sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
buff[offset + 1] = 0; // padding sock.sQpush32(flags);
buff[offset + 2] = 0; // padding
buff[offset + 3] = 0; // padding
buff[offset + 4] = flags >> 24; // flags sock.sQpush8(payload.length);
buff[offset + 5] = flags >> 16; sock.sQpushString(payload);
buff[offset + 6] = flags >> 8;
buff[offset + 7] = flags;
const n = payload.length;
buff[offset + 8] = n; // length
for (let i = 0; i < n; i++) {
buff[offset + 9 + i] = payload.charCodeAt(i);
}
sock._sQlen += 9 + n;
sock.flush(); sock.flush();
}, },
enableContinuousUpdates(sock, enable, x, y, width, height) { enableContinuousUpdates(sock, enable, x, y, width, height) {
const buff = sock._sQ; sock.sQpush8(150); // msg-type
const offset = sock._sQlen;
buff[offset] = 150; // msg-type sock.sQpush8(enable);
buff[offset + 1] = enable; // enable-flag
buff[offset + 2] = x >> 8; // x sock.sQpush16(x);
buff[offset + 3] = x; sock.sQpush16(y);
buff[offset + 4] = y >> 8; // y sock.sQpush16(width);
buff[offset + 5] = y; sock.sQpush16(height);
buff[offset + 6] = width >> 8; // width
buff[offset + 7] = width;
buff[offset + 8] = height >> 8; // height
buff[offset + 9] = height;
sock._sQlen += 10;
sock.flush(); sock.flush();
}, },
pixelFormat(sock, depth, trueColor) { pixelFormat(sock, depth, trueColor) {
const buff = sock._sQ;
const offset = sock._sQlen;
let bpp; let bpp;
if (depth > 16) { if (depth > 16) {
@ -3254,100 +3161,69 @@ RFB.messages = {
const bits = Math.floor(depth/3); const bits = Math.floor(depth/3);
buff[offset] = 0; // msg-type sock.sQpush8(0); // msg-type
buff[offset + 1] = 0; // padding sock.sQpush8(0); // padding
buff[offset + 2] = 0; // padding sock.sQpush8(0); // padding
buff[offset + 3] = 0; // padding sock.sQpush8(0); // padding
buff[offset + 4] = bpp; // bits-per-pixel sock.sQpush8(bpp);
buff[offset + 5] = depth; // depth sock.sQpush8(depth);
buff[offset + 6] = 0; // little-endian sock.sQpush8(0); // little-endian
buff[offset + 7] = trueColor ? 1 : 0; // true-color sock.sQpush8(trueColor ? 1 : 0);
buff[offset + 8] = 0; // red-max sock.sQpush16((1 << bits) - 1); // red-max
buff[offset + 9] = (1 << bits) - 1; // red-max sock.sQpush16((1 << bits) - 1); // green-max
sock.sQpush16((1 << bits) - 1); // blue-max
buff[offset + 10] = 0; // green-max sock.sQpush8(bits * 0); // red-shift
buff[offset + 11] = (1 << bits) - 1; // green-max sock.sQpush8(bits * 1); // green-shift
sock.sQpush8(bits * 2); // blue-shift
buff[offset + 12] = 0; // blue-max sock.sQpush8(0); // padding
buff[offset + 13] = (1 << bits) - 1; // blue-max sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
buff[offset + 14] = bits * 0; // red-shift
buff[offset + 15] = bits * 1; // green-shift
buff[offset + 16] = bits * 2; // blue-shift
buff[offset + 17] = 0; // padding
buff[offset + 18] = 0; // padding
buff[offset + 19] = 0; // padding
sock._sQlen += 20;
sock.flush(); sock.flush();
}, },
clientEncodings(sock, encodings) { clientEncodings(sock, encodings) {
const buff = sock._sQ; sock.sQpush8(2); // msg-type
const offset = sock._sQlen;
buff[offset] = 2; // msg-type sock.sQpush8(0); // padding
buff[offset + 1] = 0; // padding
buff[offset + 2] = encodings.length >> 8; sock.sQpush16(encodings.length);
buff[offset + 3] = encodings.length;
let j = offset + 4;
for (let i = 0; i < encodings.length; i++) { for (let i = 0; i < encodings.length; i++) {
const enc = encodings[i]; sock.sQpush32(encodings[i]);
buff[j] = enc >> 24;
buff[j + 1] = enc >> 16;
buff[j + 2] = enc >> 8;
buff[j + 3] = enc;
j += 4;
} }
sock._sQlen += j - offset;
sock.flush(); sock.flush();
}, },
fbUpdateRequest(sock, incremental, x, y, w, h) { fbUpdateRequest(sock, incremental, x, y, w, h) {
const buff = sock._sQ;
const offset = sock._sQlen;
if (typeof(x) === "undefined") { x = 0; } if (typeof(x) === "undefined") { x = 0; }
if (typeof(y) === "undefined") { y = 0; } if (typeof(y) === "undefined") { y = 0; }
buff[offset] = 3; // msg-type sock.sQpush8(3); // msg-type
buff[offset + 1] = incremental ? 1 : 0;
buff[offset + 2] = (x >> 8) & 0xFF; sock.sQpush8(incremental ? 1 : 0);
buff[offset + 3] = x & 0xFF;
buff[offset + 4] = (y >> 8) & 0xFF; sock.sQpush16(x);
buff[offset + 5] = y & 0xFF; sock.sQpush16(y);
sock.sQpush16(w);
sock.sQpush16(h);
buff[offset + 6] = (w >> 8) & 0xFF;
buff[offset + 7] = w & 0xFF;
buff[offset + 8] = (h >> 8) & 0xFF;
buff[offset + 9] = h & 0xFF;
sock._sQlen += 10;
sock.flush(); sock.flush();
}, },
xvpOp(sock, ver, op) { xvpOp(sock, ver, op) {
const buff = sock._sQ; sock.sQpush8(250); // msg-type
const offset = sock._sQlen;
buff[offset] = 250; // msg-type sock.sQpush8(0); // padding
buff[offset + 1] = 0; // padding
buff[offset + 2] = ver; sock.sQpush8(ver);
buff[offset + 3] = op; sock.sQpush8(op);
sock._sQlen += 4;
sock.flush(); sock.flush();
} }
}; };

View File

@ -69,7 +69,9 @@ export default class Cursor {
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options); this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options); this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
document.body.removeChild(this._canvas); if (document.contains(this._canvas)) {
document.body.removeChild(this._canvas);
}
} }
this._target = null; this._target = null;

View File

@ -94,27 +94,7 @@ export default class Websock {
return "unknown"; 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 // Receive Queue
get rQlen() {
return this._rQlen - this._rQi;
}
rQpeek8() { rQpeek8() {
return this._rQ[this._rQi]; return this._rQ[this._rQi];
} }
@ -141,42 +121,47 @@ export default class Websock {
for (let byte = bytes - 1; byte >= 0; byte--) { for (let byte = bytes - 1; byte >= 0; byte--) {
res += this._rQ[this._rQi++] << (byte * 8); res += this._rQ[this._rQi++] << (byte * 8);
} }
return res; return res >>> 0;
} }
rQshiftStr(len) { rQshiftStr(len) {
if (typeof(len) === 'undefined') { len = this.rQlen; }
let str = ""; let str = "";
// Handle large arrays in steps to avoid long strings on the stack // Handle large arrays in steps to avoid long strings on the stack
for (let i = 0; i < len; i += 4096) { for (let i = 0; i < len; i += 4096) {
let part = this.rQshiftBytes(Math.min(4096, len - i)); let part = this.rQshiftBytes(Math.min(4096, len - i), false);
str += String.fromCharCode.apply(null, part); str += String.fromCharCode.apply(null, part);
} }
return str; return str;
} }
rQshiftBytes(len) { rQshiftBytes(len, copy=true) {
if (typeof(len) === 'undefined') { len = this.rQlen; }
this._rQi += len; this._rQi += len;
return new Uint8Array(this._rQ.buffer, this._rQi - len, len); if (copy) {
return this._rQ.slice(this._rQi - len, this._rQi);
} else {
return this._rQ.subarray(this._rQi - len, this._rQi);
}
} }
rQshiftTo(target, 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 // 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)); target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
this._rQi += len; this._rQi += len;
} }
rQslice(start, end = this.rQlen) { rQpeekBytes(len, copy=true) {
return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); if (copy) {
return this._rQ.slice(this._rQi, this._rQi + len);
} else {
return this._rQ.subarray(this._rQi, this._rQi + len);
}
} }
// Check to see if we must wait for 'num' bytes (default to FBU.bytes) // 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 // to be available in the receive queue. Return true if we need to
// wait (and possibly print a debug message), otherwise false. // wait (and possibly print a debug message), otherwise false.
rQwait(msg, num, goback) { rQwait(msg, num, goback) {
if (this.rQlen < num) { if (this._rQlen - this._rQi < num) {
if (goback) { if (goback) {
if (this._rQi < goback) { if (this._rQi < goback) {
throw new Error("rQwait cannot backup " + goback + " bytes"); throw new Error("rQwait cannot backup " + goback + " bytes");
@ -190,21 +175,56 @@ export default class Websock {
// Send Queue // Send Queue
sQpush8(num) {
this._sQensureSpace(1);
this._sQ[this._sQlen++] = num;
}
sQpush16(num) {
this._sQensureSpace(2);
this._sQ[this._sQlen++] = (num >> 8) & 0xff;
this._sQ[this._sQlen++] = (num >> 0) & 0xff;
}
sQpush32(num) {
this._sQensureSpace(4);
this._sQ[this._sQlen++] = (num >> 24) & 0xff;
this._sQ[this._sQlen++] = (num >> 16) & 0xff;
this._sQ[this._sQlen++] = (num >> 8) & 0xff;
this._sQ[this._sQlen++] = (num >> 0) & 0xff;
}
sQpushString(str) {
let bytes = str.split('').map(chr => chr.charCodeAt(0));
this.sQpushBytes(new Uint8Array(bytes));
}
sQpushBytes(bytes) {
for (let offset = 0;offset < bytes.length;) {
this._sQensureSpace(1);
let chunkSize = this._sQbufferSize - this._sQlen;
if (chunkSize > bytes.length - offset) {
chunkSize = bytes.length - offset;
}
this._sQ.set(bytes.subarray(offset, offset + chunkSize), this._sQlen);
this._sQlen += chunkSize;
offset += chunkSize;
}
}
flush() { flush() {
if (this._sQlen > 0 && this.readyState === 'open') { if (this._sQlen > 0 && this.readyState === 'open') {
this._websocket.send(this._encodeMessage()); this._websocket.send(new Uint8Array(this._sQ.buffer, 0, this._sQlen));
this._sQlen = 0; this._sQlen = 0;
} }
} }
send(arr) { _sQensureSpace(bytes) {
this._sQ.set(arr, this._sQlen); if (this._sQbufferSize - this._sQlen < bytes) {
this._sQlen += arr.length; this.flush();
this.flush(); }
}
sendString(str) {
this.send(str.split('').map(chr => chr.charCodeAt(0)));
} }
// Event Handlers // Event Handlers
@ -283,17 +303,12 @@ export default class Websock {
} }
// private methods // 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, // We want to move all the unread data to the start of the queue,
// e.g. compacting. // e.g. compacting.
// The function also expands the receive que if needed, and for // The function also expands the receive que if needed, and for
// performance reasons we combine these two actions to avoid // performance reasons we combine these two actions to avoid
// unneccessary copying. // unnecessary copying.
_expandCompactRQ(minFit) { _expandCompactRQ(minFit) {
// if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place // if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
// instead of resizing // instead of resizing
@ -309,7 +324,7 @@ export default class Websock {
// we don't want to grow unboundedly // we don't want to grow unboundedly
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
this._rQbufferSize = MAX_RQ_GROW_SIZE; this._rQbufferSize = MAX_RQ_GROW_SIZE;
if (this._rQbufferSize - this.rQlen < minFit) { if (this._rQbufferSize - (this._rQlen - this._rQi) < minFit) {
throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
} }
} }
@ -327,25 +342,22 @@ export default class Websock {
} }
// push arraybuffer values onto the end of the receive que // push arraybuffer values onto the end of the receive que
_DecodeMessage(data) { _recvMessage(e) {
const u8 = new Uint8Array(data); 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;
}
const u8 = new Uint8Array(e.data);
if (u8.length > this._rQbufferSize - this._rQlen) { if (u8.length > this._rQbufferSize - this._rQlen) {
this._expandCompactRQ(u8.length); this._expandCompactRQ(u8.length);
} }
this._rQ.set(u8, this._rQlen); this._rQ.set(u8, this._rQlen);
this._rQlen += u8.length; this._rQlen += u8.length;
}
_recvMessage(e) { if (this._rQlen - this._rQi > 0) {
this._DecodeMessage(e.data);
if (this.rQlen > 0) {
this._eventHandlers.message(); 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 { } else {
Log.Debug("Ignoring empty message"); Log.Debug("Ignoring empty message");
} }

View File

@ -0,0 +1 @@
{ "version": "1.5.0" }