Cloudflare部署Snippets项目教程

本教程将详细介绍如何在Cloudflare中使用Snippets功能部署GitHub反代、Docker反代以及随机图项目。

准备工作

  1. 拥有一个已添加到Cloudflare的域名
  2. 登录Cloudflare账户

步骤一:进入Cloudflare Snippets页面

  1. 登录Cloudflare账户
  2. 选择你要使用的域名
  3. 在左侧菜单中找到"规则"选项并点击
  4. 在规则页面中选择"Snippets"选项卡

Cloudflare Snippets页面

步骤二:创建Snippets片段

代码由于2x.nz博客提供的,感谢2x.nz博客的贡献

创建片段页面

1. 创建GitHub反代片段

  1. 点击"创建片段"按钮
  2. 在名称输入框中输入一个描述性名称,例如"GitHub反代"
  3. 在编辑框中粘贴以下代码:
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// 域名白名单配置(仅保留需要的原生域名)
const domain_whitelist = [
'github.com',
'avatars.githubusercontent.com',
'github.githubassets.com',
'collector.github.com',
'api.github.com',
'raw.githubusercontent.com',
'gist.githubusercontent.com',
'github.io',
'assets-cdn.github.com',
'cdn.jsdelivr.net',
'securitylab.github.com',
'www.githubstatus.com',
'npmjs.com',
'git-lfs.github.com',
'githubusercontent.com',
'github.global.ssl.fastly.net',
'api.npms.io',
'github.community',
'desktop.github.com',
'central.github.com'
];

// 由白名单自动生成映射
const domain_mappings = Object.fromEntries(
domain_whitelist.map(domain => [domain, domain.replace(/\./g, '-') + '-'])
);

// 需要重定向的路径(屏蔽海外后可以不填写)
const redirect_paths = [];

// 中国大陆以外的地区重定向到原始GitHub域名
const enable_geo_redirect = true;

export default {
async fetch(request, env, ctx) {
return handleRequest(request);
}
};

