aboutsummaryrefslogtreecommitdiffstats
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
parent6128615d04e8ba8abbcfcdaf449451b8d68892ca (diff)
upload
-rw-r--r--Dockerfile29
-rw-r--r--cmd/server/main.go163
-rw-r--r--cmd/userkey/main.go40
-rw-r--r--go.mod10
-rw-r--r--go.sum5
5 files changed, 243 insertions, 4 deletions
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..069309c
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,29 @@
+FROM marketplace.gcr.io/google/debian11 AS build
+
+ENV BUILD_DEPS 'curl git gcc patch libc6-dev ca-certificates build-essential pkg-config libmagickwand-dev libmagickwand-6-headers'
+RUN apt-get update && apt-get install -y ${BUILD_DEPS} --no-install-recommends
+
+ENV GOPATH=/go
+ENV GOROOT_BOOTSTRAP=/usr/local/go-bootstrap
+RUN curl -sSL https://dl.google.com/go/go1.17.2.linux-amd64.tar.gz -o /tmp/go.tar.gz
+RUN mkdir -p $GOROOT_BOOTSTRAP
+RUN tar --strip=1 -C $GOROOT_BOOTSTRAP -xzf /tmp/go.tar.gz
+RUN $GOROOT_BOOTSTRAP/bin/go install golang.org/dl/gotip@latest
+#RUN /go/bin/gotip download
+
+RUN mkdir -p /workdir
+WORKDIR /workdir
+
+COPY go.mod /workdir
+COPY go.sum /workdir
+RUN $GOROOT_BOOTSTRAP/bin/go mod download
+
+COPY . /workdir
+RUN $GOROOT_BOOTSTRAP/bin/go build ./cmd/server
+
+ENV PORT=8080
+
+RUN mkdir -p /app
+RUN mv /workdir/server /app
+
+ENTRYPOINT /app/server
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)
})
diff --git a/cmd/userkey/main.go b/cmd/userkey/main.go
new file mode 100644
index 0000000..5ecd5f1
--- /dev/null
+++ b/cmd/userkey/main.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+)
+
+func main() {
+ user := os.Args[1]
+ if len(user) < 3 {
+ log.Fatalf("User name argument expected.")
+ }
+ encoded, err := ioutil.ReadAll(os.Stdin)
+ if err != nil || len(encoded) == 0 {
+ log.Fatalf("Expected exactly 1 secret")
+ }
+ sk, err := hex.DecodeString(string(encoded))
+ if err != nil || len(sk) < 32 {
+ log.Fatalf("hex.DecodeString() = %d, %v", len(sk), err)
+ return
+ }
+ mac := hmac.New(sha256.New, sk)
+ mac.Write([]byte(user))
+ fmt.Println(hex.EncodeToString(mac.Sum(nil)))
+}
+
+// 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)
+}
+
+var sk []byte
diff --git a/go.mod b/go.mod
index b710dff..240dd23 100644
--- a/go.mod
+++ b/go.mod
@@ -2,10 +2,15 @@ module git.toothrot.net/i.dis.band
go 1.17
-require cloud.google.com/go/storage v1.18.2
+require (
+ cloud.google.com/go v0.97.0
+ cloud.google.com/go/secretmanager v1.0.0
+ cloud.google.com/go/storage v1.18.2
+ google.golang.org/genproto v0.0.0-20211016002631-37fc39342514
+ gopkg.in/gographics/imagick.v2 v2.6.0
+)
require (
- cloud.google.com/go v0.97.0 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
@@ -18,7 +23,6 @@ require (
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/api v0.58.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto v0.0.0-20211016002631-37fc39342514 // indirect
google.golang.org/grpc v1.40.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
)
diff --git a/go.sum b/go.sum
index 17abd21..82c5d75 100644
--- a/go.sum
+++ b/go.sum
@@ -38,6 +38,8 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/secretmanager v1.0.0 h1:Wbw6lsRrpatsE8GVpuwYqImn+sY5DmRjaEImYPwcSMY=
+cloud.google.com/go/secretmanager v1.0.0/go.mod h1:+Qkm5qxIJ5mk74xxIXA+87fseaY1JLYBcFPQoc/GQxg=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
@@ -497,6 +499,7 @@ google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEc
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211016002631-37fc39342514 h1:Rp1vYDPD4TdkMH5S/bZbopsGCsWhPcrLBUwOVhAQCxM=
google.golang.org/genproto v0.0.0-20211016002631-37fc39342514/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
@@ -544,6 +547,8 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/gographics/imagick.v2 v2.6.0 h1:ewRsUQk3QkjGumERlndbFn/kTYRjyMaPY5gxwpuAhik=
+gopkg.in/gographics/imagick.v2 v2.6.0/go.mod h1:/QVPLV/iKdNttRKthmDkeeGg+vdHurVEPc8zkU0XgBk=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=