Springboot实现VNC的反向代理功能

背景

​ 用户需要通过前端HTML页面的noVNC(noVNC是什么?)客户端连接底层VNC Server服务端,为了防止VNC Server的IP暴露,因此需要做一层代理。正常情况下使用Nginx、Apache等都可以搞定,但是由于项目架构的一些问题,暂时不能再加一台反向代理服务器,所以决定写一个单独的模块实现反向代理的功能。

​ 在网上和Github上找了一下,使用了HTTP-Proxy-Servlet,引入该依赖搭建一个Spring Boot项目。

搭建

引入代理的依赖

<dependency>
    <groupId>org.mitre.dsmiley.httpproxy</groupId>
    <artifactId>smiley-http-proxy-servlet</artifactId>
    <version>1.12</version>
</dependency>

通过注册bean拦截指定URL路径进行自定义操作

@Configuration
public class ProxyServletConfiguration {

    // 拦截所有请求交给下面的VNCProxyServlet去处理
    private final static String SERVLET_URL = "/*";

    @Bean
    public ServletRegistrationBean<VNCProxyServlet> servletServletRegistrationBean() {
        ServletRegistrationBean<VNCProxyServlet> servletRegistrationBean = new ServletRegistrationBean<>(new VNCProxyServlet(), SERVLET_URL);
        //设置网址以及参数
        Map<String, String> params = ImmutableMap.of(
                "targetUri", "null", //这里写null是因为targetUri是在自定义的VNCProxyServlet类中动态传入的,而且这里必须要有值
                ProxyServlet.P_LOG, "true",
                ProxyServlet.P_PRESERVEHOST,"true",
                ProxyServlet.P_PRESERVECOOKIES,"true"
        );
        servletRegistrationBean.setInitParameters(params);
        return servletRegistrationBean;
    }

}

这里遇到的坑:

