-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathworker.js
More file actions
210 lines (184 loc) · 7.05 KB
/
worker.js
File metadata and controls
210 lines (184 loc) · 7.05 KB
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
/**
* Cloudflare Worker for 123pan Authentication
* 作者:https://github.com/hcllmsx
* 项目:https://github.com/hcllmsx/Authn123Pan
*/
// MD5 implementation for Cloudflare Workers
async function md5(text) {
const msgUint8 = new TextEncoder().encode(text);
const hashBuffer = await crypto.subtle.digest('MD5', msgUint8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
// Sign URL function (same logic as 123pan example)
async function signURL(originURL, privateKey, uid, validDurationSeconds) {
const ts = Math.floor((Date.now() / 1000) + validDurationSeconds); // 有效时间戳,单位:秒
const rInt = Math.floor(Math.random() * 1000000); // 随机正整数
let objURL;
try {
objURL = new URL(originURL);
} catch (e) {
return originURL;
}
const pathForSign = decodeURIComponent(objURL.pathname);
const signString = `${pathForSign}-${ts}-${rInt}-${uid}-${privateKey}`;
const hash = await md5(signString);
const authKey = `${ts}-${rInt}-${uid}-${hash}`;
objURL.searchParams.append('auth_key', authKey);
return objURL.toString();
}
export default {
async fetch(request, env) {
// Handle CORS preflight requests
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',
'Access-Control-Max-Age': '86400',
},
});
}
// We support POST for /api/sign and GET for /api/m3u8Proxy
if (request.method !== 'POST' && request.method !== 'GET') {
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
status: 405,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
}
const { pathname, searchParams } = new URL(request.url);
try {
// Get environment variables
const UID = env.UID;
const PRIVATE_KEY = env.PRIVATE_KEY;
const VALID_DURATION = parseInt(env.VALID_DURATION || '900'); // Default: 900 seconds (15 minutes)
// Check if required environment variables are set
if (!UID || !PRIVATE_KEY) {
return new Response(JSON.stringify({ error: 'Server configuration error' }), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
}
// Get URL from body (POST) or query string (GET)
let url;
if (request.method === 'POST') {
const body = await request.json();
url = body.url;
} else {
url = searchParams.get('url');
}
if (!url) {
return new Response(JSON.stringify({ error: 'Missing URL parameter' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
}
// Validate that the URL is from 123pan
if (!url.includes('123pan.cn') && !url.includes('123pan.com') && !url.includes('123yx.com')) {
return new Response(JSON.stringify({ error: 'Invalid URL: Only 123pan URLs are allowed' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
}
if (pathname.includes('/api/m3u8Proxy')) {
// 1. 获取原 M3U8 时也需要鉴权
const signedM3u8Url = await signURL(url, PRIVATE_KEY, UID, VALID_DURATION);
// Use incoming referer if available, or fallback to the site domain
const referer = request.headers.get('Referer') || 'https://jixu.oooq.cc/';
const response = await fetch(signedM3u8Url, {
headers: {
'Referer': referer,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
},
});
if (!response.ok) {
return new Response(JSON.stringify({ error: 'Failed to fetch m3u8 from 123pan' }), {
status: response.status,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
}
});
}
const text = await response.text();
const lines = text.split('\n');
// 2. 遍历 M3U8 内容,替换所有的分片/标签 URI 为鉴权后的绝对链接
const signedLines = await Promise.all(lines.map(async (line) => {
const trimmed = line.trim();
if (!trimmed) return line;
// 处理带 URI 的标签 (e.g. #EXT-X-MAP:URI="init.mp4")
if (trimmed.startsWith('#')) {
if (line.includes('URI="')) {
// 使用拆分方法处理异步替换
const parts = line.split('URI="');
if (parts.length > 1) {
const afterFull = parts[1];
const uri = afterFull.substring(0, afterFull.indexOf('"'));
const remaining = afterFull.substring(afterFull.indexOf('"'));
try {
const absoluteUrl = new URL(uri, url).toString();
const signed = await signURL(absoluteUrl, PRIVATE_KEY, UID, VALID_DURATION);
return `${parts[0]}URI="${signed}${remaining}`;
} catch (e) { return line; }
}
}
return line;
}
try {
// 将分片相对路径转换为绝对路径
const segmentUrl = new URL(trimmed, url).toString();
// 对每个分片进行鉴权提取
return await signURL(segmentUrl, PRIVATE_KEY, UID, VALID_DURATION);
} catch (e) {
return line;
}
}));
const finalM3u8 = signedLines.join('\n');
const proxyHeaders = new Headers();
proxyHeaders.set('Content-Type', 'application/vnd.apple.mpegurl');
proxyHeaders.set('Access-Control-Allow-Origin', '*');
proxyHeaders.set('Cache-Control', 's-maxage=60, stale-while-revalidate=120');
return new Response(finalM3u8, {
status: 200,
headers: proxyHeaders
});
} else {
// Generate signed URL
const signedURL = await signURL(url, PRIVATE_KEY, UID, VALID_DURATION);
// Return signed URL
return new Response(JSON.stringify({
signedUrl: signedURL,
expiresIn: VALID_DURATION, // in seconds
}), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
}
} catch (error) {
return new Response(JSON.stringify({ error: 'Internal server error', message: error.message }), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
}
},
};