利用cloudflare works边缘计算搭建在线网页代理

利用cloudflare works边缘计算搭建在线网页代理

本文最后更新于 96 天前,其中的信息可能已经有所发展或是发生改变。

很早之前就想写一篇这样的干货文,但奈何一直没有时间。但这周五就要大物考试,所以...我不咕咕咕了!借着Java实训课间的空余时间,把这篇文章写出来。正好今天是2019年的最后一天,也算在今年内把这篇文章发布出来了,而不是跨年鸽掉QAQ

工具

  1. 开源项目jsproxy
  2. 一个cloudflare账号
  3. 一个Github账号,或者一台服务器+域名

一点说明:

要利用cloudflare works边缘计算搭建在线网页代理,需要用到大神EtherDream的开源项目jsproxy

这个项目使用了Service Worker,它能让 JS 拦截网页产生的请求,并能自定义返回内容,相当于在浏览器内部实现一个反向代理。这使得绝大部分的内容处理都可以在浏览器上完成,服务器只需纯粹的转发流量。

你可以使用Github pages服务,快速搭建起页面前端,从而做到真正的serverless。当然,如果你有一台服务器+域名,你也可以把服务器放在自己的服务器上。这一步只是给cloudflare一个回源服务器,用户访问的一切流量都要经过cloudflaer服务器,而不是Github或者你的服务器。所以服务器位置并不会影响网页代理的速度,而是用户到所连接到的cloudflare服务器的速度。建议使用Github pages的服务即可,下面的教程也将演示利用Github pages搭建此代理的过程。

操作步骤

GitHub方面

  1. 登录你的Github账号,fork jsproxy项目到你的仓库中
  2. 进入你fork的jsproxy项目的setting中,启用下方的Github pages,其中项目分支选择gh-pages branch分支即可,配置见下图:Github pages页面
  3. 进入你fork的jsproxy项目的source(源代码)中,切换到gh-pages branch分支,新建一个index.html,内容为空即可。
  4. 访问你Github pages服务生成的网址,如果为白屏,没有报404错误的话,回到刚才的源代码,将index.html删除即可。3、4两部可以在Github里直接操作,也可以用git命令拉取到本地进行修改,这里不再赘述。
  5. 如果你想自定义页面的样式,可以修改gh-pages branch分支中的index_v3.html

Cloudflare方面

  1. https://dash.cloudflare.com/登录你的cloudflare账号,点击右侧大大的workers进入workers控制面板。
  2. 第一次使用workers功能,需要完成一个新手引导教程。第一步,需要选择一个cloudflare提供的*.workers.dev的二级域名,根据自己的喜好填写,按照提示next就可以了。
  3. 新手教程第二步会让你选择plan,我们白嫖党当然要选择Free Plan啦,每天有100000个请求配额,个人使用绰绰有余。
  4. 下一步可能要验证邮箱,到注册cloudflare的邮箱里点击链接激活一下就可以。
  5. 完成新手引导后,回到workers面板,点击蓝色的Create a Worker按钮,新建一个worker。
  6. 这时会打开一个带有代码编辑器的新标签页,在左侧选择Script标签,粘贴以下内容:注意在第六行里填写好你Github pages的网址,即https://xxx.github.io/jsproxy

'use strict'

/**
 * static files (404.html, sw.js, conf.js)
 */
const ASSET_URL = 'https://xxx.github.io/jsproxy'//这里填写你Github pages的网址!

const JS_VER = 10
const MAX_RETRY = 1

/** @type {RequestInit} */
const PREFLIGHT_INIT = {
  status: 204,
  headers: new Headers({
    'access-control-allow-origin': '*',
    'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS',
    'access-control-max-age': '1728000',
  }),
}

/**
 * @param {any} body
 * @param {number} status
 * @param {Object<string, string>} headers
 */
function makeRes(body, status = 200, headers = {}) {
  headers['--ver'] = JS_VER
  headers['access-control-allow-origin'] = '*'
  return new Response(body, {status, headers})
}


/**
 * @param {string} urlStr 
 */
function newUrl(urlStr) {
  try {
    return new URL(urlStr)
  } catch (err) {
    return null
  }
}


addEventListener('fetch', e => {
  const ret = fetchHandler(e)
    .catch(err => makeRes('cfworker error:\n' + err.stack, 502))
  e.respondWith(ret)
})


