找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 164|回复: 1

Cloudflare之Workers初试

[复制链接]

307

主题

224

回帖

7178

积分

用户组: 真·技术宅

UID
2
精华
75
威望
283 点
宅币
5459 个
贡献
247 次
宅之契约
0 份
在线时间
931 小时
注册时间
2014-1-25
发表于 2023-8-21 19:59:40 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
本帖最后由 lichao 于 2023-8-26 08:33 编辑

前言

  Cloudflare是国外一款提供网络攻击防护服务的产品,相信做后台开发的朋友都熟悉。其原理是通过接管域名服务器保护域名,域名在访问时会先到达Cloudflare的边缘服务器,而边缘服务器会对请求进行处理从而防DDos。其特点有:

  • 可免费使用,不需要域名备案   
  • 支持全站https,不用自己配置ssl证书(包括免费和收费的)
  • 从国内用户角度看,对国外服务器会加速网络访问速度,对国内服务器会降低网络访问速度   
  • 提供附加服务如对象存储,Workers,Pages   

(PS:如果你的域名是备案过的且服务器在国内,还是建议使用DNSPod和各大云服务器厂商提供的类似服务)   

  Workers是Cloudflare(以下简称CF)提供的用于部署在边缘计算服务器上的执行单元,可以通过开发脚本直接处理请求。笔者最近看到该功能,简单的学习了一下,然后用它实现了一个实用的功能。  

学习

  官方教程https://developers.cloudflare.com/workers/;笔者喜欢用里面的playgroud来测试https://developers.cloudflare.com/workers/learning/playground/   

// 返回普通页面
export default {
  async fetch(request) {
    const html = `<!DOCTYPE html>
        <body>
          <h1>Hello World</h1>
          <p>This markup was generated by a Cloudflare Worker.</p>
        </body>`;
    return new Response(html, {
      headers: {
        "content-type": "text/html;charset=UTF-8",
      },
    });
  },
};
// 返回json
export default {
  async fetch(request) {
    const data = {
      hello: "world",
    };
    const json = JSON.stringify(data, null, 2);
    return new Response(json, {
      headers: {
        "content-type": "application/json;charset=UTF-8",
      },
    });
  },
};
// 从其他服务器请求json,并作为响应返回
export default {
  async fetch(request, env, ctx) {
    const someHost = "https://examples.cloudflareworkers.com/demos";
    const url = someHost + "/static/json";
    async function gatherResponse(response) {
      const { headers } = response;
      const contentType = headers.get("content-type") || "";
      if (contentType.includes("application/json")) {
        return JSON.stringify(await response.json());
      }
      return response.text();
    }
    const init = {
      headers: {
        "content-type": "application/json;charset=UTF-8",
      },
    };
    const response = await fetch(url, init);
    const results = await gatherResponse(response);
    return new Response(results, init);
  },
};
// 以301重定向信息作为响应返回
export default {
  async fetch(request) {
    const base = "https://example.com";
    const statusCode = 301;
    const url = new URL(request.url);
    const { pathname, search } = url;
    const destinationURL = `${base}${pathname}${search}`;
    console.log(destinationURL);
    return Response.redirect(destinationURL, statusCode);
  },
};
// 从其他2个url分别请求数据,结果叠加后作为响应返回
export default {
  async fetch(request) {
    const someHost = "https://examples.cloudflareworkers.com/demos";
    const url1 = someHost + "/requests/json";
    const url2 = someHost + "/requests/json";
    const type = "application/json;charset=UTF-8";
    async function gatherResponse(response) {
      const { headers } = response;
      const contentType = headers.get("content-type") || "";
      if (contentType.includes("application/json")) {
        return JSON.stringify(await response.json());
      } else if (contentType.includes("application/text")) {
        return response.text();
      } else if (contentType.includes("text/html")) {
        return response.text();
      } else {
        return response.text();
      }
    }
    const init = {
      headers: {
        "content-type": type,
      },
    };
    const responses = await Promise.all([fetch(url1, init), fetch(url2, init)]);
    const results = await Promise.all([
      gatherResponse(responses[0]),
      gatherResponse(responses[1]),
    ]);
    return new Response(results.join(), init);
  },
};
// 对响应的headers做增删改
export default {
  async fetch(request) {
    const response = await fetch(request);

    // Clone the response so that it's no longer immutable
    const newResponse = new Response(response.body, response);

    // Add a custom header with a value
    newResponse.headers.append(
      "x-workers-hello",
      "Hello from Cloudflare Workers"
    );

    // Delete headers
    newResponse.headers.delete("x-header-to-delete");
    newResponse.headers.delete("x-header2-to-delete");

    // Adjust the value for an existing header
    newResponse.headers.set("x-header-to-change", "NewValue");
    return newResponse;
  },
};
// 获取地理位置
export default {
  async fetch(request) {
    let html_content = "";
    let html_style =
      "body{padding:6em; font-family: sans-serif;} h1{color:#f6821f;}";
    html_content += "<p> Colo: " + request.cf.colo + "</p>";
    html_content += "<p> Country: " + request.cf.country + "</p>";
    html_content += "<p> City: " + request.cf.city + "</p>";
    html_content += "<p> Continent: " + request.cf.continent + "</p>";
    html_content += "<p> Latitude: " + request.cf.latitude + "</p>";
    html_content += "<p> Longitude: " + request.cf.longitude + "</p>";
    html_content += "<p> PostalCode: " + request.cf.postalCode + "</p>";
    html_content += "<p> MetroCode: " + request.cf.metroCode + "</p>";
    html_content += "<p> Region: " + request.cf.region + "</p>";
    html_content += "<p> RegionCode: " + request.cf.regionCode + "</p>";
    html_content += "<p> Timezone: " + request.cf.timezone + "</p>";
    let html = `<!DOCTYPE html>
      <head>
        <title> Geolocation: Hello World </title>
        <style> ${html_style} </style>
      </head>
      <body>
        <h1>Geolocation: Hello World!</h1>
        <p>You now have access to geolocation data about where your user is visiting from.</p>
        ${html_content}
      </body>`;
    return new Response(html, {
      headers: {
        "content-type": "text/html;charset=UTF-8",
      },
    });
  },
};
// 修改响应的html页面
export default {
  async fetch(request) {
    const OLD_URL = "developer.mozilla.org";
    const NEW_URL = "mynewdomain.com";
    class AttributeRewriter {
      constructor(attributeName) {
        this.attributeName = attributeName;
      }
      element(element) {
        const attribute = element.getAttribute(this.attributeName);
        if (attribute) {
          element.setAttribute(
            this.attributeName,
            attribute.replace(OLD_URL, NEW_URL)
          );
        }
      }
    }
    const rewriter = new HTMLRewriter()
      .on("a", new AttributeRewriter("href"))
      .on("img", new AttributeRewriter("src"));
    const res = await fetch(request);
    const contentType = res.headers.get("Content-Type");
    if (contentType.startsWith("text/html")) {
      return rewriter.transform(res);
    } else {
      return res;
    }
  },
};

