关于 CloudFlare Workers 的奇技淫巧(修改版)

2024 年 4 月 28 日 星期日
/ , , ,
20
1

阅读此文章之前,你可能需要首先阅读以下的文章才能更好的理解上下文。

关于 CloudFlare Workers 的奇技淫巧(修改版)

基础

首先你要有个 Cloudflare 账户, 这是必须的. 需要注意的是 [Cloudflare] 的 [Workers] 一天只有 10 万次免费额度, 不过已经够用了.

Hello World!

登录到 Cloudflare 的面板, 先点击右上角的 English(US) 并换成 简体中文

点击左上角的 菜单 -> Workers 进入到 Workers 页面. 新注册的用户会提示设置一个 workers.dev 顶级域名下的二级子域名, 这个子域名设置好之后是可更改的, 之后你新创建的 Worker 就会使以这个域名而二级子域名开始的.

设置好二级子域名之后选择 Free 套餐计划————这样可以白嫖, 然后进入到 Worker 管理界面, 创建一个新的 Worker 然后在 Script 输入框里填入以下代码. worker.js 代码可以参考 xiaozhu20007/CFWorkers.

const t_url = "https://raw.githubusercontent.com"; // 目标

addEventListener("fetch", (event) => {
  event.respondWith(
    handleRequest(event.request).catch(
      (err) => new Response(err.stack, { status: 500 })
    )
  );
});
async function handleRequest(request) {
  const { pathname, search } = new URL(request.url);
  let html = await fetch(t_url + pathname + search);
  return html;
}

修改好代码之后点击左下角的 Save and Deploy 然后 Preview 看看页面是否显示正常,如果显示正常恭喜你成功啦.

进阶

优化 Google Analytics 使用体验

代码如下:

/* 转载并修改自 https://raw.githubusercontent.com/SukkaW/cloudflare-workers-async-google-analytics/master/worker.js */

//const AllowedReferrer = 'skk.moe'; // ['skk.moe', 'suka.js.org'] multiple domains is supported in array format

addEventListener("fetch", (event) => {
  event.respondWith(response(event));
});

async function senData(event, url, uuid, user_agent, page_url) {
  const encode = (data) => encodeURIComponent(decodeURIComponent(data));

  const getReqHeader = (key) => event.request.headers.get(key);
  const getQueryString = (name) => url.searchParams.get(name);

  const reqParameter = {
    headers: {
      Host: "www.google-analytics.com",
      "User-Agent": user_agent,
      Accept: getReqHeader("Accept"),
      "Accept-Language": getReqHeader("Accept-Language"),
      "Accept-Encoding": getReqHeader("Accept-Encoding"),
      "Cache-Control": "max-age=0",
    },
  };

  const pvData = `tid=${encode(getQueryString("ga"))}&cid=${uuid}&dl=${encode(
    page_url
  )}&uip=${getReqHeader("CF-Connecting-IP")}&ua=${user_agent}&dt=${encode(
    getQueryString("dt")
  )}&de=${encode(getQueryString("de"))}&dr=${encode(
    getQueryString("dr")
  )}&ul=${getQueryString("ul")}&sd=${getQueryString("sd")}&sr=${getQueryString(
    "sr"
  )}&vp=${getQueryString("vp")}`;

  const perfData = `plt=${getQueryString("plt")}&dns=${getQueryString(
    "dns"
  )}&pdt=${getQueryString("pdt")}&rrt=${getQueryString(
    "rrt"
  )}&tcp=${getQueryString("tcp")}&srt=${getQueryString(
    "srt"
  )}&dit=${getQueryString("dit")}&clt=${getQueryString("clt")}`;

  const pvUrl = `https://www.google-analytics.com/collect?v=1&t=pageview&${pvData}&z=${getQueryString(
    "z"
  )}`;
  const perfUrl = `https://www.google-analytics.com/collect?v=1&t=timing&${pvData}&${perfData}&z=${getQueryString(
    "z"
  )}`;

  await fetch(pvUrl, reqParameter);
  await fetch(perfUrl, reqParameter);
}

