aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/server
diff options
context:
space:
mode:
authorAlexander Rakoczy <[email protected]>2021-10-22 11:47:49 -0400
committerAlexander Rakoczy <[email protected]>2021-10-22 11:47:49 -0400
commitd05245f658c9cb7c66998790d9fdb58a419f971c (patch)
tree3ff56849fecc83452b8964319294b1c202f23c57 /cmd/server
parent6128615d04e8ba8abbcfcdaf449451b8d68892ca (diff)
upload
Diffstat (limited to 'cmd/server')
-rw-r--r--cmd/server/main.go163
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)
})