1. 前言
有道是"每一个非主流的功能实现后面都有一个非主流的业务需求",最近有一个业务需求是提供一款具备类"绿坝"功能的,核心要素是让用户访问少数指定网站,但是不能访问其他互联网网站的浏览器,由于其余一些别的非性能要求不能用市面上的浏览器,只能够用electron开搞。
实现以后将利用Electron
实现一个只能访问少数指定网站,拦截其他网络请求的浏览器的方法总结如下。
2. 实现方法
众所周知,Electron
里面有一个Chromium
内核,从原理上是可以直接作为浏览器应用的。所以要利用Electron
实现选择性的网络请求放行/拦截,和浏览器是大同小异的,只不过由于算是定制化的浏览器,我们能操作的空间更多了,除了常规的通过Proxy Server
,PAC
来筛选网络请求,还能够通过以代码在Electron
内每次触发window.open
和navigate
的时候进行拦截。
2.1 Proxy Server
拦截网络请求
常规来说,浏览器是能够通过指定Proxy Server
代理服务器和bypass list
例外列表来让所有或部分网络请求都通过代理服务器去访问互联网的。
用Proxy Server
来拦截用户网络请求是比较tricky的,就在于这种方法的核心是指定一个必然不能够生效 的代理服务器,比如http://localhost:13579
(端口瞎编一个没有服务的),来拦截任意不应让用户可以访问的网站,并通过指定bypass list
让可访问的网站能够例外,不通过这个事实并不存在的代理服务器,而直接连接互联网。
在Electron
中实现如下:
javascript
import { app } from "electron";
// 为app指定一个瞎编的proxy-server地址,从而拦截所有非预期的网络请求
app.commandLine.appendSwitch("proxy-server", "http://127.0.0.1:12580");
// 为用户可访问的互联网地址开通例外规则
app.commandLine.appendSwitch("proxy-bypass-list", "<local>;*foo.com,*.google.com");
如上方代码所示,则在该Electron应用程序中,除了符合<local>;*foo.com,*.google.com
规则的网络请求,都会被导向一个事实并不存在的代理服务器,从而被拦截,变相达到了过滤用户网络请求的目的。
2.2 PAC
实现规则代理
PAC
是一种以JS编写的规则文件,不过理论上流通的现代浏览器已经不接受直接访问PAC文件方式(file://.....
),而是要访问网络服务器上的pac文件,哪怕是localhost也行。
因此在electron
中,能够以nodejs开启一个简单的HTTP服务器,只提供对这个pac文件的访问。
Electron
中设置PAC
文件代理的方式:
js
import { app } from "electron";
// 为app指定一个网络服务器上的pac文件地址,形如:http://127.0.0.1:6666/proxy.pac
app.commandLine.appendSwitch("proxy-pac-url", config.proxy_pac_url);
简单pac文件写法,用js写的一个函数FindProxyForURL
指定规则:
js
function FindProxyForURL(url,host){
// 符合规则,可访问
if(localHostOrDomainIs(host,"qq.com")){
return "DIRECT"
}
// 其余不可访问
return "PROXY localhost:12345"
}
2.3 Electron
代码拦截
这种方法主要是在BrowserWindow
上面为特定事件绑定处理函数,要注意的是对初始窗口的事件绑定不会继承到被window.open
打开的新窗口上,因此每一个窗口都要绑定事件。
对一个BrowserWindow
的跳转/打开新窗口的事件绑定如下:
js
// 页面内跳转
currentWindow.webContents.on("will-navigate", function (event, url) {
// checkUrl()是自定义的检查函数,校验不通过后调用event.preventDefault()则可阻止跳转
if (!checkUrl(url, options)) {
showWarningDialog();
event.preventDefault();
}
});
// 打开新窗口
currentWindow.webContents.setWindowOpenHandler((detail) => {
let url = detail.url;
// 校验通过可以访问
if (checkUrl(url, options)) {
return {
action: "allow",
overrideBrowserWindowOptions: {
title: "browser window",
minimizable: BrowserWindowConfig.minimizable,
webPreferences: {
contextIsolation: true,
plugins: true,
},
},
outlivesOpener: true,
};
}
// 校验不通过,拒绝打开新窗口
return { action: "deny" };
});
之所以用setWindowOpenHandler
而不是再监听打开新窗口事件后new BrowserWindow()
是由于可能有部分新窗口的创建不是GET
方法的,在实践上setWindowOpenHandler
容错率更高。
3. 小结
经过了我们这个基于Electron的自制"绿坝"的实际应用,我个人认为在拦截的效力及灵活性上是前述第三种方法,在Electron内代码实现的方式是效果最好的,比如上面代码里的checkUrl()
方法,可以基于正则写黑名单和白名单,而且方便后期分发的时候做定制化配置,相关设置写在配置文件里不用改源码。
唉,还是成为了曾经自己最讨厌的人。