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: