package objects import ( "context" "errors" "fmt" "net/http" "github.com/aws/aws-sdk-go-v2/aws" v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/smithy-go/endpoints" smithymw "github.com/aws/smithy-go/middleware" "github.com/vaughan0/go-ini" "git.sr.ht/~sircmpwn/core-go/config" ) var s3CtxKey = &contextKey{"s3"} type contextKey struct { name string } func Middleware(client *s3.Client) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := Context(r.Context(), client) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } } func Context(ctx context.Context, client *s3.Client) context.Context { return context.WithValue(ctx, s3CtxKey, client) } func ForContext(ctx context.Context) *s3.Client { raw, ok := ctx.Value(s3CtxKey).(*s3.Client) if !ok { panic(fmt.Errorf("invalid S3 context")) } return raw } var ErrDisabled = errors.New("object storage is not enabled for this server") type S3Resolver struct { conf ini.File resolver s3.EndpointResolverV2 } func (r *S3Resolver) ResolveEndpoint( ctx context.Context, params s3.EndpointParameters, ) (transport.Endpoint, error) { upstream, _ := r.conf.Get("objects", "s3-upstream") endpoint, err := r.resolver.ResolveEndpoint(ctx, params) if err != nil { return endpoint, err } endpoint.URI.Host = upstream if params.Bucket != nil { endpoint.URI.Path = *params.Bucket } return endpoint, nil } func NewClient(conf ini.File) (*s3.Client, error) { upstream, _ := conf.Get("objects", "s3-upstream") accessKey, _ := conf.Get("objects", "s3-access-key") secretKey, _ := conf.Get("objects", "s3-secret-key") if upstream == "" || accessKey == "" || secretKey == "" { return nil, ErrDisabled } region, ok := conf.Get("objects", "s3-region") if !ok { region = "default" } scheme := "https://" if config.GetBool(conf, "objects", "s3-insecure", false) { scheme = "http://" } creds := credentials.NewStaticCredentialsProvider(accessKey, secretKey, "") return s3.NewFromConfig(aws.Config{ Region: region, Credentials: creds, }, s3.WithSigV4SigningRegion(region), func(opts *s3.Options) { // Patched (phoebe-lab): pages Publish + builds artifact upload pass // non-seekable bodies (tar.Reader, io.LimitReader) to PutObject. The // SDK's default dynamic-payload middleware only uses UnsignedPayload // over HTTPS; over HTTP (s3-insecure=true to internal Garage) it // falls through to ComputePayloadSHA256, which seeks the body and // fails. Force UnsignedPayload unconditionally. The above // WithSigV4SigningRegion pins the sigv4 region so the V1 endpoint // resolver doesn't override it back to "default". opts.APIOptions = append(opts.APIOptions, func(stack *smithymw.Stack) error { _, err := stack.Finalize.Swap("ComputePayloadHash", &v4.UnsignedPayload{}) return err }) opts.BaseEndpoint = aws.String(scheme + upstream) opts.EndpointResolverV2 = &S3Resolver{ conf, s3.NewDefaultEndpointResolverV2(), } }), nil } func URL(conf ini.File) string { upstream, _ := conf.Get("objects", "s3-upstream") if upstream == "" { return "" } scheme := "https://" if config.GetBool(conf, "objects", "s3-insecure", false) { scheme = "http://" } return scheme + upstream } func Enabled(conf ini.File) bool { upstream, _ := conf.Get("objects", "s3-upstream") accessKey, _ := conf.Get("objects", "s3-access-key") secretKey, _ := conf.Get("objects", "s3-secret-key") return upstream != "" && accessKey != "" && secretKey != "" }