本文 Cloudflare Workers 代码整体 逻辑清晰、功能完整,实现了对第三方 M3U8 流的代理、广告过滤、AES-128 密钥重写、缓存和 CORS 支持,技术上是有效的。但在 Cloudflare 的免费/付费使用政策(Acceptable Use Policy)和运行限制下,存在合规与稳定性风险。请看下面逐项分析:


✅ 一、功能有效性:✅ 基本有效

功能 是否实现 说明
M3U8 代理 + .ts 重写 正确解析相对路径并重写为代理链接
AES-128 密钥代理 单独 /aes128.key 路由处理 key 请求
广告行过滤 使用关键词跳过广告分片
缓存(Cache API) 利用 caches.default 缓存 m3u8/ts/key
CORS 支持 设置了 Access-Control-Allow-Origin: *
User-Agent 白名单 UA 白名单,防止被盗用

⚠️ 二、Cloudflare Workers 的限制与风险

1. 违反 Acceptable Use Policy(AUP)?

Cloudflare 明确禁止将 Workers 用作:

通用反向代理、视频/音频中继、或绕过内容访问控制
—— Cloudflare AUP

你的服务本质是 “第三方视频流代理”,属于高风险场景:

  • 如果用于 盗播、绕过 DRM、大规模分发他人内容 → 可能被封禁;
  • 如果用于 个人学习、合法授权内容、小流量测试 → 通常不会被主动干预。

降低风险建议

  • 不要公开分享代理链接;
  • 仅用于你有版权或授权的内容;
  • 避免高频、大流量请求(如直播转播)。

2. 运行时资源限制(关键!)

限制项 免费计划 说明
CPU 时间 ≤ 10ms(每请求) 处理大 M3U8 或复杂正则可能超限
内存 ≤ 128MB 一般够用
响应体大小 ≤ 100MB(写入缓存) .ts 文件若 >100MB 无法缓存
每日请求次数 10 万次(免费) 对视频流来说很容易耗尽(一个用户看 10 分钟 ≈ 150 个 .ts 请求)
带宽出口 免费但计入用量 超出配额会限速或收费(Pro 起步 $5/10M 请求)

📌 特别注意
每个 .ts 片段都会触发一次 Worker 执行 + 一次缓存写入。
一个 10 分钟视频(4秒/片)≈ 150 次请求 → 1000 个用户观看 = 15 万次请求超出免费额度


3. 缓存行为问题

  • caches.default分布式缓存,但 不保证持久性(可能随时失效);
  • 你设置 CACHE_TTL = 7天,但 Cloudflare 实际缓存时间可能更短(尤其低频访问内容);
  • 首次播放仍需回源,用户体验取决于源站速度。

4. 安全与滥用风险

强烈建议

  • 启用 UA 或 Token 验证;
  • 或限制 Referer(如只允许你的域名);
  • 或添加 IP 限流(需用 Durable Objects 或外部服务)。

✅ 三、结论

项目 评估
能否在 Cloudflare 免费运行? ⚠️ 短期/小流量可以,长期/高并发不行
是否违反 Cloudflare 规则? ⚠️ 有风险,取决于用途(个人 vs 公开盗播)
推荐用于生产吗? ❌ 不推荐用于公开视频平台;✅ 可用于内部工具或授权内容

🔒 最终建议

如果你拥有视频内容的合法权利(如自己生成的 HLS 流),建议:

  1. 不要代理第三方 M3U8
  2. 直接将 .m3u8 + .ts 上传到 Cloudflare Pages 或 R2
  3. 享受原生 CDN 加速 + 无合规风险

如果必须代理(如调试第三方流),请:

  • 限制 UA / Token;
  • 不公开 URL;
  • 监控用量,避免超额。

✅ 完整代码

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
// ===== 配置区 =====
const CACHE_TTL_M3U8 = 60 * 60 * 24 * 7; // 7天
const CACHE_TTL_TS = 60 * 60 * 24 * 7;

// User-Agent 白名单(支持通配符 *)
const ALLOWED_USER_AGENTS = [
'*你的UA*',
];

