diff options
Diffstat (limited to 'cmd/server')
| -rw-r--r-- | cmd/server/main.go | 163 |
1 files changed, 162 insertions, 1 deletions
diff --git a/cmd/server/main.go b/cmd/server/main.go index 4c8238c..35fbab2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,19 +2,34 @@ package main import ( "context" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" "errors" + "fmt" "io" "io/fs" + "io/ioutil" "log" + rand2 "math/rand" "mime" "net" "net/http" + "net/url" "os" "path" "strings" + "sync" + "time" + "cloud.google.com/go/compute/metadata" + "cloud.google.com/go/secretmanager/apiv1" "cloud.google.com/go/storage" "git.toothrot.net/i.dis.band/internal" + secretmanager2 "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" + "gopkg.in/gographics/imagick.v2/imagick" ) func main() { @@ -31,11 +46,153 @@ func main() { log.Fatalf("storage.NewClient() = _, %v", err) } b := cl.Bucket("i-dis-band-east4") + http.Handle("/upload", upload(b)) http.Handle("/", fileServerHandler(internal.Static, image(b, http.HandlerFunc(home)))) log.Printf("Listening on %s\n", net.JoinHostPort(host, port)) log.Fatal(http.ListenAndServe(net.JoinHostPort(host, port), nil)) } +var sk []byte +var skOnce sync.Once + +func secretKey() []byte { + skOnce.Do(func() { + project, err := metadata.NumericProjectID() + if err != nil { + log.Printf("metadata.NumericProjectID() = %v, %v. Not on GCP?", project, err) + return + } + ctx := context.Background() + client, err := secretmanager.NewClient(ctx) + if err != nil { + log.Printf("secretmanager.NewClient() = _, %v", err) + return + } + defer client.Close() + name := fmt.Sprintf("projects/%s/secrets/i-dis-band-sk/versions/latest", project) + resp, err := client.AccessSecretVersion(ctx, &secretmanager2.AccessSecretVersionRequest{Name: name}) + if err != nil { + log.Printf("client.AccessSecretVersion(%q) = %v", name, err) + } + encoded := resp.GetPayload().GetData() + sk, err = hex.DecodeString(string(encoded)) + if err != nil { + log.Printf("hex.DecodeString() = %d, %v", len(sk), err) + sk = sk[:0] + return + } + }) + if len(sk) < 32 { + skOnce = sync.Once{} + } + return sk +} + +// ValidMAC reports whether messageMAC is a valid HMAC tag for message. +func ValidMAC(message, messageMAC, key []byte) bool { + mac := hmac.New(sha256.New, key) + mac.Write(message) + expectedMAC := mac.Sum(nil) + return hmac.Equal(messageMAC, expectedMAC) +} + +const ( + Byte = 1 << (10*iota) + KiB + MiB + GiB +) + +const maxUpload = 100*MiB + +func upload(b *storage.BucketHandle) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + secret := secretKey() + if len(secret) == 0 { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + user, key, ok := r.BasicAuth() + if !ok { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + decoded, err := hex.DecodeString(key) + if err != nil { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + if !ValidMAC([]byte(user), decoded, secret) { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + file, head, err := r.FormFile("file") + if err != nil { + log.Printf("r.FormFile(%q) = _, %v", "file", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + if head.Size > maxUpload { + log.Printf("head.Size = %d, maxUpload = %d", head.Size, maxUpload) + http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) + return + } + img, err := ioutil.ReadAll(file) + if err != nil { + log.Printf("ioutil.ReadAll(file) = _, %v", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + wand := imagick.NewMagickWand() + defer wand.Destroy() + if err := wand.ReadImageBlob(img); err != nil { + log.Printf("wand.ReadImageBlob(img) = %v", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + for _, prop := range wand.GetImageProperties("") { + wand.DeleteImageProperty(prop) + } + ct := head.Header.Get("content-type") + ext, err := mime.ExtensionsByType(ct) + if err != nil { + ext = []string{path.Ext(head.Filename)} + } + outname := fmt.Sprintf("%s%s", filename(), ext[len(ext)-1]) + log.Printf("trying to write %q", outname) + fw := b.Object(outname).NewWriter(r.Context()) + fw.ContentType = ct + n, err := fw.Write(wand.GetImageBlob()) + if err != nil || n == 0{ + log.Printf("fw.Write(wand.GetImageBlob()) = %d, %v", n, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + if err := fw.Close(); err != nil { + log.Printf("fw.Close() = %v", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + u := url.URL{Scheme: "https", Host: "i.dis.band", Path: outname} + fmt.Fprintln(w, u.String()) + }) +} + +func filename() string { + b := make([]byte, 8) + n, err := rand.Read(b) + if err != nil || n != len(b) { + b = make([]byte, 8) + r := rand2.New(rand2.NewSource(time.Now().UnixNano())) + r.Read(b) + } + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b) +} + func image(b *storage.BucketHandle, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { @@ -52,7 +209,11 @@ func image(b *storage.BucketHandle, next http.Handler) http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path))) + ct := o.Attrs.ContentType + if ct == "" || ct == "text/plain" { + ct = mime.TypeByExtension(path.Ext(r.URL.Path)) + } + w.Header().Set("Content-Type", ct) w.Header().Set("Cache-Control", "no-cache, private, max-age=0") io.Copy(w, o) }) |