/**
 * @param {FetchEvent} e 
 */
async function fetchHandler(e) {
  const req = e.request
  const urlStr = req.url
  const urlObj = new URL(urlStr)
  const path = urlObj.href.substr(urlObj.origin.length)

  if (urlObj.protocol === 'http:') {
    urlObj.protocol = 'https:'
    return makeRes('', 301, {
      'strict-transport-security': 'max-age=99999999; includeSubDomains; preload',
      'location': urlObj.href,
    })
  }

  if (path.startsWith('/http/')) {
    return httpHandler(req, path.substr(6))
  }

  switch (path) {
  case '/http':
    return makeRes('请更新 cfworker 到最新版本!')
  case '/ws':
    return makeRes('not support', 400)
  case '/works':
    return makeRes('it works')
  default:
    // static files
    return fetch(ASSET_URL + path)
  }
}


/**
 * @param {Request} req
 * @param {string} pathname
 */
function httpHandler(req, pathname) {
  const reqHdrRaw = req.headers
  if (reqHdrRaw.has('x-jsproxy')) {
    return Response.error()
  }

  // preflight
  if (req.method === 'OPTIONS' &&
      reqHdrRaw.has('access-control-request-headers')
  ) {
    return new Response(null, PREFLIGHT_INIT)
  }

  let acehOld = false
  let rawSvr = ''
  let rawLen = ''
  let rawEtag = ''

  const reqHdrNew = new Headers(reqHdrRaw)
  reqHdrNew.set('x-jsproxy', '1')

  // 此处逻辑和 http-dec-req-hdr.lua 大致相同
  // https://github.com/EtherDream/jsproxy/blob/master/lua/http-dec-req-hdr.lua
  const refer = reqHdrNew.get('referer')
  const query = refer.substr(refer.indexOf('?') + 1)
  if (!query) {
    return makeRes('missing params', 403)
  }
  const param = new URLSearchParams(query)

  for (const [k, v] of Object.entries(param)) {
    if (k.substr(0, 2) === '--') {
      // 系统信息
      switch (k.substr(2)) {
      case 'aceh':
        acehOld = true
        break
      case 'raw-info':
        [rawSvr, rawLen, rawEtag] = v.split('|')
        break
      }
    } else {
      // 还原 HTTP 请求头
      if (v) {
        reqHdrNew.set(k, v)
      } else {
        reqHdrNew.delete(k)
      }
    }
  }
  if (!param.has('referer')) {
    reqHdrNew.delete('referer')
  }

  // cfworker 会把路径中的 `//` 合并成 `/`
  const urlStr = pathname.replace(/^(https?):\/+/, '$1://')
  const urlObj = newUrl(urlStr)
  if (!urlObj) {
    return makeRes('invalid proxy url: ' + urlStr, 403)
  }

  /** @type {RequestInit} */
  const reqInit = {
    method: req.method,
    headers: reqHdrNew,
    redirect: 'manual',
  }
  if (req.method === 'POST') {
    reqInit.body = req.body
  }
  return proxy(urlObj, reqInit, acehOld, rawLen, 0)
}


/**
 * 
 * @param {URL} urlObj 
 * @param {RequestInit} reqInit 
 * @param {number} retryTimes 
 */
