Skip to content

Commit

Permalink
machines: Fix security and broken packaging in noVNC
Browse files Browse the repository at this point in the history
Using unsafe-inline Content-Security-Policy opens up a security
in something as wild as noVNC. We simply shouldn't have unsafe-inline
or unsafe-eval in our packages.

In addition the noVNC component should be installed efficiently, in
a single bundle, not having to load 15 javascript files. The bundle
should be compressed, with unnecessary dependencies removed.

I've added a small custom Webpack loader to perform the task of
combining these files.

This also allows development on this component to work with pure
webpack without automake and friends. We also properly track the
dependencies and changes here.

Closes #7388
  • Loading branch information
stefwalter committed Jul 27, 2017
1 parent 0a68585 commit ce65bbe
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 206 deletions.
22 changes: 1 addition & 21 deletions pkg/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,12 @@ playground_DATA = \
dist/playground/extra.de.po: pkg/playground/extra.de.po
$(COPY_RULE)

machinesincludedir = $(pkgdatadir)/machines/include
machinesinclude_DATA = \
$(shell ls -1 $(srcdir)/node_modules/noVNC/include | grep ".*\.\(js\|css\|ttf\|woff\)" | sed -e 's/^/dist\/machines\/include\//') \
$(NULL)

machinesincludechromedir = $(pkgdatadir)/machines/include/chrome-app
machinesincludechrome_DATA = \
$(shell ls -1 $(srcdir)/node_modules/noVNC/include/chrome-app | grep ".*\.\(js\|css\|ttf\|woff\)" | sed -e 's/^/dist\/machines\/include\/chrome-app\//') \
$(NULL)

dist/machines/include/chrome-app/%: node_modules/noVNC/include/chrome-app/%
$(COPY_RULE)

dist/machines/include/%: node_modules/noVNC/include/%
$(COPY_RULE)

EXTRA_DIST += \
dist/playground/extra.de.po \
pkg/playground/extra.de.po \
pkg/users/mock \
pkg/lib/qunit-template.html \
$(shell ls -1 $(srcdir)/node_modules/noVNC/include | grep ".*\.\(js\|css\|ttf\|woff\)" | sed -e 's/^/node_modules\/noVNC\/include\//') \
$(shell ls -1 $(srcdir)/node_modules/noVNC/include/chrome-app | grep ".*\.\(js\|css\|ttf\|woff\)" | sed -e 's/^/node_modules\/noVNC\/include\/chrome-app\//') \
node_modules/noVNC/package.json \
$(shell ls -1 $(srcdir)/node_modules/noVNC/include | grep ".*\.\(js\|css\|ttf\|woff\)" | sed -e 's/^/dist\/machines\/include\//') \
$(shell ls -1 $(srcdir)/node_modules/noVNC/include/chrome-app | grep ".*\.\(js\|css\|ttf\|woff\)" | sed -e 's/^/dist\/machines\/include\/chrome-app\//') \
node_modules/noVNC/package.json \
$(playground_DATA) \
$(pkg_TESTS)

Expand Down
40 changes: 40 additions & 0 deletions pkg/machines/cat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* This is a webpack loader that concatenates files
* before the loaded javascript. Multiple files can
* be specified.
*
* require("cat?./file1.js&./file2.js|module");
*/

var fs = require("fs");
var path = require("path");

module.exports = function(source) {
var loader = this;

loader.cacheable();

var callback = loader.async();

var files = loader.query.substring(1).split("&");
var content = [ source ];

function step() {
if (files.length == 0) {
callback(null, content.join("\n"));
return;
}

var filename = require.resolve(files.pop());
loader.addDependency(filename);

fs.readFile(filename, "utf-8", function(err, data) {
if (err)
return callback(err);
content.unshift(data);
step();
});
}

step();
};
2 changes: 1 addition & 1 deletion pkg/machines/manifest.json.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"order": 60
}
},
"content-security-policy": "default-src 'self' 'unsafe-inline' data:"
"content-security-policy": "img-src 'self' data:; frame-src 'self' data:"
}
187 changes: 3 additions & 184 deletions pkg/machines/vnc.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,13 @@
<link rel="stylesheet" href="include/base.css" title="plain">
<link rel="stylesheet" href="vnc.css">

<script src="include/util.js"></script>
<script src="../base1/cockpit.js"></script>

</head>

<body class="vnc-custom-body">
<div id="vnc-main-menu" class="vnc-main-menu">
Send shortcut: <a href="#" class="vnc-button" onclick="sendCtrlAltDel()">Ctrl+Alt+Del</a>
Send shortcut: <a href="#" class="vnc-button" id="vnc-ctrl-alt-del">Ctrl+Alt+Del</a>
</div>

<div id="noVNC_container" class="vnc-custom-container">
Expand All @@ -60,186 +59,6 @@
</canvas>
</div>

<script>
/* global window, $, Util, RFB, */
"use strict";
/*
Unresolved issues when trying to bundle noVNC js files:
- sources are not UMD modules (so no "require()")
- the "modules" are dynamically loading their dependencies (via "Util.load_scripts" call) but not consistently - "preload" is still required
- the "modules" have to be in "include" subdir as the "Util.load_scripts" expects
*/
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"keysymdef.js", "keyboard.js", "input.js", "display.js",
"inflator.js", "rfb.js", "keysym.js"]);