async function handleRequest(request) {
const url = new URL(request.url);
// 统一转小写
const current_host = url.host.toLowerCase();
const host_header = request.headers.get('Host');
const effective_host = (host_header || current_host).toLowerCase();

// 检查是否需要重定向到原始GitHub(非中国用户)
if (enable_geo_redirect) {
const country = request.headers.get('CF-IPCountry') || '';
if (country && country !== 'CN') {
const host_prefix = getProxyPrefix(effective_host);
if (host_prefix) {
let target_host = null;
if (host_prefix && host_prefix.endsWith('-gh.')) {
const prefix_part = host_prefix.slice(0, -4);
for (const original of Object.keys(domain_mappings)) {
const normalized_original = original.trim().toLowerCase();
if (normalized_original.replace(/\./g, '-') === prefix_part) {
target_host = original;
break;
}
}
}
if (target_host) {
const domain_suffix = effective_host.substring(host_prefix.length);
const original_url = new URL(request.url);
original_url.host = target_host;
original_url.protocol = 'https:';
return Response.redirect(original_url.href, 302);
}
}
}
}

// 检查特殊路径,返回正常错误
if (redirect_paths.includes(url.pathname)) {
return new Response('Not Found', { status: 404 });
}

// 强制使用 HTTPS
if (url.protocol === 'http:') {
url.protocol = 'https:';
return Response.redirect(url.href);
}

// 从有效主机名中提取前缀
const host_prefix = getProxyPrefix(effective_host);
if (!host_prefix) {
return new Response(`Domain not configured for proxy. Host: ${effective_host}, Prefix check failed`, { status: 404 });
}

// 根据前缀找到对应的原始域名
let target_host = null;

// 解析 *-gh. 模式
if (host_prefix && host_prefix.endsWith('-gh.')) {
const prefix_part = host_prefix.slice(0, -4); // 移除 -gh.
// 尝试找到对应的原始域名
for (const original of Object.keys(domain_mappings)) {
const normalized_original = original.trim().toLowerCase();
if (normalized_original.replace(/\./g, '-') === prefix_part) {
target_host = original;
break;
}
}
}

if (!target_host) {
return new Response(`Domain not configured for proxy. Host: ${effective_host}, Prefix: ${host_prefix}, Target lookup failed`, { status: 404 });
}

// 直接使用正则表达式处理最常见的嵌套URL问题
let pathname = url.pathname;

// 修复特定的嵌套URL模式 - 直接移除嵌套URL部分
// 匹配 /xxx/xxx/latest-commit/main/https%3A//gh.xxx.xxx/ 或 /xxx/xxx/tree-commit-info/main/https%3A//gh.xxx.xxx/
pathname = pathname.replace(/(\/[^\/]+\/[^\/]+\/(?:latest-commit|tree-commit-info)\/[^\/]+)\/https%3A\/\/[^\/]+\/.*/, '$1');

// 同样处理非编码版本
pathname = pathname.replace(/(\/[^\/]+\/[^\/]+\/(?:latest-commit|tree-commit-info)\/[^\/]+)\/https:\/\/[^\/]+\/.*/, '$1');

// 构建新的请求URL
const new_url = new URL(url);
new_url.host = target_host;
new_url.pathname = pathname;
new_url.protocol = 'https:';

// 设置新的请求头
const new_headers = new Headers(request.headers);
new_headers.set('Host', target_host);
new_headers.set('Referer', new_url.href);
// 强制要求源站返回未压缩的内容,确保我们可以正常修改文本
new_headers.delete('accept-encoding');

try {
// 发起请求
const response = await fetch(new_url.href, {
method: request.method,
headers: new_headers,
body: request.method !== 'GET' ? request.body : undefined,
redirect: 'manual' // 处理重定向,避免自动跟随导致的问题
});

// 处理重定向
if ([301, 302, 303, 307, 308].includes(response.status)) {
const location = response.headers.get('location');
if (location) {
const modified_location = modifyUrl(location, host_prefix, effective_host);
const new_res_headers = new Headers(response.headers);
new_res_headers.set('location', modified_location);
return new Response(null, {
status: response.status,
headers: new_res_headers
});
}
}

// 设置新的响应头
const new_response_headers = new Headers(response.headers);
new_response_headers.set('access-control-allow-origin', '*');
new_response_headers.set('access-control-allow-credentials', 'true');
new_response_headers.set('cache-control', 'public, max-age=14400');
new_response_headers.delete('content-security-policy');
new_response_headers.delete('content-security-policy-report-only');
new_response_headers.delete('clear-site-data');

// 只处理 200 OK 且是文本类型的响应内容
const content_type = response.headers.get('content-type') || '';
const is_text = content_type.includes('text/') ||
content_type.includes('application/json') ||
content_type.includes('application/javascript') ||
content_type.includes('application/xml');

if (response.status === 200 && is_text) {
// 如果要修改内容,必须移除这些头,因为内容会被解压且长度会变化
new_response_headers.delete('content-encoding');
new_response_headers.delete('content-length');

let text = await response.text();
text = await modifyText(text, host_prefix, effective_host);

// 注入统计脚本
if (content_type.includes('text/html')) {
const inject_script = '<script defer src="https://u.2x.nz/script.js" data-website-id="e20f6781-b518-4bab-96be-35afe24cd0cf"></script>';
if (text.includes('</head>')) {
text = text.replace('</head>', `${inject_script}</head>`);
} else if (text.includes('</body>')) {
text = text.replace('</body>', `${inject_script}</body>`);
} else {
text = text + inject_script;
}
}

return new Response(text, {
status: response.status,
headers: new_response_headers
});
}

// 对于非文本或非 200 响应,直接返回原始流
return new Response(response.body, {
status: response.status,
headers: new_response_headers
});
} catch (err) {
return new Response(`Proxy Error: ${err.message}`, { status: 502 });
}
}

// 获取当前主机名的前缀,用于匹配反向映射
function getProxyPrefix(host) {
// 检查 *-gh. 模式
const ghMatch = host.match(/^([a-z0-9-]+-gh\.)/);
if (ghMatch) {
return ghMatch[1];
}

return null;
}