async function proxy(urlObj, reqInit, acehOld, rawLen, retryTimes) {
  const res = await fetch(urlObj.href, reqInit)
  const resHdrOld = res.headers
  const resHdrNew = new Headers(resHdrOld)

  let expose = '*'
  
  for (const [k, v] of resHdrOld.entries()) {
    if (k === 'access-control-allow-origin' ||
        k === 'access-control-expose-headers' ||
        k === 'location' ||
        k === 'set-cookie'
    ) {
      const x = '--' + k
      resHdrNew.set(x, v)
      if (acehOld) {
        expose = expose + ',' + x
      }
      resHdrNew.delete(k)
    }
    else if (acehOld &&
      k !== 'cache-control' &&
      k !== 'content-language' &&
      k !== 'content-type' &&
      k !== 'expires' &&
      k !== 'last-modified' &&
      k !== 'pragma'
    ) {
      expose = expose + ',' + k
    }
  }

  if (acehOld) {
    expose = expose + ',--s'
    resHdrNew.set('--t', '1')
  }

  // verify
  if (rawLen) {
    const newLen = resHdrOld.get('content-length') || ''
    const badLen = (rawLen !== newLen)

    if (badLen) {
      if (retryTimes < MAX_RETRY) {
        urlObj = await parseYtVideoRedir(urlObj, newLen, res)
        if (urlObj) {
          return proxy(urlObj, reqInit, acehOld, rawLen, retryTimes + 1)
        }
      }
      return makeRes(res.body, 400, {
        '--error': `bad len: ${newLen}, except: ${rawLen}`,
        'access-control-expose-headers': '--error',
      })
    }

    if (retryTimes > 1) {
      resHdrNew.set('--retry', retryTimes)
    }
  }

  let status = res.status

  resHdrNew.set('access-control-expose-headers', expose)
  resHdrNew.set('access-control-allow-origin', '*')
  resHdrNew.set('--s', status)
  resHdrNew.set('--ver', JS_VER)

  resHdrNew.delete('content-security-policy')
  resHdrNew.delete('content-security-policy-report-only')
  resHdrNew.delete('clear-site-data')

  if (status === 301 ||
      status === 302 ||
      status === 303 ||
      status === 307 ||
      status === 308
  ) {
    status = status + 10
  }

  return new Response(res.body, {
    status,
    headers: resHdrNew,
  })
}


/**
 * @param {URL} urlObj 
 */
function isYtUrl(urlObj) {
  return (
    urlObj.host.endsWith('.googlevideo.com') &&
    urlObj.pathname.startsWith('/videoplayback')
  )
}

/**
 * @param {URL} urlObj 
 * @param {number} newLen 
 * @param {Response} res 
 */
async function parseYtVideoRedir(urlObj, newLen, res) {
  if (newLen > 2000) {
    return null
  }
  if (!isYtUrl(urlObj)) {
    return null
  }
  try {
    const data = await res.text()
    urlObj = new URL(data)
  } catch (err) {
    return null
  }
  if (!isYtUrl(urlObj)) {
    return null
  }
  return urlObj
}

之后点击下方的save and deploy部署就生效啦!记下cloudflare分配给你的workers.dev的三级域名,这就是你部署好的在线代理网址。

如果你正巧有托管在Cloudflare或它旗下的Partner的话,你可以就可以自定义网页代理的域名,不必记下冗长的三级域名,方法如下:

  1. 进入你的域名控制台,点击控制台顶部的workers标签,进入对应域名的workers设置。
  2. 点击右侧的Add route按钮,部署一条新规则。
  3. 在弹出的对话框中:Route中填写 example.yourwebstie.com/* ,其中example是网页代理的二级域名,可以自定义,Worker选择你刚刚部署的worker。部署新规则
  4. 修改example.yourwebstie.com的DNS记录为cname记录,这条cname记录指向cloudflare分配给你的workers.dev下刚刚部署好的workers站点。

Then, 在你使用没有翻墙软件的电脑时,也能利用这个网页代理随心上谷歌看油管咯~enjoy it!

本文永久链接:https://blog.xmgspace.me/archives/jsproxy-on-cloudflare.html
本文文章标题:利用cloudflare works边缘计算搭建在线网页代理
如文章内无特殊说明,只要您标明转载/引用自Xiaomage's Blog,您就可以自由的转载/引用文章。禁止CSDN/采集站采集转载。
授权协议:署名-非商业性使用-相同方式共享 4.0 国际(CC BY 4.0)

评论

  1. norman
    Windows Chrome
    2月前
    2020-9-27 11:00:51

    试了一下,YouTube视频无法播放,网页可以访问。有时谷歌会验证机器人

    • Xiaomage 博主
      Windows Edge
      2月前
      2020-9-27 16:18:33

      很正常,这个东西只能作为在没有梯子的电脑上看世界的备选方案 因为这个在线代理是基于Service Worker,基于js的,像YouTube这类很依赖js的网站可能就会不正常... 出现谷歌验证机器人是因为这个代理的出口IP是cloudflare,流量比较大,谷歌可能把这个IP标记为高风险IP了,暂时无解。只是想上谷歌的话可以看看我之前的文章,有一篇讲到了如何搭建谷歌反向代理。

      • Norman
        Windows Chrome
        2月前
        2020-10-15 23:45:39

        谢啦,我用v2rayN+cloudflare+heroku 白嫖的,jsp代理这个适合做外网新闻,我添加了外媒网站给朋友看新闻用。

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