var rfb;
var resizeTimeout;

function ensureVisibility() {
setTimeout(function () {
var containerId = decodeURIComponent(WebUtil.getConfigVar("containerId", ""));
console.log("Scrolling VNC frame into view, containerId = ", containerId);

var toBeVisible = window.parent.document.querySelector("tr[data-row-id='" + containerId + "']");
var toBeVisibleRow = window.parent.document.getElementById(containerId + "-row");

if (toBeVisible && toBeVisibleRow && toBeVisible.scrollIntoView) {
toBeVisible.scrollIntoView(); // ensure maximal visibility of the VNC iframe + VM-controls
toBeVisibleRow.scrollIntoView(); // ensure the VM's name is visible
} else {
console.log('scrollIntoView() is not supported');
}
}, 50);
}

function UIresize() {
var containerId = decodeURIComponent(WebUtil.getConfigVar('containerId', ''));
if (!containerId) {
console.error("containerId not found in noVNC frame params!");
return ;
}

// Normally, the frame/container size shall resize the VM.
// Since this is not working, let's do it vice-versa: adapt browser's component height according to inner display's size.
// Another workaround is in canvas resizing which effectively leads to scaling. But this seems not look good.
if (WebUtil.getConfigVar('resize', false)) {
var height = ($D('noVNC_canvas').height + 60)+ "px";
console.log('Resizing noVNC, height = ', height);

var novncContainerId = containerId + "-novnc-frame-container";
var novncContainer = window.parent.document.getElementById(novncContainerId);
if (novncContainer)
novncContainer.style.height = height;

// no need to resize width - already 100%, potential scrollbar

ensureVisibility();
}
}

function FBUComplete(rfb, fbu) {
UIresize();
rfb.set_onFBUComplete(function() { });
console.log('Setting focus');
$D('noVNC_canvas').focus();
}
function passwordRequired(rfb) {
var msg;
msg = '<form onsubmit="return setPassword();"';
msg += ' style="margin-bottom: 0px">';
msg += 'Password Required: ';
msg += '<input type=password size=10 id="password_input" class="noVNC_status">';
msg += '<\/form>';
$D('noVNC_status_bar').setAttribute("class", "noVNC_status_warn");
$D('noVNC_status').innerHTML = msg;
}
function setPassword() {
rfb.sendPassword($D('password_input').value);
return false;
}
function sendCtrlAltDel() {
rfb.sendCtrlAltDel();
return false;
}

function updateState(rfb, state, oldstate, msg) {
var s, sb, cad, level;
s = $D('noVNC_status');
sb = $D('noVNC_status_bar');
switch (state) {
case 'failed': level = "error"; break;
case 'fatal': level = "error"; break;
case 'normal': level = "normal"; break;
case 'disconnected': level = "normal"; break;
case 'loaded': level = "normal"; break;
default: level = "warn"; break;
}

if (typeof(msg) !== 'undefined') {
sb.setAttribute("class", "noVNC_status_" + level);
s.textContent = msg;
}
}

function parseParams() {
var params = {
host: WebUtil.getConfigVar('host', 'host not provided in params'),
port: WebUtil.getConfigVar('port', 'port not provided in params'),
password: WebUtil.getConfigVar('password', ''),
encrypt: WebUtil.getConfigVar('encrypt', (window.location.protocol === "https:")),
true_color: WebUtil.getConfigVar('true_color', true),
local_cursor: WebUtil.getConfigVar('cursor', true),
shared: WebUtil.getConfigVar('shared', true),
view_only: WebUtil.getConfigVar('view_only', false),
repeaterID: WebUtil.getConfigVar('repeaterID', ''),

logging: WebUtil.getConfigVar('logging', 'warn'),
title: WebUtil.getConfigVar('title', 'noVNC'),
};

if ((!params.host) || (!params.port)) {
updateState(null, 'fatal', null, 'Must specify host and port in URL');
return;
}

return params;
}

function connect(path, params) {
try {
rfb = new RFB({
'target': $D('noVNC_canvas'),
'encrypt': params.encrypt,
'repeaterID': params.repeaterID,
'true_color': params.true_color,
'local_cursor': params.local_cursor,
'shared': params.shared,
'view_only': params.view_only,

'onUpdateState': updateState,
'onXvpInit': function () {},
'onPasswordRequired': passwordRequired,
'onFBUComplete': FBUComplete});
} catch (exc) {
updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc);
return; // don't continue trying to connect
}

rfb.connect(window.location.hostname, window.location.port, params.password, path);
}

window.onscriptsload = function () {
var params = parseParams();

WebUtil.init_logging(params.logging);
document.title = unescape(params.title);

// connect
var query = window.btoa(JSON.stringify({
payload: "stream",
protocol: "binary",
address: params.host,
port: parseInt(params.port, 10),
binary: "raw",
}));

cockpit.transport.wait(function () {
connect("cockpit/channel/" + cockpit.transport.csrf_token + "?" + query, params);
});

window.onresize = function () {
// When the window has been resized, wait until the size remains
// the same for 0.5 seconds before sending the request for changing
// the resolution of the session
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function(){
UIresize();
}, 500);
};

};
</script>

</body>
<script src="vnc.js"></script>
</body>
</html>
Loading

0 comments on commit ce65bbe

Please sign in to comment.