async function response(event) {
  const url = new URL(event.request.url);

  const getReqHeader = (key) => event.request.headers.get(key);

  const Referer = getReqHeader("Referer");
  const user_agent = getReqHeader("User-Agent");
  const ref_host = (() => {
    try {
      return new URL(Referer).hostname;
    } catch (e) {
      return "";
    }
  })();

  let needBlock = false;

  needBlock =
    !ref_host ||
    ref_host === "" ||
    !user_agent ||
    !url.search.includes("ga=UA-")
      ? true
      : false;

  if (
    typeof AllowedReferrer !== "undefined" &&
    AllowedReferrer !== null &&
    AllowedReferrer
  ) {
    let _AllowedReferrer = AllowedReferrer;

    if (!Array.isArray(AllowedReferrer)) _AllowedReferrer = [_AllowedReferrer];

    const rAllowedReferrer = new RegExp(_AllowedReferrer.join("|"), "g");

    needBlock = !rAllowedReferrer.test(ref_host) ? true : false;
    console.log(_AllowedReferrer, rAllowedReferrer, ref_host);
  }

  if (needBlock) {
    return new Response("403 Forbidden", {
      headers: { "Content-Type": "text/html" },
      status: 403,
      statusText: "Forbidden",
    });
  }

  const getCookie = (name) => {
    const pattern = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
    const r = (getReqHeader("cookie") || "").match(pattern);
    return r !== null ? unescape(r[2]) : null;
  };

  const createUuid = () => {
    let s = [];
    const hexDigits = "0123456789abcdef";
    for (let i = 0; i < 36; i++) {
      s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
    }
    s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
    s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
    s[8] = s[13] = s[18] = s[23] = "-";

    return s.join("");
  };

  const _uuid = getCookie("uuid");
  const uuid = _uuid ? _uuid : createUuid();

  // To sent data to google analytics after response id finished
  event.waitUntil(senData(event, url, uuid, user_agent, Referer));

  // Return an 204 to speed up: No need to download a gif
  let response = new Response(null, {
    status: 204,
    statusText: "No Content",
  });

  if (!_uuid)
    response.headers.set(
      "Set-Cookie",
      `uuid=${uuid}; Expires=${new Date(
        new Date().getTime() + 365 * 86400 * 30 * 1000
      ).toGMTString()}; Path='/';`
    );

  return response;
}

如此,我们还可以用 CFW 做到镜像网站同时限定指定人访问:

// 替换成你想镜像的站点
const upstream = 'google.com'
 
// 如果那个站点有专门的移动适配站点,否则保持和上面一致
const upstream_mobile = 'm.google.com'
 
// 密码访问
 
const openAuth = false //开启?
const username = 'username' //用户名?
const password = 'password' //密码?
 
// 禁止哪些国家访问
const blocked_region = ['RU']
 
// 禁止IP访问
const blocked_ip_address = ['0.0.0.0', '127.0.0.1']
 
// 替换成你想镜像的站点
const replace_dict = {
    '$upstream': '$custom_domain',
    '//google.com': ''
}
 
function unauthorized() {
  return new Response('Unauthorized', {
    headers: {
      'WWW-Authenticate': 'Basic realm="CloudFlareWorkers"',
      'Access-Control-Allow-Origin': '*'
    },
    status: 401
  });
}
 
function parseBasicAuth(auth) {
    try {
      return atob(auth.split(' ').pop()).split(':');
    } catch (e) {
      return [];
    }
}
 
