diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Dockerfile | 29 | ||||
-rw-r--r-- | go.mod | 12 | ||||
-rw-r--r-- | go.sum | 70 | ||||
-rw-r--r-- | internal/encryption/encryption.go | 93 | ||||
-rw-r--r-- | internal/renderer/renderer.go | 45 | ||||
-rw-r--r-- | internal/storer/storer.go | 124 | ||||
-rw-r--r-- | main.go | 127 | ||||
-rw-r--r-- | public/css/index.css | 280 | ||||
-rw-r--r-- | templates/includes/layout.gohtml | 19 | ||||
-rw-r--r-- | templates/pages/error.gohtml | 5 | ||||
-rw-r--r-- | templates/pages/index.gohtml | 21 | ||||
-rw-r--r-- | templates/pages/memo.gohtml | 13 | ||||
-rw-r--r-- | templates/pages/notfound.gohtml | 10 | ||||
-rw-r--r-- | templates/pages/save.gohtml | 13 |
15 files changed, 862 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8450c95 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +memos.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bb4712f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM golang:1.16-alpine AS builder + +COPY ${PWD} /app +WORKDIR /app + +# Toggle CGO based on your app requirement. CGO_ENABLED=1 for enabling CGO +RUN CGO_ENABLED=0 go build -ldflags '-s -w -extldflags "-static"' -o /app/appbin *.go +# Use below if using vendor +# RUN CGO_ENABLED=0 go build -mod=vendor -ldflags '-s -w -extldflags "-static"' -o /app/appbin *.go + +FROM alpine:latest + +# Following commands are for installing CA certs (for proper functioning of HTTPS and other TLS) +RUN apk --update add ca-certificates && \ + rm -rf /var/cache/apk/* + +# Add new user 'appuser' +RUN adduser -D appuser +USER appuser + +COPY --from=builder /app /home/appuser/app + +WORKDIR /home/appuser/app + +# Since running as a non-root user, port bindings < 1024 is not possible +# 8000 for HTTP; 8443 for HTTPS; +EXPOSE 3000 + +CMD ["./appbin"] @@ -0,0 +1,12 @@ +module github.com/neonxp/sendsafe + +go 1.16 + +require ( + github.com/boltdb/bolt v1.3.1 + github.com/dgraph-io/badger v1.6.2 + github.com/go-chi/chi/v5 v5.0.1 + github.com/google/uuid v1.2.0 + github.com/rs/xid v1.2.1 + golang.org/x/sys v0.0.0-20210317091845-390168757d9c // indirect +) @@ -0,0 +1,70 @@ +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= +github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= +github.com/dgraph-io/ristretto v0.0.2 h1:a5WaUrDa0qm0YrAAS1tUykT5El3kt62KNZZeMxQn3po= +github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-chi/chi/v5 v5.0.1 h1:ALxjCrTf1aflOlkhMnCUP86MubbWFrzB3gkRPReLpTo= +github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210317091845-390168757d9c h1:WGyvPg8lhdtSkb8BiYWdtPlLSommHOmJHFvzWODI7BQ= +golang.org/x/sys v0.0.0-20210317091845-390168757d9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/encryption/encryption.go b/internal/encryption/encryption.go new file mode 100644 index 0000000..0f9c748 --- /dev/null +++ b/internal/encryption/encryption.go @@ -0,0 +1,93 @@ +package encryption + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "io" + "strings" +) + +func addBase64Padding(value string) string { + m := len(value) % 4 + if m != 0 { + value += strings.Repeat("=", 4-m) + } + + return value +} + +func removeBase64Padding(value string) string { + return strings.Replace(value, "=", "", -1) +} + +func pad(src []byte) []byte { + padding := aes.BlockSize - len(src)%aes.BlockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + + return append(src, padtext...) +} + +func Unpad(src []byte) ([]byte, error) { + length := len(src) + unpadding := int(src[length-1]) + + if unpadding > length { + return nil, errors.New("unpad error. This could happen when incorrect encryption key is used") + } + + return src[:(length - unpadding)], nil +} + +func Encrypt(key []byte, text string) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + msg := pad([]byte(text)) + ciphertext := make([]byte, aes.BlockSize+len(msg)) + iv := ciphertext[:aes.BlockSize] + + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return "", err + } + + cfb := cipher.NewCFBEncrypter(block, iv) + cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(msg)) + finalMsg := removeBase64Padding(base64.URLEncoding.EncodeToString(ciphertext)) + + return finalMsg, nil +} + +func Decrypt(key []byte, text string) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + decodedMsg, err := base64.URLEncoding.DecodeString(addBase64Padding(text)) + if err != nil { + return "", err + } + + if (len(decodedMsg) % aes.BlockSize) != 0 { + return "", errors.New("blocksize must be multipe of decoded message length") + } + + iv := decodedMsg[:aes.BlockSize] + msg := decodedMsg[aes.BlockSize:] + + cfb := cipher.NewCFBDecrypter(block, iv) + cfb.XORKeyStream(msg, msg) + + unpadMsg, err := Unpad(msg) + if err != nil { + return "", err + } + + return string(unpadMsg), nil +} diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go new file mode 100644 index 0000000..bed603c --- /dev/null +++ b/internal/renderer/renderer.go @@ -0,0 +1,45 @@ +package renderer + +import ( + "html/template" + "net/http" + "path" + "path/filepath" +) + +// Renderer represents html template renderer +type Renderer struct { + templates map[string]*template.Template +} + +// New Renderer +func New(gpath string) (*Renderer, error) { + t := &Renderer{ + templates: make(map[string]*template.Template), + } + + includes, err := filepath.Glob(path.Join(gpath, "includes/*.gohtml")) + if err != nil { + return nil, err + } + + pages, err := filepath.Glob(path.Join(gpath, "pages/*.gohtml")) + if err != nil { + return nil, err + } + + for _, p := range pages { + tpls := append(includes, p) + tname := path.Base(p) + t.templates[tname] = template.Must(template.New(tname).ParseFiles(tpls...)) + } + + return t, nil +} + +func (r *Renderer) Render(tpl string, data Map, rw http.ResponseWriter) error { + rw.Header().Set("Content-Type", "text/html") + return r.templates[tpl].Execute(rw, data) +} + +type Map map[string]interface{} diff --git a/internal/storer/storer.go b/internal/storer/storer.go new file mode 100644 index 0000000..b8b4ffd --- /dev/null +++ b/internal/storer/storer.go @@ -0,0 +1,124 @@ +package storer + +import ( + "bytes" + "encoding/gob" + "time" + + "github.com/dgraph-io/badger" + "github.com/neonxp/sendsafe/internal/encryption" + "github.com/rs/xid" +) + +type Store struct { + db *badger.DB +} + +func New(dbFile string) (*Store, error) { + db, err := badger.Open(badger.DefaultOptions(dbFile)) + if err != nil { + return nil, err + } + + return &Store{ + db: db, + }, nil +} + +func (s *Store) Save(text string, pin string, ttl int) (string, error) { + var err error + + encrypted := false + + if pin != "" { + text, err = encryption.Encrypt([]byte(pin), text) + if err != nil { + return "", err + } + + encrypted = true + } + + record := memo{ + Text: text, + Encrypted: encrypted, + } + + buf := bytes.NewBuffer([]byte{}) + if err := gob.NewEncoder(buf).Encode(record); err != nil { + return "", err + } + + id := xid.New() + err = s.db.Update(func(txn *badger.Txn) error { + return txn.SetEntry(&badger.Entry{ + Key: id.Bytes(), + Value: buf.Bytes(), + ExpiresAt: uint64(time.Now().Add(time.Duration(ttl) * time.Minute).Unix()), + }) + }) + + return id.String(), err +} + +func (s *Store) IsEncrypted(id string) (bool, error) { + var encrypted bool + + return encrypted, s.db.View(func(txn *badger.Txn) error { + value, err := txn.Get([]byte(id)) + if err != nil { + return err + } + record := new(memo) + return value.Value(func(val []byte) error { + if err := gob.NewDecoder(bytes.NewBuffer(val)).Decode(record); err != nil { + return err + } + encrypted = record.Encrypted + return nil + }) + }) +} + +func (s *Store) Get(id string, pin string) (string, error) { + var text string + + return text, s.db.Update(func(txn *badger.Txn) error { + uid, err := xid.FromString(id) + if err != nil { + return err + } + value, err := txn.Get(uid.Bytes()) + if err != nil { + return err + } + record := new(memo) + err = value.Value(func(val []byte) error { + if err := gob.NewDecoder(bytes.NewBuffer(val)).Decode(record); err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + + text = record.Text + if record.Encrypted { + text, err = encryption.Decrypt([]byte(pin), text) + if err != nil { + return err + } + } + return txn.Delete(uid.Bytes()) + }) +} + +func init() { + gob.Register(memo{}) +} + +type memo struct { + Text string + Encrypted bool +} @@ -0,0 +1,127 @@ +package main + +import ( + "log" + "net/http" + "os" + "path" + "path/filepath" + "strconv" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/google/uuid" + + "github.com/neonxp/sendsafe/internal/renderer" + "github.com/neonxp/sendsafe/internal/storer" +) + +var workDir, _ = os.Getwd() + +func main() { + s, err := storer.New(path.Join(workDir, "memos.db")) + if err != nil { + log.Fatal(err) + } + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(middleware.Compress(9)) + + tpl, err := renderer.New(path.Join(workDir, "templates")) + if err != nil { + log.Fatal(err) + } + + r.Get("/", func(rw http.ResponseWriter, _ *http.Request) { + csrf := uuid.NewString() + http.SetCookie(rw, &http.Cookie{ + Name: "csrf", + Value: csrf, + Expires: time.Now().Add(30 * time.Minute), + HttpOnly: true, + }) + + if err := tpl.Render("index.gohtml", renderer.Map{"csrf": csrf}, rw); err != nil { + log.Println(err) + _ = tpl.Render("error.gohtml", nil, rw) + } + }) + + r.Post("/save", func(rw http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + log.Println(err) + _ = tpl.Render("error.gohtml", nil, rw) + return + } + secret := r.FormValue("secret") + days, err := strconv.Atoi(r.FormValue("days")) + if err != nil { + log.Println(err) + _ = tpl.Render("error.gohtml", nil, rw) + return + } + hours, err := strconv.Atoi(r.FormValue("hours")) + if err != nil { + log.Println(err) + _ = tpl.Render("error.gohtml", nil, rw) + return + } + minutes, err := strconv.Atoi(r.FormValue("minutes")) + if err != nil { + log.Println(err) + _ = tpl.Render("error.gohtml", nil, rw) + return + } + pin := r.FormValue("pin") + csrf := r.FormValue("csrf") + csrfCookie, err := r.Cookie("csrf") + if err != nil { + log.Println(err) + _ = tpl.Render("error.gohtml", nil, rw) + return + } + if csrfCookie.Value != csrf { + log.Println(err) + _ = tpl.Render("error.gohtml", nil, rw) + return + } + ttl := days*24*60 + hours*60 + minutes + id, err := s.Save(secret, pin, ttl) + if err != nil { + log.Println(err) + _ = tpl.Render("error.gohtml", nil, rw) + return + } + if err := tpl.Render("save.gohtml", renderer.Map{"id": id}, rw); err != nil { + log.Println(err) + _ = tpl.Render("error.gohtml", nil, rw) + } + }) + + r.Get("/s/{id}", func(rw http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + secret, err := s.Get(id, "") + if err != nil { + log.Println(err) + _ = tpl.Render("notfound.gohtml", nil, rw) + return + } + if err := tpl.Render("memo.gohtml", renderer.Map{"secret": secret}, rw); err != nil { + log.Println(err) + _ = tpl.Render("error.gohtml", nil, rw) + } + }) + + filesDir := http.Dir(filepath.Join(workDir, "public")) + + r.Get("/*", func(w http.ResponseWriter, r *http.Request) { + fs := http.StripPrefix("/", http.FileServer(filesDir)) + fs.ServeHTTP(w, r) + }) + + if err := http.ListenAndServe(":3000", r); err != nil { + log.Fatal(err) + } +} diff --git a/public/css/index.css b/public/css/index.css new file mode 100644 index 0000000..db341af --- /dev/null +++ b/public/css/index.css @@ -0,0 +1,280 @@ +* { + --active: rgb(43, 43, 48); + --active-inner: #f3f3f3; + --focus: 2px rgba(39, 94, 254, .3); + --border: #a3a3a3; + --border-hover: #f3f3f3; + --background: rgb(255, 255, 255); + --background-secondary: rgb(230, 230, 230); + --disabled: rgb(43, 43, 48); + --disabled-inner: #rgb(43, 43, 48); + --text: #000; +} + +@media (prefers-color-scheme: dark) { + * { + --active: rgb(43, 43, 48); + --active-inner: #f3f3f3; + --focus: 2px rgba(39, 94, 254, .3); + --border: #f3f3f3; + --border-hover: #f3f3f3; + --background: rgb(31, 32, 39); + --background-secondary: rgb(43, 43, 48); + --disabled: rgb(43, 43, 48); + --disabled-inner: #rgb(43, 43, 48); + --text: #f3f3f3; + } +} + +html, body { + padding: 0; + margin: 0; + background-color: var(--background); + color: var(--text); + font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; + font-size: 16px; +} + +.turbolinks-progress-bar { + height: 5px; + background-color: rgb(52, 52, 175); +} + +a, a:hover, a:active, a:visited { + color: var(--text); +} + +body { + padding-top: 60px; +} + +header { + position: fixed; + top: 0; + left: 0; + height: 50px; + width: 100%; + border-bottom: 1px solid var(--border); + display: flex; + flex-direction: row; + justify-content: space-between; + background-color: var(--background); +} + +hr { + width: auto; + border-top: 0; + border-bottom: 1px solid var(--border); + margin: 16px 8px; +} + +h1 { + margin: 16px 8px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); +} + +#header-logo { + display: block; + padding: 16px; + text-decoration: none; + font-family: 'Courier New', Courier, monospace; + cursor: pointer; +} + +.header-menu { + list-style: none; + display: flex; + flex-direction: row; +} + +.header-menu .header-menu--item { + transition: background 0.3s, border-color 0.3s, box-shadow 0.2s; + display: inline-block; + padding: 16px; + text-decoration: none; +} + +.header-menu .header-menu--item:hover { + background-color: var(--background-secondary); +} + +.layout-center { + display: flex; + flex-direction: row; + justify-content: space-around; +} + +form { + display: flex; + flex-direction: column; + /* max-width: 50vh; */ + width: 600px; + border: 1px solid var(--border); + border-radius: 8px; + padding: 16px; +} + +form .required { + font-size: 14px; + font-style: italic; + color: rgb(255, 100, 100); + align-self: flex-end; +} + +form .optional { + font-size: 14px; + font-style: italic; + align-self: flex-end; +} + +form label { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.form-row { + display: flex; + flex-direction: row; + align-items: center; +} + +input, textarea { + background-color: var(--background-secondary); + color: var(--text); + border: 1px solid var(--border); + padding: 8px; + margin: 8px; + border-radius: 4px; +} + +textarea { + resize: vertical; +} + +label { + padding: 8px; +} + +input:active { + background-color: rgb(33, 33, 38); +} + +@supports (-webkit-appearance: none) or (-moz-appearance: none) { + input[type=checkbox], input[type=radio] { + -webkit-appearance: none; + -moz-appearance: none; + height: 21px; + outline: none; + display: inline-block; + vertical-align: top; + position: relative; + margin: 0 8px 0 0; + cursor: pointer; + border: 1px solid var(--bc, var(--border)); + background: var(--b, var(--background-secondary)); + transition: background 0.3s, border-color 0.3s, box-shadow 0.2s; + } + input[type=checkbox]:after, input[type=radio]:after { + content: ""; + display: block; + left: 0; + top: 0; + position: absolute; + transition: transform var(--d-t, 0.3s) var(--d-t-e, ease), opacity var(--d-o, 0.2s); + } + input[type=checkbox]:checked, input[type=radio]:checked { + --b: var(--active); + --bc: var(--active); + --d-o: .3s; + --d-t: .6s; + --d-t-e: cubic-bezier(.2, .85, .32, 1.2); + } + input[type=checkbox]:disabled, input[type=radio]:disabled { + --b: var(--disabled); + cursor: not-allowed; + opacity: 0.9; + } + input[type=checkbox]:disabled:checked, input[type=radio]:disabled:checked { + --b: var(--disabled-inner); + --bc: var(--border); + } + input[type=checkbox]:disabled+label, input[type=radio]:disabled+label { + cursor: not-allowed; + } + input[type=checkbox]:hover:not(:checked):not(:disabled), input[type=radio]:hover:not(:checked):not(:disabled) { + --bc: var(--border-hover); + } + input[type=checkbox]:focus, input[type=radio]:focus { + box-shadow: 0 0 0 var(--focus); + } + input[type=checkbox]:not(.switch), input[type=radio]:not(.switch) { + width: 21px; + } + input[type=checkbox]:not(.switch):after, input[type=radio]:not(.switch):after { + opacity: var(--o, 0); + } + input[type=checkbox]:not(.switch):checked, input[type=radio]:not(.switch):checked { + --o: 1; + } + input[type=checkbox]+label, input[type=radio]+label { + font-size: 14px; + line-height: 21px; + display: inline-block; + vertical-align: top; + cursor: pointer; + margin-left: 4px; + } + input[type=checkbox]:not(.switch) { + border-radius: 7px; + } + input[type=checkbox]:not(.switch):after { + width: 5px; + height: 9px; + border: 2px solid var(--active-inner); + border-top: 0; + border-left: 0; + left: 7px; + top: 4px; + transform: rotate(var(--r, 20deg)); + } + input[type=checkbox]:not(.switch):checked { + --r: 43deg; + } + input[type=checkbox].switch { + width: 38px; + border-radius: 11px; + } + input[type=checkbox].switch:after { + left: 2px; + top: 2px; + border-radius: 50%; + width: 15px; + height: 15px; + background: var(--ab, var(--border)); + transform: translateX(var(--x, 0)); + } + input[type=checkbox].switch:checked { + --ab: var(--active-inner); + --x: 17px; + } + input[type=checkbox].switch:disabled:not(:checked):after { + opacity: 0.6; + } + input[type=radio] { + border-radius: 50%; + } + input[type=radio]:after { + width: 19px; + height: 19px; + border-radius: 50%; + background: var(--active-inner); + opacity: 0; + transform: scale(var(--s, 0.7)); + } + input[type=radio]:checked { + --s: .5; + } +} diff --git a/templates/includes/layout.gohtml b/templates/includes/layout.gohtml new file mode 100644 index 0000000..adebd99 --- /dev/null +++ b/templates/includes/layout.gohtml @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>SendSafe</title> + <link rel="stylesheet" href="/css/index.css" /> + <script src="https://unpkg.com/turbolinks/dist/turbolinks.js" crossorigin="anonymous"></script> +</head> + +<body> +<header> + <a id="header-logo" href="/"></> SendSafe</a> +</header> +{{template "content" .}} +</body> +</html> diff --git a/templates/pages/error.gohtml b/templates/pages/error.gohtml new file mode 100644 index 0000000..8d63bbd --- /dev/null +++ b/templates/pages/error.gohtml @@ -0,0 +1,5 @@ +{{template "layout.gohtml" .}} +{{define "content"}} +<h1>Error happened.</h1> +<h2>Nothing can be done.</h2> +{{ end }} diff --git a/templates/pages/index.gohtml b/templates/pages/index.gohtml new file mode 100644 index 0000000..8cfcae1 --- /dev/null +++ b/templates/pages/index.gohtml @@ -0,0 +1,21 @@ +{{template "layout.gohtml" .}} +{{define "content"}} +<div class="layout-center"> + <form method="POST" action="/save"> + <h1>New secret</h1> + <label for="secret">Secret content:<span class="required">(required)</span></label> + <textarea id="secret" name="secret" rows="6" required></textarea> + <label><span>Expires after (or <b>first</b> read):</span><span class="required">(required)</span></label> + <div class="form-row"> + <label><input id="days" name="days" type="number" value="0" min="0" max="365" required />days</label> + <label><input id="hours" name="hours" type="number" value="0" min="0" max="23" required />hours</label> + <label><input id="minutes" name="minutes" type="number" value="30" min="0" max="59" required />minutes</label> + </div> + {{/* <label for="pin">Pin code to open:<span class="optional">(optional)</span></label> */}} + {{/* <input id="pin" name="pin" type="string" value="" /> */}} + <hr /> + <input type="hidden" name="csrf" value={{.csrf}} /> + <input type="submit" value="Send" /> + </form> +</div> +{{ end }} diff --git a/templates/pages/memo.gohtml b/templates/pages/memo.gohtml new file mode 100644 index 0000000..804d7b7 --- /dev/null +++ b/templates/pages/memo.gohtml @@ -0,0 +1,13 @@ +{{template "layout.gohtml" .}} +{{define "content"}} +<div class="layout-center"> + <form action="/"> + <h1>Secret:</h1> + <b>Warning!</b> This text already deleted from server! If you refresh or close page - secret will be completely lost! + <hr /> + <pre>{{.secret}}</pre> + <hr /> + <input type="submit" value="← Back"> + </form> +</div> +{{ end }} diff --git a/templates/pages/notfound.gohtml b/templates/pages/notfound.gohtml new file mode 100644 index 0000000..911f8e1 --- /dev/null +++ b/templates/pages/notfound.gohtml @@ -0,0 +1,10 @@ +{{template "layout.gohtml" .}} +{{define "content"}} +<div class="layout-center"> + <form action="/"> + <h1>Not found.</h1> + <p>Link expired or already viewed.</p> + <input type="submit" value="← Back"> + </form> +</div> +{{ end }} diff --git a/templates/pages/save.gohtml b/templates/pages/save.gohtml new file mode 100644 index 0000000..f940502 --- /dev/null +++ b/templates/pages/save.gohtml @@ -0,0 +1,13 @@ +{{template "layout.gohtml" .}} +{{define "content"}} +<div class="layout-center"> + <form action="/"> + <h1>Saved</h1> + Secret url: + <hr /> + <a href="https://sendsafe.xyz/s/{{.id}}">https://sendsafe.xyz/s/{{.id}}</a> + <hr /> + <input type="submit" value="← Back"> + </form> +</div> +{{ end }} |