Use ListObjectsV2Command for S3 test instead of raw HTTP signing
This commit is contained in:
parent
1bc30010a4
commit
43aa18f963
1 changed files with 14 additions and 71 deletions
85
server.js
85
server.js
|
|
@ -3,7 +3,7 @@ const express = require("express");
|
||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const { S3Client, PutObjectCommand, HeadBucketCommand, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand } = require("@aws-sdk/client-s3");
|
const { S3Client, PutObjectCommand, HeadBucketCommand, DeleteObjectCommand, ListObjectsV2Command, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand } = require("@aws-sdk/client-s3");
|
||||||
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||||||
const { Upload } = require("@aws-sdk/lib-storage");
|
const { Upload } = require("@aws-sdk/lib-storage");
|
||||||
const { NodeHttpHandler } = require("@smithy/node-http-handler");
|
const { NodeHttpHandler } = require("@smithy/node-http-handler");
|
||||||
|
|
@ -442,7 +442,6 @@ app.put("/api/s3/config", requireAdmin, (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/s3/test", requireAdmin, async (req, res) => {
|
app.post("/api/s3/test", requireAdmin, async (req, res) => {
|
||||||
// Merge submitted values with saved config — secret is often blank (keep existing)
|
|
||||||
const saved = db.s3Config || {};
|
const saved = db.s3Config || {};
|
||||||
const testCfg = {
|
const testCfg = {
|
||||||
endpoint: (req.body.endpoint || saved.endpoint || "").trim(),
|
endpoint: (req.body.endpoint || saved.endpoint || "").trim(),
|
||||||
|
|
@ -455,77 +454,21 @@ app.post("/api/s3/test", requireAdmin, async (req, res) => {
|
||||||
if (!testCfg.bucket || !testCfg.accessKeyId || !testCfg.secretAccessKey)
|
if (!testCfg.bucket || !testCfg.accessKeyId || !testCfg.secretAccessKey)
|
||||||
return res.status(400).json({ success: false, error: "Bucket, Access Key ID, and Secret Key are required to test" });
|
return res.status(400).json({ success: false, error: "Bucket, Access Key ID, and Secret Key are required to test" });
|
||||||
|
|
||||||
// Use raw AWS Signature V4 HTTP request — compatible with RustFS/MinIO/generic S3
|
const testClient = buildS3Client(testCfg);
|
||||||
try {
|
try {
|
||||||
const url = new URL(`${testCfg.endpoint}/${testCfg.bucket}/`);
|
const result = await testClient.send(new ListObjectsV2Command({ Bucket: testCfg.bucket, MaxKeys: 1 }));
|
||||||
const now = new Date();
|
console.log("[S3 Test] ListObjects OK, KeyCount:", result.KeyCount);
|
||||||
const dateStamp = now.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
|
res.json({ success: true, message: "✓ S3 connection confirmed! Credentials are valid and the bucket is accessible." });
|
||||||
const shortDate = dateStamp.slice(0, 8);
|
|
||||||
const host = url.host;
|
|
||||||
const method = "GET";
|
|
||||||
const canonicalUri = `/${testCfg.bucket}/`;
|
|
||||||
|
|
||||||
// AWS Signature V4
|
|
||||||
function hmacSha256(key, data) {
|
|
||||||
return crypto.createHmac("sha256", key).update(data).digest();
|
|
||||||
}
|
|
||||||
function sha256Hex(data) {
|
|
||||||
return crypto.createHash("sha256").update(data).digest("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
const credentialScope = `${shortDate}/${testCfg.region}/s3/aws4_request`;
|
|
||||||
const canonicalHeaders = `host:${host}\nx-amz-content-sha256:UNSIGNED-PAYLOAD\nx-amz-date:${dateStamp}\n`;
|
|
||||||
const signedHeaders = "host;x-amz-content-sha256;x-amz-date";
|
|
||||||
const canonicalRequest = `${method}\n${canonicalUri}\n\n${canonicalHeaders}\n${signedHeaders}\nUNSIGNED-PAYLOAD`;
|
|
||||||
const stringToSign = `AWS4-HMAC-SHA256\n${dateStamp}\n${credentialScope}\n${sha256Hex(canonicalRequest)}`;
|
|
||||||
|
|
||||||
const signingKey = hmacSha256(
|
|
||||||
hmacSha256(
|
|
||||||
hmacSha256(
|
|
||||||
hmacSha256(`AWS4${testCfg.secretAccessKey}`, shortDate),
|
|
||||||
testCfg.region
|
|
||||||
),
|
|
||||||
"s3"
|
|
||||||
),
|
|
||||||
"aws4_request"
|
|
||||||
);
|
|
||||||
const signature = hmacSha256(signingKey, stringToSign).toString("hex");
|
|
||||||
const authHeader = `AWS4-HMAC-SHA256 Credential=${testCfg.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
||||||
|
|
||||||
const fetchUrl = `${testCfg.endpoint}/${testCfg.bucket}/?list-type=2&max-keys=1`;
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
||||||
|
|
||||||
const fetchRes = await fetch(fetchUrl, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Host": host,
|
|
||||||
"x-amz-date": dateStamp,
|
|
||||||
"x-amz-content-sha256": "UNSIGNED-PAYLOAD",
|
|
||||||
"Authorization": authHeader,
|
|
||||||
},
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
clearTimeout(timeout);
|
|
||||||
|
|
||||||
const body = await fetchRes.text();
|
|
||||||
console.log("[S3 Test] Status:", fetchRes.status, "Body:", body.slice(0, 300));
|
|
||||||
|
|
||||||
if (fetchRes.ok) {
|
|
||||||
res.json({ success: true, message: "✓ S3 connection confirmed! Credentials are valid and the bucket is accessible." });
|
|
||||||
} else if (fetchRes.status === 403) {
|
|
||||||
res.status(400).json({ success: false, error: "Access denied — check your Access Key and Secret Key" });
|
|
||||||
} else if (fetchRes.status === 404) {
|
|
||||||
res.status(400).json({ success: false, error: `Bucket "${testCfg.bucket}" does not exist` });
|
|
||||||
} else {
|
|
||||||
res.status(400).json({ success: false, error: `S3 returned ${fetchRes.status}: ${body.slice(0, 200)}` });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[S3 Test] Error:", err.message);
|
console.error("[S3 Test] Full error:", JSON.stringify({ name: err.name, message: err.message, code: err.Code || err.code, status: err.$metadata?.httpStatusCode }));
|
||||||
if (err.name === "AbortError") {
|
let friendly = err.message || "Unknown error";
|
||||||
return res.status(400).json({ success: false, error: "Connection timed out — check the endpoint URL" });
|
if (err.name === "NoSuchBucket" || err.Code === "NoSuchBucket") friendly = `Bucket "${testCfg.bucket}" does not exist`;
|
||||||
}
|
else if (err.name === "InvalidAccessKeyId" || err.Code === "InvalidAccessKeyId") friendly = "Invalid Access Key ID";
|
||||||
res.status(400).json({ success: false, error: err.message });
|
else if (err.name === "SignatureDoesNotMatch" || err.Code === "SignatureDoesNotMatch") friendly = "Invalid Secret Access Key";
|
||||||
|
else if (err.name === "AccessDenied" || err.Code === "AccessDenied") friendly = "Access denied — credentials don't have permission for this bucket";
|
||||||
|
else if (err.message?.includes("ECONNREFUSED")) friendly = "Connection refused — check endpoint URL";
|
||||||
|
else if (err.message?.includes("ENOTFOUND")) friendly = "Endpoint not found — check the URL";
|
||||||
|
res.status(400).json({ success: false, error: friendly });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue