s3-proxy/main.go

218 lines
4.8 KiB
Go
Raw Permalink Normal View History

2021-12-31 00:03:38 +00:00
package main
import (
"context"
"io"
"log"
"net/http"
"strings"
"github.com/alexflint/go-arg"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
type args struct {
S3Endpoint string `arg:"--s3-endpoint,required,env:S3_ENDPOINT" help:"host[:port]" placeholder:"ENDPOINT"`
S3Bucket string `arg:"--s3-bucket,required,env:S3_BUCKET" placeholder:"BUCKET"`
S3AccessKey string `arg:"--s3-access-key,env:S3_ACCESS_KEY" placeholder:"ACCESS_KEY"`
S3SecretKey string `arg:"--s3-secret-key,env:S3_SECRET_KEY" placeholder:"SECRET_KEY"`
S3DisableSSL bool `arg:"--s3-disable-ssl,env:S3_DISABLE_SSL" default:"false"`
Address string `arg:"--address,env:ADDRESS" default:":3000" help:"what address to listen on" placeholder:"ADDRESS"`
}
func (args) Version() string {
// TODO
return "s3-proxy 0.1"
}
var s3Client *minio.Client
func main() {
// Warning: Shitty code
var args args
arg.MustParse(&args)
client, err := newClientFromArgs(args)
if err != nil {
log.Panicf("Failed to create s3 client: %s", err.Error())
}
s3Client = client
log.Printf("Starting webserver on: %s", args.Address)
err = http.ListenAndServe(args.Address, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// Only allow GET and HEAD request
if r.Method != http.MethodGet && r.Method != http.MethodHead {
rw.WriteHeader(http.StatusMethodNotAllowed)
return
}
// Edge case with root "/"
if path == "/" {
succ, err := tryObject(createIndexPath(path), rw, r)
if err != nil {
handleError(err, rw, r)
return
}
if !succ {
handle404(rw, r)
return
}
return
}
// Edge case with paths ending with "/index.html"
if strings.HasSuffix(path, "/index.html") {
http.Redirect(rw, r, strings.TrimSuffix(path, "index.html"), http.StatusMovedPermanently)
return
}
// If path ends with "/"
if strings.HasSuffix(path, "/") {
// Try the index file first
succ, err := tryObject(createIndexPath(path), rw, r)
if err != nil {
handleError(err, rw, r)
return
}
if succ {
return
}
// If file without the "/" exists then redirect
if objectExists(r.Context(), strings.TrimSuffix(path, "/")) {
// Redirect
http.Redirect(rw, r, strings.TrimSuffix(path, "/"), http.StatusMovedPermanently)
return
}
// Else 404
handle404(rw, r)
return
} else {
// Try file first
succ, err := tryObject(path, rw, r)
if err != nil {
handleError(err, rw, r)
return
}
if succ {
return
}
// If if path + "/" + "index.html" exists then redirect to path + "/"
if objectExists(r.Context(), createIndexPath(path)) {
redirectUrl := path + "/"
http.Redirect(rw, r, redirectUrl, http.StatusMovedPermanently)
return
}
// else 404
handle404(rw, r)
return
}
}))
if err != nil {
log.Printf("Failed to start webserver: %s", err.Error())
}
}
func objectExists(ctx context.Context, key string) bool {
_, err := s3Client.StatObject(ctx, "www", key, minio.GetObjectOptions{})
response := minio.ToErrorResponse(err)
return !(response.StatusCode == http.StatusNotFound)
}
func tryObject(key string, rw http.ResponseWriter, r *http.Request) (bool, error) {
stat, err := s3Client.StatObject(r.Context(), "www", key, minio.GetObjectOptions{})
if err != nil {
if is404(err) {
return false, nil
} else {
return false, err
}
}
reqEtag := r.Header.Get("If-None-Match")
if reqEtag == stat.ETag {
rw.WriteHeader(http.StatusNotModified)
return true, nil
}
rw.Header().Add("content-type", stat.ContentType)
rw.Header().Add("ETag", stat.ETag)
rw.Header().Add("Server", "Yo mama lol")
if r.Method == http.MethodGet {
obj, err := s3Client.GetObject(r.Context(), "www", key, minio.GetObjectOptions{})
if err != nil {
return false, err
}
_, err = io.Copy(rw, obj)
if err != nil {
return false, err
}
}
return true, nil
}
func createIndexPath(s string) string {
index := s
if !strings.HasSuffix(s, "/") {
index += "/"
}
index += "index.html"
return index
}
func handleError(err error, rw http.ResponseWriter, r *http.Request) {
log.Printf("error: %s : %s", r.URL.Path, err.Error())
rw.WriteHeader(http.StatusInternalServerError)
}
func handle404(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNotFound)
}
func is404(err error) bool {
resp := minio.ToErrorResponse(err)
return resp.StatusCode == http.StatusNotFound
}
func newClientFromArgs(args args) (*minio.Client, error) {
if args.S3AccessKey == "" {
return minio.New(args.S3Endpoint, &minio.Options{
Secure: !args.S3DisableSSL,
})
} else {
return minio.New(args.S3Endpoint, &minio.Options{
Secure: !args.S3DisableSSL,
Creds: credentials.NewStaticV4(args.S3AccessKey, args.S3SecretKey, ""),
})
}
}