使用 CloudFlare Workers 搭建 telegram 频道镜像站

更新

一次偶遇

昨天在咱的 让图片飞起来 oh-my-webp.sh ! 收到了 ChrAlpha 大佬的回复,咱就拜访了一下大佬的博客😂,无意间发现 Cloudflare Worder 免费搭建镜像站 这篇博客。于是呐,咱也想着能不能玩一玩这个 Workers 。虽然之前听说过有用 Workers 做很多好玩的事儿,比如加速网站、代理 Google 镜像站什么的。不过这些对于咱来说不是很刚需就没有折腾。刚好咱的 telegram 电报频道 RSS Kubernetes 人也比较多了,为了提高一点影响力,咱就想着能不能把频道的预览界面 t.me/s/rss_kubernetes 反代到咱域名上。虽然之前尝试使用 nginx 进行反代,但是效果不尽人意,于是当时就弃坑了。在春节的时候咱也看到过有人反代 2019-nCoV疫情实时播报🅥 ,比如 2019ncov.ga,不过当时那个项目折腾起来也是很麻烦,咱这种菜鸡还是溜了溜了😂。直到今天看到 Cloudflare Worder 免费搭建镜像站 这篇博客后就心血来潮,就搞一搞吧😂

劝退一连

首先你要有个 Cloudflare 账户,这是必须的。关于 Cloudflare 的注册咱就不多说啦,不过咱倒是建议大家伙把域名的 DNS 解析放到 Cloudflare 上来,好处多多:有把 https 小绿锁、免费的 加速 减速 CDN (墙内)、域名访问统计等等可玩性比较高😋。需要注意的是 Cloudflare 的 Worker 一天 10 万次免费额度,也够咱喝一壶的啦,不用担心不够用。

新建 Worker

登录到 Cloudflare大盘面板,点击左上角的 Menu —-> Workers 进入到 Workers 页面。新注册的用户会提示设置一个 workers.dev 顶级域名下的二级子域名,这个子域名设置好之后是不可更改的,之后你新创建的 Worker 就会使以这个域名而二级子域名开始的,类似于 WorkerName.yousetdomain.workers.devyousetdomain 就是你要设置的二级子域名,WorkerName 可以自定义,默认是随机生成的。也可以给自己的域名添加一条 CNAME 到 WorkerName.yousetdomain.workers.dev ,这样使用自己的域名就可以访问到 Worker 了。

设置好二级子域名之后选择 free 套餐计划,然后进入到 Worker 管理界面,创建一个新的 Worker 然后在 Script 输入框里填入以下代码。俗话说代码写得好,同行抄到老,咱的 worker.js 代码是参考自 Workers-Proxy ,不过咱去掉了一些无关紧要的代码,原代码是加入了辨别移动设备适配域名、屏蔽某些 IP 和国家的功能。对于咱的 telegram 频道镜像站来说,这都是多余的,于是就去掉了。

image-20200326184802562

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Website you intended to retrieve for users.
const upstream = 't.me'

// Custom pathname for the upstream website.
const upstream_path = '/s/rss_kubernetes'

// Whether to use HTTPS protocol for upstream address.
const https = true

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

async function fetchAndApply(request) {

let response = null;
let url = new URL(request.url);
let url_hostname = url.hostname;

if (https == true) {
url.protocol = 'https:';
} else {
url.protocol = 'http:';
}

var upstream_domain = upstream;
url.host = upstream_domain;
if (url.pathname == '/') {
url.pathname = upstream_path;
} else {
url.pathname = upstream_path + url.pathname;
}

let method = request.method;
let request_headers = request.headers;
let new_request_headers = new Headers(request_headers);

new_request_headers.set('Host', url.hostname);
new_request_headers.set('Referer', url.hostname);

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;

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_hostname);
} else {
original_text = original_response_clone.body
}

response = new Response(original_text, {
status,
headers: new_response_headers
})
let text = await response.text()

let modified = text.replace(/telegram.org/g, "telegram.k8srss.workers.dev")

return new Response(modified, {
status: response.status,
statusText: response.statusText,
headers: response.headers
})
return response;
}

需要注意的是,像 t.me 域名下的站点,比如我的 https://t.me/s/rss_kubernetes ,它的 js 和 css 样式文件是使用的 telegram.org 域名。所以我们需要在 replace_dict 那里定义好替换的正则表达式,https://t.me/s/rss_kubernetes页面里的 telegram.org 同样进行反代才行,这需要为 telegram 建一个单独的 Worker 😑。这也是评论区 ChrAlpha 小伙伴提到的办法。

1
2
3
4
5
6
7
8
[email protected]:~$ curl https://t.me/s/rss_kubernetes | grep "<script src="
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 141k 100 141k 0 0 85287 0 0:00:01 0:00:01 --:--:-- 85237
<script src="//telegram.org/js/jquery.min.js"></script>
<script src="//telegram.org/js/jquery-ui.min.js"></script>
<script src="//telegram.org/js/widget-frame.js?29"></script>
<script src="//telegram.org/js/telegram-web.js?8"></script>

