server: custom redirect status code and expiration

This commit is contained in:
Nugraha 2022-12-24 04:43:39 +07:00
parent 61986da0b1
commit d2fd7057a4
Signed by: ii64
GPG key ID: E41C08AD390E7C49
6 changed files with 71 additions and 21 deletions

View file

@ -11,4 +11,7 @@ AWS_SECRET_KEY=example-minio-secret
# accessible S3 gateway # accessible S3 gateway
OBS_REDIRECT_SECURE=false OBS_REDIRECT_SECURE=false
OBS_HOST_REDIRECT=127.0.0.1:9000 OBS_HOST_REDIRECT=127.0.0.1:9000
# OBS_REDIRECT_CODE=307 # temporary redirect
# OBS_URL_EXPIRY=48h # 2 days

3
.gitignore vendored
View file

@ -1 +1,2 @@
.env .env
obs-access-signer

View file

@ -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. 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 support custom redirection status code and expiry.
## License ## License
Apache-2.0 Apache-2.0

39
main.go
View file

@ -4,6 +4,7 @@ import (
"flag" "flag"
"os" "os"
"strconv" "strconv"
"time"
_ "github.com/joho/godotenv/autoload" _ "github.com/joho/godotenv/autoload"
"go.uber.org/zap" "go.uber.org/zap"
@ -11,9 +12,8 @@ import (
) )
var ( var (
httpAddr string httpAddr string
logLevel string logLevel string
// obsSignedUrlExpiry time.Duration
zapLogLevel zapcore.Level zapLogLevel zapcore.Level
postFlagParse = []func(){} postFlagParse = []func(){}
) )
@ -22,10 +22,10 @@ func init() {
var err error var err error
_ = err _ = err
// app // -- app
flag.StringVar(&httpAddr, "addr", os.Getenv("HTTP_ADDR"), "Server address") flag.StringVar(&httpAddr, "addr", os.Getenv("HTTP_ADDR"), "Server address")
// log // -- log
flag.StringVar(&logLevel, "log-level", os.Getenv("LOG_LEVEL"), "Log level") flag.StringVar(&logLevel, "log-level", os.Getenv("LOG_LEVEL"), "Log level")
qpostFlagParse(func() { qpostFlagParse(func() {
err := zapLogLevel.UnmarshalText([]byte(logLevel)) 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.Endpoint, "obs-endpoint", os.Getenv("OBS_ENDPOINT"), "OBS host")
flag.StringVar(&defaultObsOpts.Region, "obs-region", os.Getenv("OBS_REGION"), "OBS region") 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") 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.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") 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")) // redirect http code
// if err != nil { var obsRedirectCode = int64(defaultObsOpts.RedirectCode)
// // max signed value if obsRedirectCodeStr := os.Getenv("OBS_REDIRECT_CODE"); obsRedirectCodeStr != "" {
// obsSignedUrlExpiry = time.Duration(^uint64(0) / 2) obsRedirectCode, err = strconv.ParseInt(obsRedirectCodeStr, 10, 64)
// } if err != nil {
// flag.DurationVar(&obsSignedUrlExpiry, "obs-signed-url-expiry", obsSignedUrlExpiry, "OBS ") 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()) { func qpostFlagParse(f func()) {
@ -77,6 +90,8 @@ func main() {
"obs_endpoint", defaultObsOpts.Endpoint, "obs_endpoint", defaultObsOpts.Endpoint,
"obs_redirect_secure", defaultObsOpts.RedirectSecure, "obs_redirect_secure", defaultObsOpts.RedirectSecure,
"obs_host_redirect", defaultObsOpts.HostRedirect, "obs_host_redirect", defaultObsOpts.HostRedirect,
"obs_redirect_code", defaultObsOpts.RedirectCode,
"obs_url_expiry", defaultObsOpts.URLExpiry.String(),
) )
client := unwrap1(newObsClient(defaultObsOpts)) client := unwrap1(newObsClient(defaultObsOpts))

10
obs.go
View file

@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"reflect" "reflect"
"time"
"unsafe" "unsafe"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
@ -21,10 +22,17 @@ type obsOptions struct {
BucketName string BucketName string
RedirectSecure bool RedirectSecure bool
RedirectCode int // HTTP redirect status code
URLExpiry time.Duration
HostRedirect string 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) { func newObsClient(opts obsOptions) (*minio.Client, error) {
client, err := minio.New(opts.Endpoint, &minio.Options{ client, err := minio.New(opts.Endpoint, &minio.Options{

View file

@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -24,8 +25,6 @@ type serverOptions struct {
Logger *zap.Logger Logger *zap.Logger
OBS *obsOptions OBS *obsOptions
ObjectExpiry time.Duration
S3 *minio.Client S3 *minio.Client
} }
@ -99,11 +98,12 @@ func (s *server) handle(ctx *fasthttp.RequestCtx) {
} }
// compose initial request // compose initial request
expireSeconds := int64(s.opts.OBS.URLExpiry / time.Second)
req, err := newRequest(s.opts.S3, ctx, http.MethodGet, requestMetadata{ req, err := newRequest(s.opts.S3, ctx, http.MethodGet, requestMetadata{
presignURL: true, presignURL: true,
bucketName: bucketName, bucketName: bucketName,
objectName: objectName, objectName: objectName,
expires: 1, // to trigger presigned generator expires: expireSeconds, // to trigger presigned generator
queryValues: url.Values{}, queryValues: url.Values{},
}) })
if err != nil { if err != nil {
@ -120,8 +120,28 @@ func (s *server) handle(ctx *fasthttp.RequestCtx) {
return return
} }
// clear given params, set max signed value for expire, and re-presign. var statusCode = s.opts.OBS.RedirectCode
exp := strconv.FormatInt(int64(^uint64(0)/2), 10) // ~250years
// 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.Header.Set("Expires", exp)
req.URL.RawQuery = "" req.URL.RawQuery = ""
req = signer.PreSignV2(*req, value.AccessKeyID, value.SecretAccessKey, 0, isVirtualHostStyle) 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 := req.URL.Query()
query.Set("Expires", exp) query.Set("Expires", exp)
req.URL.RawQuery = s3utils.QueryEncode(query) req.URL.RawQuery = s3utils.QueryEncode(query)
if s.opts.OBS.RedirectSecure { if s.opts.OBS.RedirectSecure {
req.URL.Scheme = "https" req.URL.Scheme = "https"
} else { } else {
@ -140,7 +161,7 @@ func (s *server) handle(ctx *fasthttp.RequestCtx) {
req.URL.Host = hostRedirect req.URL.Host = hostRedirect
} }
ctx.Redirect(req.URL.String(), http.StatusMovedPermanently) ctx.Redirect(req.URL.String(), statusCode)
} }
func (s *server) Run() { func (s *server) Run() {