- 콘솔 탭 스크롤 동작 개선 (조건부 자동 스크롤, 맨 아래로 버튼) - 탭 전환 시 레이아웃 쉬프트 방지 (scrollbar-gutter: stable) - 맨 아래로 버튼에 그림자 효과 추가 - Sidebar에 소켓 기반 닉네임 실시간 동기화 로직 추가 - /link/status API에서 displayName 사용하도록 수정
153 lines
4.1 KiB
JavaScript
153 lines
4.1 KiB
JavaScript
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 };
|