- Go 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
test / test (push) Successful in 38s
Gives canonicalAndStringToSign a payload-override parameter ("" keeps the
existing header-or-body derivation) so a presigned URL can sign the literal
"UNSIGNED-PAYLOAD" without ever setting X-Amz-Content-Sha256 as a real
header — a browser dereferencing the URL sends no such header, and signing
one that will never arrive would make the signature unverifiable. The
presigned request itself carries no headers at all, so the only signed
header is host, synthesised from the URL.
signAt is a caller-supplied parameter, not time.Now(), so the same mint
(same bucket/key/signAt/ttl) produces a byte-identical URL — the contract a
CDN-cacheable image URL depends on. ttl is capped at seven days (SigV4's own
limit) and rejected at zero or below.
PresignGetObject also takes extra url.Values, merged into the auth
parameters BEFORE canonicalisation so it is covered by the signature like
everything else — the next task's media signer needs to mint URLs carrying
response-cache-control, and an S3 response-* override only takes effect if
it is part of the signed query string. A key beginning with "X-Amz-"
(case-insensitive) is refused: those are the auth parameters themselves,
and letting a caller set X-Amz-Expires or X-Amz-Signature through this door
is how a presigned URL quietly becomes forgeable or unverifiable. Nil or
empty extra behaves exactly as before this parameter existed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JrZHGRKXBTBjYHpScDjdub
|
||
| .forgejo/workflows | ||
| testdata/sigv4_vectors | ||
| .gitignore | ||
| client.go | ||
| client_test.go | ||
| errors.go | ||
| errors_test.go | ||
| go.mod | ||
| LICENSE | ||
| live_test.go | ||
| presign.go | ||
| presign_test.go | ||
| README.md | ||
| signer.go | ||
| signer_test.go | ||
| validate_test.go | ||
s3lite
Minimal, stdlib-only Go client for S3-compatible object storage.
Why this exists
The official AWS SDK works fine against non-AWS S3-compatible endpoints (MinIO, Hetzner Object Storage, R2, Backblaze, Spaces) but it drags in an AWS-flavoured configuration model and a non-trivial dependency tree. s3lite is the inverse: one package, no dependencies, one operation. Suitable when you need GetObject and nothing else.
Install
import "git.kristofferopsahl.com/kristofferopsahl/s3lite"
go get git.kristofferopsahl.com/kristofferopsahl/s3lite@latest
Quickstart
client, err := s3lite.New(s3lite.Config{
Endpoint: "https://hel1.your-objectstorage.com",
Region: "hel1",
AccessKey: os.Getenv("S3_ACCESS_KEY"),
SecretKey: os.Getenv("S3_SECRET_KEY"),
})
if err != nil { panic(err) }
body, hdr, err := client.GetObject(ctx, "my-bucket", "path/to/object.jpg")
if errors.Is(err, s3lite.ErrNotFound) {
// 404 handling
}
if err != nil { panic(err) }
defer body.Close()
io.Copy(os.Stdout, body)
Supported endpoints
Tested against:
- Hetzner Object Storage
By construction (path-style + SigV4) it should work against any S3-compatible service that supports path-style addressing — MinIO, Backblaze B2 (S3 API), Cloudflare R2 in path mode, DigitalOcean Spaces, Wasabi. Open an issue if it doesn't.
Scope
v0.1.0 ships exactly one operation: GetObject. Returns the response body (caller closes) and headers.
Not yet supported (open an issue if you need any of these): CopyObject, HeadObject, multipart uploads, virtual-hosted-style addressing, retries, server-side encryption.
PresignGetObject(bucket, key, signAt, ttl) mints a query-string-signed GET URL (no Authorization header, so it works from a browser or an <img> tag) that is byte-identical for the same signAt+ttl — callers that need a stable, cacheable URL pass a fixed signAt rather than time.Now().
SigV4
The library implements AWS Signature Version 4 in signer.go. It is exercised against AWS's published GET-only test vectors (in testdata/sigv4_vectors/). Path-style addressing only. The signer accepts any service name (the AWS-supplied vectors verify against the literal service string); Client always passes s3 in the credential scope. The region comes from Config.Region.
Tests
go test ./... # unit tests; stdlib + httptest only
S3LITE_TEST_*=... go test -tags=live ./... # opt-in real-endpoint test
License
MIT. See LICENSE.