From 14007fb946a06ea6ed933d5591d3c98e1958a2ca Mon Sep 17 00:00:00 2001 From: Nugraha Date: Sat, 24 Dec 2022 04:43:39 +0700 Subject: [PATCH] server: custom redirect status code and expiration --- .env.example | 5 ++++- .gitignore | 3 ++- README.md | 2 ++ main.go | 39 +++++++++++++++++++++++++++------------ obs.go | 10 +++++++++- server.go | 33 +++++++++++++++++++++++++++------ 6 files changed, 71 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index 1766585..a78ff86 100644 --- a/.env.example +++ b/.env.example @@ -11,4 +11,7 @@ AWS_SECRET_KEY=example-minio-secret # accessible S3 gateway OBS_REDIRECT_SECURE=false -OBS_HOST_REDIRECT=127.0.0.1:9000 \ No newline at end of file +OBS_HOST_REDIRECT=127.0.0.1:9000 + +# OBS_REDIRECT_CODE=307 # temporary redirect +# OBS_URL_EXPIRY=48h # 2 days \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2eea525..b628434 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +obs-access-signer \ No newline at end of file diff --git a/README.md b/README.md index aa28994..4d72031 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ There's an example of using it with Varnish Cache, which you can see [here](dock Some S3-compatible gateways might not support ACL endpoints but they support presigned access. Currently, the behavior of `obs-access-signer` is similar to `public-read` ACL where clients can access objects anonymously and redirect them (permanently) to presigned URL with `Expires` set to the max signed value of `int64` which has roughly 250yrs lifetime since UNIX time started. +Update: starting at v0.0.3, obs-access-signer supports custom redirection status code and expiry. + ## License Apache-2.0 diff --git a/main.go b/main.go index bae2250..09b063b 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "flag" "os" "strconv" + "time" _ "github.com/joho/godotenv/autoload" "go.uber.org/zap" @@ -11,9 +12,8 @@ import ( ) var ( - httpAddr string - logLevel string - // obsSignedUrlExpiry time.Duration + httpAddr string + logLevel string zapLogLevel zapcore.Level postFlagParse = []func(){} ) @@ -22,10 +22,10 @@ func init() { var err error _ = err - // app + // -- app flag.StringVar(&httpAddr, "addr", os.Getenv("HTTP_ADDR"), "Server address") - // log + // -- log flag.StringVar(&logLevel, "log-level", os.Getenv("LOG_LEVEL"), "Log level") qpostFlagParse(func() { err := zapLogLevel.UnmarshalText([]byte(logLevel)) @@ -34,7 +34,7 @@ func init() { } }) - // OBS + // -- OBS flag.StringVar(&defaultObsOpts.Endpoint, "obs-endpoint", os.Getenv("OBS_ENDPOINT"), "OBS host") flag.StringVar(&defaultObsOpts.Region, "obs-region", os.Getenv("OBS_REGION"), "OBS region") flag.BoolVar(&defaultObsOpts.Secure, "obs-secure", ok1(strconv.ParseBool(os.Getenv("OBS_SECURE"))), "OBS secure transport") @@ -43,12 +43,25 @@ func init() { flag.BoolVar(&defaultObsOpts.RedirectSecure, "obs-redirect-secure", ok1(strconv.ParseBool(os.Getenv("OBS_REDIRECT_SECURE"))), "OBS redirect secure transport") flag.StringVar(&defaultObsOpts.HostRedirect, "obs-host-redirect", os.Getenv("OBS_HOST_REDIRECT"), "OBS host redirect") - // obsSignedUrlExpiry, err = time.ParseDuration(os.Getenv("OBS_SIGNED_URL_EXPIRY")) - // if err != nil { - // // max signed value - // obsSignedUrlExpiry = time.Duration(^uint64(0) / 2) - // } - // flag.DurationVar(&obsSignedUrlExpiry, "obs-signed-url-expiry", obsSignedUrlExpiry, "OBS ") + // redirect http code + var obsRedirectCode = int64(defaultObsOpts.RedirectCode) + if obsRedirectCodeStr := os.Getenv("OBS_REDIRECT_CODE"); obsRedirectCodeStr != "" { + obsRedirectCode, err = strconv.ParseInt(obsRedirectCodeStr, 10, 64) + if err != nil { + obsRedirectCode = int64(defaultObsOpts.RedirectCode) + } + } + flag.IntVar(&defaultObsOpts.RedirectCode, "obs-redirect-code", int(obsRedirectCode), "OBS redirect http code") + + // url expiry + var obsUrlExpiry = defaultObsOpts.URLExpiry + if obsUrlExpiryStr := os.Getenv("OBS_URL_EXPIRY"); obsUrlExpiryStr != "" { + if obsUrlExpiry, err = time.ParseDuration(obsUrlExpiryStr); err != nil { + obsUrlExpiry = defaultObsOpts.URLExpiry + } + } + flag.DurationVar(&defaultObsOpts.URLExpiry, "obs-url-expiry", obsUrlExpiry, "OBS url expiry") + } func qpostFlagParse(f func()) { @@ -77,6 +90,8 @@ func main() { "obs_endpoint", defaultObsOpts.Endpoint, "obs_redirect_secure", defaultObsOpts.RedirectSecure, "obs_host_redirect", defaultObsOpts.HostRedirect, + "obs_redirect_code", defaultObsOpts.RedirectCode, + "obs_url_expiry", defaultObsOpts.URLExpiry.String(), ) client := unwrap1(newObsClient(defaultObsOpts)) diff --git a/obs.go b/obs.go index e18d700..544d9f2 100644 --- a/obs.go +++ b/obs.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "reflect" + "time" "unsafe" "github.com/minio/minio-go/v7" @@ -21,10 +22,17 @@ type obsOptions struct { BucketName string RedirectSecure bool + RedirectCode int // HTTP redirect status code + URLExpiry time.Duration HostRedirect string } -var defaultObsOpts obsOptions +const maxURLExpiry = time.Duration(int64(^uint64(0) / 2)) + +var defaultObsOpts = obsOptions{ + URLExpiry: maxURLExpiry, + RedirectCode: http.StatusMovedPermanently, // 301 +} func newObsClient(opts obsOptions) (*minio.Client, error) { client, err := minio.New(opts.Endpoint, &minio.Options{ diff --git a/server.go b/server.go index f4fc9bd..581ab1a 100644 --- a/server.go +++ b/server.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "fmt" "net/http" "net/url" "strconv" @@ -24,8 +25,6 @@ type serverOptions struct { Logger *zap.Logger OBS *obsOptions - ObjectExpiry time.Duration - S3 *minio.Client } @@ -99,11 +98,12 @@ func (s *server) handle(ctx *fasthttp.RequestCtx) { } // compose initial request + expireSeconds := int64(s.opts.OBS.URLExpiry / time.Second) req, err := newRequest(s.opts.S3, ctx, http.MethodGet, requestMetadata{ presignURL: true, bucketName: bucketName, objectName: objectName, - expires: 1, // to trigger presigned generator + expires: expireSeconds, // to trigger presigned generator queryValues: url.Values{}, }) if err != nil { @@ -120,8 +120,28 @@ func (s *server) handle(ctx *fasthttp.RequestCtx) { return } - // clear given params, set max signed value for expire, and re-presign. - exp := strconv.FormatInt(int64(^uint64(0)/2), 10) // ~250years + var statusCode = s.opts.OBS.RedirectCode + + // custom "expiry" + var exp string + if expiry := s.opts.OBS.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.OBS.URLExpiry) + exp = strconv.FormatInt(int64(expireAt.Unix()), 10) + // set object 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) @@ -130,6 +150,7 @@ func (s *server) handle(ctx *fasthttp.RequestCtx) { query := req.URL.Query() query.Set("Expires", exp) req.URL.RawQuery = s3utils.QueryEncode(query) + if s.opts.OBS.RedirectSecure { req.URL.Scheme = "https" } else { @@ -140,7 +161,7 @@ func (s *server) handle(ctx *fasthttp.RequestCtx) { req.URL.Host = hostRedirect } - ctx.Redirect(req.URL.String(), http.StatusMovedPermanently) + ctx.Redirect(req.URL.String(), statusCode) } func (s *server) Run() {