package auth import ( "context" "fmt" "net/http" "net/url" "strconv" "strings" "time" v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2" v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/smithy-go/encoding/httpbinding" "github.com/aws/smithy-go/logging" "go.uber.org/zap" ) type RequestData struct { Method string Endpoint string Bucket string Object string } type PresignData struct { Service string Region string Lifetime time.Duration SignTime time.Time Headers map[string]string } // PresignRequest forms pre-signed request to access objects without aws credentials. func PresignRequest(ctx context.Context, creds aws.Credentials, reqData RequestData, presignData PresignData, log *zap.Logger) (*http.Request, error) { urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, httpbinding.EscapePath(reqData.Bucket, false), httpbinding.EscapePath(reqData.Object, false)) req, err := http.NewRequest(strings.ToUpper(reqData.Method), urlStr, nil) if err != nil { return nil, fmt.Errorf("failed to create new request: %w", err) } for k, v := range presignData.Headers { req.Header.Set(k, v) // maybe we should filter system header (or keep responsibility on caller) } req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z")) req.Header.Set(AmzExpires, strconv.FormatFloat(presignData.Lifetime.Round(time.Second).Seconds(), 'f', 0, 64)) signer := v4.NewSigner(func(options *v4.SignerOptions) { options.DisableURIPathEscaping = true options.LogSigning = true options.Logger = &logWrapper{log: log} }) signedURI, _, err := signer.PresignHTTP(ctx, creds, req, presignData.Headers[AmzContentSHA256], presignData.Service, presignData.Region, presignData.SignTime) if err != nil { return nil, fmt.Errorf("presign: %w", err) } if req.URL, err = url.ParseRequestURI(signedURI); err != nil { return nil, fmt.Errorf("parse signed URI: %w", err) } return req, nil } // PresignRequestV4a forms pre-signed request to access objects without aws credentials. func PresignRequestV4a(cred aws.Credentials, reqData RequestData, presignData PresignData, log *zap.Logger) (*http.Request, error) { urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, httpbinding.EscapePath(reqData.Bucket, false), httpbinding.EscapePath(reqData.Object, false)) req, err := http.NewRequest(strings.ToUpper(reqData.Method), urlStr, nil) if err != nil { return nil, fmt.Errorf("failed to create new request: %w", err) } for k, v := range presignData.Headers { req.Header.Set(k, v) // maybe we should filter system header (or keep responsibility on caller) } req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z")) req.Header.Set(AmzExpires, strconv.FormatFloat(presignData.Lifetime.Round(time.Second).Seconds(), 'f', 0, 64)) signer := v4a.NewSigner(func(options *v4a.SignerOptions) { options.DisableURIPathEscaping = true options.LogSigning = true options.Logger = &logWrapper{log: log} }) credAdapter := v4a.SymmetricCredentialAdaptor{ SymmetricProvider: credentials.NewStaticCredentialsProvider(cred.AccessKeyID, cred.SecretAccessKey, ""), } creds, err := credAdapter.RetrievePrivateKey(req.Context()) if err != nil { return nil, fmt.Errorf("failed to derive assymetric key from credentials: %w", err) } presignedURL, _, err := signer.PresignHTTP(req.Context(), creds, req, presignData.Headers[AmzContentSHA256], presignData.Service, []string{presignData.Region}, presignData.SignTime) if err != nil { return nil, fmt.Errorf("presign: %w", err) } return http.NewRequest(reqData.Method, presignedURL, nil) } type logWrapper struct { log *zap.Logger } func (l *logWrapper) Logf(classification logging.Classification, format string, args ...interface{}) { msg := fmt.Sprintf(format, args...) switch classification { case logging.Warn: l.log.Warn(msg) case logging.Debug: l.log.Debug(msg) default: l.log.Info(msg) } }