noVNC能够给linux系统提供基于VNC虚拟桌面的WEB服务,这使得openstack使用noVNC对外提供虚拟机的WEB版虚拟桌面。javascript
不过用这个noVNC也有一些问题,在使用HTML2canvas截图或者使用一些须要外部操控的操做就出问题。css
问题重现GIF以下:html
经查,HTML2canvas这个js控件的工做原理是读取HTML元素,可是noVNC或openstack提供的noVNC窗口url都是与如今用的系统不一样域(简单来讲这些服务就是运行在不一样的机子上),这一步由于headers不支持跨域的问题失败了——下载的截图noVNC画面部分为空白,键入F12查看控制台,显示Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.vue
网上不少解决方案,无一不是在服务端修改配置、修改headers、修改HTML2canvas参数使被跨域的服务支持跨域,可是画面截图这个功能,发起者是外部系统,跨的域是noVNC或openstack的ip和端口,没有支持跨域的配置,又不能随意修改里面的代码,修改HTML2canvas参数也无效。java
这时就有一个想法:既然noVNC的窗口本质上就是一堆HTML代码,是否能够将代码直接贴在本系统上?linux
noVNC窗口代码web
<!DOCTYPE html> <html> <head> <!-- noVNC example: lightweight example using minimal UI and features Copyright (C) 2012 Joel Martin Copyright (C) 2017 Samuel Mannehed for Cendio AB noVNC is licensed under the MPL 2.0 (see LICENSE.txt) This file is licensed under the 2-Clause BSD license (see LICENSE.txt). Connect parameters are provided in query string: http://example.com/?host=HOST&port=PORT&encrypt=1 or the fragment: http://example.com/#host=HOST&port=PORT&encrypt=1 --> <title>noVNC</title> <meta charset="utf-8"> <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame Remove this if you use the .htaccess --> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <!-- Icons (see Makefile for what the sizes are for) --> <link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png"> <link rel="icon" sizes="24x24" type="image/png" href="app/images/icons/novnc-24x24.png"> <link rel="icon" sizes="32x32" type="image/png" href="app/images/icons/novnc-32x32.png"> <link rel="icon" sizes="48x48" type="image/png" href="app/images/icons/novnc-48x48.png"> <link rel="icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png"> <link rel="icon" sizes="64x64" type="image/png" href="app/images/icons/novnc-64x64.png"> <link rel="icon" sizes="72x72" type="image/png" href="app/images/icons/novnc-72x72.png"> <link rel="icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png"> <link rel="icon" sizes="96x96" type="image/png" href="app/images/icons/novnc-96x96.png"> <link rel="icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png"> <link rel="icon" sizes="144x144" type="image/png" href="app/images/icons/novnc-144x144.png"> <link rel="icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png"> <link rel="icon" sizes="192x192" type="image/png" href="app/images/icons/novnc-192x192.png"> <!-- Firefox currently mishandles SVG, see #1419039 <link rel="icon" sizes="any" type="image/svg+xml" href="app/images/icons/novnc-icon.svg"> --> <!-- Repeated last so that legacy handling will pick this --> <link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png"> <!-- Apple iOS Safari settings --> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <!-- Home Screen Icons (favourites and bookmarks use the normal icons) --> <link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png"> <link rel="apple-touch-icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png"> <link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png"> <link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png"> <!-- Stylesheets --> <link rel="stylesheet" href="app/styles/lite.css"> <!-- <script type='text/javascript' src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script> --> <!-- promise polyfills promises for IE11 --> <script src="vendor/promise.js"></script> <!-- ES2015/ES6 modules polyfill --> <script type="module"> window._noVNC_has_module_support = true; </script> <script> window.addEventListener("load", function() { if (window._noVNC_has_module_support) return; var loader = document.createElement("script"); loader.src = "vendor/browser-es-module-loader/dist/browser-es-module-loader.js"; document.head.appendChild(loader); }); </script> <!-- actual script modules --> <script type="module" crossorigin="anonymous"> // Load supporting scripts import * as WebUtil from './app/webutil.js'; import RFB from './core/rfb.js'; var rfb; var desktopName; function updateDesktopName(e) { desktopName = e.detail.name; } function credentials(e) { var html; var form = document.createElement('form'); form.innerHTML = '<label></label>'; form.innerHTML += '<input type=password size=10 id="password_input">'; form.onsubmit = setPassword; // bypass status() because it sets text content document.getElementById('noVNC_status_bar').setAttribute("class", "noVNC_status_warn"); document.getElementById('noVNC_status').innerHTML = ''; document.getElementById('noVNC_status').appendChild(form); document.getElementById('noVNC_status').querySelector('label').textContent = 'Password Required: '; } function setPassword() { rfb.sendCredentials({ password: document.getElementById('password_input').value }); return false; } function sendCtrlAltDel() { rfb.sendCtrlAltDel(); return false; } function machineShutdown() { rfb.machineShutdown(); return false; } function machineReboot() { rfb.machineReboot(); return false; } function machineReset() { rfb.machineReset(); return false; } function status(text, level) { switch (level) { case 'normal': case 'warn': case 'error': break; default: level = "warn"; } document.getElementById('noVNC_status_bar').className = "noVNC_status_" + level; document.getElementById('noVNC_status').textContent = text; } function connected(e) { document.getElementById('sendCtrlAltDelButton').disabled = false; if (WebUtil.getConfigVar('encrypt', (window.location.protocol === "https:"))) { status("Connected (encrypted) to " + desktopName, "normal"); } else { status("Connected (unencrypted) to " + desktopName, "normal"); } } function disconnected(e) { document.getElementById('sendCtrlAltDelButton').disabled = true; updatePowerButtons(); if (e.detail.clean) { status("Disconnected", "normal"); } else { status("Something went wrong, connection is closed", "error"); } } function updatePowerButtons() { var powerbuttons; powerbuttons = document.getElementById('noVNC_power_buttons'); if (rfb.capabilities.power) { powerbuttons.className= "noVNC_shown"; } else { powerbuttons.className = "noVNC_hidden"; } } document.getElementById('sendCtrlAltDelButton').onclick = sendCtrlAltDel; document.getElementById('machineShutdownButton').onclick = machineShutdown; document.getElementById('machineRebootButton').onclick = machineReboot; document.getElementById('machineResetButton').onclick = machineReset; WebUtil.init_logging(WebUtil.getConfigVar('logging', 'warn')); document.title = WebUtil.getConfigVar('title', 'noVNC'); // By default, use the host and port of server that served this file var host = WebUtil.getConfigVar('host', window.location.hostname); var port = WebUtil.getConfigVar('port', window.location.port); // if port == 80 (or 443) then it won't be present and should be // set manually if (!port) { if (window.location.protocol.substring(0,5) == 'https') { port = 443; } else if (window.location.protocol.substring(0,4) == 'http') { port = 80; } } var password = WebUtil.getConfigVar('password', ''); //这里还有个问题,每次进入这个窗口都要输密码,那这里是否是能够直接输对密码直接经过 var path = WebUtil.getConfigVar('path', 'websockify'); // If a token variable is passed in, set the parameter in a cookie. // This is used by nova-novncproxy. var token = WebUtil.getConfigVar('token', null); if (token) { // if token is already present in the path we should use it path = WebUtil.injectParamIfMissing(path, "token", token); WebUtil.createCookie('token', token, 1) } (function() { status("Connecting", "normal"); if ((!host) || (!port)) { status('Must specify host and port in URL', 'error'); } var url; if (WebUtil.getConfigVar('encrypt', (window.location.protocol === "https:"))) { url = 'wss'; } else { url = 'ws'; } //noVNC本质上是用webSocket实时传输信息的 url += '://' + host; if(port) { url += ':' + port; } url += '/' + path; rfb = new RFB(document.body, url, { repeaterID: WebUtil.getConfigVar('repeaterID', ''), shared: WebUtil.getConfigVar('shared', true), credentials: { password: password } }); rfb.viewOnly = WebUtil.getConfigVar('view_only', false); rfb.addEventListener("connect", connected); rfb.addEventListener("disconnect", disconnected); rfb.addEventListener("capabilities", function () { updatePowerButtons(); }); rfb.addEventListener("credentialsrequired", credentials); rfb.addEventListener("desktopname", updateDesktopName); rfb.scaleViewport = WebUtil.getConfigVar('scale', false); rfb.resizeSession = WebUtil.getConfigVar('resize', false); })(); </script> </head> <body> <div id="noVNC_status_bar"> <div id="noVNC_left_dummy_elem"></div> <div id="noVNC_status">Loading</div> <div id="noVNC_buttons"> <input type=button value="Send CtrlAltDel" id="sendCtrlAltDelButton" class="noVNC_shown"> <span id="noVNC_power_buttons" class="noVNC_hidden"> <input type=button value="Shutdown" id="machineShutdownButton"> <input type=button value="Reboot" id="machineRebootButton"> <input type=button value="Reset" id="machineResetButton"> </span> </div> </div> </body> </html>
从代码里面能够看到,传输noVNC虚拟桌面关键点在225行的url对应的webSocket连接,而这个连接刚好就是noVNC提供服务的ip和端口,实际上彻底能够把整个页面内嵌在提供虚拟桌面的窗口,或者写在同域系统里面,让其余页面在iframe框架里面调用。chrome
咱们项目用的是Vue.js,为了使页面能适应Vue系统,把代码重构成了这样:canvas
<template> <div id="noVNC_all"> <div id="noVNC_status_bar"> <div id="noVNC_left_dummy_elem"></div> <div id="noVNC_status">Loading</div> <div id="noVNC_buttons"> <input type=button value="Send CtrlAltDel" id="sendCtrlAltDelButton" class="noVNC_shown"> <span id="noVNC_power_buttons" class="noVNC_hidden"> <input type=button value="Shutdown" id="machineShutdownButton"> <input type=button value="Reboot" id="machineRebootButton"> <input type=button value="Reset" id="machineResetButton"> </span> </div> </div> </div> </template> <script> import * as WebUtil from './webutil.js'; import RFB from '@novnc/novnc/core/rfb.js'; export default { components:{ }, data() { return { rfb:null, desktopName:null }; }, methods: { connectVNC () {}, updateDesktopName(e) { this.desktopName = e.detail.name; }, credentials(e) { var html; var form = document.createElement('form'); form.innerHTML = '<label></label>'; form.innerHTML += '<input type=password size=10 id="password_input">'; form.onsubmit = this.setPassword; // bypass status() because it sets text content document.getElementById('noVNC_status_bar').setAttribute("class", "noVNC_status_warn"); document.getElementById('noVNC_status').innerHTML = ''; document.getElementById('noVNC_status').appendChild(form); document.getElementById('noVNC_status').querySelector('label').textContent = 'Password Required: '; }, setPassword() { this.rfb.sendCredentials({ password: document.getElementById('password_input').value }); return false; }, sendCtrlAltDel() { this.rfb.sendCtrlAltDel(); return false; }, machineShutdown() { this.rfb.machineShutdown(); return false; }, machineReboot() { this.rfb.machineReboot(); return false; }, machineReset() { this.rfb.machineReset(); return false; }, status(text, level) { switch (level) { case 'normal': case 'warn': case 'error': break; default: level = "warn"; } document.getElementById('noVNC_status_bar').className = "noVNC_status_" + level; document.getElementById('noVNC_status').textContent = text; }, connected(e) { document.getElementById('sendCtrlAltDelButton').disabled = false; if (WebUtil.getConfigVar('encrypt', (window.location.protocol === "https:"))) { this.status("Connected (encrypted) to " + this.desktopName, "normal"); } else { this.status("Connected (unencrypted) to " + this.desktopName, "normal"); } }, disconnected(e) { document.getElementById('sendCtrlAltDelButton').disabled = true; this.updatePowerButtons(); if (e.detail.clean) { this.status("Disconnected", "normal"); } else { this.status("Something went wrong, connection is closed", "error"); } }, updatePowerButtons() { var powerbuttons; powerbuttons = document.getElementById('noVNC_power_buttons'); if (this.rfb.capabilities.power) { powerbuttons.className= "noVNC_shown"; } else { powerbuttons.className = "noVNC_hidden"; } } }, mounted() { document.getElementById('sendCtrlAltDelButton').onclick = this.sendCtrlAltDel; document.getElementById('machineShutdownButton').onclick = this.machineShutdown; document.getElementById('machineRebootButton').onclick = this.machineReboot; document.getElementById('machineResetButton').onclick = this.machineReset; WebUtil.init_logging(WebUtil.getConfigVar('logging', 'warn')); document.title = WebUtil.getConfigVar('title', 'noVNC'); // By default, use the host and port of server that served this file var host = WebUtil.getConfigVar('host', window.location.hostname); var port = WebUtil.getConfigVar('port', window.location.port); // if port == 80 (or 443) then it won't be present and should be // set manually if (!port) { if (window.location.protocol.substring(0,5) == 'https') { port = 443; } else if (window.location.protocol.substring(0,4) == 'http') { port = 80; } } if(this.$route.params.ipport.indexOf('-') == -1) var password = WebUtil.getConfigVar('password', '123456');//猜测彻底正确,直接就不用验证了 else var password = WebUtil.getConfigVar('password', ''); var path = WebUtil.getConfigVar('path', 'websockify'); // If a token variable is passed in, set the parameter in a cookie. // This is used by nova-novncproxy. var token = WebUtil.getConfigVar('token', null); if (token) { // if token is already present in the path we should use it path = WebUtil.injectParamIfMissing(path, "token", token); WebUtil.createCookie('token', token, 1) } this.status("Connecting", "normal"); if ((!host) || (!port)) { this.status('Must specify host and port in URL', 'error'); } var url; if (WebUtil.getConfigVar('encrypt', (window.location.protocol === "https:"))) { url = 'wss'; } else { url = 'ws'; } if(this.$route.params.ipport == null) url += '://192.168.80.61:30926/websockify'; else if(this.$route.params.ipport.indexOf('-') == -1) url += '://' + this.$route.params.ipport + '/websockify'; else url += '://localhost:10003/websockify/websockify?token=' + this.$route.params.ipport.split(':-')[1] + '&ip=' + this.$route.params.ipport.split(':-')[0]; this.rfb = new RFB(document.querySelector('#noVNC_all'), url, { repeaterID: WebUtil.getConfigVar('repeaterID', ''), shared: WebUtil.getConfigVar('shared', true), credentials: { password: password } }); this.rfb.viewOnly = WebUtil.getConfigVar('view_only', false); this.rfb.addEventListener("connect", this.connected); this.rfb.addEventListener("disconnect", this.disconnected); this.rfb.addEventListener("capabilities", function () { this.updatePowerButtons(); }); this.rfb.addEventListener("credentialsrequired", this.credentials); this.rfb.addEventListener("desktopname", this.updateDesktopName); this.rfb.scaleViewport = WebUtil.getConfigVar('scale', false); this.rfb.resizeSession = WebUtil.getConfigVar('resize', false); } }; </script> <style lang='scss' scoped> #noVNC_status_bar { width: 100%; display:flex; justify-content: space-between; } #noVNC_status { color: #fff; font: bold 12px Helvetica; margin: auto; } .noVNC_status_normal { background: linear-gradient(#b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); } .noVNC_status_error { background: linear-gradient(#c83737 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); } .noVNC_status_warn { background: linear-gradient(#b4b41e 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); } .noNVC_shown { display: inline; } .noVNC_hidden { display: none; } #noVNC_left_dummy_elem { flex: 1; } #noVNC_buttons { padding: 1px; flex: 1; display: flex; justify-content: flex-end; } </style>
代码中this.$route.params.ipport能够改为其余提供noVNC服务的ip端口。跨域
WebSocket协议的链接是不会验证跨域的,因此即便WebSocket的ip端口和本页面的不一样也不要紧。
这个页面是写成单独的vue文件,让其余vue经过iframe调用的,这个iframe里面的src和外部页面同域,HTML2canvas成功截到图
这是成功截图的GIF: