summaryrefslogtreecommitdiffstats
path: root/roseh.moe.go
diff options
context:
space:
mode:
Diffstat (limited to 'roseh.moe.go')
-rw-r--r--roseh.moe.go159
1 files changed, 158 insertions, 1 deletions
diff --git a/roseh.moe.go b/roseh.moe.go
index 5d6d2d5..681ab8e 100644
--- a/roseh.moe.go
+++ b/roseh.moe.go
@@ -1,24 +1,64 @@
package main
import (
+ "bytes"
+ "crypto/ed25519"
+ "crypto/sha512"
+ "crypto/subtle"
"embed"
+ "encoding/base64"
+ "encoding/hex"
"flag"
"fmt"
"html/template"
"io"
"log"
"net/http"
+ "os"
+ "path/filepath"
"strings"
"time"
)
var (
- port = flag.Int("port", 42069, "port to listen on")
+ port = flag.Int("port", 42069, "port to listen on")
+ secretsFile = flag.String("secrets", "secrets", "path to the secrets file")
+ notepadFile = flag.String("notepad", "notepad", "path to save user notes")
serverStartTime = time.Now()
)
var (
+ notepadPassword []byte
+ secretKey ed25519.PrivateKey
+)
+
+func loadSecrets() error {
+ secrets, err := os.ReadFile(*secretsFile)
+ if err != nil {
+ return err
+ }
+ for _, line := range bytes.Split(bytes.TrimSpace(secrets), []byte("\n")) {
+ if pw, ok := bytes.CutPrefix(line, []byte("notepad-password=")); ok {
+ notepadPassword = make([]byte, hex.DecodedLen(len(pw)))
+ if _, err := hex.Decode(notepadPassword, pw); err != nil {
+ return err
+ }
+ } else if key, ok := bytes.CutPrefix(line, []byte("secret-key=")); ok {
+ seed := make([]byte, hex.DecodedLen(len(key)))
+ if _, err := hex.Decode(seed, key); err != nil {
+ return err
+ }
+ if len(seed) != ed25519.SeedSize {
+ return fmt.Errorf("invalid ed25519 key")
+ }
+ secretKey = ed25519.NewKeyFromSeed(seed)
+ }
+ }
+ return nil
+}
+
+var (
//go:embed templates/404.html.template
notFoundString string
notFoundTemplate = template.Must(template.New("notFound").Parse(notFoundString))
@@ -83,10 +123,127 @@ func favicon(w http.ResponseWriter, r *http.Request) {
serveStaticFile(w, r, "static/favicon.ico")
}
+const cookieExpiration = 400 * 24 * time.Hour
+
+func attachCookie(w http.ResponseWriter) error {
+ nowBytes, err := time.Now().MarshalBinary()
+ if err != nil {
+ return err
+ }
+ sig := ed25519.Sign(secretKey, nowBytes)
+ http.SetCookie(w, &http.Cookie{
+ Name: "auth",
+ Value: base64.RawStdEncoding.EncodeToString(append(nowBytes, sig...)),
+ Path: "/notepad",
+ Expires: time.Now().Add(cookieExpiration),
+ // Secure: true,
+ HttpOnly: true,
+ SameSite: http.SameSiteStrictMode,
+ // Partitioned: true,
+ })
+ return nil
+}
+
+func cookieOK(cookie string) bool {
+ bytes, err := base64.RawStdEncoding.DecodeString(cookie)
+ if err != nil {
+ return false
+ }
+ if len(bytes) < ed25519.SignatureSize {
+ return false
+ }
+ msg, sig := bytes[:len(bytes)-ed25519.SignatureSize], bytes[len(bytes)-ed25519.SignatureSize:]
+ if !ed25519.Verify(secretKey.Public().(ed25519.PublicKey), msg, sig) {
+ return false
+ }
+ var t time.Time
+ if err := t.UnmarshalBinary(msg); err != nil {
+ return false
+ }
+ return time.Since(t) < cookieExpiration
+}
+
+func authenticate(next http.Handler) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if cookie, err := r.Cookie("auth"); err == nil && cookieOK(cookie.Value) {
+ attachCookie(w)
+ next.ServeHTTP(w, r)
+ return
+ }
+ _, password, ok := r.BasicAuth()
+ if !ok {
+ w.Header().Set("WWW-Authenticate", `Basic realm="notepad"`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+ pwHash := sha512.Sum512([]byte(password))
+ if subtle.ConstantTimeCompare(pwHash[:], notepadPassword) == 0 {
+ w.Header().Set("WWW-Authenticate", `Basic realm="notepad"`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+ attachCookie(w)
+ next.ServeHTTP(w, r)
+ }
+}
+
+var (
+ //go:embed templates/note.html.template
+ notepadString string
+ notepadTemplate = template.Must(template.Must(outlineTemplate.Clone()).New("body").Parse(notepadString)).Lookup("outline")
+)
+
+func notepad(w http.ResponseWriter, r *http.Request) {
+ var currentContent string
+ if currentContentBytes, err := os.ReadFile(*notepadFile); err != nil {
+ currentContent = fmt.Sprintf("Error reading notepad file: %s", err)
+ } else {
+ currentContent = string(currentContentBytes)
+ }
+ type args struct {
+ Content string
+ }
+ if err := notepadTemplate.Execute(w, args{Content: currentContent}); err != nil {
+ log.Printf("Warning: notepad: %s", err)
+ }
+}
+
+func saveNote(content []byte) error {
+ f, err := os.CreateTemp(filepath.Dir(*notepadFile), "notepad")
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ if _, err = f.Write(content); err != nil {
+ return err
+ }
+ if err := f.Close(); err != nil {
+ return err
+ }
+ return os.Rename(f.Name(), *notepadFile)
+}
+
+func autosave(w http.ResponseWriter, r *http.Request) {
+ msg := "Saved ✓"
+ if err := saveNote([]byte(r.FormValue("content"))); err != nil {
+ msg = fmt.Sprintf("Error: %s", err)
+ }
+ if err := notepadTemplate.ExecuteTemplate(w, "saveIndicator", msg); err != nil {
+ log.Printf("Warning: autosave: %s", err)
+ }
+}
+
func main() {
flag.Parse()
+ if err := loadSecrets(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
http.HandleFunc("GET /pong", pong)
+ http.Handle("GET /notepad", authenticate(http.HandlerFunc(notepad)))
+ http.HandleFunc("POST /notepad/autosave", autosave)
http.HandleFunc("GET /static/", static)
http.HandleFunc("GET /favicon.ico", favicon)
http.HandleFunc("GET /{$}", index)