修改下处代码为,将https://t.me/s/rss_kubernetes页面里的 telegram.org 同样进行一次反代。这样访问到 https://t.me/s/rss_kubernetes页面时,把的 telegram.org 替换为另一个 Worker 的域名,比如我的 telegram.k8srss.workers.dev 。不过像频道里的图片、文件、视频等资源 telegram 是使用的 CDN ,而且有好几个域名……这点很僵硬,暂时找不到合适的办法。貌似一个 Worker 只能反代一个域名?如果汝有合适的办法,欢迎与咱交流,咱感激不尽😋

1
let modified = text.replace(/telegram.org/g, "telegram.k8srss.workers.dev")

这样再使用 curl 访问测试一下,原来的 telegram.org 已经全部替换成 telegram.k8srss.workers.dev 了😂。现在墙内用户也可以无痛访问啦。在此感谢 ChrAlpha 小伙伴😂提出宝贵的建议。

1
2
3
4
5
6
7
8
</main>
<script src="//telegram.k8srss.workers.dev/js/jquery.min.js"></script>
<script src="//telegram.k8srss.workers.dev/js/jquery-ui.min.js"></script>

<script src="//telegram.k8srss.workers.dev/js/widget-frame.js?29"></script>
<script src="//telegram.k8srss.workers.dev/js/telegram-web.js?8"></script>

<!-- page generated in 121.26ms -->

这个文本替换功能很好玩儿,在 Cloudflare 官方的博客里还有个 demo introducing-cloudflare-workers 。使用这个功能咱有解锁了一个玩具,稍后再讲😂。

Here is a worker which performs a site-wide search-and-replace, replacing the word “Worker” with “Minion”. Try it out on this blog post.

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

image-20200326190914520

如果你想使用这种办法反代其他频道,只需要把 const upstream_path = '/s/rss_kubernetes' 后的 rss_kubernetes 替换为你想要代理的 telegram 频道 username 即可。之所以加上 upstream_path 而不反代整个 t.me 是为了防止别人滥用,毕竟 10W 次不经薅😂

改进

周四晚上睡觉前在推特上发了个推文,向大家请教了一下之前一个 Worker 里只能反代一个域名的问题。第二天 @Echowxsy 就回复咱了,而且还特意注册了 CouldFare 账号使用 Workers 帮咱测试了一下。在此非常感谢 @Echowxsy 帮咱。按照 @Echowxsy 小伙伴所说的:

我没用过 CloudFlare,不过我看了一下你的blog,貌似可以用两个upstream实现这个功能。 在modified那里替换为当前worker的地址,然后在后面加上一个不会重复的路径,例如xxx。 然后在fetchAndApply里面判断,如果当前请求的pathname=/xxx,使用upstream2,否则使用upstream1。 理论上是可以实现的。

就是一个 Workers 可以做很多事情,他实际上就是 Node.js 代码。 然后这里是将 http://telegram.org/xxxx 映射到 http://tg.k8s.li/static/xxxx 。 然后在 Workers 里面判断,如果有 /static 则从 http://telegram.org 获取,否则从 http://t.me 获取。

此处引用 @Echowxsy推文

Woerker 首先我们获取完要反代的 https://t.me/s/rss_kubernetes 页面 html 源码,然后使用 replace 函数把 telegram.org 以及cdn[1-5].telesco.pe 等域名进行替换,替换为 /static 、/cdn[1-5] 等不同的 url 路径。然后将修改后的 html 页面返回给客户端。客户端客户端在请求 /static 时 Worker 就会去 http://telegram.org 获取相应的资源返回给用户。这样就实现了一个 Worker 反代多个域名骚操作。修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
const upstream_me = 't.me';
const upstream_org = 'telegram.org';

// Custom pathname for the upstream website.
const upstream_path = '/s/rss_kubernetes';

// Whether to use HTTPS protocol for upstream address.
const https = true;

// Replace texts.
const replace_dict = {
$upstream: '$custom_domain'
};

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

async function fetchAndApply(request) {
let response = null;
let url = new URL(request.url);
let url_hostname = url.hostname;

if (https == true) {
url.protocol = 'https:';
} else {
url.protocol = 'http:';
}

var upstream_domain = upstream_me;

// Check telegram.org
let pathname = url.pathname;
console.log(pathname);
if (pathname.startsWith('/static')) {
console.log('here');
upstream_domain = upstream_org;
url.pathname = pathname.replace('/static', '');
} else {
if (pathname == '/') {
url.pathname = upstream_path;
} else {
url.pathname = upstream_path + url.pathname;
}
}

url.host = upstream_domain;

let method = request.method;
let request_headers = request.headers;
let new_request_headers = new Headers(request_headers);

new_request_headers.set('Host', url.hostname);
new_request_headers.set('Referer', url.hostname);

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;

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_hostname
);
} else {
original_text = original_response_clone.body;
}