// 修改文本中的域名引用
async function modifyText(text, host_prefix, effective_hostname) {
// 使用有效主机名获取域名后缀部分(用于构建完整的代理域名)
const domain_suffix = effective_hostname.substring(host_prefix.length);

// 替换所有域名引用
for (const [original_domain, _] of Object.entries(domain_mappings)) {
const escaped_domain = original_domain.replace(/\./g, '\\.');

// 统一为 [原生域名]-github.com
const current_prefix = original_domain.replace(/\./g, '-') + '-gh.';
const full_proxy_domain = `${current_prefix}${domain_suffix}`;

// 替换完整URLs
text = text.replace(
new RegExp(`https?://${escaped_domain}(?=/|"|'|\\s|$)`, 'g'),
`https://${full_proxy_domain}`
);

// 替换协议相对URLs
text = text.replace(
new RegExp(`//${escaped_domain}(?=/|"|'|\\s|$)`, 'g'),
`//${full_proxy_domain}`
);
}

return text;
}

// 修改 URL(用于重定向等)
function modifyUrl(url_str, host_prefix, effective_hostname) {
try {
const url = new URL(url_str);
const domain_suffix = effective_hostname.substring(host_prefix.length);

for (const [original_domain, _] of Object.entries(domain_mappings)) {
if (url.host === original_domain) {
const current_prefix = original_domain.replace(/\./g, '-') + '-gh.';
url.host = `${current_prefix}${domain_suffix}`;
break;
}
}
return url.href;
} catch (e) {
return url_str;
}
}
  1. 点击"保存"按钮、

2. 创建Docker反代片段

  1. 点击"创建片段"按钮
  2. 在名称输入框中输入一个描述性名称,例如"Docker反代"
  3. 在编辑框中粘贴以下代码:
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
const DEFAULT_HUB_HOST = 'registry-1.docker.io'
const AUTH_URL = 'https://auth.docker.io'
const DEFAULT_BLOCKED_UA = ['netcraft']

const SNIPPET_CONFIG = {
URL302: '',
URL: '',
UA: ''
}

function routeByHosts(host, defaultHubHost) {
const routes = {
quay: 'quay.io',
gcr: 'gcr.io',
'k8s-gcr': 'k8s.gcr.io',
k8s: 'registry.k8s.io',
ghcr: 'ghcr.io',
cloudsmith: 'docker.cloudsmith.io',
nvcr: 'nvcr.io',
test: 'registry-1.docker.io'
}
if (host in routes) return [routes[host], false]
return [defaultHubHost, true]
}

const PREFLIGHT_INIT = {
headers: new Headers({
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS',
'access-control-max-age': '1728000'
})
}

function makeRes(body, status = 200, headers = {}) {
headers['access-control-allow-origin'] = '*'
return new Response(body, { status, headers })
}

function newUrl(urlStr, base) {
try {
return new URL(urlStr, base)
} catch {
return null
}
}

async function nginx() {
return `
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
`
}

