很久很久以前,在我对计算机还一无所知的时候,看到一个漂亮的网页,我总是想把它下载下来,但每次这样做了页面要不就缺东西要不就乱掉。

而最近有这么一个需求,其他同学在某网站上做了一个作品,但是这个作品只能在网页里面在线看,页面里面还有广告,无论是将这个作品导出还是去除广告什么的都需要钱钱。

老师希望把这个页面能放在一个机器上开机全屏显示,这个我会,这很简单,用Electron就好啦,然后老师不希望看到广告,这个也简单,用Electron往里注入一个脚本给它关了就好啦。

本来这样就好了,但仅仅这样不能满足我,这样毕竟还是依赖服务器的,万一哪天网站样式一改,地址一改或者有什么不测之类的话软件不就不能用了嘛。

于是,我要开始整活了。

中间人攻击

假如有这样一个代理服务器,它把Electron发出的所有请求都缓存下来存磁盘里面,每个请求只请求第一次,我把所有可能发出的请求在一开始都请求一遍,这样不就好了嘛。(“Electron,证书错误什么的,睁一只眼闭一只眼就过去了

于是我拿出Golang,写(copy)了一个代理服务器,代理服务器这东西我也不懂,似乎无法正常代理,在我修BUG的时候,忽然想到,这个方法可能并不最优。

各种事情我总是喜欢先拿Golang做,但是代理服务器这个功能做在Electron的主进程里面岂不是更好,不用考虑需要开代理服务器进程的事情了。

请求缓存本来是打算用Golang里的SQL库存,JS可能没有轻量的SQL库,找了一下,JS有个叫LevelDB的键值数据库,于是,新技术路线可行且更优 ψ(`∇´)ψ

拦截Electron请求

正当我打算这么做时,想到,Electron既然这么灵活,应该有给提供拦截URL然后替换成其他东西的接口吧?浏览器的F12都有这个功能虽然从来没用过

查了好多东西,找到了藏得很深的BrowserWindow.webContents.session.webRequest (恕我没翻完官方文档)(~ ̄▽ ̄)~

进一步研究,这个里面提供的一些方法能够在HTTP请求的各个阶段该里面的某些信息、重定向之类的,但是这个东西拦截的请求就是拦截了,这样的请求直接失败,不拦截的请求就发出去了,而重定向可以重定向给某个本地文件,但是会有后遗症,这样html的URL会变。

然后发现了BrowserWindow.webContents.session.protocol这个东西,这个东西乍一看没什么,不就是注册自定义协议的嘛,注册个my://这样的东西,但是它能注册http://https://

于是,有下面这个hello world:

    let proxyHandler = (request: GlobalRequest): (GlobalResponse | Promise<GlobalResponse>) => {
        return fetch(request)
    }
    window.webContents.session.protocol.handle("http", proxyHandler)
    window.webContents.session.protocol.handle("https", proxyHandler)

上面的代码可以让连接正常进行,接下来就不用多说了 ( •̀ ω •́ )✧

小细节

将HTTP请求中和响应内容有关的东西拼成字符串作为键,响应内容作为值,这样大部分内容已经能够正常缓存使用了,正常一个请求200ms,缓存之后的只要1ms。

这个网站会发出大量的请求,因为使用了缓存,响应太快了,网站略微有些掉帧,于是我给缓存加了个响应随机延迟。

此时视频还不能正常响应,看了一下视频请求有range头,MDN上说range头服务器可以忽略,于是为了缓存方便直接忽略掉range头,再次测试依然不能正常响应。

经过数据包分析发现,视频未播放时像服务器请求的视频是带有只要图片的accept头,这样服务器就只发了视频首帧图片,于是将accept头的长度也作为键的一部分,完成!ψ(`∇´)ψ

最后,添加一个常量,为true就拒绝所有未缓存请求,真正离线化。

我能想到的,最大的成功就是无愧于自己的心。