response = new Response(original_text, {
status,
headers: new_response_headers
});
let text = await response.text();

// Modify it.
let modified = text.replace(/telegram.org/g,'tg.k8s.li/static');

// Return modified response.
return new Response(modified, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}

自定义域名

回到 Workers 的管理页面,点击 rename 即可修改 Worker 的三级子域名。不过咱还是不太喜欢 WorkerName.yousetdomain.workers.dev 这么长的域名,想使用咱自己的二级子域名访问。

首先回到域名管理的页面,进入到自己域名顶部那一栏里的 Workers ,在那里添加相应的路由即可。

image-20200326191340166

点击 Add Route ,在 Route 那一栏输入好自己的域名,注意最后的 /* 也要加上,然后 Worker 选择刚刚创建的那个即可。接着再添加 CNAME 记录到自己的 WorkerName.yousetdomain.workers.dev ,这样就能使用自己的域名访问啦😋

image-20200326191435923

文本替换

前文提到的文本替换功能帮咱解决了一个小问题。咱友链关注的一个博客 chanshiyu.com 没有提供 RSS ,他是将博客内容放在 GitHub issue 上,所以只能通过 RSSHUB 来订阅 GitHub 的 issue 来获取博客的更新。但 RSSHUB 获取的是 GitHub issue 的链接而非原博客的链接,于是咱想了想就用 Worker 进行替换不得了。

通过 RSSHUB 获取到的 RSS 数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>
<![CDATA[ chanshiyucx/blog Issues ]]>
</title>
<link>https://github.com/chanshiyucx/blog/issues</link>
<atom:link href="http://rsshub.app/github/issue/chanshiyucx/blog" rel="self" type="application/rss+xml"/>
<description>
<![CDATA[
chanshiyucx/blog Issues - Made with love by RSSHub(https://github.com/DIYgod/RSSHub)
]]>
</description>
<generator>RSSHub</generator>
<webMaster>[email protected] (DIYgod)</webMaster>
<language>zh-cn</language>
<lastBuildDate>Thu, 26 Mar 2020 11:00:06 GMT</lastBuildDate>
<ttl>60</ttl>
<item>
<title>
<![CDATA[ Telegram 电报机器人 ]]>
</title>
<description>
<![CDATA[
]]>
</description>
<pubDate>Wed, 25 Mar 2020 03:54:04 GMT</pubDate>
<guid isPermaLink="false">https://github.com/chanshiyucx/blog/issues/108</guid>
<link>https://github.com/chanshiyucx/blog/issues/108</link>

其中的 <guid isPermaLink="false"><link> 中的链接 github.com/chanshiyucx/blog/issues/ 替换为 chanshiyu.com/#/post/ 即可。于是还是同样的方法新建一个 Worker,然后修改一下 worker.js 的代码就可以啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Website you intended to retrieve for users.
const upstream = 'rsshub.app'

// Custom pathname for the upstream website.
const upstream_path = '/github/issue/chanshiyucx/blog'

// Whether to use HTTPS protocol for upstream address.
const https = true

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

async function fetchAndApply(request) {

let response = null;
let url = new URL(request.url);
let url_hostname = url.hostname;

if (https == true) {
url.protocol = 'https:';
} else {
url.protocol = 'http:';
}

var upstream_domain = upstream;
url.host = upstream_domain;
if (url.pathname == '/') {
url.pathname = upstream_path;
} else {
url.pathname = upstream_path + url.pathname;
}

let method = request.method;
let request_headers = request.headers;
let new_request_headers = new Headers(request_headers);

new_request_headers.set('Host', url.hostname);
new_request_headers.set('Referer', url.hostname);

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;

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_hostname);
} else {
original_text = original_response_clone.body
}

response = new Response(original_text, {
status,
headers: new_response_headers
})
let text = await response.text()

// Modify it.
let modified = text.replace(/github.com\/chanshiyucx\/blog\/issues\//g, "chanshiyu.com\/#\/post\/")

// Return modified response.
return new Response(modified, {
status: response.status,
statusText: response.statusText,
headers: response.headers
})
return response;
}

部署好 Worker 之后就可以使用 RSS 来订阅啦😋

image-20200326193643610

解锁更多好玩儿的😋

关于 Cloudflare 的 Workers 还有更多好玩的等待你去发现,咱就推荐一下啦:

最后宣传一下咱的 @rss_kubernetes 频道,国内用户可以访问 tg.k8s.li,如果你对 docker 、K8s、云原生等感兴趣,就到咱碗里来吧😂。不订阅咱的频道也可以通过咱的 tg.k8s.li 镜像站来查看 RSS 推送信息。

image-20200327003346742

GitHub page 又被墙一波,rsshub.app 也被墙掉了。墙越来越高了,这个社会也来越可笑了……不知道未来的互联网会变成什么样子,但我们作为一只屁民能做的就是不为墙添砖加瓦,不为极权专制独裁暴政唱赞歌。最后一张自己制作 kindle 电子书时喜欢使用的封面图片送给大家。