// 预编译正则,避免每次请求重复生成
const UA_RULES = ALLOWED_USER_AGENTS.map(rule => {
if (typeof rule === 'string' && rule.includes('*')) {
const pattern = '^' + rule.replace(/\*/g, '.*?') + '$';
return new RegExp(pattern);
}
return rule; // 字面量字符串
});

function isValidUserAgent(ua) {
if (!ua || !ALLOWED_USER_AGENTS.length) return true;
for (const rule of UA_RULES) {
if (rule instanceof RegExp) {
if (rule.test(ua)) return true;
} else if (ua.includes(rule)) {
return true;
}
}
return false;
}

// 广告关键词集合(Set 查找 O(1))
const AD_KEYWORDS = new Set([
'广告', '%E5%B9%BF%E5%91%8A', '/advertisement', '/ad-creative', '/adjump/',
'/ad/', '/ad-end', 'ad_break_end', '/advert/', '/adserver/', 'ad-break',
'/commercial/', '/adsegment/', '/ad_', '/ad-', 'creative', 'preroll',
'midroll', 'postroll'
]);

function isAdLine(line) {
if (!line) return false;
for (const kw of AD_KEYWORDS) {
if (line.includes(kw)) return true;
}
return /ad[-_]\d+\.ts/i.test(line);
}

/**
- 重写 M3U8 内容(单次遍历 + 减少字符串拼接)
*/
function rewriteM3U8(content, baseOrigin, proxyOrigin) {
const lines = content.split('\n');
let encryptionInfo = null;
let hasKeyLine = false;

// 第一遍:提取 #EXT-X-KEY(仅第一个)
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#EXT-X-KEY:')) {
const attrs = {};
const parts = line.substring(11).split(',');
for (const part of parts) {
const [key, value] = part.split('=');
if (key && value !== undefined) {
attrs[key.trim()] = value.trim().replace(/^["']|["']$/g, '');
}
}
if (attrs.METHOD === 'AES-128' && attrs.URI) {
try {
const absoluteKeyUri = new URL(attrs.URI, baseOrigin).href;
encryptionInfo = {
method: attrs.METHOD,
uri: absoluteKeyUri,
iv: attrs.IV || null
};
} catch (e) {
console.warn('Failed to resolve key URI:', attrs.URI);
}
}
hasKeyLine = true;
break;
}
}

// 构建新 KEY 行(如果需要)
let newKeyLine = '';
if (encryptionInfo) {
let uriParam = `${proxyOrigin}/aes128.key?url=${encodeURIComponent(encryptionInfo.uri)}`;
newKeyLine = `#EXT-X-KEY:METHOD=${encryptionInfo.method},URI="${uriParam}"`;
if (encryptionInfo.iv) {
newKeyLine += `,IV=${encryptionInfo.iv}`;
}
}

// 第二遍:构建输出(跳过广告行,重写媒体行)
const output = [];
for (let i = 0; i < lines.length; i++) {
const originalLine = lines[i];
const trimmed = originalLine.trim();

// 跳过空行
if (trimmed === '') {
output.push(originalLine);
continue;
}

// 跳过广告相关行
if (isAdLine(trimmed)) {
continue;
}

// 替换 KEY 行
if (hasKeyLine && trimmed.startsWith('#EXT-X-KEY:')) {
output.push(newKeyLine);
hasKeyLine = false; // 只替换一次
continue;
}

// 重写 .ts / .m3u8 媒体行(非注释行且以这些结尾)
if (!trimmed.startsWith('#') && /\.(ts|m3u8)$/i.test(trimmed)) {
try {
const absoluteUrl = trimmed.startsWith('http')
? trimmed
: new URL(trimmed, baseOrigin).href;
const proxied = `${proxyOrigin}/?url=${encodeURIComponent(absoluteUrl)}`;
output.push(proxied);
} catch (e) {
console.error('URL resolve error:', trimmed, e);
output.push(originalLine); // fallback
}
} else {
output.push(originalLine);
}
}

return output.join('\n');
}

