🏭Workers & Pages

最强大的CloudFlare Workers

讲到免费云服务必有CloudFlare,大善人出品的Workers应该是目前市面上生态最全、功能和性能最强大的免费Serverless计算平台(无服务器计算平台)了,即便放到付费服务里也是顶尖的存在

Workers一般用来直接构建并运行JS脚本,Pages则是PHP托管服务,类似于GitHub Pages,但是性能和可用性更强,Pages可以允许你直接上传网站内容,包括HTML、PHP、JS、CSS等,非常简单易用,而且可以快速更新构建,因为没有任何门槛,这里就不赘述了,下面主要是介绍几个典型的Workers项目。

EDtunnel

这是一个利用Workers快速搭建VLESS节点的项目,项目GitHub地址:https://github.com/3Kmfi6HP/EDtunnel

workers代码地址:https://raw.githubusercontent.com/3Kmfi6HP/EDtunnel/main/_worker.js

我们的备份:https://raw.githubusercontent.com/shentong0722/list/main/installsh/worker-vless.js

配置方法:替换掉开头的uuid(可以用官方推荐的Powershell -NoExit -Command "[guid]::NewGuid()命令在本机电脑上生成,也可以像我一样直接叫GPT生成一个)

部署后添加自己的域名,然后访问可以看到一个随机的学习强国页面,不用管他直接在网址后面加上/uuid就可以进入节点订阅地址页面

Redirect

这是一个极其简单的301重定向js代码,直接替换其中的网址即可使用,当然如果有特殊的需要也可以替换成302等其他重定向方法:

addEventListener("fetch", event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  return Response.redirect("example.com", 301)
}

Easy Proxy

这是一个最简易的反代代码,直接替换其中的网址即可,该方法仅适用于简易静态页面,非常适合用来反代openai api等json服务,不适合用来反代复杂页面以及里面有外部链接的页面:

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    url.host = 'ai.fakeopen.com';
    return fetch(new Request(url, request))
  }
}

Web Proxy

这是一个更强大的反代代码,可以用来反代完整的网页服务,但是对于一些流媒体等服务还是用不了的,GitHub链接:https://raw.githubusercontent.com/shentong0722/list/main/installsh/worker-proxy.js,修改里面的三处网址即可使用,完整代码:

const upstream = 'gitlab.com'
const upstream_mobile = 'gitlab.com'
const blocked_region = ['KP']
const blocked_ip_address = ['0.0.0.0', '127.0.0.1']
const replace_dict = {
    '$upstream': '$custom_domain',
    '//gitlab.com': ''
}

addEventListener('fetch', event => {
    event.respondWith(fetchAndApply(event.request));
})
 
async function fetchAndApply(request) {
 
    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;
}
 
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;
}

Web Tunnel

一个直接在网页端实现隧道的代码,可以允许用户输入任意网址然后穿过隧道,相对来说可以实现的反代类型比上面的项目更多,但是也会有很多不允许反代的内容,GitHub链接:https://raw.githubusercontent.com/shentong0722/list/main/installsh/worker-tunnel.js,300行代码这里就不放完整版了

R2

这是一个可以让CloudFlare R2直接用域名外部访问的代码,顺带一提,大善人出品的R2是一个对象存储服务,在workers设置的变量中绑定R2 存储桶即可直接使用下面的worker代码:

