diff --git a/app/ui.js b/app/ui.js index 8af7dd08..9cc254ad 100644 --- a/app/ui.js +++ b/app/ui.js @@ -30,9 +30,11 @@ window.updateSetting = (name, value) => { } } +import "core-js/stable"; +import "regenerator-runtime/runtime"; import * as Log from '../core/util/logging.js'; import _, { l10n } from './localization.js'; -import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold } +import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold, supportsBinaryClipboard, isFirefox } from '../core/util/browser.js'; import { setCapture, getPointerEvent } from '../core/util/events.js'; import KeyTable from "../core/input/keysym.js"; @@ -1100,20 +1102,12 @@ const UI = { }, clipboardReceive(e) { - if (UI.rfb.clipboardDown && UI.rfb.clipboardSeamless ) { + if (UI.rfb.clipboardDown) { var curvalue = document.getElementById('noVNC_clipboard_text').value; if (curvalue != e.detail.text) { Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0, 40) + "..."); document.getElementById('noVNC_clipboard_text').value = e.detail.text; Log.Debug("<< UI.clipboardReceive"); - if (navigator.clipboard && navigator.clipboard.writeText){ - navigator.clipboard.writeText(e.detail.text) - .then(function () { - //UI.popupMessage("Selection Copied"); - }, function () { - console.error("Failed to write system clipboard (trying to copy from NoVNC clipboard)") - }); - } } } }, @@ -1142,26 +1136,22 @@ const UI = { UI.copyFromLocalClipboard(); }, copyFromLocalClipboard: function copyFromLocalClipboard() { + if (!document.hasFocus()) { + Log.Debug("window does not have focus"); + return; + } if (UI.rfb && UI.rfb.clipboardUp && UI.rfb.clipboardSeamless) { - UI.readClipboard(function (text) { - var maximumBufferSize = 10000; - var clipVal = document.getElementById('noVNC_clipboard_text').value; - if (clipVal != text) { - document.getElementById('noVNC_clipboard_text').value = text; // The websocket has a maximum buffer array size - - if (text.length > maximumBufferSize) { - UI.popupMessage("Clipboard contents too large. Data truncated", 2000); - UI.rfb.clipboardPasteFrom(text.slice(0, maximumBufferSize)); - } else { - //UI.popupMessage("Copied from Local Clipboard"); - UI.rfb.clipboardPasteFrom(text); + if (UI.rfb.clipboardBinary) { + navigator.clipboard.read().then((data) => { + if (UI.rfb) { + UI.rfb.clipboardPasteDataFrom(data); } - } // Reset flag to prevent checking too often - - - UI.needToCheckClipboardChange = false; - }); + UI.needToCheckClipboardChange = false; + }, (err) => { + Log.Debug("No data in clipboard"); + }); + } } }, @@ -1304,6 +1294,13 @@ const UI = { UI.rfb.clipboardUp = UI.getSetting('clipboard_up'); UI.rfb.clipboardDown = UI.getSetting('clipboard_down'); UI.rfb.clipboardSeamless = UI.getSetting('clipboard_seamless'); + UI.rfb.clipboardBinary = supportsBinaryClipboard() && UI.rfb.clipboardSeamless; + + //Only explicitly request permission to clipboard on browsers that support binary clipboard access + if (supportsBinaryClipboard()) { + // explicitly request permission to the clipboard + navigator.permissions.query({ name: "clipboard-read" }).then((result) => { Log.Debug('binary clipboard enabled') }); + } // KASM-960 workaround, disable seamless on Safari if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { diff --git a/core/rfb.js b/core/rfb.js index f80986dc..ba1d72f8 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -10,6 +10,7 @@ import { toUnsigned32bit, toSigned32bit } from './util/int.js'; import * as Log from './util/logging.js'; import { encodeUTF8, decodeUTF8 } from './util/strings.js'; +import { hashUInt8Array } from './util/int.js'; import { dragThreshold, supportsCursorURIs, isTouchDevice, isWindows, isMac } from './util/browser.js'; import { clientToElement } from './util/element.js'; import { setCapture } from './util/events.js'; @@ -140,6 +141,7 @@ export default class RFB extends EventTargetMixin { this._frameRate = 30; this._maxVideoResolutionX = 960; this._maxVideoResolutionY = 540; + this._clipboardBinary = true; this._trackFrameStats = false; @@ -332,10 +334,14 @@ export default class RFB extends EventTargetMixin { this._qualityLevel = 6; this._compressionLevel = 2; + this._clipHash = 0; } // ===== PROPERTIES ===== + get clipboardBinary() { return this._clipboardMode; } + set clipboardBinary(val) { this._clipboardMode = val; } + get videoQuality() { return this._videoQuality; } set videoQuality(quality) { this._videoQuality = quality; } @@ -763,21 +769,94 @@ export default class RFB extends EventTargetMixin { clipboardPasteFrom(text) { if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; } + if (!(typeof text === 'string' && text.length > 0)) { return; } + this.sentEventsCounter+=1; - if (this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] && - this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) { - this._clipboardText = text; - RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]); - } else { - let data = new Uint8Array(text.length); - for (let i = 0; i < text.length; i++) { - // FIXME: text can have values outside of Latin1/Uint8 - data[i] = text.charCodeAt(i); - } - - RFB.messages.clientCutText(this._sock, data); + let data = new Uint8Array(text.length); + for (let i = 0; i < text.length; i++) { + data[i] = text.charCodeAt(i); } + + let h = hashUInt8Array(data); + if (h === this._clipHash) { + Log.Debug('No clipboard changes'); + return; + } else { + this._clipHash = h; + } + + let dataset = []; + let mimes = [ 'text/plain' ]; + dataset.push(data); + + RFB.messages.sendBinaryClipboard(this._sock, dataset, mimes); + } + + async clipboardPasteDataFrom(clipdata) { + if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; } + this.sentEventsCounter+=1; + + let dataset = []; + let mimes = []; + let h = 0; + for (let i = 0; i < clipdata.length; i++) { + for (let ti = 0; ti < clipdata[i].types.length; ti++) { + let mime = clipdata[i].types[ti]; + + switch (mime) { + case 'image/png': + case 'text/plain': + case 'text/html': + let blob = await clipdata[i].getType(mime); + if (!blob) { + continue; + } + let buff = await blob.arrayBuffer(); + let data = new Uint8Array(buff); + + if (!h) { + h = hashUInt8Array(data); + if (h === this._clipHash) { + Log.Debug('No clipboard changes'); + return; + } else { + this._clipHash = h; + } + } + + if (mimes.includes(mime)) { + continue; + } + + mimes.push(mime); + dataset.push(data); + Log.Debug('Sending mime type: ' + mime); + break; + default: + Log.Info('skipping clip send mime type: ' + mime) + } + + } + } + + //if png is present and text/plain is not, remove other variations of images to save bandwidth + //if png is present with text/plain, then remove png. Word will put in a png of copied text + if (mimes.includes('image/png') && !mimes.includes('text/plain')) { + let i = mimes.indexOf('image/png'); + mimes = mimes.slice(i, i+1); + dataset = dataset.slice(i, i+1); + } else if (mimes.includes('image/png') && mimes.includes('text/plain')) { + let i = mimes.indexOf('image/png'); + mimes.splice(i, 1); + dataset.splice(i, 1); + } + + + if (dataset.length > 0) { + RFB.messages.sendBinaryClipboard(this._sock, dataset, mimes); + } + } requestBottleneckStats() { @@ -980,7 +1059,7 @@ export default class RFB extends EventTargetMixin { try { if (x > 1280 && limited && this.videoQuality == 1) { var ratio = y / x; - console.log(ratio); + Log.Debug(ratio); x = 1280; y = x * ratio; } @@ -989,7 +1068,7 @@ export default class RFB extends EventTargetMixin { y = 720; } } catch (err) { - console.log(err); + Log.Debug(err); } return { w: x, @@ -2383,6 +2462,100 @@ export default class RFB extends EventTargetMixin { return true; } + _handleBinaryClipboard() { + Log.Debug("HandleBinaryClipboard"); + + if (this._sock.rQwait("Binary Clipboard header", 2, 1)) { return false; } + + let num = this._sock.rQshift8(); // how many different mime types + let mimes = []; + let clipItemData = {}; + let buffByteLen = 2; + let textdata = ''; + Log.Info(num + ' Clipboard items recieved.'); + Log.Debug('Started clipbooard processing with Client sockjs buffer size ' + this._sock.rQlen); + + + + for (let i = 0; i < num; i++) { + if (this._sock.rQwait("Binary Clipboard mimelen", 1, buffByteLen)) { return false; } + buffByteLen++; + let mimelen = this._sock.rQshift8(); + + if (this._sock.rQwait("Binary Clipboard mime", Math.abs(mimelen), buffByteLen)) { return false; } + buffByteLen+=mimelen; + let mime = this._sock.rQshiftStr(mimelen); + + if (this._sock.rQwait("Binary Clipboard data len", 4, buffByteLen)) { return false; } + buffByteLen+=4; + let len = this._sock.rQshift32(); + + if (this._sock.rQwait("Binary Clipboard data", Math.abs(len), buffByteLen)) { return false; } + let data = this._sock.rQshiftBytes(len); + buffByteLen+=len; + + switch(mime) { + case "image/png": + case "text/html": + case "text/plain": + mimes.push(mime); + + if (mime == "text/plain") { + + //textdata = new TextDecoder().decode(data); + for (let i = 0; i < data.length; i++) { + textdata+=String.fromCharCode(data[i]); + } + + if ((textdata.length > 0) && "\0" === textdata.charAt(textdata.length - 1)) { + textdata = textdata.slice(0, -1); + } + + Log.Debug("Plain text clipboard recieved and placed in text element, size: " + textdata.length); + this.dispatchEvent(new CustomEvent( + "clipboard", + { detail: { text: textdata } }) + ); + } + + if (!this.clipboardBinary) { continue; } + + Log.Info("Processed binary clipboard of MIME " + mime + " of length " + len); + + clipItemData[mime] = new Blob([data], { type: mime }); + break; + default: + Log.Debug('Mime type skipped: ' + mime); + break; + } + } + + Log.Debug('Finished processing binary clipboard with client sockjs buffer size ' + this._sock.rQlen); + + if (Object.keys(clipItemData).length > 0) { + if (this.clipboardBinary) { + this._clipHash = 0; + navigator.clipboard.write([new ClipboardItem(clipItemData)]).then( + function() {}, + function(err) { + Log.Error("Error writing to client clipboard: " + err); + // Lets try writeText + if (textdata.length > 0) { + navigator.clipboard.writeText(textdata).then( + function() {}, + function(err2) { + Log.Error("Error writing text to client clipboard: " + err2); + } + ); + } + } + ); + } + } + + return true; + } + _handle_server_stats_msg() { this._sock.rQskipBytes(3); // Padding const length = this._sock.rQshift32(); @@ -2390,8 +2563,8 @@ export default class RFB extends EventTargetMixin { const text = this._sock.rQshiftStr(length); - console.log("Received KASM bottleneck stats:"); - console.log(text); + Log.Debug("Received KASM bottleneck stats:"); + Log.Debug(text); this.dispatchEvent(new CustomEvent( "bottleneck_stats", { detail: { text: text } })); @@ -2523,6 +2696,9 @@ export default class RFB extends EventTargetMixin { this._trackFrameStats = true; return true; + case 180: // KASM binary clipboard + return this._handleBinaryClipboard(); + case 248: // ServerFence return this._handleServerFenceMsg(); @@ -3184,6 +3360,61 @@ RFB.messages = { }, + sendBinaryClipboard(sock, dataset, mimes) { + + + const buff = sock._sQ; + let offset = sock._sQlen; + + buff[offset] = 180; // msg-type + buff[offset + 1] = dataset.length; // how many mime types + sock._sQlen += 2; + offset += 2; + + for (let i=0; i < dataset.length; i++) { + let mime = mimes[i]; + let data = dataset[i]; + + buff[offset++] = mime.length; + + for (let i = 0; i < mime.length; i++) { + buff[offset++] = mime.charCodeAt(i); // change to [] if not a string + } + + let length = data.length; + + Log.Info('Clipboard data sent mime type ' + mime + ' len ' + length); + + buff[offset++] = length >> 24; + buff[offset++] = length >> 16; + buff[offset++] = length >> 8; + buff[offset++] = length; + + sock._sQlen += 1 + mime.length + 4; + + // 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; + } + + offset = sock._sQlen; + } + }, + setDesktopSize(sock, width, height, id, flags) { const buff = sock._sQ; const offset = sock._sQlen; diff --git a/core/util/browser.js b/core/util/browser.js index 24b5e960..39ca4468 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -101,3 +101,9 @@ export function isFirefox() { return navigator && !!(/firefox/i).exec(navigator.userAgent); } +export function supportsBinaryClipboard() { + //Safari does support the clipbaord API but has a lot of security restrictions + if (isSafari()) { return false; } + return (navigator.clipboard && typeof navigator.clipboard.read === "function"); +} + diff --git a/core/util/int.js b/core/util/int.js index 001f40f2..79c9f724 100644 --- a/core/util/int.js +++ b/core/util/int.js @@ -13,3 +13,14 @@ export function toUnsigned32bit(toConvert) { export function toSigned32bit(toConvert) { return toConvert | 0; } + +/* + * Fast hashing function with low entropy, not for security uses. +*/ +export function hashUInt8Array(data) { + let h; + for (let i = 0; i < data.length; i++) { + h = Math.imul(31, h) + data[i] | 0; + } + return h; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3f57a171..bc278c5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "chai": "*", "clean-webpack-plugin": "^3.0.0", "commander": "*", - "core-js": "*", + "core-js": "^3.18.3", "css-loader": "^5.0.1", "css-minimizer-webpack-plugin": "^1.1.5", "es-module-loader": "*", @@ -48,6 +48,7 @@ "po2json": "*", "postcss-loader": "^4.1.0", "preload-webpack-plugin": "^3.0.0-beta.4", + "regenerator-runtime": "^0.13.9", "requirejs": "*", "rollup": "*", "rollup-plugin-node-resolve": "*", @@ -4010,9 +4011,9 @@ } }, "node_modules/core-js": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.2.tgz", - "integrity": "sha512-P0KPukO6OjMpjBtHSceAZEWlDD1M2Cpzpg6dBbrjFqFhBHe/BwhxaP820xKOjRn/lZRQirrCusIpLS/n2sgXLQ==", + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.3.tgz", + "integrity": "sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw==", "dev": true, "hasInstallScript": true, "funding": { @@ -18310,7 +18311,8 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "acorn-node": { "version": "1.8.2", @@ -18364,13 +18366,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true + "dev": true, + "requires": {} }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "alphanum-sort": { "version": "1.0.2", @@ -18684,7 +18688,8 @@ "version": "10.0.0", "resolved": "https://registry.npmjs.org/babelify/-/babelify-10.0.0.tgz", "integrity": "sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg==", - "dev": true + "dev": true, + "requires": {} }, "babylon": { "version": "6.18.0", @@ -19806,9 +19811,9 @@ "dev": true }, "core-js": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.2.tgz", - "integrity": "sha512-P0KPukO6OjMpjBtHSceAZEWlDD1M2Cpzpg6dBbrjFqFhBHe/BwhxaP820xKOjRn/lZRQirrCusIpLS/n2sgXLQ==", + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.3.tgz", + "integrity": "sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw==", "dev": true }, "core-js-compat": { @@ -20819,7 +20824,8 @@ "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "dev": true + "dev": true, + "requires": {} } } }, @@ -22436,7 +22442,8 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true + "dev": true, + "requires": {} }, "ieee754": { "version": "1.2.1", @@ -23347,19 +23354,22 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/karma-safari-launcher/-/karma-safari-launcher-1.0.0.tgz", "integrity": "sha1-lpgqLMR9BmquccVTursoMZEVos4=", - "dev": true + "dev": true, + "requires": {} }, "karma-script-launcher": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/karma-script-launcher/-/karma-script-launcher-1.0.0.tgz", "integrity": "sha1-zQF8TeXvCeWp2nkydhdhCN1LVC0=", - "dev": true + "dev": true, + "requires": {} }, "karma-sinon-chai": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/karma-sinon-chai/-/karma-sinon-chai-2.0.2.tgz", "integrity": "sha512-SDgh6V0CUd+7ruL1d3yG6lFzmJNGRNQuEuCYXLaorruNP9nwQfA7hpsp4clx4CbOo5Gsajh3qUOT7CrVStUKMw==", - "dev": true + "dev": true, + "requires": {} }, "kew": { "version": "0.7.0", @@ -25647,7 +25657,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -26312,7 +26323,8 @@ "version": "3.0.0-beta.4", "resolved": "https://registry.npmjs.org/preload-webpack-plugin/-/preload-webpack-plugin-3.0.0-beta.4.tgz", "integrity": "sha512-6hhh0AswCbp/U4EPVN4fbK2wiDkXhmgjjgEYEmXa21UYwjYzCIgh3ZRMXM21ZPLfbQGpdFuSL3zFslU+edjpwg==", - "dev": true + "dev": true, + "requires": {} }, "prelude-ls": { "version": "1.2.1", @@ -27246,7 +27258,8 @@ "version": "2.14.0", "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-2.14.0.tgz", "integrity": "sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ==", - "dev": true + "dev": true, + "requires": {} }, "slash": { "version": "2.0.0", @@ -29645,7 +29658,8 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.0.tgz", "integrity": "sha512-uYhVJ/m9oXwEI04iIVmgLmugh2qrZihkywG9y5FfZV2ATeLIzHf93qs+tUNqlttbQK957/VX3mtwAS+UfIwA4g==", - "dev": true + "dev": true, + "requires": {} }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index 06b07997..5cee3929 100644 --- a/package.json +++ b/package.json @@ -30,29 +30,27 @@ "url": "git+https://github.com/kasmtech/noVNC.git" }, "author": "Kasm Technologies (https://www.kasmweb.com)", - "contributors": [ - ], + "contributors": [], "license": "MPL-2.0", "bugs": { "url": "https://github.com/kasmtech/noVNC/issues" }, "homepage": "https://github.com/kasmtech/noVNC", "devDependencies": { - "@babel/core": "^7.12.10", - "@babel/preset-env": "^7.12.11", + "@babel/cli": "*", "@babel/core": "*", - "babel-loader": "^8.2.2", "@babel/plugin-syntax-dynamic-import": "*", "@babel/plugin-transform-modules-commonjs": "*", "@babel/preset-env": "*", - "@babel/cli": "*", + "@chiragrupani/karma-chromium-edge-launcher": "*", + "babel-loader": "^8.2.2", "babel-plugin-import-redirect": "*", - "browserify": "*", "babelify": "*", - "core-js": "*", + "browserify": "*", "chai": "*", "clean-webpack-plugin": "^3.0.0", "commander": "*", + "core-js": "^3.18.3", "css-loader": "^5.0.1", "css-minimizer-webpack-plugin": "^1.1.5", "es-module-loader": "*", @@ -64,11 +62,10 @@ "html-webpack-plugin": "^4.5.0", "jsdom": "*", "karma": "*", - "karma-mocha": "*", "karma-chrome-launcher": "*", - "@chiragrupani/karma-chromium-edge-launcher": "*", "karma-firefox-launcher": "*", "karma-ie-launcher": "*", + "karma-mocha": "*", "karma-mocha-reporter": "*", "karma-safari-launcher": "*", "karma-script-launcher": "*", @@ -79,6 +76,7 @@ "po2json": "*", "postcss-loader": "^4.1.0", "preload-webpack-plugin": "^3.0.0-beta.4", + "regenerator-runtime": "^0.13.9", "requirejs": "*", "rollup": "*", "rollup-plugin-node-resolve": "*", @@ -90,7 +88,6 @@ "webpack": "^4.29.6", "webpack-cli": "^3.2.3" }, - "dependencies": {}, "keywords": [ "vnc", "rfb",