diff --git a/server.js b/server.js index a6e8523..c99b9e1 100644 --- a/server.js +++ b/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) { - 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)}` }); - } + 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." }); } 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 }); } });