feat: S3 client compatibility — virtual-hosted style, CORS, presigned multi-method, ListMultipartUploads, edge case fixes
- Virtual-hosted bucket detection from Host header (extractS3BucketFromHost) - S3 CORS headers + OPTIONS preflight + x-amz-id-2/HostId everywhere - Presigned GET/PUT/HEAD/DELETE via centralized auth (no GET-only restriction) - ListMultipartUploads with DB/xml helpers - UploadPart partNumber range validation (1-10000) - CompleteMultipartUpload ETag matching and ascending order validation - DeleteObjects quiet mode support - CopyObject URL-decode and conditional if-match/if-none-match - encoding-type=url support in ListObjects V1/V2 XML - Safe range-based prefix matching (replaces SQL LIKE) - STREAMING-AWS4-HMAC-SHA256-PAYLOAD → 501 NotImplemented - Traefik wildcard HostRegex for virtual-hosted style - S3_VHOST_DOMAINS config env var
This commit is contained in:
+36
-33
@@ -13,6 +13,7 @@ import logger from './utils/logger';
|
||||
import { metricsCollector } from './utils/metrics';
|
||||
import { cleanupRateLimitCache, withRateLimit } from './utils/rateLimit';
|
||||
import { isS3Request } from './utils/s3/auth';
|
||||
import { extractS3BucketFromHost } from './utils/s3/virtual-host';
|
||||
|
||||
// ─── Auto-run migration at startup ──────────────────────────────────────────
|
||||
try {
|
||||
@@ -30,6 +31,29 @@ try {
|
||||
logger.warn('Auto-migration warning (non-fatal)');
|
||||
}
|
||||
|
||||
const getS3RouteBucket = (req: Request): string | null => {
|
||||
const host = req.headers.get('host') || '';
|
||||
return extractS3BucketFromHost(host, config.s3VhostDomains);
|
||||
};
|
||||
|
||||
const shouldHandleS3 = (req: Request, headers: Record<string, string>): boolean => {
|
||||
const url = new URL(req.url);
|
||||
return Boolean(
|
||||
getS3RouteBucket(req) || isS3Request(headers) || url.searchParams.has('X-Amz-Signature'),
|
||||
);
|
||||
};
|
||||
|
||||
const handleMaybeS3Root = (req: Request): Response | Promise<Response> => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return handleS3Request(req, getS3RouteBucket(req));
|
||||
}
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
if (shouldHandleS3(req, headers)) {
|
||||
return handleS3Request(req, getS3RouteBucket(req));
|
||||
}
|
||||
return new Response('Not Allowed', { status: 405 });
|
||||
};
|
||||
|
||||
const server = serve({
|
||||
port: config.port,
|
||||
routes: {
|
||||
@@ -54,40 +78,16 @@ const server = serve({
|
||||
'/': {
|
||||
GET: (req: Request) => {
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
const url = new URL(req.url);
|
||||
if (isS3Request(headers) || url.searchParams.has('X-Amz-Signature')) {
|
||||
return handleS3Request(req);
|
||||
if (shouldHandleS3(req, headers)) {
|
||||
return handleS3Request(req, getS3RouteBucket(req));
|
||||
}
|
||||
return handleHome();
|
||||
},
|
||||
PUT: (req: Request) => {
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
if (isS3Request(headers)) {
|
||||
return handleS3Request(req);
|
||||
}
|
||||
return new Response('Not Allowed', { status: 405 });
|
||||
},
|
||||
HEAD: (req: Request) => {
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
if (isS3Request(headers)) {
|
||||
return handleS3Request(req);
|
||||
}
|
||||
return new Response('Not Allowed', { status: 405 });
|
||||
},
|
||||
DELETE: (req: Request) => {
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
if (isS3Request(headers)) {
|
||||
return handleS3Request(req);
|
||||
}
|
||||
return new Response('Not Allowed', { status: 405 });
|
||||
},
|
||||
POST: (req: Request) => {
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
if (isS3Request(headers)) {
|
||||
return handleS3Request(req);
|
||||
}
|
||||
return new Response('Not Allowed', { status: 405 });
|
||||
},
|
||||
PUT: handleMaybeS3Root,
|
||||
HEAD: handleMaybeS3Root,
|
||||
DELETE: handleMaybeS3Root,
|
||||
POST: handleMaybeS3Root,
|
||||
OPTIONS: handleMaybeS3Root,
|
||||
},
|
||||
'/api/v1/*': {
|
||||
GET: handleWebApiV1,
|
||||
@@ -97,9 +97,12 @@ const server = serve({
|
||||
},
|
||||
},
|
||||
fetch: async (req: Request) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return handleS3Request(req, getS3RouteBucket(req));
|
||||
}
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
if (isS3Request(headers) || new URL(req.url).searchParams.has('X-Amz-Signature')) {
|
||||
return handleS3Request(req);
|
||||
if (shouldHandleS3(req, headers)) {
|
||||
return handleS3Request(req, getS3RouteBucket(req));
|
||||
}
|
||||
return new Response('Not Found', { status: 404 });
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user