/* * This file is part of Xpra. * Copyright (C) 2016 Antoine Martin * Copyright (c) 2016 Spikes, Inc. * Licensed under MPL 2.0, see: * http://www.mozilla.org/MPL/2.0/ * */ const Utilities = { VERSION : "19", REVISION : 0, LOCAL_MODIFICATIONS: 0, BRANCH: "v16.x", exc: console_error_safe, error: console_error_safe, warn: console_warn_safe, log: console_log_safe, debug: console_debug_safe, //these versions should not be redirected: cexc: console_error_safe, cerror: console_error_safe, cwarn: console_warn_safe, clog: console_log_safe, cdebug: console_debug_safe, stristrue(v, default_value) { if (!v) { return default_value; } return ["true", "on", "1", "yes", "enabled"].includes( String(v).toLowerCase() ); }, removeChars(validChars, inputString) { const regex = new RegExp('[^' + validChars + ']', 'g'); return inputString.replace(regex, ''); }, getHexUUID() { const s = []; const hexDigits = "0123456789abcdef"; for (let index = 0; index < 36; index++) { if (index === 8 || index === 13 || index === 18 || index === 23) { s[index] = "-"; } else { s[index] = hexDigits.slice( Math.floor(Math.random() * 0x10), Math.floor(Math.random() * 0x10) + 1 ); } } return s.join(""); }, getSecureRandomBytes(len) { const u = new Uint8Array(len); crypto.getRandomValues(u); return u; }, getSecureRandomString(len) { const u = Utilities.getSecureRandomBytes(len); return String.fromCharCode.apply(null, u); }, xorString(string1, string2) { if (string1.length !== string2.length) { throw "strings must be equal length"; } let result = ""; for (let index = 0; index < string1.length; index++) { const character1 = string1[index]; const character2 = string2[index]; result += String.fromCharCode(character1.charCodeAt(0) ^ character2.charCodeAt(0)); } return result; }, u8(value) { const type = typeof value; if (type === 'object' && value.constructor === Uint8Array) { return value; } if (type === "string") { return Uint8Array.from(value.split("").map(x => x.charCodeAt())); } return new Uint8Array(value); }, xor(str1, str2) { const trimmed_str2 = str2.slice(0, str1.length); return Utilities.xorString(str1, trimmed_str2); }, gendigest(digest, password, salt) { // Utilities.clog("gendigest(", digest, ", ", password, ", ", salt, ")"); // Utilities.clog("gendigest(", digest, ", ", Utilities.convertToHex(password), ", ", Utilities.convertToHex(salt), ")"); if (digest === "xor") { const trimmed_salt = salt.slice(0, password.length); // Utilities.debug("xoring with trimmed salt:", Utilities.convertToHex(trimmed_salt)); return new Promise(function(resolve, reject) { const xored = Utilities.xorString(trimmed_salt, password); resolve(xored); }); } if (!digest.startsWith("hmac")) { return new Promise(function(resolve, reject) { reject(new Error("unsupported digest '" + digest + "'")); }); } let hash = "SHA-1"; if (digest.indexOf("+") > 0) { // "hmac+sha512" -> "sha512" hash = digest.split("+")[1]; } hash = hash.toUpperCase(); if (hash.startsWith("SHA") && !hash.startsWith("SHA-")) { hash = "SHA-" + hash.substring(3); } if (typeof crypto.subtle === "undefined") { // use hmac.js return new Promise(function(resolve, reject) { if (hash !== "SHA-256") { reject(new Error("crypto.subtle API is not available in this context")); } else { value = hmac(Utilities.u8(password), Utilities.u8(salt)); resolve(value); } }); } return new Promise(function(resolve, reject) { Utilities.clog("crypto.subtle=", crypto.subtle); Utilities.clog("crypto.subtle.importKey=", crypto.subtle.importKey); crypto.subtle.importKey("raw", Utilities.u8(password), { name: "HMAC", hash: hash }, false, ["sign", "verify"]) .then( (key) => { Utilities.cdebug("imported hmac key: ", key); const u8salt = Utilities.u8(salt); crypto.subtle.sign("HMAC", key, u8salt).then( (result) => { const u8digest = new Uint8Array(result); Utilities.clog("hmac result=", u8digest); resolve(Utilities.ArrayBufferToString(u8digest)); }, (error) => { Utilities.clog("hmac signing failed:", error); reject(error); } ); }, (error) => { Utilities.clog("hmac key import failed:", error); reject(error); } ); }); }, trimString(string_, trimLength) { if (!string_) { return ""; } return string_.length > trimLength ? `${string_.slice(0, Math.max(0, trimLength - 3))}...` : string_; }, convertToHex(value) { if (value instanceof Uint8Array) { return Utilities.arrayhex(value); } let hex = ""; for (let index = 0; index < value.length; index++) { hex += `${value.charCodeAt(index).toString(16).padStart(2, "0")}`; } return hex; }, hexToString(hexval) { const hex = hexval.toString(); let str = ''; for (let i = 0; i < hex.length; i += 2) { str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); } return str; }, arrayhex(arr) { return Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join(""); }, hexarray(hex) { const bytes = new Uint8Array(Math.ceil(hex.length / 2)); for (let i = 0; i < bytes.length; i++) { bytes[i] = parseInt(hex.substr(i * 2, 2), 16); } return bytes; }, getPlatformProcessor() { //mozilla property: if (navigator.oscpu) { return navigator.oscpu; } //ie: if (Object.hasOwn(navigator, "cpuClass")) { return navigator.cpuClass; } return "unknown"; }, getPlatformName() { if (navigator.appVersion.includes("Win")) { return "Microsoft Windows"; } if (navigator.appVersion.includes("Mac")) { return "Mac OSX"; } if (navigator.appVersion.includes("Linux")) { return "Linux"; } if (navigator.appVersion.includes("X11")) { return "Posix"; } return "unknown"; }, getFirstBrowserLanguage() { const nav = window.navigator; const browserLanguagePropertyKeys = [ "language", "browserLanguage", "systemLanguage", "userLanguage", ]; let language; // support for HTML 5.1 "navigator.languages" if (Array.isArray(nav.languages)) { for (let language of nav.languages) { if (language && language.length > 0) { return language; } } } // support for other well known properties in browsers for (const property of browserLanguagePropertyKeys) { language = nav[property]; if (language && language.length > 0) { return language; } } return null; }, getKeyboardLayout() { let v = Utilities.getFirstBrowserLanguage(); if (v === null) { return "us"; } let layout = LANGUAGE_TO_LAYOUT[v]; if (!layout) { //ie: v="en_GB"; v = v.split(",")[0]; let l = v.split("-", 2); if (l.length === 1) { l = v.split("_", 2); } //ie: "en" layout = l[0].toLowerCase(); const temporary = LANGUAGE_TO_LAYOUT[layout]; if (temporary) { layout = temporary; } } return layout; }, isMacOS() { return navigator.platform.includes("Mac"); }, isWindows() { return navigator.platform.includes("Win"); }, isFirefox() { const ua = navigator.userAgent.toLowerCase(); return ua.includes("firefox"); }, isOpera() { const ua = navigator.userAgent.toLowerCase(); return ua.includes("opera"); }, isSafari() { const ua = navigator.userAgent.toLowerCase(); return ua.includes("safari") && !ua.includes("chrome"); }, isWebkit() { const ua = navigator.userAgent.toLowerCase(); return ua.includes("webkit"); }, isEdge() { return navigator.userAgent.includes("Edge"); }, isChrome() { const isChromium = Object.hasOwn(window, "chrome"); const winNav = window.navigator; const vendorName = winNav.vendor; const isOpera = winNav.userAgent.includes("OPR"); const isIEedge = winNav.userAgent.includes("Edge"); const isIOSChrome = winNav.userAgent.match("CriOS"); if (isIOSChrome) { return true; } else return isChromium !== null && isChromium !== undefined && vendorName === "Google Inc." && isOpera === false && isIEedge === false; }, isIE() { return ( navigator.userAgent.includes("MSIE") || navigator.userAgent.includes("Trident/") ); }, getBrowserName() { if (this.isFirefox()) { return "Firefox"; } if (this.isOpera()) { return "Opera"; } if (this.isChrome()) { return "Chrome"; } if (this.isEdge()) { return "Edge"; } if (this.isSafari()) { return "Safari"; } return "Unknown"; }, getOS() { const userAgent = navigator.userAgent; let os = "Unknown"; if (userAgent.includes("Windows NT")) { os = "MS Windows"; } else if (userAgent.includes("Macintosh") || userAgent.includes("Mac OS X")) { os = "macOS"; } else if (userAgent.includes("Android")) { os = "Android"; } else if (userAgent.includes("iPhone") || userAgent.includes("iPad")) { os = "iOS/iPadOS"; } else if (userAgent.includes("Linux")) { os = "Linux"; } return os; }, is_64bit() { const _to_check = []; if (Object.hasOwn(window.navigator, "cpuClass")) _to_check.push(`${window.navigator.cpuClass}`.toLowerCase()); if (window.navigator.platform) _to_check.push(`${window.navigator.platform}`.toLowerCase()); if (navigator.userAgent) _to_check.push(`${navigator.userAgent}`.toLowerCase()); const _64bits_signatures = [ "x86_64", "x86-64", "Win64", "x64;", "amd64", "AMD64", "WOW64", "x64_64", "ia64", "sparc64", "ppc64", "IRIX64", ]; for (const a of _to_check) { for (const b of _64bits_signatures) { if (a.includes(b.toLowerCase())) { return true; } } } return false; }, isMobile() { return /iphone|ipad|ipod|android/i.test(navigator.userAgent); }, getSimpleUserAgentString() { if (Utilities.isFirefox()) { return "Firefox"; } else if (Utilities.isOpera()) { return "Opera"; } else if (Utilities.isSafari()) { return "Safari"; } else if (Utilities.isChrome()) { return "Chrome"; } else if (Utilities.isIE()) { return "MSIE"; } else { return ""; } }, getColorGamut() { if (!window.matchMedia) { //unknown return ""; } else if (window.matchMedia("(color-gamut: rec2020)").matches) { return "rec2020"; } else if (window.matchMedia("(color-gamut: p3)").matches) { return "P3"; } else if (window.matchMedia("(color-gamut: srgb)").matches) { return "srgb"; } else { return ""; } }, isEventSupported(event) { let testElement = document.createElement("div"); let isSupported; event = `on${event}`; isSupported = event in testElement; if (!isSupported) { testElement.setAttribute(event, "return;"); isSupported = typeof testElement[event] === "function"; } testElement = null; return isSupported; }, //https://github.com/facebook/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js //BSD license normalizeWheel( /*object*/ event) /*object*/ { // Reasonable defaults const PIXEL_STEP = 10; const LINE_HEIGHT = 40; const PAGE_HEIGHT = 800; let sX = 0; let sY = 0; // spinX, spinY // Legacy if ("detail" in event) { sY = event.detail; } if ("wheelDelta" in event) { sY = -event.wheelDelta / 120; } if ("wheelDeltaY" in event) { sY = -event.wheelDeltaY / 120; } if ("wheelDeltaX" in event) { sX = -event.wheelDeltaX / 120; } // side scrolling on FF with DOMMouseScroll if ("axis" in event && event.axis === event.HORIZONTAL_AXIS) { sX = sY; sY = 0; } let pX = sX * PIXEL_STEP; // pixelX let pY = sY * PIXEL_STEP; // pixelY if ("deltaY" in event) { pY = event.deltaY; } if ("deltaX" in event) { pX = event.deltaX; } if ((pX || pY) && event.deltaMode) { if (event.deltaMode === 1) { // delta in LINE units pX *= LINE_HEIGHT; pY *= LINE_HEIGHT; } else { // delta in PAGE units pX *= PAGE_HEIGHT; pY *= PAGE_HEIGHT; } } // Fall-back if spin cannot be determined if (pX && !sX) { sX = pX < 1 ? -1 : 1; } if (pY && !sY) { sY = pY < 1 ? -1 : 1; } return { spinX: sX, spinY: sY, pixelX: pX, pixelY: pY, deltaMode: event.deltaMode || 0, }; }, saveFile(filename, data, mimetype) { const a = document.createElement("a"); a.setAttribute("style", "display: none"); document.body.append(a); const blob = new Blob([data], mimetype); const url = window.URL.createObjectURL(blob); if (navigator.msSaveOrOpenBlob) { navigator.msSaveOrOpenBlob(blob, filename); } else { a.href = url; a.download = filename; a.click(); window.URL.revokeObjectURL(url); } }, StringToUint8(value) { return new TextEncoder().encode(value); }, Uint8ToString(value) { return new TextDecoder().decode(value); }, s(v) { if (v === undefined) { return ""; } const type = typeof v; if (type === "object" && v.constructor === Uint8Array) { return Utilities.Uint8ToString(v); } return v.toString(); }, u(v) { if (v === undefined) { return new Uint8Array(0); } const type = typeof v; if (type === 'object' && v.constructor === Uint8Array) { return v; } return Utilities.StringToUint8(v.toString()); }, ArrayBufferToString(uintArray) { // apply in chunks of 10400 to avoid call stack overflow // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply let s = ""; const skip = 10_400; if (uintArray.subarray) { for ( let index = 0, length_ = uintArray.length; index < length_; index += skip ) { s += String.fromCharCode.apply( null, uintArray.subarray(index, Math.min(index + skip, length_)) ); } } else { for ( let index = 0, length_ = uintArray.length; index < length_; index += skip ) { s += String.fromCharCode.apply( null, uintArray.slice(index, Math.min(index + skip, length_)) ); } } return s; }, ArrayBufferToBase64(uintArray) { return window.btoa(Utilities.ArrayBufferToString(uintArray)); }, ToBase64(v) { try { return window.btoa(v); } catch { return ArrayBufferToBase64(v); } }, convertDataURIToBinary(dataURI) { const BASE64_MARKER = ";base64,"; const base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length; const base64 = dataURI.slice(Math.max(0, base64Index)); const raw = window.atob(base64); const rawLength = raw.length; const array = new Uint8Array(new ArrayBuffer(rawLength)); for (let index = 0; index < rawLength; index++) { array[index] = raw.charCodeAt(index); } return array; }, parseINIString(data) { const regex = { section: /^\s*\[\s*([^\]]*)\s*]\s*$/, param: /^\s*([^=]+?)\s*=\s*(.*?)\s*$/, comment: /^\s*[#;].*$/, }; const value = {}; const lines = data.split(/[\n\r]+/); let section = null; for (const line of lines) { if (regex.comment.test(line)) { // just continue } else if (regex.param.test(line)) { const match = line.match(regex.param); if (section) { value[section][match[1]] = match[2]; } else { value[match[1]] = match[2]; } } else if (regex.section.test(line)) { const match = line.match(regex.section); value[match[1]] = {}; section = match[1]; } else if (line.length === 0 && section) { section = null; } } return value; }, isSafeHost(host) { return host && ["localhost", "127.0.0.1", "::1"].indexOf(host) >= 0; }, /** * XmlHttpRequest's getAllResponseHeaders() method returns a string of response * headers according to the format described here: * http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders-method * This method parses that string into a user-friendly key/value pair object. */ ParseResponseHeaders(headerString) { const headers = {}; if (!headerString) { return headers; } const headerPairs = headerString.split("\u000D\u000A"); for (const headerPair of headerPairs) { // Can't use split() here because it does the wrong thing // if the header value has the string ": " in it. const index = headerPair.indexOf("\u003A\u0020"); if (index > 0) { const key = headerPair.slice(0, Math.max(0, index)); headers[key] = headerPair.slice(Math.max(0, index + 2)); } } return headers; }, parseParams(q) { const parameters = {}; let e; const a = /\+/g; // Regex for replacing addition symbol with a space const r = /([^&=]+)=?([^&]*)/g; const d = (s) => { return decodeURIComponent(s.replace(a, " ")); }; while ((e = r.exec(q))) parameters[d(e[1])] = d(e[2]); return parameters; }, getparam(property) { let getParameter = window.location.getParameter; if (!getParameter) { getParameter = function(key) { if (!window.location.queryStringParams) window.location.queryStringParams = Utilities.parseParams( window.location.search.slice(1) ); return window.location.queryStringParams[key]; }; } let value = getParameter(property); if (value === undefined) { try { value = Utilities.getSessionStorageValue(property); } catch {} } return value; }, hasSessionStorage() { if (typeof Storage === "undefined") { return false; } try { const key = "just for testing sessionStorage support"; sessionStorage.setItem(key, "store-whatever"); sessionStorage.removeItem(key); return true; } catch { return false; } }, getSessionStorageValue(property) { const params = JSON.parse(sessionStorage.getItem(Utilities.getSessionStoragePrefix())) if (property in params) { return String(params[property]); } return undefined; }, setSessionStorageValue(property, value) { const prefix = Utilities.getSessionStoragePrefix(); let params = JSON.parse(sessionStorage.getItem(prefix)) || {} if (value === null || value === "undefined") { delete params[property]; } else { params[property] = String(value); } sessionStorage.setItem(prefix, JSON.stringify(params)); }, clearSessionStorage() { sessionStorage.removeItem(Utilities.getSessionStoragePrefix()); }, getSessionStoragePrefix() { const urlPath = new URL(window.location.href).pathname return urlPath.substring(0, urlPath.lastIndexOf("/")); }, getConnectionInfo() { if (!Object.hasOwn(navigator, "connection")) { return {}; } const c = navigator.connection; const index = {}; if (c.type) { index["type"] = c.type; } if (Object.hasOwn(c, "effectiveType")) { index["effective-type"] = c.effectiveType; } if (!isNaN(c.downlink) && c.downlink > 0 && isFinite(c.downlink)) { index["downlink"] = Math.round(c.downlink * 1000 * 1000); } if ( Object.hasOwn(c, "downlinkMax") && !isNaN(c.downlinkMax) && !isNaN(c.downlinkMax) && c.downlinkMax > 0 && isFinite(c.downlinkMax) ) { index["downlink.max"] = Math.round(c.downlinkMax * 1000 * 1000); } if (!isNaN(c.rtt) && c.rtt > 0) { index["rtt"] = c.rtt; } return index; }, json_action(uri, success_function, error_function, username, password) { Utilities.log( "json_action(", uri, ", ", success_function, ", ", error_function, ")" ); const xhr = new XMLHttpRequest(); let url = uri; if (uri.startsWith("/")) { //relative URI url = document.location.href.split("/connect.html")[0] + uri; } xhr.open("GET", url, true); if (username && password) { const credentials = btoa(`${username}:${password}`); xhr.setRequestHeader("Authorization", `Basic ${credentials}`); } xhr.responseType = "json"; xhr.addEventListener("load", function() { Utilities.log("loaded", url, "status", xhr.status); const status = xhr.status; if (status === 200) { success_function(xhr, xhr.response); } else { Utilities.log(uri, "failed:", status + xhr.response); if (error_function) { error_function(`failed: ${status}${xhr.response}`); } } }); xhr.addEventListener("error", function(e) { Utilities.log(uri, "error:", e); if (error_function) { error_function(e); } }); xhr.addEventListener("abort", function(e) { Utilities.log(uri, "abort:", e); if (error_function) { error_function(e); } }); xhr.send(); return xhr; }, }; //convert a language code into an X11 keyboard layout code: const LANGUAGE_TO_LAYOUT = { en_GB: "gb", en: "us", zh: "cn", af: "za", sq: "al", ca: "ca", "zh-TW": "tw", "zh-CN": "cn", cs: "cz", da: "dk", "nl-BE": "be", "en-US": "us", "en-AU": "us", "en-GB": "gb", "en-CA": "ca", "en-NZ": "us", "en-IE": "ie", "en-ZA": "za", "en-JM": "us", "en-TT": "tr", et: "ee", fa: "ir", "fr-BE": "be", "fr-CA": "ca", "fr-CH": "ch", "fr-LU": "fr", "gd-IE": "ie", "de-CH": "ch", "de-AT": "at", "de-LU": "de", "de-LI": "de", he: "il", hi: "in", "it-CH": "ch", ja: "jp", ko: "kr", "pt-BR": "br", pt: "pt", sr: "rs", sl: "si", es: "es", sv: "se", "sv-FI": "fi", tr: "tr", uk: "ua", ur: "pk", vi: "vn", //Unknown conversions //"en-BZ": ?? //"en-EG": ?? //"gd": ?? //"id": ?? //"ji": ?? //"rm": ?? //"ro-MO": ?? //"ru-MI": ?? //"sb": ?? //"sx": ?? //"sz": ?? //"ts": ?? //"tn": ?? //"ve": ?? //"xh": ?? //"zh-HK": ?? //"zh-SG": ?? //"zu": ?? }; function console_debug_safe() { // if (console) console.debug.apply(console, arguments); } function console_error_safe() { // if (console) console.error.apply(console, arguments); } function console_warn_safe() { // if (console) console.warn.apply(console, arguments); } function console_log_safe() { // if (console) console.log.apply(console, arguments); }