Skip to content

Commit 8a909bf

Browse files
author
Taois
committed
feat:完成通用代理功能
1 parent 423053f commit 8a909bf

File tree

8 files changed

+1609
-903
lines changed

8 files changed

+1609
-903
lines changed

controllers/file-proxy.js

Lines changed: 46 additions & 268 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@
44
* @module file-proxy-controller
55
*/
66

7-
import {ENV} from '../utils/env.js';
8-
import https from 'https';
9-
import http from 'http';
10-
import {URL} from 'url';
7+
import {
8+
createHealthResponse,
9+
createStatusResponse,
10+
decodeParam,
11+
forwardResponseHeaders,
12+
getDefaultHeaders,
13+
makeRemoteRequest,
14+
PROXY_CONSTANTS,
15+
setCorsHeaders,
16+
verifyAuth
17+
} from '../utils/proxy-util.js';
1118

1219
/**
1320
* 远程文件代理控制器插件
@@ -18,186 +25,23 @@ import {URL} from 'url';
1825
export default (fastify, options, done) => {
1926
// 请求缓存
2027
const requestCache = new Map();
21-
// 缓存超时时间(5分钟)
22-
const cacheTimeout = 5 * 60 * 1000;
23-
24-
/**
25-
* 验证身份认证
26-
* @param {Object} request - Fastify请求对象
27-
* @param {Object} reply - Fastify响应对象
28-
* @returns {boolean} 验证是否通过
29-
*/
30-
function verifyAuth(request, reply) {
31-
const requiredAuth = ENV.get('PROXY_AUTH', 'drpys');
32-
const providedAuth = request.query.auth;
33-
34-
if (!providedAuth || providedAuth !== requiredAuth) {
35-
reply.status(401).send({
36-
error: 'Unauthorized',
37-
message: 'Missing or invalid auth parameter',
38-
code: 401
39-
});
40-
return false;
41-
}
42-
return true;
43-
}
44-
45-
/**
46-
* 解码参数 - 支持 base64 解码
47-
* @param {string} param - 需要解码的参数
48-
* @param {boolean} isJson - 是否为 JSON 格式
49-
* @returns {string|Object} 解码后的参数
50-
*/
51-
function decodeParam(param, isJson = false) {
52-
if (!param) return isJson ? {} : '';
53-
54-
let decoded = param;
55-
56-
try {
57-
// 首先尝试 URL 解码
58-
decoded = decodeURIComponent(param);
59-
} catch (e) {
60-
// URL 解码失败,使用原始参数
61-
decoded = param;
62-
}
63-
64-
// 对于 URL 参数,如果不是 http 开头,尝试 base64 解码
65-
if (!isJson && !decoded.startsWith('http://') && !decoded.startsWith('https://')) {
66-
try {
67-
const base64Decoded = Buffer.from(decoded, 'base64').toString('utf8');
68-
if (base64Decoded.startsWith('http://') || base64Decoded.startsWith('https://')) {
69-
decoded = base64Decoded;
70-
}
71-
} catch (e) {
72-
// base64 解码失败,保持原值
73-
}
74-
}
75-
76-
// 对于 headers 参数,如果不是 JSON 格式,尝试 base64 解码
77-
if (isJson && !decoded.startsWith('{') && !decoded.endsWith('}')) {
78-
try {
79-
const base64Decoded = Buffer.from(decoded, 'base64').toString('utf8');
80-
if (base64Decoded.startsWith('{') && base64Decoded.endsWith('}')) {
81-
decoded = base64Decoded;
82-
}
83-
} catch (e) {
84-
// base64 解码失败,保持原值
85-
}
86-
}
87-
88-
// 如果是 JSON 格式,尝试解析
89-
if (isJson) {
90-
try {
91-
return JSON.parse(decoded);
92-
} catch (e) {
93-
console.warn('Failed to parse headers as JSON:', decoded);
94-
return {};
95-
}
96-
}
97-
98-
return decoded;
99-
}
100-
101-
/**
102-
* 获取默认请求头
103-
* @param {Object} request - Fastify 请求对象
104-
* @returns {Object} 默认请求头
105-
*/
106-
function getDefaultHeaders(request) {
107-
const defaultHeaders = {};
108-
109-
// 复制一些重要的请求头
110-
const headersToForward = [
111-
'user-agent',
112-
'accept',
113-
'accept-language',
114-
'accept-encoding',
115-
'referer',
116-
'origin'
117-
];
118-
119-
headersToForward.forEach(header => {
120-
if (request.headers[header]) {
121-
defaultHeaders[header] = request.headers[header];
122-
}
123-
});
124-
125-
// 如果没有 user-agent,设置默认值
126-
if (!defaultHeaders['user-agent']) {
127-
defaultHeaders['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
128-
}
129-
130-
return defaultHeaders;
131-
}
132-
133-
/**
134-
* 发起远程请求
135-
* @param {string} url - 远程文件 URL
136-
* @param {Object} headers - 请求头
137-
* @param {string} method - 请求方法
138-
* @param {string} range - Range 头
139-
* @returns {Promise} 请求结果
140-
*/
141-
function makeRemoteRequest(url, headers, method = 'GET', range = null) {
142-
return new Promise((resolve, reject) => {
143-
try {
144-
const urlObj = new URL(url);
145-
const isHttps = urlObj.protocol === 'https:';
146-
const httpModule = isHttps ? https : http;
147-
148-
const requestHeaders = { ...headers };
149-
if (range) {
150-
requestHeaders['range'] = range;
151-
}
152-
153-
const options = {
154-
hostname: urlObj.hostname,
155-
port: urlObj.port || (isHttps ? 443 : 80),
156-
path: urlObj.pathname + urlObj.search,
157-
method: method,
158-
headers: requestHeaders,
159-
timeout: 30000 // 30秒超时
160-
};
161-
162-
const req = httpModule.request(options, (res) => {
163-
resolve({
164-
statusCode: res.statusCode,
165-
headers: res.headers,
166-
stream: res
167-
});
168-
});
169-
170-
req.on('error', (error) => {
171-
reject(new Error(`Request failed: ${error.message}`));
172-
});
173-
174-
req.on('timeout', () => {
175-
req.destroy();
176-
reject(new Error('Request timeout'));
177-
});
178-
179-
req.end();
180-
} catch (error) {
181-
reject(new Error(`Invalid URL or request setup: ${error.message}`));
182-
}
183-
});
184-
}
18528

18629
/**
18730
* 远程文件代理健康检查接口
18831
* GET /file-proxy/health - 检查远程文件代理服务状态
18932
*/
19033
fastify.get('/file-proxy/health', async (request, reply) => {
191-
console.log(`[fileProxyController] Health check request`);
192-
193-
return reply.send({
194-
status: 'ok',
195-
service: 'Remote File Proxy',
196-
timestamp: new Date().toISOString(),
34+
// console.log(`[fileProxyController] Health check request`);
35+
36+
setCorsHeaders(reply);
37+
38+
const healthData = createHealthResponse('Remote File Proxy', {
19739
cache: {
19840
requests: requestCache.size
19941
}
20042
});
43+
44+
return reply.send(healthData);
20145
});
20246

20347
/**
@@ -215,7 +59,7 @@ export default (fastify, options, done) => {
21559

21660
const { url: urlParam, headers: headersParam } = request.query;
21761

218-
console.log(`[fileProxyController] ${request.method} request for URL: ${urlParam}`);
62+
// console.log(`[fileProxyController] ${request.method} request for URL: ${urlParam}`);
21963

22064
// 验证必需参数
22165
if (!urlParam) {
@@ -260,28 +104,9 @@ export default (fastify, options, done) => {
260104
// 设置响应状态码
261105
reply.status(remoteResponse.statusCode);
262106

263-
// 转发重要的响应头
264-
const headersToForward = [
265-
'content-type',
266-
'content-length',
267-
'content-range',
268-
'accept-ranges',
269-
'last-modified',
270-
'etag',
271-
'cache-control',
272-
'expires'
273-
];
274-
275-
headersToForward.forEach(header => {
276-
if (remoteResponse.headers[header]) {
277-
reply.header(header, remoteResponse.headers[header]);
278-
}
279-
});
280-
281-
// 设置 CORS 头
282-
reply.header('Access-Control-Allow-Origin', '*');
283-
reply.header('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
284-
reply.header('Access-Control-Allow-Headers', 'Range, Content-Type');
107+
// 转发响应头和设置CORS
108+
forwardResponseHeaders(reply, remoteResponse.headers);
109+
setCorsHeaders(reply);
285110

286111
// 对于 HEAD 请求,只返回头部信息
287112
if (request.method === 'HEAD') {
@@ -317,7 +142,7 @@ export default (fastify, options, done) => {
317142

318143
const { url: urlParam, headers: headersParam } = request.query;
319144

320-
console.log(`[fileProxyController] Info request for URL: ${urlParam}`);
145+
// console.log(`[fileProxyController] Info request for URL: ${urlParam}`);
321146

322147
// 验证必需参数
323148
if (!urlParam) {
@@ -379,85 +204,38 @@ export default (fastify, options, done) => {
379204
});
380205

381206
/**
382-
* 缓存管理接口
383-
* DELETE /file-proxy/cache - 清理缓存
207+
* 清理缓存路由
384208
*/
385209
fastify.delete('/file-proxy/cache', async (request, reply) => {
386-
// 验证身份认证
387-
if (!verifyAuth(request, reply)) {
388-
return;
389-
}
210+
if (!verifyAuth(request, reply)) return;
390211

391-
console.log(`[fileProxyController] Cache clear request`);
212+
setCorsHeaders(reply);
392213

393-
try {
394-
// 非VERCEL环境可在设置中心控制此功能是否开启
395-
if (!process.env.VERCEL) {
396-
if (!Number(process.env.allow_file_cache_clear)) {
397-
return reply.status(403).send({ error: 'Cache clear is not allowed by owner' });
398-
}
399-
}
400-
401-
const cacheCount = requestCache.size;
402-
403-
// 清理缓存
404-
requestCache.clear();
405-
406-
return reply.send({
407-
success: true,
408-
message: 'Cache cleared successfully',
409-
cleared: {
410-
requests: cacheCount
411-
}
412-
});
413-
} catch (error) {
414-
console.error('[fileProxyController] Cache clear error:', error);
415-
return reply.status(500).send({ error: error.message });
416-
}
214+
const beforeSize = requestCache.size;
215+
requestCache.clear();
216+
217+
reply.send({
218+
status: 'success',
219+
message: 'Cache cleared',
220+
cleared: beforeSize,
221+
timestamp: new Date().toISOString()
222+
});
417223
});
418224

419225
/**
420-
* 远程文件代理状态接口
421-
* GET /file-proxy/status - 获取代理服务状态
226+
* 状态路由
422227
*/
423228
fastify.get('/file-proxy/status', async (request, reply) => {
424-
console.log(`[fileProxyController] Status request`);
425-
426-
try {
427-
return reply.send({
428-
service: 'Remote File Proxy Controller',
429-
version: '1.0.0',
430-
status: 'running',
431-
cache: {
432-
requests: requestCache.size,
433-
timeout: cacheTimeout
434-
},
435-
features: [
436-
'Remote file proxying',
437-
'Base64 parameter decoding',
438-
'Range request support',
439-
'Custom headers support',
440-
'CORS support',
441-
'Authentication protection'
442-
],
443-
endpoints: [
444-
'GET /file-proxy/health - Health check (no auth required)',
445-
'GET /file-proxy/proxy?url=<remote_url>&auth=<auth_code>&headers=<custom_headers> - Proxy remote file',
446-
'HEAD /file-proxy/proxy?url=<remote_url>&auth=<auth_code>&headers=<custom_headers> - Get remote file headers',
447-
'GET /file-proxy/info?url=<remote_url>&auth=<auth_code>&headers=<custom_headers> - Get remote file information',
448-
'DELETE /file-proxy/cache?auth=<auth_code> - Clear cache',
449-
'GET /file-proxy/status - Get service status (no auth required)'
450-
],
451-
auth: {
452-
required: true,
453-
parameter: 'auth',
454-
description: 'Authentication code required for protected endpoints'
455-
}
456-
});
457-
} catch (error) {
458-
console.error('[fileProxyController] Status request error:', error);
459-
return reply.status(500).send({ error: error.message });
460-
}
229+
setCorsHeaders(reply);
230+
231+
const statusData = createStatusResponse('file-proxy', '1.0.0', {
232+
cache: {
233+
size: requestCache.size,
234+
timeout: PROXY_CONSTANTS.CACHE_TIMEOUT
235+
}
236+
});
237+
238+
reply.send(statusData);
461239
});
462240

463241
done();

0 commit comments

Comments
 (0)