addEventListener("fetch", event => {
    event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
    const url = new URL(request.url)
    const objectName = url.pathname.slice(1)

    console.log(`${request.method} object ${objectName}: ${request.url}`)

    if (request.method === 'GET' || request.method === 'HEAD') {
        if (objectName === '') {
            if (request.method == 'HEAD') {
                return new Response(undefined, { status: 400 })
            }

            const options = {
                prefix: url.searchParams.get('prefix') ?? undefined,
                delimiter: url.searchParams.get('delimiter') ?? undefined,
                cursor: url.searchParams.get('cursor') ?? undefined,
                include: ['customMetadata', 'httpMetadata'],
            }
            console.log(JSON.stringify(options))

            const listing = await R2.list(options)
            return new Response(JSON.stringify(listing), {
                headers: {
                    'content-type': 'application/json; charset=UTF-8',
                }
            })
        }

        if (request.method === 'GET') {
            const range = parseRange(request.headers.get('range'))
            const object = await R2.get(objectName, {
                range,
                onlyIf: request.headers,
            })

            if (object === null) {
                return objectNotFound(objectName)
            }

            const headers = new Headers()
            //object.writeHttpMetadata(headers)
            headers.set('etag', object.httpEtag)
            const status = object.body ? (range ? 206 : 200) : 304
            return new Response(object.body, {
                headers,
                status
            })
        }

        const object = await R2.head(objectName, {
            onlyIf: request.headers,
        })

        if (object === null) {
            return objectNotFound(objectName)
        }

        const headers = new Headers()
        //object.writeHttpMetadata(headers)
        headers.set('etag', object.httpEtag)
        return new Response(null, {
            headers,
        })
    }
    if (request.method === 'PUT' || request.method == 'POST') {
        const object = await R2.put(objectName, request.body, {
            httpMetadata: request.headers,
        })
        return new Response(null, {
            headers: {
                'etag': object.httpEtag,
            }
        })
    }
    if (request.method === 'DELETE') {
        await R2.delete(url.pathname.slice(1))
        return new Response()
    }

    return new Response(`Unsupported method`, {
        status: 400
    })
}

function parseRange(encoded) {
    if (encoded === null) {
        return
    }

    const parts = encoded.split("bytes=")[1]?.split("-") ?? []
    if (parts.length !== 2) {
        throw new Error('Not supported to skip specifying the beginning/ending byte at this time')
    }

    return {
        offset: Number(parts[0]),
        length: Number(parts[1]) + 1 - Number(parts[0]),
    }
}

function objectNotFound(objectName) {
    return new Response(`<html><body>R2 object "<b>${objectName}</b>" not found</body></html>`, {
        status: 404,
        headers: {
            'content-type': 'text/html; charset=UTF-8'
        }
    })
}

URL Shorten

这是一个短链生成服务,并带有图形化界面,GitHub项目地址:https://github.com/xyTom/Url-Shorten-Worker,我们也对其进行了优化与精简(大幅优化了JS代码使其行数减少了一半,然后删除了安全审查功能)并进行了备份:

const config = {
  no_ref: "off",
  cors: "on",
  unique_link: true,
  custom_link: false,
};

const html404 = `<!DOCTYPE html>
<body>
  <h1>404 Not Found.</h1>
  <p>The url you visit is not found.</p>
  <a href="https://cn.st0722.top" target="_self">Powered by ST-STUDIO</a>
</body>`;

let response_header = {
  "content-type": "text/html;charset=UTF-8"
};

if (config.cors === "on") {
  response_header = {
    ...response_header,
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "POST"
  };
}

async function randomString(len = 6) {
  const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
  let result = '';
  for (let i = 0; i < len; i++) {
    result += $chars.charAt(Math.floor(Math.random() * $chars.length));
  }
  return result;
}

async function sha512(url) {
  const url_digest = await crypto.subtle.digest({ name: "SHA-512" }, new TextEncoder().encode(url));
  const hashArray = Array.from(new Uint8Array(url_digest));
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

async function checkURL(URL) {
  const objExp = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
  return objExp.test(URL) && URL[0] === 'h';
}

async function save_url(URL) {
  const random_key = await randomString();
  const is_exist = await LINKS.get(random_key);
  if (is_exist == null) {
    await LINKS.put(random_key, URL);
    return random_key;
  } else {
    return save_url(URL);
  }
}

async function is_url_exist(url_sha512) {
  const is_exist = await LINKS.get(url_sha512);
  return is_exist ? is_exist : false;
}

async function handleRequest(request) {
  if (request.method === "POST") {
    const req = await request.json();
    if (!(await checkURL(req.url))) {
      return new Response(`{"status":500,"key":": Error: Url illegal."}`, { headers: response_header });
    }
    let stat, random_key;
    if (config.unique_link) {
      const url_sha512 = await sha512(req.url);
      const url_key = await is_url_exist(url_sha512);
      random_key = url_key || (stat, await save_url(req.url));
    } else {
      random_key = stat, await save_url(req.url);
    }
    return new Response(`{"status":200,"key":"/${random_key}"}`, { headers: response_header });
  } else if (request.method === "OPTIONS") {
    return new Response(``, { headers: response_header });
  }

  const requestURL = new URL(request.url);
  const path = requestURL.pathname.split("/")[1];
  const params = requestURL.search;

  if (!path) {
    const html = await fetch(`https://short-url-y8h.pages.dev`);
    return new Response(await html.text(), { headers: { "content-type": "text/html;charset=UTF-8" } });
  }

  const value = await LINKS.get(path);
  const location = params ? value + params : value;

  if (location) {
    if (config.no_ref === "on") {
      const no_ref = await fetch("https://xytom.github.io/Url-Shorten-Worker/no-ref.html");
      return new Response(no_ref.replace(/{Replace}/gm, location), { headers: { "content-type": "text/html;charset=UTF-8" } });
    } else {
      return Response.redirect(location, 302);
    }
  }

  return new Response(html404, { headers: { "content-type": "text/html;charset=UTF-8" }, status: 404 });
}

addEventListener("fetch", async event => {
  event.respondWith(handleRequest(event.request));
});

注意我们修改后的代码中其中的页面图形化显示是通过抓取我们的页面文件实现的(源作者代码当然是通过抓作者自己的页面实现的),因此当我们的(或者你使用原版的就是作者的)图形化服务出现问题时,你的下游代码也是跑不起来的,介意的可以自己抓取并且自己托管。另外,(虽然用不光但还是简单提一下),这个服务会用到KV命名空间,也是我们大善人CloudFlare提供的免费服务,限制是每天读取100000次和写入1000次,注意一下消耗量。

DNS Redirect

这是一个DoH的转发服务,简单来说就是把支持DoH的DNS服务的域名改成自己的,项目地址:https://github.com/IrineSistiana/cfdohpw,代码如下:

// 请求路径。请修改此路径,避免该 worker 所有人都能使用。
const endpointPath = '/dns-query';
// 上游 DoH 地址。必需是域名,不能是 IP。Cloudflare 有限制。
const upstream = 'https://dns.google/dns-query';

async function handleRequestGet(request, clientUrl) {
  const dnsValue = clientUrl.searchParams.get('dns')

  if (dnsValue == null) {
    return new Response('missing parameters', { status: 400 });
  }

  if (request.headers.get('accept') != 'application/dns-message') {
    return new Response('bad request header', { status: 400 });
  }

  const upstreamUrl = new URL(upstream);
  upstreamUrl.searchParams.set('dns', dnsValue);
  const upstreamRequest = new Request(upstreamUrl.toString(), {
    headers: request.headers,
    method: 'GET',
  });
  upstreamRequest.headers.set('host', upstreamUrl.hostname)
  return await fetch(upstreamRequest);
}

async function handleRequestPost(request, clientUrl) {
  if (request.headers.get('content-type') != 'application/dns-message') {
    return new Response('bad request header', { status: 400 });
  }
  const upstreamRequest = new Request(upstream, {
    method: 'POST',
    headers: {
      'accept': 'application/dns-message',
      'content-type': 'application/dns-message',
    },
    body: await request.arrayBuffer()
  });
  return await fetch(upstreamRequest);
}

async function handleRequest(request) {
  const clientUrl = new URL(request.url);
  if (clientUrl.pathname != endpointPath) {
    return new Response('Hello World!', { status: 404 });
  }

  switch (request.method) {
    case 'GET':
      return handleRequestGet(request, clientUrl)
    case 'POST':
      return handleRequestPost(request, clientUrl)
    default:
      return new Response('method not allowed', { status: 405 });
  }
}

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

这个DNS转发服务的原理是很简单的,不过你要知道为什么这么做,因为DoH会加密DNS数据并且伪装成HTTPS流量,因此也是墙的重点关照对象,像AdGuard这些DoH提供商的域名经常被污染导致无法使用,所以我们就要反代DNS,此时DoH地址就是https://yourdomain.com/dns-query,在这个示例中用的是Google的DoH服务,我们推荐换成AdGuard或者NextDNS的服务;另外,原则上这个域名还可以套国内的CDN来进一步加速,但是我尝试失败了大家感兴趣可以自行摸索

Load Balancer & Port Forwarding

负载均衡与端口转发的实现,项目地址:https://github.com/KawaiiZapic/HidePortWorker,在我们那么多的教程中介绍过很多免费资源,但是免费资源的限制一般都非常严格(除了大善人CF,是真慷慨啊),那么最简单粗暴的方法就是多注册几个账号,然后利用负载均衡分散使用;另外这个项目还实现了端口转发,看代码就懂,代码如下:

let SvrGrp = [{
        "Host": "example.com",
        "Port": 4443,
        "Protocol": "https",
        "Weight": 10
}, {
        "Host": "example2.com",
        "Port": 4444,
        "Protocol": "https",
        "Weight": 10
}];

let getSrv = () => {
    let SrvMap = [];
    for(let Srv in SrvGrp) {
      let w = typeof SrvGrp[Srv].Weight != "undefined" ? SrvGrp[Srv].Weight : 0;
      while (w >= 0){
        SrvMap.push(Srv);
        --w;
      }
    }
    return SrvGrp[SrvMap[Math.floor(SrvMap.length * Math.random())]];
};

addEventListener(
  "fetch",
  (e) => {
    let Url = new URL(e.request.url);
    let Srv = getSrv();
    Url.host = Url.hostname.replaceAll(".","-") + "." + Svr.Host;
    Url.port = Svr.Port;
    Url.protocol = Svr.Protocol + ":";
    let Req = new Request(Url,e.request);
    Req.headers.set('X-Real-Host', e.request.headers.get('Host'));
    let Res = fetch(Req).then((d)=>{
      if(d.status < 500){
        return d;
      } else {
        return new Response(d.status + " " + d.statusText, {
          headers: {
             "content-type": "text/html;charset=UTF-8",
          },
          status: d.status,
          statusText: d.statusText
        });
      }
    });
    e.respondWith(Res);
  }
)

最后更新于