diff --git a/server.js b/server.js index 63437a1..a6e8523 100644 --- a/server.js +++ b/server.js @@ -455,22 +455,77 @@ 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" }); - const testClient = buildS3Client(testCfg); - + // Use raw AWS Signature V4 HTTP request — compatible with RustFS/MinIO/generic S3 try { - await testClient.send(new HeadBucketCommand({ Bucket: testCfg.bucket })); + 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}/`; - res.json({ success: true, message: "✓ S3 connection confirmed! Credentials are valid and the bucket is accessible." }); + // 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) { - let friendly = err.message; - if (err.name === "NoSuchBucket") friendly = `Bucket "${testCfg.bucket}" does not exist`; - else if (err.name === "InvalidAccessKeyId") friendly = "Invalid Access Key ID"; - else if (err.name === "SignatureDoesNotMatch") friendly = "Invalid Secret Access Key"; - else if (err.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) friendly = "Connection refused — check endpoint URL and port"; - else if (err.code === "ENOTFOUND" || err.message?.includes("ENOTFOUND")) friendly = "Endpoint not found — check the URL"; - else if (err.message?.includes("TLS") || err.message?.includes("certificate")) friendly = "TLS/SSL error — endpoint certificate issue"; - console.error("[S3 Test] Error:", err.message, "| name:", err.name, "| code:", err.Code || err.code, "| statusCode:", err.$metadata?.httpStatusCode, "| full:", JSON.stringify(err, Object.getOwnPropertyNames(err))); - res.status(400).json({ success: false, error: friendly }); + 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 }); } });