​ 刚开始其实是准备在已有的一个模块中加上这个代理功能,因为可以指定拦截的路径,比如只拦截请求路径为/proxy/*的,然后交给自定义的Servlet去代理,后来写好测试时,发现代理过去后代理目标主页一片空白,看了控制台的Network后,主页确实是返回200且加载正常,但是由主页发起的js、css和img等静态资源状态码都为404。

​ 当时以为是代码的问题,后来发现静态资源都是相对路径的有问题,如果前端的静态资源是引入第三方的,比如从CDN中引入Vue.js则不会出现问题,都可以正常的被代理。既然状态码是404,那肯定是找不到这个资源,看了一下发现如果在静态资源的路径前加上指定拦截的路径/proxy/就可以被正常代理。此时才明白,因为访问首页的路径中带/proxy/是在地址栏主动输入的,所以请求到后台,后台Servlet拦截发现路径中带/proxy/,把该请求交给自定义的代理Servlet去处理然后返回。而主页上的js、css等静态资源发起请求的路径是不会带/proxy/*的,因此不会走到代理Servlet,并且代理模块中也没有相应资源路径,所以就理所应当的返回了404。

​ 为此还专门在GitHub上问了一下作者,作者也是回复说这并不是这个代理模块该做的事,最好是前端处理,或者让前端使用绝对路径。附上地址(Discussions

​ 最后就是决定单独拉出来写一个Spring Boot项目做这个代理功能模块,直接代理/*,这样所有请求到这个模块的都会被代理。

自定义Servlet实现动态代理目标地址

// VNCProxyServlet继承了ProxyServlet 重写了service方法 在方法中添加自定义操作 从请求地址中动态获取
@Override
    protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException {
        // 获取请求地址
        String targetUri = servletRequest.getRequestURL().toString();

        // 正则取请求地址中的参数 参数是放在域名中的
        Matcher matcher = DOMAIN_REX.matcher(targetUri);
        if(!matcher.find()){
            // 自定义的异常
            throw new GenericException("从域名中获取vmId异常!");
        }
        // 取域名中的第一个 eg: http://vmId.xxx.cn得 [vmId,xxx,cn] 得 vmId
        Long vmId = Long.valueOf(matcher.group().split("\\.")[0]);

        // eg:业务逻辑根据vmId去拿 targetUri
    	targetUri = vmService.getTargetUrl(vmId);

        if (StringUtils.isEmpty(targetUri)) {
            throw new GenericException("代理路径不正确,请确认路径");
        }

        // 设置Url
        if (servletRequest.getAttribute(ATTR_TARGET_URI) == null) {
            servletRequest.setAttribute(ATTR_TARGET_URI, targetUri);
        }

        // 设置Host
        if (servletRequest.getAttribute(ATTR_TARGET_HOST) == null) {
            URL trueUrl = URLUtil.url(targetUri);
            servletRequest.setAttribute(ATTR_TARGET_HOST, new HttpHost(trueUrl.getHost(), trueUrl.getPort(), trueUrl.getProtocol()));
        }
		// 下面大部分都是父类的源码 没有需要特别修改的地方
        String method = servletRequest.getMethod();
        // 替换多余路径
        String proxyRequestUri = this.rewriteUrlFromRequest(servletRequest);

        HttpRequest proxyRequest;
        if (servletRequest.getHeader(HttpHeaders.CONTENT_LENGTH) != null ||
                servletRequest.getHeader(HttpHeaders.TRANSFER_ENCODING) != null) {
            proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
        } else {
            proxyRequest = this.newProxyRequestWithEntity(method, proxyRequestUri, servletRequest);
        }

        this.copyRequestHeaders(servletRequest, proxyRequest);
        setXForwardedForHeader(servletRequest, proxyRequest);
        HttpResponse proxyResponse = null;

        try {
            // Execute the request
            proxyResponse = this.doExecute(servletRequest, servletResponse, proxyRequest);

            // Process the response
            int statusCode = proxyResponse.getStatusLine().getStatusCode();

            // "reason phrase" is deprecated but it's the only way to pass
            servletResponse.setStatus(statusCode, proxyResponse.getStatusLine().getReasonPhrase());

            // copying response headers to make sure SESSIONID or other Cookie which comes from remote server
            // will be saved in client when the proxied url was redirected to another one.
            copyResponseHeaders(proxyResponse, servletRequest, servletResponse);

            if (statusCode == HttpServletResponse.SC_NOT_MODIFIED) {
                servletResponse.setIntHeader(HttpHeaders.CONTENT_LENGTH, 0);
            } else {
                copyResponseEntity(proxyResponse, servletResponse, proxyRequest, servletRequest);
            }
        } catch (Exception e) {
            handleRequestException(proxyRequest, proxyResponse, e);
        } finally {
            if (proxyResponse != null) {
                EntityUtils.consumeQuietly(proxyResponse.getEntity());
            }
        }
    }

​ 这里主要列出关键部分,因为方案还没有完结。

问题

​ 本以为这样就成功了,但是测试之后发现页面和静态资源都代理过去了,但是有一个websocket请求失败了。像noVNC这种网页版的黑窗口,早就该想到肯定是用websocket这种长链接的请求进行交互的。后来去搜了一下这个叫websockify的请求,就是最开始介绍noVNC博客中介绍的:

​ 浏览器不支持VNC,所以不能直接连接VNC,但是可以使用代理,使用noVNC通过WebSocket建立连接,而VNC Server不支持WebSocket,所以需要开启Websockify代理来做WebSocket和TCP Socket之间的转换,这个代理在noVNC的目录里,叫做websockify。

​ 此时项目是能够拦截到websockify这个请求的,但是由于servlet把这个请求当成普通的请求去代理到目标服务器,这样是无法成功的,所以要做的就是类似实现一个websocket的反向代理,搜了一下的话发现例子不是很多,大多都是在前端做的,前端作为客户端与服务端建立websocket连接,但目前的状况很明显是需要这个代理模块既做websocket服务端与web端建立连接,再作为websocket客户端与VNC 服务端建立连接,然后进行交互传递通信。

​ 后面也找到了这篇通过noVNC和websockify连接到QEMU/KVM,然后总结一下从用户发出请求到得到响应的流程:

PC Chrome(客户端) => noVNC Server(noVNC端) => websockify(websocket转TCP Socket) => VNC Server(VNC服务端) => websockify(TCP Socket转websocket) => noVNC Server(noVNC端)=> PC Chrome(客户端)

用户使用PC Chrome浏览器请求 noVNC端(因为无法直接访问VNC Server端,VNC Server是不支持Websocket连接),经由websockify将websocket转为TCP Socket请求到VNC服务端,返回TCP响应,经由websockify转换为websocket返回给客户端浏览器,这样来进行交互。整个过程 websockify 代理器是关键,noVNC 可以被放在浏览器端。

1.noVNC网页端与代理模块建立websocket通信

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
@ServerEndpoint("/websockify")
@Component
public class WebSocketServer {

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        logger.info("open...");
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        logger.info("close...");
    }

    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        logger.info(message);
    }

    /**
     * 发生错误调用的方法
     */
    @OnError
    public void onError(Session session, Throwable error) {
        logger.error("用户错误原因:"+error.getMessage());
        error.printStackTrace();
    }

}

