~bigbes/core-go

ref: c2c2f3848fa9a88d96952ff08f886483f8b9d1f7 core-go/objects/middleware.go -rw-r--r-- 3.6 KiB
c2c2f384 — bigbes objects: force UnsignedPayload for S3 uploads (phoebe-lab patch) 30 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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 != ""
}