import crypto from "crypto"; import http from "http"; // S3 설정 (RustFS) - 환경변수에서 로드 const s3Config = { endpoint: process.env.S3_ENDPOINT, accessKey: process.env.S3_ACCESS_KEY, secretKey: process.env.S3_SECRET_KEY, bucket: "minecraft", publicUrl: "https://s3.caadiq.co.kr", }; /** * AWS Signature V4 서명 */ function signV4(method, path, headers, payload, accessKey, secretKey) { const service = "s3"; const region = "us-east-1"; const now = new Date(); const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, ""); const dateStamp = amzDate.slice(0, 8); headers["x-amz-date"] = amzDate; headers["x-amz-content-sha256"] = crypto .createHash("sha256") .update(payload) .digest("hex"); const signedHeaders = Object.keys(headers) .map((k) => k.toLowerCase()) .sort() .join(";"); const canonicalHeaders = Object.keys(headers) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) .map((k) => `${k.toLowerCase()}:${headers[k].trim()}`) .join("\n"); const canonicalRequest = [ method, path, "", canonicalHeaders + "\n", signedHeaders, headers["x-amz-content-sha256"], ].join("\n"); const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`; const stringToSign = [ "AWS4-HMAC-SHA256", amzDate, credentialScope, crypto.createHash("sha256").update(canonicalRequest).digest("hex"), ].join("\n"); const getSignatureKey = (key, ds, rg, sv) => { const kDate = crypto.createHmac("sha256", `AWS4${key}`).update(ds).digest(); const kRegion = crypto.createHmac("sha256", kDate).update(rg).digest(); const kService = crypto.createHmac("sha256", kRegion).update(sv).digest(); return crypto .createHmac("sha256", kService) .update("aws4_request") .digest(); }; const signingKey = getSignatureKey(secretKey, dateStamp, region, service); const signature = crypto .createHmac("sha256", signingKey) .update(stringToSign) .digest("hex"); headers[ "Authorization" ] = `AWS4-HMAC-SHA256 Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`; return headers; } /** * S3(RustFS)에 파일 업로드 */ function uploadToS3( bucket, key, data, contentType = "application/octet-stream" ) { return new Promise((resolve, reject) => { const url = new URL(s3Config.endpoint); const path = `/${bucket}/${key}`; const headers = { Host: url.host, "Content-Type": contentType, "Content-Length": data.length.toString(), }; signV4("PUT", path, headers, data, s3Config.accessKey, s3Config.secretKey); const options = { hostname: url.hostname, port: url.port || 80, path, method: "PUT", headers, }; const req = http.request(options, (res) => { let body = ""; res.on("data", (chunk) => (body += chunk)); res.on("end", () => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(`${s3Config.publicUrl}/${bucket}/${key}`); } else { reject(new Error(`S3 Upload failed: ${res.statusCode}`)); } }); }); req.on("error", reject); req.write(data); req.end(); }); } /** * S3(RustFS)에서 파일 다운로드 */ function downloadFromS3(bucket, key) { return new Promise((resolve, reject) => { const url = new URL(s3Config.endpoint); const path = `/${bucket}/${key}`; const headers = { Host: url.host, }; signV4("GET", path, headers, "", s3Config.accessKey, s3Config.secretKey); const options = { hostname: url.hostname, port: url.port || 80, path, method: "GET", headers, }; const req = http.request(options, (res) => { const chunks = []; res.on("data", (chunk) => chunks.push(chunk)); res.on("end", () => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(Buffer.concat(chunks)); } else { reject(new Error(`S3 Download failed: ${res.statusCode}`)); } }); }); req.on("error", reject); req.end(); }); } export { s3Config, uploadToS3, downloadFromS3 };