Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 559|回复: 0

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

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-9-7 16:15:17 | 显示全部楼层 |阅读模式

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

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

      问题重现GIF如下:

     

      经查,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成功截到图

      这是成功截图的GIF:

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-4-19 21:15 , Processed in 0.080559 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表