/
server_s3.go
166 lines (138 loc) · 4.45 KB
/
server_s3.go
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package main
import (
"bytes"
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/s3utils"
"github.com/minio/minio-go/v7/pkg/signer"
"github.com/pkg/errors"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)
type serverS3 struct {
opts obsOptions
s3opts obsS3Options
logger *zap.SugaredLogger
s3c *minio.Client
}
func (s *serverS3) Init(ctx context.Context, opts serverOptions) (err error) {
s.opts = opts.GetOpts()
s.s3opts = opts.GetS3Opts()
s.logger = opts.Logger.Named(s.Name()).Sugar()
if s.s3c, err = newObsS3Client(s.s3opts); err != nil {
err = errors.Wrap(err, "obs s3 client")
return
}
return
}
func (s *serverS3) Name() string {
return "s3"
}
func (s *serverS3) getLogger() *zap.SugaredLogger { return s.logger }
func (s *serverS3) reportError(ctx *fasthttp.RequestCtx, errType string, err any) {
reportError(s, ctx, errType, err)
}
var (
ErrKind_S3ComposeRequest = "S3_COMPOSE_REQUEST"
ErrKind_S3CredsProvider = "S3_CREDS_PROVIDER"
)
func (s *serverS3) handle(ctx *fasthttp.RequestCtx) {
isMethodGet := bytes.Equal(ctx.Method(), MethodGet)
isMethodHead := bytes.Equal(ctx.Method(), MethodHead)
if !isMethodGet && !isMethodHead {
ctx.SetStatusCode(http.StatusMethodNotAllowed)
s.reportError(ctx, ErrKind_MethodNotAllowed, "")
return
}
if isMethodHead {
// Doc: https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.2-1
ctx.Response.Header.Set("Content-Length", "0")
}
bucketName := s.opts.BucketName
isVirtualHostStyle := isVirtualHostStyleRequest(s.s3c, *s.s3c.EndpointURL(), bucketName)
path := ctx.Path()
_path := bytes.TrimLeft(path, "/")
if s.opts.RemoveBucketName {
if _, _pathWithoutBucketName, found := bytes.Cut(_path, []byte(`/`)); found {
// no need to check `isVirtualHostStyle` since this is our own implementation of handling request URI
_path = _pathWithoutBucketName
}
}
objectName := unsafeByteSliceToString(_path)
s.logger.Debugw("handle",
"bucket", bucketName,
"objectName", objectName)
// check if we had access to the object
if meta, err := s.s3c.StatObject(ctx, bucketName, objectName, minio.GetObjectOptions{}); err != nil {
ctx.SetStatusCode(http.StatusNotFound)
s.reportError(ctx, ErrKind_ResourceNotFound, err)
return
} else {
_ = meta
}
// compose initial request
expireSeconds := int64(s.opts.URLExpiry / time.Second)
req, err := newRequest(s.s3c, ctx, http.MethodGet, requestMetadata{
presignURL: true,
bucketName: bucketName,
objectName: objectName,
expires: expireSeconds, // to trigger presigned generator
queryValues: url.Values{},
})
if err != nil {
ctx.SetStatusCode(http.StatusInternalServerError)
s.reportError(ctx, ErrKind_S3ComposeRequest, err)
return
}
// grab creds from provider
value, err := getCredsProvider(s.s3c).Get()
if err != nil {
ctx.SetStatusCode(http.StatusInternalServerError)
s.reportError(ctx, ErrKind_S3CredsProvider, err)
return
}
var statusCode = s.opts.RedirectCode
// custom "expiry"
var exp string
if expiry := s.opts.URLExpiry; expiry == maxURLExpiry || expiry <= 0 {
// clear given params, set max signed value for expire, and re-presign.
exp = strconv.FormatInt(int64(^uint64(0)/2), 10) // ~250years
} else {
// we can't allow a permanent redirect here since we already have
// expiry set, the redirected url needs to be updated.
if statusCode == http.StatusMovedPermanently || (statusCode < 300 || statusCode > 399) {
statusCode = http.StatusTemporaryRedirect
}
expireAt := time.Now().UTC().Add(s.opts.URLExpiry)
exp = strconv.FormatInt(int64(expireAt.Unix()), 10)
// set redirect cache lifetime
if statusCode == http.StatusTemporaryRedirect {
ctx.Response.Header.Set("Cache-Control", fmt.Sprintf("max-age=%d", expireSeconds))
ctx.Response.Header.Set("Expires", expireAt.Format("Mon, 02 Jan 2006 15:04:05 GMT"))
}
}
req.Header.Set("Expires", exp)
req.URL.RawQuery = ""
req = signer.PreSignV2(*req, value.AccessKeyID, value.SecretAccessKey, 0, isVirtualHostStyle)
// re-encode query string with Expires hack.
query := req.URL.Query()
query.Set("Expires", exp)
req.URL.RawQuery = s3utils.QueryEncode(query)
if s.opts.RedirectSecure {
req.URL.Scheme = "https"
} else {
req.URL.Scheme = "http"
}
if hostRedirect := s.opts.HostRedirect; hostRedirect != "" {
req.URL.Host = hostRedirect
}
ctx.Redirect(req.URL.String(), statusCode)
}
func (s *serverS3) GetHandler() fasthttp.RequestHandler {
return s.handle
}