async function searchInterface() {
return `
<!DOCTYPE html>
<html>
<head>
<title>Docker Hub 镜像搜索</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root {
--github-color: rgb(27,86,198);
--github-bg-color: #ffffff;
--primary-color: #0066ff;
--primary-dark: #0052cc;
--gradient-start: #1a90ff;
--gradient-end: #003eb3;
--text-color: #ffffff;
--shadow-color: rgba(0,0,0,0.1);
--transition-time: 0.3s;
}

* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
padding: 20px;
color: var(--text-color);
overflow-x: hidden;
}
.container {
text-align: center;
width: 100%;
max-width: 800px;
padding: 20px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 60vh;
animation: fadeIn 0.8s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.github-corner {
position: fixed;
top: 0;
right: 0;
z-index: 999;
transition: transform var(--transition-time) ease;
}
.github-corner:hover {
transform: scale(1.08);
}
.github-corner svg {
fill: var(--github-bg-color);
color: var(--github-color);
position: absolute;
top: 0;
border: 0;
right: 0;
width: 80px;
height: 80px;
filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.2));
}
.logo {
margin-bottom: 20px;
transition: transform var(--transition-time) ease;
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.logo:hover {
transform: scale(1.08) rotate(5deg);
}
.logo svg {
filter: drop-shadow(0 5px 15px rgba(0, 0, 0, 0.2));
}
.title {
color: var(--text-color);
font-size: 2.3em;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
font-weight: 700;
letter-spacing: -0.5px;
animation: slideInFromTop 0.5s ease-out 0.2s both;
}
@keyframes slideInFromTop {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
.subtitle {
color: rgba(255, 255, 255, 0.9);
font-size: 1.1em;
margin-bottom: 25px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
line-height: 1.4;
animation: slideInFromTop 0.5s ease-out 0.4s both;
}
.search-container {
display: flex;
align-items: stretch;
width: 100%;
max-width: 600px;
margin: 0 auto;
height: 55px;
position: relative;
animation: slideInFromBottom 0.5s ease-out 0.6s both;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
border-radius: 12px;
overflow: hidden;
}
@keyframes slideInFromBottom {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
#search-input {
flex: 1;
padding: 0 20px;
font-size: 16px;
border: none;
outline: none;
transition: all var(--transition-time) ease;
height: 100%;
}
#search-input:focus {
padding-left: 25px;
}
#search-button {
width: 60px;
background-color: var(--primary-color);
border: none;
cursor: pointer;
transition: all var(--transition-time) ease;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
#search-button svg {
transition: transform 0.3s ease;
stroke: white;
}
#search-button:hover {
background-color: var(--primary-dark);
}
#search-button:hover svg {
transform: translateX(2px);
}
#search-button:active svg {
transform: translateX(4px);
}
.tips {
color: rgba(255, 255, 255, 0.8);
margin-top: 20px;
font-size: 0.9em;
animation: fadeIn 0.5s ease-out 0.8s both;
transition: transform var(--transition-time) ease;
}
.tips:hover {
transform: translateY(-2px);
}
@media (max-width: 768px) {
.container {
padding: 20px 15px;
min-height: 60vh;
}
.title {
font-size: 2em;
}
.subtitle {
font-size: 1em;
margin-bottom: 20px;
}
.search-container {
height: 50px;
}
}
@media (max-width: 480px) {
.container {
padding: 15px 10px;
min-height: 60vh;
}
.github-corner svg {
width: 60px;
height: 60px;
}
.search-container {
height: 45px;
}
#search-input {
padding: 0 15px;
}
#search-button {
width: 50px;
}
#search-button svg {
width: 18px;
height: 18px;
}
.title {
font-size: 1.7em;
margin-bottom: 8px;
}
.subtitle {
font-size: 0.95em;
margin-bottom: 18px;
}
}
</style>
</head>
<body>
<a href="https://github.com/cmliu/CF-Workers-docker.io" target="_blank" class="github-corner" aria-label="View source on Github">
<svg viewBox="0 0 250 250" aria-hidden="true">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
</svg>
</a>
<div class="container">
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="#ffffff" width="110" height="85">
<path d="M23.763 6.886c-.065-.053-.673-.512-1.954-.512-.32 0-.659.03-1.01.087-.248-1.703-1.651-2.533-1.716-2.57l-.345-.2-.227.328a4.596 4.596 0 0 0-.611 1.433c-.23.972-.09 1.884.403 2.666-.596.331-1.546.418-1.744.42H.752a.753.753 0 0 0-.75.749c-.007 1.456.233 2.864.692 4.07.545 1.43 1.355 2.483 2.409 3.13 1.181.725 3.104 1.14 5.276 1.14 1.016 0 2.03-.092 2.93-.266 1.417-.273 2.705-.742 3.826-1.391a10.497 10.497 0 0 0 2.61-2.14c1.252-1.42 1.998-3.005 2.553-4.408.075.003.148.005.221.005 1.371 0 2.215-.55 2.68-1.01.505-.5.685-.998.704-1.053L24 7.076l-.237-.19Z"></path>
<path d="M2.216 8.075h2.119a.186.186 0 0 0 .185-.186V6a.186.186 0 0 0-.185-.186H2.216A.186.186 0 0 0 2.031 6v1.89c0 .103.083.186.185.186Zm2.92 0h2.118a.185.185 0 0 0 .185-.186V6a.185.185 0 0 0-.185-.186H5.136A.185.185 0 0 0 4.95 6v1.89c0 .103.083.186.186.186Zm2.964 0h2.118a.186.186 0 0 0 .185-.186V6a.186.186 0 0 0-.185-.186H8.1A.185.185 0 0 0 7.914 6v1.89c0 .103.083.186.186.186Zm2.928 0h2.119a.185.185 0 0 0 .185-.186V6a.185.185 0 0 0-.185-.186h-2.119a.186.186 0 0 0-.185.186v1.89c0 .103.083.186.185.186Zm-5.892-2.72h2.118a.185.185 0 0 0 .185-.186V3.28a.186.186 0 0 0-.185-.186H5.136a.186.186 0 0 0-.186.186v1.89c0 .103.083.186.186.186Zm2.964 0h2.118a.186.186 0 0 0 .185-.186V3.28a.186.186 0 0 0-.185-.186H8.1a.186.186 0 0 0-.186.186v1.89c0 .103.083.186.186.186Zm2.928 0h2.119a.185.185 0 0 0 .185-.186V3.28a.185.185 0 0 0-.185-.186h-2.119a.186.186 0 0 0-.185.186v1.89c0 .103.083.186.185.186Zm0-2.72h2.119a.186.186 0 0 0 .185-.186V.56a.185.185 0 0 0-.185-.186h-2.119a.186.186 0 0 0-.185.186v1.89c0 .103.083.186.185.186Zm2.955 5.44h2.118a.185.185 0 0 0 .186-.186V6a.185.185 0 0 0-.186-.186h-2.118a.185.185 0 0 0-.185.186v1.89c0 .103.083.186.185.186Z"></path>
</svg>
</div>
<h1 class="title">Docker Hub 镜像搜索</h1>
<p class="subtitle">快速查找、下载和部署 Docker 容器镜像</p>
<div class="search-container">
<input type="text" id="search-input" placeholder="输入关键词搜索镜像,如: nginx, mysql, redis...">
<button id="search-button" title="搜索">
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M13 5l7 7-7 7M5 5l7 7-7 7" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
</div>
<p class="tips">基于 Cloudflare Workers / Pages 构建,利用全球边缘网络实现毫秒级响应。</p>
</div>
<script>
function performSearch() {
const query = document.getElementById('search-input').value;
if (query) {
window.location.href = '/search?q=' + encodeURIComponent(query);
}
}
document.getElementById('search-button').addEventListener('click', performSearch);
document.getElementById('search-input').addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
performSearch();
}
});
window.addEventListener('load', function() {
document.getElementById('search-input').focus();
});
</script>
</body>
</html>
`
}

