218 lines
4.8 KiB
Go
218 lines
4.8 KiB
Go
|
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, ""),
|
||
|
})
|
||
|
}
|
||
|
}
|