6.6 KiB
S3 Compatibility Completion Design
Date: 2026-07-07
Goal
Close the remaining known S3 compatibility gaps in TeleUploader while preserving strict SigV4 security. The target is real-client compatibility for common S3 flows against the production endpoint, especially AWS SDK v3, without accepting malformed or unverifiable signatures.
Scope
Implement and verify:
- Presigned
GETURLs return object content instead of403 SignatureDoesNotMatch. GetObjectsupportsRange: bytes=...for single-part objects.- Multipart
GetObjectreturns the complete logical object, not just the first Telegram part. - Multipart
GetObjectsupports byte ranges across part boundaries. - AWS SDK multipart upload is investigated and fixed under strict SigV4 if the mismatch is in TeleUploader canonicalization/routing.
- The remaining CSS lint warning in
src/home.htmlis removed.
Out of scope:
- Weakening SigV4 verification to accept invalid signatures.
- Full AWS S3 feature parity for ACLs, bucket policy, object tags, virtual-hosted-style routing, or checksum-specific APIs.
- Replacing Telegram storage with a different backend.
Design Choice
Use a strict compatibility, shared object streaming layer.
Rather than adding one-off fixes inside handleGetObject, introduce small helpers with clear responsibilities:
- request authentication remains strict and spec-based;
- object lookup returns object metadata and an ordered list of physical Telegram parts;
- range parsing converts a
Rangeheader into byte offsets; - response streaming emits either a full body (
200) or a partial body (206) with correct S3/HTTP headers.
This keeps S3 auth, object resolution, byte planning, and streaming separately testable.
Presigned URL Verification
Current behavior builds a URL with http://localhost/..., which can produce a host mismatch for presigned signatures generated against the public production host.
New behavior:
- pass the original request URL and headers into presigned verification;
- compute canonical URI from the actual URL path;
- compute canonical query string by excluding
X-Amz-Signature, sorting encoded key/value pairs, and preserving AWS SigV4 encoding rules; - validate credential scope parts: access key, date, region, service
s3, terminationaws4_request; - use the signed
hostheader value when present, otherwise the request URL host; - parse
X-Amz-Dateas UTC and enforceX-Amz-Expires.
Production E2E must change from accepting [200, 302, 403] to requiring 200 and body content for presigned GET when PROXY_S3_GET is enabled.
Object Body Model
Represent the physical body as ordered parts:
interface ObjectPartSource {
telegramFileId: string;
sizeBytes: number;
partNumber: number;
}
Single-part objects produce one source from the files row.
Multipart objects produce sources from multipart_parts ordered by part_number.
The logical object size is:
file.sizeBytesfor single-part;- sum of part sizes for multipart, cross-checked against
file.sizeByteswhen available.
Range Handling
Support single-range requests only:
bytes=0-4bytes=5-bytes=-500
Invalid or unsatisfiable ranges return 416 with Content-Range: bytes */<size>.
Multiple ranges such as bytes=0-1,3-4 are not supported and return 416.
For a valid range:
- return status
206; - set
Content-Range: bytes <start>-<end>/<size>; - set
Content-Lengthtoend - start + 1; - keep
Accept-Ranges: bytes.
Telegram Streaming
For each needed part:
- Call
getFileInfo(telegramFileId). - Build the Telegram CDN URL.
- Fetch only the needed range when possible using a
Rangerequest to Telegram. - If Telegram returns
206, stream that response body. - If Telegram returns
200for a partial request, slice the response in TeleUploader for correctness. - If Telegram fetch fails, return S3 XML
InternalErrorwith HTTP502.
Multipart full-object responses concatenate part streams in order with a ReadableStream.
Multipart range responses calculate part overlap using cumulative byte offsets and only stream overlapped segments.
AWS SDK Multipart Upload
Add a real AWS SDK multipart test path using:
CreateMultipartUploadCommandUploadPartCommandCompleteMultipartUploadCommandGetObjectCommand- cleanup via
AbortMultipartUploadCommandon failure
Keep SigV4 strict. If the SDK multipart signature still fails:
- capture server-side canonical request components in a temporary diagnostic path or test-only log;
- compare against the SDK request inputs;
- fix TeleUploader canonicalization if wrong;
- if the mismatch is caused by upstream proxy/header mutation outside the app, leave strict behavior intact and document the blocker with evidence.
No compatibility fallback is allowed in this pass.
CSS Lint Warning
Remove noDescendingSpecificity by reordering the .modal input rule before the more specific .topbar .search input rule, or by making selector ordering explicitly non-conflicting. No visual redesign is required.
Tests and Verification
Unit tests:
- presigned URL canonicalization and expiration edge cases;
- range parser valid/invalid forms;
- multipart range planning across part boundaries.
Production E2E:
- presigned
GETreturns200and expected body; - single-part range returns
206and expected substring; - multipart upload via manual signer returns complete body;
- multipart range returns expected cross-part substring;
- invalid range returns
416.
AWS SDK E2E:
- existing SDK suite remains passing;
GetObjectCommand({ Range: 'bytes=...' })verifies partial body;- real SDK multipart is enabled if strict SigV4 succeeds after canonicalization fixes.
Deploy verification:
bun run linthas no errors and no warnings;- targeted unit tests pass;
- production E2E passes;
- AWS SDK E2E passes;
./deploy.shcompletes and container is healthy;- post-deploy production E2E and SDK E2E pass.
Error Handling
- Authentication failures return S3 XML errors with
403. - Missing bucket/key behavior is unchanged.
- Telegram fetch failures return S3 XML
InternalErrorwith502. - Invalid ranges return
InvalidRangeXML with416. - Multipart objects with no parts return
InternalError.
Rollout
The existing PROXY_S3_GET flag remains:
- default
true: S3-compatible streaming responses; false: legacy redirect path for single-part objects only.
Multipart complete-body support requires proxy mode. If proxy mode is disabled and a multipart object is requested, the response may fall back to the first-part redirect only as legacy behavior; production compatibility should keep proxy mode enabled.