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
83
server.js
83
server.js
|
|
@ -3,7 +3,7 @@ const express = require("express");
|
|||
const multer = require("multer");
|
||||
const path = require("path");
|
||||
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 { Upload } = require("@aws-sdk/lib-storage");
|
||||
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) => {
|
||||
// Merge submitted values with saved config — secret is often blank (keep existing)
|
||||
const saved = db.s3Config || {};
|
||||
const testCfg = {
|
||||
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)
|
||||
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 {
|
||||
const url = new URL(`${testCfg.endpoint}/${testCfg.bucket}/`);
|
||||
const now = new Date();
|
||||
const dateStamp = now.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
|
||||
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) {
|
||||
const result = await testClient.send(new ListObjectsV2Command({ Bucket: testCfg.bucket, MaxKeys: 1 }));
|
||||
console.log("[S3 Test] ListObjects OK, KeyCount:", result.KeyCount);
|
||||
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) {
|
||||
console.error("[S3 Test] Error:", err.message);
|
||||
if (err.name === "AbortError") {
|
||||
return res.status(400).json({ success: false, error: "Connection timed out — check the endpoint URL" });
|
||||
}
|
||||
res.status(400).json({ success: false, 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 }));
|
||||
let friendly = err.message || "Unknown error";
|
||||
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";
|
||||
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