async function httpHandler(req, pathname, baseHost) {
const reqHdrRaw = req.headers
if (req.method === 'OPTIONS' && reqHdrRaw.has('access-control-request-headers')) {
return new Response(null, PREFLIGHT_INIT)
}
const reqHdrNew = new Headers(reqHdrRaw)
reqHdrNew.delete('Authorization')
const urlObj = newUrl(pathname, `https://${baseHost}`)
const reqInit = {
method: req.method,
headers: reqHdrNew,
redirect: 'follow',
body: req.body
}
return proxy(urlObj, reqInit, '')
}

async function proxy(urlObj, reqInit, rawLen) {
const res = await fetch(urlObj.href, reqInit)
const resHdrOld = res.headers
const resHdrNew = new Headers(resHdrOld)
if (rawLen) {
const newLen = resHdrOld.get('content-length') || ''
if (rawLen !== newLen) {
return makeRes(res.body, 400, {
'--error': `bad len: ${newLen}, except: ${rawLen}`,
'access-control-expose-headers': '--error'
})
}
}
resHdrNew.set('access-control-expose-headers', '*')
resHdrNew.set('access-control-allow-origin', '*')
resHdrNew.set('Cache-Control', 'max-age=1500')
resHdrNew.delete('content-security-policy')
resHdrNew.delete('content-security-policy-report-only')
resHdrNew.delete('clear-site-data')
return new Response(res.body, {
status: res.status,
headers: resHdrNew
})
}

