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, ""), }) } }