Cloudflare之Workers初试
本帖最后由 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/>
```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;
}
},
};
```
## 应用
  假设笔者有以下域名绑定:
```
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台服务器,且速度也快了,
```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
  正常情况下从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]