All checks were successful
continuous-integration/drone Build is passing
default value for option to remove bucket name is `false`
166 lines
4.4 KiB
Go
166 lines
4.4 KiB
Go
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
|
|
}
|