如何解决Vue.js里面noVNC的截图问题——论可以跨域的webSocket

Posted dgutfly

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何解决Vue.js里面noVNC的截图问题——论可以跨域的webSocket相关的知识,希望对你有一定的参考价值。

  noVNC可以给linux系统提供基于VNC虚拟桌面的WEB服务,这使得openstack使用noVNC对外提供虚拟机的WEB版虚拟桌面。

  不过用这个noVNC也有一些问题,在使用html2canvas截图或者使用一些需要外部操控的操作就出问题。

  经查,HTML2canvas这个js控件的工作原理是读取HTML元素,但是noVNC或openstack提供的noVNC窗口url都是与现在用的系统不同域(简单来说这些服务就是运行在不同的机子上),这一步因为headers不支持跨域的问题失败了——下载的截图noVNC画面部分为空白,键入F12查看控制台,显示Uncaught SecurityError: Failed to execute ‘toDataURL‘ on ‘HTMLCanvasElement‘: Tainted canvases may not be exported.

  网上很多解决方案,无一不是在服务端修改配置、修改headers、修改HTML2canvas参数使被跨域的服务支持跨域,但是画面截图这个功能,发起者是外部系统,跨的域是noVNC或openstack的ip和端口,没有支持跨域的配置,又不能随意修改里面的代码,修改HTML2canvas参数也无效。

  这时就有一个想法:既然noVNC的窗口本质上就是一堆HTML代码,是否可以将代码直接贴在本系统上?

  noVNC窗口代码

技术图片
<!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的代码

  从代码里面可以看到,传输noVNC虚拟桌面关键点在225行的url对应的webSocket链接,而这个链接恰好就是noVNC提供服务的ip和端口,实际上完全可以把整个页面内嵌在提供虚拟桌面的窗口,或者写在同域系统里面,让其他页面在iframe框架里面调用。

  我们项目用的是Vue.js,为了使页面能适应Vue系统,把代码重构成了这样:

技术图片
<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>
Vue版noVNC虚拟桌面

  代码中this.$route.params.ipport可以改成其他提供noVNC服务的ip端口。

  WebSocket协议的连接是不会验证跨域的,所以即使WebSocket的ip端口和本页面的不同也没关系。

  这个页面是写成单独的vue文件,让其他vue通过iframe调用的,这个iframe里面的src和外部页面同域,HTML2canvas成功截到图

以上是关于如何解决Vue.js里面noVNC的截图问题——论可以跨域的webSocket的主要内容,如果未能解决你的问题,请参考以下文章

Vue.js项目实战-打造线上商城

vue.js 2.x 版本script里面的dom被过滤,从而获取不到dom字符串的解决方案

如何解决QQ热键冲突的问题

vue.js初级教程--02.环境搭建

如何在 Vue.js 中的 4 个元素之后每次添加一个元素?

前端框架论短长