应用

  假设笔者有以下域名绑定:

qwe.chao.click <-> 45.77.1.0   
rty.chao.click <-> 45.77.1.1   
uio.chao.click <-> 45.77.1.2    

  这是笔者设计的安全模型(这里仅以3域名模型做说明,可根据实际情况部署更多域名),这里0/1/2三个服务器都接入了CF,只有1/2对外服务,将uio/rty的请求转发到qwe,而0作为主服务器不对外服务,这种结构可减缓被DDos的可能,rty/uio俩域名是存在于App当中的,正常情况下用户抓包只能看到rty服务器,如果rty受到攻击且CF搞不定时,App会自动切到下一个服务器即uio进行访问,以此类推。笔者的初步方案是在rty/uio这2台服务器上用socat做http(s)端口转发:

socat -T8 TCP-LISTEN:8080,fork,reuseaddr TCP:45.77.1.0:80
socat -T8 TCP-LISTEN:8443,fork,reuseaddr TCP:45.77.1.0:443

  用过一段时间发现速度慢,http问题不大但https失败率有30%以上;而在使用workers后,rty/uio这2台服务器直接用CF提供的Workers来做,于是用免费的边缘服务器节省了自己的2台服务器,且速度也快了,

export default {
    async fetch(request) {
        const host = "qwe.chao.click";
        var relay_host;
        async function MethodNotAllowed(request) {
            return new Response("Method Not Allowed", { status: 405 });
        }
        async function PortNotAllowed(request) {
            return new Response("Port Not Allowed", { status: 406 });
        }
        if (request.method != "GET" && request.method != "POST") {
            return MethodNotAllowed(request);
        }
        const urlobj = new URL(request.url);
        if (urlobj.port != "8080" && urlobj.port != "8443") {
            return PortNotAllowed(request);
        } else if (urlobj.port == "8080") {
            relay_host = host + ":80";
        } else if (urlobj.port == "8443") {
            relay_host = host + ":443";
        }
        const old_host = request.url.split("/")[2];
        const new_url = request.url.replace(old_host, relay_host);
        const new_request = new Request(new_url, request)
        return fetch(new_request);
    },
};

注意:

  • 转发请求只能指定域名而不能是IP
  • 服务器要保证80/443端口开着不然请求特殊端口会失败,还会有其他莫名其妙的问题。

如何从CF获取真实IP

  正常情况下从CF流向我们自己后台的请求,得到的IP是CF边缘计算服务器的IP,而原始IP CF也帮我们存在了header部分,以下是CF到后台请求的header部分,可以看到X-Forwarded-For存放了真实IP:

Accept-Encoding: gzip
X-Forwarded-For: 45.77.1.100
Cf-Ray: 7fa1974de3907201-LHR
X-Forwarded-Proto: http
Cf-Visitor: {"scheme":"http"}
Cf-Ew-Via: 15
Cdn-Loop: cloudflare; subreqs=1
Accept: */*
User-Agent: curl/7.64.1
Cf-Connecting-Ip: 45.77.1.100
回复

使用道具 举报

本版积分规则

QQ|申请友链||Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-2-25 21:58 , Processed in 0.036713 second(s), 32 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表