Web应用私有化,说白了就是,把别人的网站下载下来在自己服务器上部署。

在此之前,已经做过了这个:

https://srcblog.ffeng123.win:23443/archives/84356b73-8e86-4eaf-8916-acf2c9a3b858

这个本质上是,程序作为代理,缓存一切结果,以后再次通过此代理时就不回源了,以此做到私有化,如下图:

但是这个有一个致命的问题是,条件必须允许使用这个代理(拦截器)。

正常的浏览器没有这个东西,对于需要陌生的用户直接拿浏览器访问这个需求,这就做不到了。

解决问题

要解决这个问题,理论上很简单,如下图:

在浏览器里面,没有了可编程的拦截器,只能一个一个去处理JS和HTML标签咯,只要让它们原本指向原站的东西指向我们的网站就可以啦

(。◕∀◕。)

注入JS

给HTML加一个script标签就好啦,我做了这样的工程化:

// lib
export function FormatExecFunctionJs(...funcs: Function[]): string {
    return funcs.map(f => {
        return `(()=>{${f.toString()};${f.name}();})();`
    }).join('\n')
}
// main
const hackFunctionsData = (this.cfg.hack_funcs ?? []).map(f => ({
    name: `f.name-${crypto.createHash('md5').update(f.toString()).digest('hex').substring(0, 4)}.js`,
    code: FormatExecFunctionJs(f),
}))
const hackFunctionsHtml = hackFunctionsData.reduce((html, f) => `${html}<script src="/${f.name}"></script>`, "")
for (const func of hackFunctionsData) {
    app.get(`/${func.name}`, (_, resp) => {
        resp
            .type("application/javascript")
            .send(func.code)
            .end()
    })
}

上述代码将函数数组this.cfg.hack_funcs 挂载到http服务器的多个挂载点上,并计算需要往html中插的代码hackFunctionsHtml

在用户请求html文件时插到head里面就好啦~

if (meta.headers["content-type"].includes("text/html")) {
    text = text.replace(/<head>/, (match) => {
        return `${match}${hackFunctionsHtml}`
    })
}

直接将Nodejs服务器上的一个函数toString序列化给浏览器执行。

html和js执行时拦截

JS注入之后直接替换掉原本的fetch等网络请求函数,监听dom树变化,修改src是一个看似可以的方法。

但是实际测试发现这里很难实现,注入了甚至给Image的构造函数覆盖了,给src加了setter,依然会有大量漏掉的请求。

这样做并不可行,而且监听dom树不是很优雅,效率比较低。

执行前替换host

Js、Html、Json等文件里面会有写死的地址,延迟到执行的时候拦截不可行,那么就在之前处理呗。

虽然代码是别人的,但是服务器是我们的呀,于是有了下面的代码:

const scheme = req.protocol;
const host = req.get("host")
const removeHosts = new Set(this.cfg.remove_hosts ?? [])
(d, meta) => {
  if (!isText(null, d)) {
      return d;
  }
  let text = d.toString("utf-8");
  // 替换域名
  text = text.replace(/(https?:)?\/\/[a-zA-Z0-9\-\.]+(?::\d+)?/g, (match) => {
      if (removeHosts.has(match)) {
          if (match.startsWith("http")) {
              return `${scheme}://${host}`
          } else {
              return `//${host}`
          }

      }
      return match
  })
  //
  return Buffer.from(text)
}

上面的代码通过正则表达式匹配类似于http://aa.aa//bb.cn 这样子的链接,然后查Set看看是不是要替换。

经测试,Set是必要的,不要替换每一个链接,否则会导致两个问题:

  • 像XML开头总是会有一个链接,这个是不希望替换的

  • 正则表达式会将疑似链接的JS代码换掉导致无法运行

最终效果

基于之前的页面容器化,先使用之前的容器化程序去加载页面,丰富数据库,差不多了之后,数据库交给本次的私有化服务器,即可快速高效完成页面私有化~

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