/**
- 处理 /aes128.key 请求
*/
async function handleAES128Key(request, ctx) {
const url = new URL(request.url);
const keyUriParam = url.searchParams.get('url');
if (!keyUriParam) {
return new Response('Missing key URI parameter', { status: 400 });
}

let decodedKeyUri;
try {
decodedKeyUri = decodeURIComponent(keyUriParam);
} catch (e) {
return new Response('Invalid key URI encoding', { status: 400 });
}

const cache = caches.default;
let response = await cache.match(decodedKeyUri);

if (!response) {
const target = new URL(decodedKeyUri);
response = await fetch(decodedKeyUri, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; M3U8-Proxy/1.0)',
'Referer': target.origin + '/',
},
});

if (response.ok) {
const cloned = response.clone();
ctx.waitUntil(cache.put(decodedKeyUri, cloned));
}
}

const newHeaders = new Headers(response.headers);
newHeaders.set('Content-Type', 'application/octet-stream');
newHeaders.set('Access-Control-Allow-Origin', '*');
newHeaders.set('Cache-Control', 'public, max-age=86400'); // 1天

return new Response(response.body, {
status: response.status,
headers: newHeaders,
});
}

/**
- 主请求处理函数
*/
async function handleRequest(request, ctx) {
const userAgent = request.headers.get('User-Agent') || '';
if (!isValidUserAgent(userAgent)) {
return new Response('Forbidden', { status: 403 });
}

const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, User-Agent',
};

const url = new URL(request.url);

// 处理密钥请求
if (url.pathname === '/aes128.key') {
return handleAES128Key(request, ctx);
}

const paramURL = url.searchParams.get('url');
if (!paramURL) {
return new Response(JSON.stringify({ error: "Missing 'url' query" }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}

let targetUrl;
try {
targetUrl = decodeURIComponent(paramURL);
} catch (e) {
return new Response(JSON.stringify({ error: 'Invalid URL encoding' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}

if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
return new Response('URL must start with http:// or https://', { status: 400 });
}

const target = new URL(targetUrl);
const cacheKey = targetUrl; // 使用原始 URL 作为缓存键(简单高效)
const cache = caches.default;

let response = await cache.match(cacheKey);
if (response) {
return response;
}

// 发起源站请求
const originResponse = await fetch(targetUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; M3U8-Proxy/1.0)',
'Referer': target.origin + '/',
},
});

if (!originResponse.ok) {
return originResponse;
}

const contentType = (originResponse.headers.get('content-type') || '').toLowerCase();
const isM3U8 =
contentType.includes('apple.mpegurl') ||
contentType.includes('x-mpegurl') ||
target.pathname.toLowerCase().endsWith('.m3u8');
const isTS = target.pathname.toLowerCase().endsWith('.ts');

let responseBody;
let finalContentType = contentType;

if (isM3U8) {
const bodyText = await originResponse.text();
responseBody = rewriteM3U8(bodyText, target.origin, url.origin);
finalContentType = 'application/vnd.apple.mpegurl';
} else {
responseBody = originResponse.body;
}

const newResponse = new Response(responseBody, {
status: originResponse.status,
headers: new Headers(originResponse.headers),
});

// 强制设置关键头
newResponse.headers.set('Content-Type', finalContentType);
newResponse.headers.set('Access-Control-Allow-Origin', '*');

// 设置缓存策略
if (isM3U8) {
newResponse.headers.set('Cache-Control', `public, max-age=${CACHE_TTL_M3U8}`);
} else if (isTS || contentType.includes('video/mp2t')) {
newResponse.headers.set('Cache-Control', `public, max-age=${CACHE_TTL_TS}`);
} else {
newResponse.headers.set('Cache-Control', 'public, max-age=3600');
}

ctx.waitUntil(cache.put(cacheKey, newResponse.clone()));
return newResponse;
}

// 入口
export default {
async fetch(request, env, ctx) {
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, User-Agent',
},
});
}
return handleRequest(request, ctx);
},
};