alist-proxy/source.js
2024-11-07 17:15:39 +08:00

423 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const http = require('http');
const https = require('https');
const url = require('url');
const querystring = require('querystring');
const fs = require('fs');
const pathModule = require('path');
const crypto = require('crypto');
const requestTimeout = 20000; // 10 seconds
const cacheDir = pathModule.join(__dirname, '.cache');
const args = process.argv.slice(2);
const pathIndex = {};
// 增加访问计数器
const viewsInfo = {
// 请求次数
request: 0,
// 缓存命中次数
cacheHit: 0,
// API调用次数
apiCall: 0,
// 缓存调用次数
cacheCall: 0,
// 缓存读取错误次数
cacheReadError: 0,
// API 调用错误次数
fetchApiError: 0,
// API 调用错误次数
fetchApiWarning: 0,
};
// 默认端口号和 API 地址
let port = 9001;
let apiEndpoint = 'https://oss.x-php.com/get/';
// 解析命令行参数
args.forEach(arg => {
// 去掉--
if (arg.startsWith('--')) {
arg = arg.substring(2);
}
const [key, value] = arg.split('=');
if (key === 'port') {
port = parseInt(value, 10);
} else if (key === 'api') {
apiEndpoint = value;
}
});
// 确保缓存目录存在
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir);
}
// 定时清理过期缓存数据
setInterval(() => {
const currentTime = Date.now();
for (const key in pathIndex) {
if (currentTime - pathIndex[key].timestamp > 24 * 60 * 60 * 1000) {
delete pathIndex[key];
}
}
}, 60 * 60 * 1000); // 每隔 1 小时执行一次
// 处理请求并返回数据
const server = http.createServer(async (req, res) => {
req.url = req.url.replace(/\/{2,}/g, '/');
const parsedUrl = url.parse(req.url, true);
// 解析得到 sign 字段
const sign = parsedUrl.query.sign || '';
// 获取第一个路径
let reqPath = parsedUrl.pathname.split('/')[1];
// 取第一个路径以外的路径
let token = parsedUrl.pathname.split('/').slice(2).join('/');
// 处理根路径请求
if (reqPath === 'favicon.ico') {
res.writeHead(204);
res.end();
return;
}
// 返回 endpoint, 缓存目录, 缓存数量, 用于监听服务是否正常运行
if (reqPath === 'endpoint') {
if (parsedUrl.query.api) {
// 判断 parsedUrl.query.api 是否为网址
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w.-]*)*\/?$/;
if (urlRegex.test(parsedUrl.query.api)) {
apiEndpoint = parsedUrl.query.api;
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
code: 200,
data: {
api: apiEndpoint,
port: port,
cacheDir: cacheDir,
pathIndexCount: Object.keys(pathIndex).length,
viewsInfo: viewsInfo
}
}));
return;
}
// 当没有 token 或 undefined
if (token === '' || typeof token === 'undefined') {
token = reqPath;
reqPath = 'go';
}
// 检查第一个路径只能是 avatar,endpoint,go,bbs,www
if (!['avatar', 'go', 'bbs', 'www', 'url', 'thumb'].includes(reqPath)) {
res.writeHead(404, { 'Content-Type': 'text/plain;charset=UTF-8' });
res.end('Not Found');
return;
}
if (!token || reqPath === '') {
res.writeHead(400, { 'Content-Type': 'text/plain;charset=UTF-8' });
res.end('Bad Request: Missing Token or path (' + reqPath + ')');
return;
}
// 增加请求次数
viewsInfo.request++;
const uniqidhex = crypto.createHash('md5').update(reqPath + token + sign).digest('hex');
let cacheMetaFile = '';
let cacheContentFile = '';
let tempCacheContentFile = '';
if (pathIndex[uniqidhex]) {
cacheMetaFile = pathModule.join(cacheDir, `${pathIndex[uniqidhex].uniqid}.meta`);
cacheContentFile = pathModule.join(cacheDir, `${pathIndex[uniqidhex].uniqid}.content`);
}
if (pathIndex[uniqidhex] && isCacheValid(cacheMetaFile, cacheContentFile)) {
const { cacheData, isNotModified } = await checkCacheHeaders(req, cacheMetaFile);
if (isNotModified) {
res.writeHead(304);
res.end();
} else {
// 增加缓存命中次数
viewsInfo.cacheHit++;
serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res);
}
} else {
try {
// 增加 API 调用次数
viewsInfo.apiCall++;
const apiData = await fetchApiData(reqPath, token, sign);
// 302 或 301 重定向
if (apiData.code === 302 || apiData.code === 301) {
res.writeHead(302, { Location: apiData.data.url });
res.end();
return;
}
if (apiData.code === 200 && apiData.data && apiData.data.url) {
const { url: realUrl, cloudtype, expiration, path, headers, uniqid } = apiData.data;
const data = { realUrl, cloudtype, expiration: expiration * 1000, path, headers, uniqid };
// 修改 pathIndex 记录时,添加时间戳
pathIndex[uniqidhex] = { uniqid: data.uniqid, timestamp: Date.now() };
cacheMetaFile = pathModule.join(cacheDir, `${data.uniqid}.meta`);
cacheContentFile = pathModule.join(cacheDir, `${data.uniqid}.content`);
tempCacheContentFile = pathModule.join(cacheDir, `${data.uniqid}_${crypto.randomBytes(16).toString('hex')}.temp`);
// 重新写入 meta 缓存
fs.writeFileSync(cacheMetaFile, JSON.stringify(data));
// 如果内容缓存存在, 则直接调用
if (fs.existsSync(cacheContentFile)) {
serveFromCache(data, cacheContentFile, cacheMetaFile, res);
} else {
fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res);
}
} else {
// 记录响应错误
viewsInfo.fetchApiWarning++;
// 如果有缓存有忽略过期,直接调用
if (pathIndex[uniqidhex] && fs.existsSync(cacheMetaFile) && fs.existsSync(cacheContentFile)) {
serveFromCache(JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8')), cacheContentFile, cacheMetaFile, res);
} else {
// utf8 解码
res.writeHead(502, { 'Content-Type': 'text/plain;charset=UTF-8;' });
res.end(apiData.message || 'Bad Gateway');
}
// utf8 解码
}
} catch (error) {
// 记录响应错误
viewsInfo.fetchApiError++;
// 起用备用 API
// apiEndpoint = 'https://x-mo.cn:9001/get/';
// utf8 解码
res.writeHead(502, { 'Content-Type': 'text/plain;charset=UTF-8;' });
res.end('Bad Gateway: Failed to decode JSON' + error);
}
}
});
// 检查缓存头并返回是否为304
const checkCacheHeaders = async (req, cacheMetaFile) => {
const cacheData = JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8'));
const ifNoneMatch = req.headers['if-none-match'];
const ifModifiedSince = req.headers['if-modified-since'];
let isNotModified = false;
if (ifNoneMatch && ifNoneMatch === cacheData.uniqid) {
isNotModified = true;
} else if (ifModifiedSince) {
const lastModified = new Date(cacheData.headers['Last-Modified']);
const modifiedSince = new Date(ifModifiedSince);
if (lastModified <= modifiedSince) {
isNotModified = true;
}
}
return { cacheData, isNotModified };
};
// 检查缓存是否有效
const isCacheValid = (cacheMetaFile, cacheContentFile) => {
if (!fs.existsSync(cacheMetaFile) || !fs.existsSync(cacheContentFile)) return false;
const cacheData = JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8'));
return cacheData.expiration > Date.now();
};
// 从 API 获取数据
const fetchApiData = (reqPath, token, sign) => {
return new Promise((resolve, reject) => {
// 将请求路径和参数进行编码
const queryParams = querystring.stringify({
type: reqPath,
sign: sign
});
const apiUrl = `${apiEndpoint}?${queryParams}`;
const apiReq = https.request(apiUrl, {
method: 'GET',
headers: {
'Accept': 'application/json; charset=utf-8',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36',
'token': token
},
timeout: requestTimeout,
rejectUnauthorized: false
}, (apiRes) => {
let data = '';
apiRes.on('data', chunk => data += chunk);
apiRes.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (error) {
reject(error);
}
});
});
apiReq.on('error', reject);
apiReq.end();
});
};
// 从真实 URL 获取数据并写入缓存
const fetchAndServe = (data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res) => {
https.get(data.realUrl, { timeout: requestTimeout * 10, rejectUnauthorized: false }, (realRes) => {
const cacheStream = fs.createWriteStream(tempCacheContentFile, { flags: 'w' });
let isVideo = data.path && typeof data.path === 'string' && data.path.includes('.mp4');
// 确保 content-length 是有效的
const contentLength = realRes.headers['content-length'];
if (contentLength) {
data.headers['content-length'] = contentLength;
// 更新 data 到缓存 cacheMetaFile
fs.writeFileSync(cacheMetaFile, JSON.stringify(data));
} else {
console.warn('Warning: content-length is undefined for the response from:', data.realUrl);
}
const defaultHeaders = {
'Cloud-Type': data.cloudtype,
'Cloud-Expiration': data.expiration,
'Content-Type': isVideo ? 'video/mp4' : 'application/octet-stream',
'ETag': data.uniqid || '',
'Cache-Control': 'public, max-age=31536000',
'Expires': new Date(Date.now() + 31536000000).toUTCString(),
'Accept-Ranges': 'bytes',
'Connection': 'keep-alive',
'Date': new Date().toUTCString(),
'Last-Modified': new Date().toUTCString(),
};
res.writeHead(realRes.statusCode, Object.assign({}, defaultHeaders, data.headers));
realRes.pipe(cacheStream);
realRes.pipe(res);
realRes.on('end', () => {
cacheStream.end();
if (fs.existsSync(tempCacheContentFile)) {
try {
fs.renameSync(tempCacheContentFile, cacheContentFile);
} catch (err) {
console.error(`Error renaming file: ${err}`);
}
}
});
realRes.on('error', (e) => {
handleResponseError(res, tempCacheContentFile, data.realUrl);
});
}).on('error', (e) => {
handleResponseError(res, tempCacheContentFile, data.realUrl);
});
};
// 从缓存中读取数据并返回
const serveFromCache = (cacheData, cacheContentFile, cacheMetaFile, res) => {
// 增加缓存调用次数
viewsInfo.cacheCall++;
const readStream = fs.createReadStream(cacheContentFile);
let isVideo = cacheData.path && typeof cacheData.path === 'string' && cacheData.path.includes('.mp4');
// 查询 cacheData.headers['content-length'] 是否存在
if (!cacheData.headers['content-length'] || cacheData.headers['content-length'] === 0) {
// 读取文件大小并更新 cacheData.headers['content-length']
const contentLength = fs.statSync(cacheContentFile).size;
if (contentLength) {
cacheData.headers['content-length'] = contentLength;
// 更新 cacheData 到缓存 cacheMetaFile
fs.writeFileSync(cacheMetaFile, JSON.stringify(cacheData));
} else {
console.warn('Warning: content-length is undefined for cached content file:', cacheContentFile);
}
}
readStream.on('open', () => {
const defaultHeaders = {
'Cloud-Type': cacheData.cloudtype,
'Cloud-Expiration': cacheData.expiration,
'Content-Type': isVideo ? 'video/mp4' : 'application/octet-stream',
'ETag': cacheData.uniqid || '',
'Cache-Control': 'public, max-age=31536000',
'Expires': new Date(Date.now() + 31536000000).toUTCString(),
'Accept-Ranges': 'bytes',
'Connection': 'keep-alive',
'Date': new Date().toUTCString(),
'Last-Modified': new Date().toUTCString(),
}
res.writeHead(200, Object.assign({}, defaultHeaders, cacheData.headers));
readStream.pipe(res);
});
readStream.on('error', (err) => {
handleCacheReadError(res);
});
};
// 处理响应错误
const handleResponseError = (res, tempCacheContentFile, realUrl) => {
// 增加缓存读取错误次数
viewsInfo.cacheReadError++;
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'text/plain;charset=UTF-8' });
res.end(`Bad Gateway: ${realUrl}`);
}
if (fs.existsSync(tempCacheContentFile)) {
fs.unlinkSync(tempCacheContentFile);
}
};
// 处理缓存读取错误
const handleCacheReadError = (res) => {
// 增加缓存读取错误次数
viewsInfo.cacheReadError++;
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain;charset=UTF-8' });
res.end('Internal Server Error: Unable to read cache content file');
}
};
// 启动服务器
server.listen(port, () => {
console.log(`Proxy server is running on http://localhost:${port}`);
});
// 处理 SIGINT 信号Ctrl+C
process.on('SIGINT', () => {
console.log('Received SIGINT. Shutting down gracefully...');
server.close(() => {
console.log('Server closed.');
process.exit(0);
});
setTimeout(() => {
console.error('Forcing shutdown...');
process.exit(1);
}, 10000);
});