server: custom redirect status code and expiration
This commit is contained in:
parent
61986da0b1
commit
14007fb946
6 changed files with 71 additions and 21 deletions
|
|
@ -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
3
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
.env
|
.env
|
||||||
|
obs-access-signer
|
||||||
|
|
@ -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 supports custom redirection status code and expiry.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Apache-2.0
|
Apache-2.0
|
||||||
|
|
|
||||||
39
main.go
39
main.go
|
|
@ -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
10
obs.go
|
|
@ -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{
|
||||||
|
|
|
||||||
33
server.go
33
server.go
|
|
@ -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() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue