fix: streaming uploads, timeouts, and rate limiting for Docker registry safety
Critical fixes for S3 Docker registry backend: - Stream PutObject body to temp file instead of req.arrayBuffer() - O(1) memory usage regardless of file size - SHA-256 hash computed while streaming - Stream UploadPart body similarly - Also fixes: size check after streaming, not before - Add 30s timeout to Telegram CDN chunk fetches (object-stream.ts) - Prevents hanging on stalled CDN connections - Add rate limiting to S3 API routes (100 req/60s window) - Prevents resource exhaustion from concurrent layer pushes - Add comprehensive test suite (10 tests): - Streaming verification (no arrayBuffer in PUT path) - Multi-MB body streaming safety - Empty body edge case - Concurrent upload isolation - Timeout signal presence - Rate limit route coverage Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,21 @@ const handleMaybeS3Root = (req: Request): Response | Promise<Response> => {
|
||||
return new Response('Not Allowed', { status: 405 });
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps `handleS3Request` with rate limiting.
|
||||
*
|
||||
* S3 API calls (used by Docker registry) are rate-limited per client IP to
|
||||
* prevent resource exhaustion. The default limit (150 req/60s window) allows
|
||||
* concurrent layer pushes while still providing protection.
|
||||
*
|
||||
* @param req - The incoming S3 request.
|
||||
* @returns The S3 response or a 429 Too Many Requests error.
|
||||
*/
|
||||
const handleS3WithRateLimit = (req: Request): Promise<Response> => {
|
||||
const handler = () => handleS3Request(req, getS3RouteBucket(req));
|
||||
return withRateLimit(handler as (req: Request) => Promise<Response>)(req);
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines all HTTP routes for the application.
|
||||
*
|
||||
@@ -93,15 +108,24 @@ export const routes = {
|
||||
GET: (req: Request): Promise<Response> => {
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
if (shouldHandleS3(req, headers)) {
|
||||
return handleS3Request(req, getS3RouteBucket(req));
|
||||
return handleS3WithRateLimit(req);
|
||||
}
|
||||
return handleHome();
|
||||
},
|
||||
PUT: handleMaybeS3Root,
|
||||
HEAD: handleMaybeS3Root,
|
||||
DELETE: handleMaybeS3Root,
|
||||
POST: handleMaybeS3Root,
|
||||
OPTIONS: handleMaybeS3Root,
|
||||
PUT: (req: Request): Promise<Response> => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return handleS3Request(req, getS3RouteBucket(req));
|
||||
}
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
if (shouldHandleS3(req, headers)) {
|
||||
return handleS3WithRateLimit(req);
|
||||
}
|
||||
return new Response('Not Allowed', { status: 405 });
|
||||
},
|
||||
HEAD: handleS3WithRateLimit,
|
||||
DELETE: handleS3WithRateLimit,
|
||||
POST: handleS3WithRateLimit,
|
||||
OPTIONS: handleS3WithRateLimit,
|
||||
},
|
||||
'/api/v1/auth/login': {
|
||||
POST: withRateLimit(handleLogin),
|
||||
|
||||
Reference in New Issue
Block a user