Files
image-service/app.js
2026-02-13 17:46:13 +08:00

110 lines
3.7 KiB
JavaScript
Raw 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 url = require('url');
const path = require('path');
const fs = require('fs');
const sharp = require('sharp');
const PORT = process.env.PORT || 3000;
const IMAGE_BASE_DIR = process.env.IMAGE_BASE_DIR || '/app/images';
/**
* 安全检查:防止路径遍历攻击(如 ../../etc/passwd
* @param {string} targetPath 目标文件路径
* @param {string} baseDir 基础目录
* @returns {boolean} 是否合法
*/
function isSafePath(targetPath, baseDir) {
const resolvedTarget = path.resolve(targetPath).toLowerCase();
const resolvedBase = path.resolve(baseDir).toLowerCase();
return resolvedTarget.startsWith(resolvedBase);
}
/**
* 发送错误响应
* @param {http.ServerResponse} res 响应对象
* @param {number} statusCode 状态码
* @param {string} message 错误信息
*/
function sendError(res, statusCode, message) {
res.writeHead(statusCode, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(message);
}
const server = http.createServer(async (req, res) => {
if (req.method !== 'GET') {
return sendError(res, 405, '仅支持GET请求');
}
try {
const parsedUrl = url.parse(req.url, true);
const requestPath = parsedUrl.pathname;
const query = parsedUrl.query;
// 过滤根路径请求(避免直接访问服务根目录)
if (requestPath === '/') {
return sendError(res, 200, `
图片处理服务已启动!
使用示例:
1. 访问原图片http://localhost:${PORT}/a/b/c.png
2. 调整质量http://localhost:${PORT}/a/b/c.png?q=80
图片基础目录:${IMAGE_BASE_DIR}
`);
}
const requestedFilePath = requestPath.slice(1);
const fullImagePath = path.join(IMAGE_BASE_DIR, requestedFilePath);
// 安全检查:防止路径遍历
if (!isSafePath(fullImagePath, IMAGE_BASE_DIR)) {
return sendError(res, 403, '访问拒绝:非法路径(路径遍历攻击)');
}
if (!fs.existsSync(fullImagePath)) {
return sendError(res, 404, `图片不存在:${requestedFilePath}`);
}
if (!query.q) {
const fileStream = fs.createReadStream(fullImagePath);
const ext = path.extname(fullImagePath).slice(1); // 如 png/jpg
res.writeHead(200, { 'Content-Type': `image/${ext}` });
// 流式传输文件(适合大文件)
fileStream.pipe(res);
fileStream.on('error', (err) => {
sendError(res, 500, `读取文件失败:${err.message}`);
});
return;
}
const quality = parseInt(query.q);
if (isNaN(quality)) {
return sendError(res, 400, '质量参数q必须是数字1-100');
}
const finalQuality = Math.max(1, Math.min(100, quality));
const imageBuffer = await sharp(fullImagePath).rotate().jpeg({ quality: finalQuality }).toBuffer();
res.writeHead(200, {
'Content-Type': 'image/jpeg',
'Content-Length': imageBuffer.length
});
res.end(imageBuffer);
} catch (error) {
console.error('服务器异常:', error);
sendError(res, 500, `服务器内部错误:${error.message}`);
}
});
// ===================== 启动服务器 =====================
server.listen(PORT, '0.0.0.0', () => {
console.log(`服务地址http://0.0.0.0:${PORT}`);
});
// 捕获全局未处理异常
process.on('uncaughtException', (err) => {
console.error('全局未捕获异常:', err);
});
process.on('unhandledRejection', (reason) => {
console.error('全局未处理Promise拒绝', reason);
});