​ 都是很常用的websocket服务端的代码,唯一要注意的是前端请求'/websockify'地址发起websocket连接时,要注意用ip,尤其是本地,使用localhost会报错,要使用127.0.0.1。最后测试连接成功,返回状态码101,并且消息可以正常接收。noVNC网页端与代理模块建立websocket通信完成。

2.代理模块与VNC Server建立websocket通信

java后台作为websocket客户端很少,大多是用Netty去写的,但是不适合目前的情况,最后还是找到了一个感觉比较合适的

public class MyWebSocketClient {
    public static WebSocketClient mWs;
    public static void main(String[] args) {
        try {
            //
            String url = "ws://172.28.132.11:8888/websocketify";
            URI uri = new URI(url);
            HashMap<String, String> httpHeadersMap = new HashMap<>();
            httpHeadersMap.put("Sec-WebSocket-Version", "13");
            httpHeadersMap.put("Sec-WebSocket-Key", "YBhzbbwLI83U5EH8Tlutwg==");
            httpHeadersMap.put("Connection","Upgrade");
            httpHeadersMap.put("Upgrade","websocket");
            httpHeadersMap.put("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36");
            httpHeadersMap.put("Cookie","token=8asda2das-84easdac-asdaqwe4-2asda-asdsadas");
            httpHeadersMap.put("Sec-WebSocket-Extensions","permessage-deflate; client_max_window_bits");
            mWs = new WebSocketClient(uri,httpHeadersMap){
                @Override
                public void onOpen(ServerHandshake serverHandshake) {
                    System.out.println("open...");
                    System.out.println(serverHandshake.getHttpStatus());
                    mWs.send("666");
                }

                @Override
                public void onMessage(String s) {
                    System.out.println(s);
                }

                @Override
                public void onClose(int i, String s, boolean b) {
                    System.out.println("close...");
                    System.out.println(i);
                    System.out.println(s);
                    System.out.println(b);
                }

                @Override
                public void onError(Exception e) {
                    System.out.println("发生了错误...");
                }
            };
            mWs.connect();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

// 调用后报错  直接关闭了连接  状态码为1002
// close...
// 1002
// Invalid status code received: 400 Status line: HTTP/1.1 400 Client must support 'binary' or 'base64' protocol

​ 发生错误后,发现关键地方,客户端必须支持 binary或base64协议,一番搜索后再Stack Overflow找到了线索,并且是Kanaka(noVNC和websockify的开发者)亲自回答的,大概意思就是你需要在构造函数中提供这些协议。

​ 然后我又在websockify.js的源码中找到了这个构造,确实需要传递一个protocols的数组参数,可是这是前端,并不知道Java如何完成这个操作。

后续

​ 首先再次感谢开源项目和各位博主大佬的分享,依旧在寻找解决方案......

到此这篇关于Springboot实现VNC的反向代理功能的文章就介绍到这了,更多相关Springboot反向代理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • spring boot实现超轻量级网关(反向代理、转发)

    在我们的rest服务中,需要暴露一个中间件的接口给用户,但是需要经过rest服务的认证,这是典型的网关使用场景.可以引入网关组件来搞定,但是引入zuul等中间件会增加系统复杂性,这里实现一个超轻量级的网关,只实现请求转发,认证等由rest服务的spring security来搞定. 如何进行请求转发呢? 熟悉网络请求的同学应该很清楚,请求无非就是请求方式.HTTP header,以及请求body,我们将这些信息取出来,透传给转发的url即可. 举例: /graphdb/** 转发到 Graph_

  • Spring boot2X负载均衡和反向代理实现过程解析

    这篇文章主要介绍了Spring boot2X负载均衡和反向代理实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 zuul 是netflix开源的一个API Gateway 服务器 所有从设备或网站来的请求都会经过Zuul到达后端的Netflix应用程序. 作为一个边界性质的应用程序,Zuul提供了动态路由.监控.弹性负载和安全功能. 实现反向代理 1.服务注册发现中心Consul 启动 consul agent -dev 2.服务端

  • Nginx反向代理springboot的jar包过程解析

    springboot项目部署到服务器常见的方式就是打成war包部署Tomcat或者打成jar包直接使用内置容易运行,很多人现在都打成war包部署到tomcat,这种方式虽然没问题 但是后期维护比较麻烦.从官方的说明中 打成jar部署是最好的方式,但是这样又有个问题 如果同时部署多个spring-boot项目 端口不一样 怎么通过域名来访问呢,接下来就需要Nginx出手了,Nginx 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器.很适合部署springboot

  • Springboot实现VNC的反向代理功能

    背景 ​ 用户需要通过前端HTML页面的noVNC(noVNC是什么?)客户端连接底层VNC Server服务端,为了防止VNC Server的IP暴露,因此需要做一层代理.正常情况下使用Nginx.Apache等都可以搞定,但是由于项目架构的一些问题,暂时不能再加一台反向代理服务器,所以决定写一个单独的模块实现反向代理的功能. ​ 在网上和Github上找了一下,使用了HTTP-Proxy-Servlet,引入该依赖搭建一个Spring Boot项目. 搭建 引入代理的依赖 <dependen

  • 利用Nginx反向代理功能解决WEB网站80端口被封的解决方法

    大陆的网络环境,都在天朝神兽的制度下让我等小P民悲剧一片:动不动就拔网线.封机房:现在更厉害的一招,从网关封杀你的80端口,一旦被封,网站域名就无法访问: 以上便废话,下面转入正题,其它就不帖了,直接给代码,利用nginx的反向代理功能,一般大家都很看懂,也不多记录了: [code] server { listen 80; server_name a.jb51.net; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOT

  • 如何阻止网站被恶意反向代理访问(防网站镜像)

    什么是反向代理? 先说说正向代理的概念: 正向代理,也就是传说中的代理,他的工作原理就像一个跳板.简单的说,我是一个用户,我访问不了某网站,但是我能访问一个代理服务器.这个代理服务器呢,他能访问那个我不能访问的网站,于是我先连上代理服务器,告诉他我需要那个无法访问网站的内容,代理服务器去取回来,然后返回给我.从网站的角度,只在代理服务器来取内容的时候有一次记录,有时候并不知道是用户的请求,也隐藏了用户的资料,这取决于代理告不告诉网站. 结论就是,正向代理是一个位于客户端和原始服务器(origin

  • Windows安装nginx1.10.1反向代理访问IIS网站

    首先去官网下载软件包,解压,路径最好不要有中文 Nginx配置的路径问题 由于在Windows下文件路径可以用"\", 也可以用"\\", 也可以用"/"作为路径做分隔符.但"\"最容易引发问题,所以要尽量避免使用. 不要添加PATH,否则会引发错误,config文件路径找不到 比如我解压在E盘 cmd命令定位到nginx.exe所在文件夹cd E:\WorkSoftWare\nginx-1.10.1 然后执行,首先保证ngi

  • 详解Nginx 反向代理、负载均衡、页面缓存、URL重写及读写分离详解

    注,操作系统为 CentOS 6.4 x86_64 , Nginx 是版本是最新版的1.4.2,所以实验用到的软件请点击这里下载: CentOS 6.4下载地址:http://www.jb51.net/softs/78243.html Nginx下载地址:http://www.jb51.net/softs/35633.html 一.前言 在前面的几篇博文中我们主要讲解了Nginx作为Web服务器知识点,主要的知识点有nginx的理论详解.nginx作为web服务器的操作讲解.nginx作为LNM

  • ubuntu系统下apache配置虚拟主机及反向代理详解

    前言 自己有两个域名blogsir.com.cn 和 s7star.cn ,都绑定在了我的腾讯云主机上,还有七星网络安全攻防平台的搭建,这里记录下我搭建虚拟主机和反向代理的过程,下面来一起看看详细的介绍: 配置虚拟主机 $ sudo vi /etc/apache2/sites-available/000-default.conf #DocumentRoot /var/www/html ##注释掉该行 $ cat /etc/apache2/sites-available/000-default.c

  • IIS实现反向代理时Cookie域的设置方法

    反向代理 神马是反向代理?指以代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器.我们可以通过反向代理实现负载平衡.突破防火墙限制等一些非常实用的Web服务器功能,目前反向代理不管在私有云还是公有云的虚拟机上用的很多很多. 引用 IIS通过URL重写可以实现反向代理,通过简单的配置即可以将请求转发到其它内部站点. 此时被代理的所有站点的cookie的域(domai

  • Nginx反向代理与负载均衡实战篇

    反向代理 反向代理指的是以代理服务器接收用户的的访问请求,代理用户向内部服务器重新发起请求,最后把内部服务器的响应信息返回给用户.这样,代理服务器对外就表现为一台服务器,而访问内部服务器的客户端用的就是代理服务器,而不是真实网站访问用户. 为什么使用反向代理 可以起到保护网站安全的作用,因为任何来自Internet的请求都必须先经过代理服务器. 通过缓存静态资源,加速Web请求. 实现负载均衡 反向代理例子 环境说明 假如有AB两个服务器.A服务器提供web资源,并且只给内网访问.B服务器有两块

  • nginx反向代理webSocket配置详解

    最近在做项目的时候用到了webSocket协议,而且是在微信小程序中用到了webSocket,微信小程序中使用wss协议的时候不能设置端口,只能使用默认的443端口.我擦,我的https已经监听了443端口,webSocket再去监听443,肯定不行啊.要想办法解决,老大把这个问题交给我了,我愉快(手动懵逼)的接收了这个任务.想到了两种办法解决.一种解决办法是把webSocket部署到另一台服务器上,这样成本也太高了.另一种办法,就是使用nginx反向代理. 因为webSocket协议是基于ht

  • Nginx反向代理与负载均衡概念理解及模块使用

    目录 Nginx负载均衡概念 upstream模块的使用方法 upstream模块说明 http_proxy_module模块 完整的案例 Nginx负载均衡概念 准确来说,Nginx是作为Nginx Proxy反向代理使用的,因为这个反向代理功能表现出负载均衡的效果,所以称之为Nginx负载均衡.实现负载均衡的效果主要依赖两个组件: ngx_http_proxy_module ngx_http_upstream_module 前者是proxy代理模块,用于把请求传给服务器节点或upstream

随机推荐