function parseCsvInput(input) {
let text = input.replace(/[\t |"'\r\n]+/g, ',').replace(/,+/g, ',')
if (text.charAt(0) === ',') text = text.slice(1)
if (text.charAt(text.length - 1) === ',') text = text.slice(0, text.length - 1)
return text ? text.split(',') : []
}

export default {
async fetch(request) {
const getReqHeader = (key) => request.headers.get(key)
const originalUrl = new URL(request.url)
let url = new URL(request.url)
const userAgentHeader = request.headers.get('User-Agent')
const userAgent = userAgentHeader ? userAgentHeader.toLowerCase() : 'null'
const workersUrl = `https://${originalUrl.hostname}`
const ns = url.searchParams.get('ns')
const hostname = url.searchParams.get('hubhost') || originalUrl.hostname
const hostTop = hostname.split('.')[0]
let blockedUA = [...DEFAULT_BLOCKED_UA]
if (SNIPPET_CONFIG.UA) blockedUA = blockedUA.concat(parseCsvInput(SNIPPET_CONFIG.UA))
let checkHost
let hubHost = DEFAULT_HUB_HOST

if (ns) {
hubHost = ns === 'docker.io' ? 'registry-1.docker.io' : ns
} else {
checkHost = routeByHosts(hostTop, DEFAULT_HUB_HOST)
hubHost = checkHost[0]
}

const fakePage = checkHost ? checkHost[1] : false
url.hostname = hubHost
const hubParams = ['/v1/search', '/v1/repositories']

if (blockedUA.some((ua) => userAgent.includes(ua)) && blockedUA.length > 0) {
return new Response(await nginx(), {
headers: { 'Content-Type': 'text/html; charset=UTF-8' }
})
}

if ((userAgent && userAgent.includes('mozilla')) || hubParams.some((param) => url.pathname.includes(param))) {
if (url.pathname === '/') {
if (SNIPPET_CONFIG.URL302) return Response.redirect(SNIPPET_CONFIG.URL302, 302)
if (SNIPPET_CONFIG.URL) {
if (SNIPPET_CONFIG.URL.toLowerCase() === 'nginx') {
return new Response(await nginx(), {
headers: { 'Content-Type': 'text/html; charset=UTF-8' }
})
}
return fetch(new Request(SNIPPET_CONFIG.URL, request))
}
if (fakePage) {
return new Response(await searchInterface(), {
headers: { 'Content-Type': 'text/html; charset=UTF-8' }
})
}
} else {
if (url.pathname.startsWith('/v1/')) url.hostname = 'index.docker.io'
else if (fakePage) url.hostname = 'hub.docker.com'
if (url.searchParams.get('q')?.includes('library/') && url.searchParams.get('q') !== 'library/') {
const search = url.searchParams.get('q')
url.searchParams.set('q', search.replace('library/', ''))
}
return fetch(new Request(url, request))
}
}

if (!/%2F/.test(url.search) && /%3A/.test(url.toString())) {
const modifiedUrl = url.toString().replace(/%3A(?=.*?&)/, '%3Alibrary%2F')
url = new URL(modifiedUrl)
}

if (url.pathname.includes('/token')) {
const tokenParameter = {
headers: {
Host: 'auth.docker.io',
'User-Agent': getReqHeader('User-Agent'),
Accept: getReqHeader('Accept'),
'Accept-Language': getReqHeader('Accept-Language'),
'Accept-Encoding': getReqHeader('Accept-Encoding'),
Connection: 'keep-alive',
'Cache-Control': 'max-age=0'
}
}
const tokenUrl = AUTH_URL + url.pathname + url.search
return fetch(new Request(tokenUrl, request), tokenParameter)
}

if (hubHost === 'registry-1.docker.io' && /^\/v2\/[^/]+\/[^/]+\/[^/]+$/.test(url.pathname) && !/^\/v2\/library/.test(url.pathname)) {
url.pathname = '/v2/library/' + url.pathname.split('/v2/')[1]
}

if (
url.pathname.startsWith('/v2/') &&
(url.pathname.includes('/manifests/') ||
url.pathname.includes('/blobs/') ||
url.pathname.includes('/tags/') ||
url.pathname.endsWith('/tags/list'))
) {
let repo = ''
const v2Match = url.pathname.match(/^\/v2\/(.+?)(?:\/(manifests|blobs|tags)\/)/)
if (v2Match) repo = v2Match[1]
if (repo) {
const tokenUrl = `${AUTH_URL}/token?service=registry.docker.io&scope=repository:${repo}:pull`
const tokenRes = await fetch(tokenUrl, {
headers: {
'User-Agent': getReqHeader('User-Agent'),
Accept: getReqHeader('Accept'),
'Accept-Language': getReqHeader('Accept-Language'),
'Accept-Encoding': getReqHeader('Accept-Encoding'),
Connection: 'keep-alive',
'Cache-Control': 'max-age=0'
}
})
const tokenData = await tokenRes.json()
const parameter = {
headers: {
Host: hubHost,
'User-Agent': getReqHeader('User-Agent'),
Accept: getReqHeader('Accept'),
'Accept-Language': getReqHeader('Accept-Language'),
'Accept-Encoding': getReqHeader('Accept-Encoding'),
Connection: 'keep-alive',
'Cache-Control': 'max-age=0',
Authorization: `Bearer ${tokenData.token}`
},
cacheTtl: 3600
}
if (request.headers.has('X-Amz-Content-Sha256')) {
parameter.headers['X-Amz-Content-Sha256'] = getReqHeader('X-Amz-Content-Sha256')
}
const originalResponse = await fetch(new Request(url, request), parameter)
const newResponseHeaders = new Headers(originalResponse.headers)
if (newResponseHeaders.get('Www-Authenticate')) {
const re = new RegExp(AUTH_URL, 'g')
newResponseHeaders.set(
'Www-Authenticate',
originalResponse.headers.get('Www-Authenticate').replace(re, workersUrl)
)
}
if (newResponseHeaders.get('Location')) {
const location = newResponseHeaders.get('Location')
return httpHandler(request, location, hubHost)
}
return new Response(originalResponse.clone().body, {
status: originalResponse.status,
headers: newResponseHeaders
})
}
}

const parameter = {
headers: {
Host: hubHost,
'User-Agent': getReqHeader('User-Agent'),
Accept: getReqHeader('Accept'),
'Accept-Language': getReqHeader('Accept-Language'),
'Accept-Encoding': getReqHeader('Accept-Encoding'),
Connection: 'keep-alive',
'Cache-Control': 'max-age=0'
},
cacheTtl: 3600
}
if (request.headers.has('Authorization')) {
parameter.headers.Authorization = getReqHeader('Authorization')
}
if (request.headers.has('X-Amz-Content-Sha256')) {
parameter.headers['X-Amz-Content-Sha256'] = getReqHeader('X-Amz-Content-Sha256')
}

const originalResponse = await fetch(new Request(url, request), parameter)
const newResponseHeaders = new Headers(originalResponse.headers)
if (newResponseHeaders.get('Www-Authenticate')) {
const re = new RegExp(AUTH_URL, 'g')
newResponseHeaders.set(
'Www-Authenticate',
originalResponse.headers.get('Www-Authenticate').replace(re, workersUrl)
)
}
if (newResponseHeaders.get('Location')) {
const location = newResponseHeaders.get('Location')
return httpHandler(request, location, hubHost)
}
return new Response(originalResponse.clone().body, {
status: originalResponse.status,
headers: newResponseHeaders
})
}
}
  1. 点击"保存"按钮

3. 创建随机图项目片段

  1. 点击"创建片段"按钮
  2. 在名称输入框中输入一个描述性名称,例如"随机图"
  3. 在编辑框中粘贴以下代码:
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
let countsCache = null;
let lastFetch = 0;
const CACHE_TTL = 10 * 60 * 1000; // 10分钟缓存

async function getCounts() {
// 简单缓存,避免每次请求都 fetch
if (countsCache && (Date.now() - lastFetch < CACHE_TTL)) {
return countsCache;
}

const resp = await fetch('https://p.域名/random.js');
const text = await resp.text();

// 提取 {"h":1124,"v":4382}
const match = text.match(/counts\s*=\s*(\{[^}]+\})/);

if (!match) {
throw new Error('无法解析 counts');
}

countsCache = JSON.parse(match[1]);
lastFetch = Date.now();

return countsCache;
}

function randomInt(max) {
return Math.floor(Math.random() * max) + 1;
}

export default {
async fetch(request) {
const url = new URL(request.url);
const path = url.pathname;

try {
const counts = await getCounts();

if (path === '/h') {
const n = randomInt(counts.h);
const location = `https://p.域名/ri/h/${n}.webp`;

return new Response(null, {
status: 302,
headers: {
Location: location
}
});
}

if (path === '/v') {
const n = randomInt(counts.v);
const location = `https://p.域名/ri/v/${n}.webp`;

return new Response(null, {
status: 302,
headers: {
Location: location
}
});
}

return new Response('Not Found', { status: 404 });

} catch (e) {
return new Response('Error: ' + e.message, { status: 500 });
}
}
};
  1. 点击"保存"按钮

步骤三:配置片段规则

配置片段规则页面

1. 为GitHub反代配置规则

  1. 找到刚创建的"GitHub反代"片段
  2. 点击"添加规则"按钮
  3. 在规则配置页面中:
    • 选择"自定义筛选表达式"
    • 字段选择"主机名"
    • 运算符选择"通配符"
    • 值输入*-gh.域名(或者你想要使用的具体子域名,例如github.com
  4. 点击"保存"按钮

2. 为Docker反代配置规则

  1. 找到刚创建的"Docker反代"片段
  2. 点击"添加规则"按钮
  3. 在规则配置页面中:
    • 选择"自定义筛选表达式"
    • 字段选择"主机名"
    • 运算符选择"通配符"
    • 值输入docker.域名(或者你想要使用的具体子域名,例如docker.yourdomain.com
  4. 点击"保存"按钮

3. 为随机图项目配置规则

  1. 找到刚创建的"随机图"片段
  2. 点击"添加规则"按钮
  3. 在规则配置页面中:
    • 选择"自定义筛选表达式"
    • 字段选择"主机名"
    • 运算符选择"通配符"
    • 值输入c-p.域名(或者你想要使用的具体子域名,例如c-p.yourdomain.com
  4. 点击"保存"按钮

步骤四:部署和测试

  1. 确保所有片段和规则都已保存
  2. 等待Cloudflare生效(通常需要几分钟时间)
  3. 测试访问:
    • GitHub反代:访问github.com(替换为你的实际域名)
    • Docker反代:访问docker.yourdomain.com(替换为你的实际域名)
    • 随机图:访问c-p.yourdomain.com(替换为你的实际域名)

注意事项

  1. 确保你的域名已经正确配置了DNS记录,指向Cloudflare
  2. 如果你使用的是子域名,确保已经在DNS中添加了相应的A记录或CNAME记录
  3. 某些地区可能无法访问GitHub或Docker Hub,即使使用反代也可能受到影响
  4. 随机图服务可能会受到外部API的限制,建议使用多个备用API以提高可靠性

故障排除

  • 如果访问时出现错误,请检查片段代码是否正确
  • 确认规则配置中的主机名是否与你的实际域名匹配
  • 检查Cloudflare的安全设置,确保没有阻止请求
  • 尝试清除浏览器缓存后再次访问

通过以上步骤,你应该已经成功在Cloudflare上部署了GitHub反代、Docker反代和随机图项目。如果遇到任何问题,请参考Cloudflare的官方文档或寻求社区支持。