function doBasicAuth(request) {
  const auth = request.headers.get('Authorization');
 
  if (!auth || !/^Basic [A-Za-z0-9._~+/-]+=*$/i.test(auth)) {
    return false;
  }
 
  const [user, pass] = parseBasicAuth(auth);
  return user === username && pass === password;
}
 
 
async function fetchAndApply(request) {
  if (request.method === 'OPTIONS') // allow preflight request
    return new Response('', {
      status: 200,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': '*',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, HEAD, OPTIONS'
      }
    });
 
  if (openAuth && !doBasicAuth(request)) {
    return unauthorized();
  }
    const region = request.headers.get('cf-ipcountry').toUpperCase();
    const ip_address = request.headers.get('cf-connecting-ip');
    const user_agent = request.headers.get('user-agent');
 
    let response = null;
    let url = new URL(request.url);
    let url_host = url.host;
 
    if (url.protocol == 'http:') {
        url.protocol = 'https:'
        response = Response.redirect(url.href);
        return response;
    }
 
    if (await device_status(user_agent)) {
        upstream_domain = upstream
    } else {
        upstream_domain = upstream_mobile
    }
 
    url.host = upstream_domain;
 
    if (blocked_region.includes(region)) {
        response = new Response('Access denied: WorkersProxy is not available in your region yet.', {
            status: 403
        });
    } else if(blocked_ip_address.includes(ip_address)){
        response = new Response('Access denied: Your IP address is blocked by WorkersProxy.', {
            status: 403
        });
    } else{
        let method = request.method;
        let request_headers = request.headers;
        let new_request_headers = new Headers(request_headers);
 
        new_request_headers.set('Host', upstream_domain);
        new_request_headers.set('Referer', url.href);
 
        let original_response = await fetch(url.href, {
            method: method,
            headers: new_request_headers
        })
 
        let original_response_clone = original_response.clone();
        let original_text = null;
        let response_headers = original_response.headers;
        let new_response_headers = new Headers(response_headers);
        let status = original_response.status;
 
        new_response_headers.set('access-control-allow-origin', '*');
        new_response_headers.set('access-control-allow-credentials', true);
        new_response_headers.delete('content-security-policy');
        new_response_headers.delete('content-security-policy-report-only');
        new_response_headers.delete('clear-site-data');
 
        const content_type = new_response_headers.get('content-type');
        if (content_type.includes('text/html') && content_type.includes('UTF-8')) {
            original_text = await replace_response_text(original_response_clone, upstream_domain, url_host);
        } else {
            original_text = original_response_clone.body
        }
 
        response = new Response(original_text, {
            status,
            headers: new_response_headers
        })
    }
    return response;
}
 
addEventListener('fetch', event => {
    event.respondWith(fetchAndApply(event.request).catch(err => {
      console.error(err);
      new Response(JSON.stringify(err.stack), {
        status: 500,
        headers: {
          'Content-Type': 'application/json'
        }
      });
    }));
})
 
 
async function replace_response_text(response, upstream_domain, host_name) {
    let text = await response.text()
 
    var i, j;
    for (i in replace_dict) {
        j = replace_dict[i]
        if (i == '$upstream') {
            i = upstream_domain
        } else if (i == '$custom_domain') {
            i = host_name
        }
 
        if (j == '$upstream') {
            j = upstream_domain
        } else if (j == '$custom_domain') {
            j = host_name
        }
 
        let re = new RegExp(i, 'g')
        text = text.replace(re, j);
    }
    return text;
}
 
async function device_status (user_agent_info) {
    var agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
    var flag = true;
    for (var v = 0; v < agents.length; v++) { if (user_agent_info.indexOf(agents[v]) > 0) {
            flag = false;
            break;
        }
    }
    return flag;
}

通过 fetch 我们还可以实现多聚合 CDN(聚合多个 S3 储存,使用 CFW 隐藏桶的公开访问地址,通过路径指向不同的 S3 服务):

const b2csEndpoint = 'https://<桶名称>.s3.<桶地域>.backblazeb2.com'
const bitfulEndpoint = 'https://<桶名称>.s3.ladydaily.com'

async function cdnHandler(pathname) {
    var url_pathname = pathname
    console.info(url_pathname)
    if (url_pathname.startsWith('/b2cs')) {
        var file = await fetch(b2csEndpoint + url_pathname.split('/b2cs')[1]).text()
        // ! DEBUGGER !
        console.log(file)
        // ! DEBUGGER !
        return new Response(file);
    } else if (url_pathname.startsWith('/s4')) {
        var file = await (await fetch(bitfulEndpoint + url_pathname.split('/s4')[1])).text()
        return new Response(file);
    } else if (url_pathname.startsWith('/npm')) {
        // /npm/package@version/file
        console.log('https://cdn.jsdelivr.net/npm' + url_pathname.split('/npm')[1])
        var file = await fetch('https://cdn.jsdelivr.net/npm' + url_pathname.split('/npm')[1])).text()
        return new Response(file);
    } else {
        return new Response(`<?xml version="1.0" encoding="UTF-8"?><Error><Code>AccessDenied</Code><Message>Access Denied.</Message></Error>`, { status: 404 });
    }

}

export default {
    /**
     * @param {Request} req
     */
    async fetch(req) {
        try {
            const { url, pathname, search } = new URL(req.url)
            switch (pathname) {
                case '/':
                    return new Response(null, { status: 204 });
                default:
                    return await cdnHandler(pathname, search);
            }
        } catch (err) {
            /** @type {Error} */
            let e = err;
            return new Response(`Error: ${e.toString()}`, { status: 500 });
        }
    },
};

好的,那么这篇内容就到这里啦!感谢您的阅读,有任何问题欢迎评论指出~

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...