lichao 发表于 2023-8-21 19:59:40

Cloudflare之Workers初试

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


## 前言

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

* 可免费使用,不需要域名备案   
* 支持全站https,不用自己配置ssl证书(包括免费和收费的)
* 从国内用户角度看,对国外服务器会加速网络访问速度,对国内服务器会降低网络访问速度   
* 提供附加服务如对象存储,Workers,Pages   
   
(PS:如果你的域名是备案过的且服务器在国内,还是建议使用DNSPod和各大云服务器厂商提供的类似服务)   

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

## 学习

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

```js
// 返回普通页面
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",
      },
    });
},
};
```

```js
// 返回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",
      },
    });
},
};
```

```js
// 从其他服务器请求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);
},
};
```

```js
// 以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);
},
};
```

```js
// 从其他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();
    const results = await Promise.all([
      gatherResponse(responses),
      gatherResponse(responses),
    ]);
    return new Response(results.join(), init);
},
};
```

```js
// 对响应的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;
},
};

```

```js
// 获取地理位置
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",
      },
    });
},
};

```

```js
// 修改响应的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;
    }
},
};
```

## 应用

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

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

&emsp;&emsp;这是笔者设计的安全模型(这里仅以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
```

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

```js
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("/");
      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

&emsp;&emsp;正常情况下从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
```


页: [1]
查看完整版本: Cloudflare之Workers初试