summaryrefslogtreecommitdiffstats
path: root/roseh.moe.go
diff options
context:
space:
mode:
Diffstat (limited to 'roseh.moe.go')
-rw-r--r--roseh.moe.go312
1 files changed, 278 insertions, 34 deletions
diff --git a/roseh.moe.go b/roseh.moe.go
index a7279cd..e6db419 100644
--- a/roseh.moe.go
+++ b/roseh.moe.go
@@ -6,6 +6,8 @@ import (
"crypto/subtle"
"embed"
"encoding/base64"
+ "encoding/binary"
+ "encoding/gob"
"encoding/hex"
"errors"
"flag"
@@ -15,18 +17,18 @@ import (
"log"
"net/http"
"os"
- "path/filepath"
"strings"
"sync"
"time"
+ "gitlab.com/rhogenson/roseh.moe/internal/api"
"gitlab.com/rhogenson/roseh.moe/internal/cryptoutil"
)
var (
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")
+ notepadDir = flag.String("notepad", "notepad", "directory to save user notes")
serverStartTime = time.Now()
)
@@ -131,16 +133,40 @@ func favicon(w http.ResponseWriter, r *http.Request) {
serveStaticFile(w, r, "static/favicon.ico")
}
+func makeToken() (string, error) {
+ nowBytes, err := time.Now().MarshalBinary()
+ if err != nil {
+ return "", err
+ }
+ return base64.RawStdEncoding.EncodeToString(secretKey.Sign(nowBytes)), nil
+}
+
+func checkToken(token string) bool {
+ authCookie, err := base64.RawStdEncoding.DecodeString(token)
+ if err != nil {
+ return false
+ }
+ msg, ok := secretKey.Verify(authCookie)
+ if !ok {
+ return false
+ }
+ var t time.Time
+ if err := t.UnmarshalBinary(msg); err != nil {
+ return false
+ }
+ return time.Since(t) < cookieExpiration
+}
+
const cookieExpiration = 180 * 24 * time.Hour
func attachCookie(w http.ResponseWriter) error {
- nowBytes, err := time.Now().MarshalBinary()
+ token, err := makeToken()
if err != nil {
return err
}
http.SetCookie(w, &http.Cookie{
Name: "auth",
- Value: base64.RawStdEncoding.EncodeToString(secretKey.Sign(nowBytes)),
+ Value: token,
Path: "/notepad",
Expires: time.Now().Add(cookieExpiration),
Secure: true,
@@ -158,19 +184,7 @@ func cookieAuth(w http.ResponseWriter, r *http.Request) (string, bool) {
if err != nil {
return "", false
}
- authCookie, err := base64.RawStdEncoding.DecodeString(cookie.Value)
- if err != nil {
- return "", false
- }
- msg, ok := secretKey.Verify(authCookie)
- if !ok {
- return "", false
- }
- var t time.Time
- if err := t.UnmarshalBinary(msg); err != nil {
- return "", false
- }
- if time.Since(t) > cookieExpiration {
+ if !checkToken(cookie.Value) {
return "", false
}
attachCookie(w)
@@ -194,6 +208,25 @@ func cookieAuth(w http.ResponseWriter, r *http.Request) (string, bool) {
return csrfToken, true
}
+func setEncryptionKey(rawKey cryptoutil.RawKey) error {
+ encryptionKeyMu.Lock()
+ currentKey := encryptionKey
+ encryptionKeyMu.Unlock()
+ if currentKey != nil {
+ return nil
+ }
+ key, err := rawKey.EncryptionKey()
+ if err != nil {
+ return err
+ }
+ encryptionKeyMu.Lock()
+ if encryptionKey == nil {
+ encryptionKey = key
+ }
+ encryptionKeyMu.Unlock()
+ return nil
+}
+
var (
//go:embed templates/login.html.template
loginString string
@@ -212,27 +245,16 @@ func login(w http.ResponseWriter, r *http.Request) {
}
return
}
- encryptionKeyMu.Lock()
- currentKey := encryptionKey
- encryptionKeyMu.Unlock()
- if currentKey == nil {
- key, err := rawKey.EncryptionKey()
- if err != nil {
- http.Error(w, fmt.Sprintf("failed to derive encryption key: %s", err), http.StatusInternalServerError)
- return
- }
- encryptionKeyMu.Lock()
- if encryptionKey == nil {
- encryptionKey = key
- }
- encryptionKeyMu.Unlock()
+ if err := setEncryptionKey(rawKey); err != nil {
+ http.Error(w, fmt.Sprintf("Error: %s", err), http.StatusInternalServerError)
+ return
}
attachCookie(w)
http.Redirect(w, r, "/notepad", http.StatusSeeOther)
}
func readNotepad(key cryptoutil.EncryptionKey) (string, error) {
- encrypted, err := os.ReadFile(*notepadFile)
+ encrypted, err := os.ReadFile(*notepadDir + "/notepad")
if err != nil {
return "", err
}
@@ -297,7 +319,7 @@ func saveNote(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return err
}
- f, err := os.CreateTemp(filepath.Dir(*notepadFile), "notepad")
+ f, err := os.CreateTemp(*notepadDir, "notepad")
if err != nil {
return err
}
@@ -310,7 +332,7 @@ func saveNote(w http.ResponseWriter, r *http.Request) error {
os.Remove(f.Name())
return err
}
- return os.Rename(f.Name(), *notepadFile)
+ return os.Rename(f.Name(), *notepadDir+"/notepad")
}
func autosave(w http.ResponseWriter, r *http.Request) {
@@ -323,6 +345,224 @@ func autosave(w http.ResponseWriter, r *http.Request) {
}
}
+func writeGob(w io.Writer, resp any) {
+ if err := gob.NewEncoder(w).Encode(&api.Response{Ok: resp}); err != nil {
+ log.Printf("Warning: write gob response: %s", err)
+ }
+}
+
+func gobError(w io.Writer, err error) {
+ var code api.ErrorCode
+ if !errors.As(err, &code) {
+ code = api.Internal
+ }
+ if err := gob.NewEncoder(w).Encode(&api.Response{Status: code, Err: err.Error()}); err != nil {
+ log.Printf("Warning: write gob response: %s", err)
+ }
+}
+
+func apiLogin(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/gob")
+ rawKey, err := notepadPassword.CheckPassword(r.Header.Get("Roseh-Password"))
+ if err != nil {
+ gobError(w, fmt.Errorf("%w: incorrect password", api.PermissionDenied))
+ return
+ }
+ if err := setEncryptionKey(rawKey); err != nil {
+ gobError(w, err)
+ return
+ }
+ token, err := makeToken()
+ if err != nil {
+ gobError(w, err)
+ return
+ }
+ writeGob(w, &api.LoginResponse{Token: token})
+}
+
+type encoder struct {
+ enc *gob.Encoder
+}
+
+func (e *encoder) send(resp any) error {
+ return e.enc.Encode(&api.Response{Ok: resp})
+}
+
+func listNotes(req *api.ListNotesRequest) (*api.ListNotesResponse, error) {
+ notes, err := os.ReadDir(*notepadDir + "/notes")
+ if err != nil {
+ return nil, err
+ }
+ names := make([]string, len(notes))
+ for i, note := range notes {
+ names[i] = note.Name()
+ }
+ return &api.ListNotesResponse{Notes: names}, nil
+}
+
+var (
+ //go:embed wordlist.txt
+ wordListString string
+ wordList = strings.Split(strings.TrimSuffix(wordListString, "\n"), "\n")
+)
+
+func createNote(stream *gob.Decoder) (*api.CreateNoteResponse, error) {
+ encryptionKeyMu.Lock()
+ key := encryptionKey
+ encryptionKeyMu.Unlock()
+ if key == nil {
+ return nil, fmt.Errorf("%w: need login", api.PermissionDenied)
+ }
+ var name string
+ var f *os.File
+ for n := 1; ; n++ {
+ buf := make([]byte, 2*n)
+ rand.Read(buf)
+ words := make([]string, n)
+ for i := range words {
+ words[i] = wordList[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff]
+ }
+ name = strings.Join(words, "-")
+ var err error
+ f, err = os.OpenFile(*notepadDir+"/notes/"+name, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
+ if err != nil {
+ if errors.Is(err, os.ErrExist) {
+ continue
+ }
+ return nil, fmt.Errorf("create note: create note file: %s", err)
+ }
+ break
+ }
+ defer f.Close()
+ encryptingWriter, err := key.Writer(f)
+ if err != nil {
+ return nil, err
+ }
+ for {
+ req := new(api.CreateNoteRequestStream)
+ if err := stream.Decode(req); err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ return nil, fmt.Errorf("create note: read input stream: %s", err)
+ }
+ if _, err := encryptingWriter.Write(req.Chunk); err != nil {
+ return nil, fmt.Errorf("create note: write note to file: %s", err)
+ }
+ }
+ if err := encryptingWriter.Flush(); err != nil {
+ return nil, err
+ }
+ if err := f.Close(); err != nil {
+ return nil, fmt.Errorf("create note: write file: %s", err)
+ }
+ return &api.CreateNoteResponse{Name: name}, nil
+}
+
+func readNote(stream *encoder, req *api.ReadNoteRequest) error {
+ f, err := os.OpenInRoot(*notepadDir+"/notes", req.Note)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("%w: note %q", api.NotFound, req.Note)
+ }
+ return err
+ }
+ defer f.Close()
+ encryptionKeyMu.Lock()
+ key := encryptionKey
+ encryptionKeyMu.Unlock()
+ if key == nil {
+ return fmt.Errorf("%w: need login", api.PermissionDenied)
+ }
+ decryptingReader, err := key.Reader(f)
+ if err != nil {
+ return err
+ }
+ buf := make([]byte, 4*1024*1024)
+ for {
+ n, err := decryptingReader.Read(buf)
+ if n > 0 {
+ if err := stream.send(&api.ReadNoteResponseStream{Chunk: buf[:n]}); err != nil {
+ return err
+ }
+ }
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ return err
+ }
+ }
+ return nil
+}
+
+func tokenAuth(w http.ResponseWriter, r *http.Request) bool {
+ token := r.Header.Get("Roseh-Token")
+ if token == "" {
+ gobError(w, api.Unauthenticated)
+ return false
+ }
+ if !checkToken(token) {
+ gobError(w, fmt.Errorf("%w: bad token", api.PermissionDenied))
+ return false
+ }
+ return true
+}
+
+func gobReqRespMiddleware[Request, Response any](next func(*Request) (*Response, error)) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/gob")
+ if !tokenAuth(w, r) {
+ return
+ }
+ req := new(Request)
+ if err := gob.NewDecoder(r.Body).Decode(req); err != nil {
+ gobError(w, fmt.Errorf("%w: read body: %s", api.BadRequest, err))
+ return
+ }
+ resp, err := next(req)
+ if err != nil {
+ gobError(w, err)
+ return
+ }
+ writeGob(w, resp)
+ }
+}
+
+func gobReqStreamMiddleware[Response any](next func(*gob.Decoder) (*Response, error)) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/gob")
+ if !tokenAuth(w, r) {
+ return
+ }
+ resp, err := next(gob.NewDecoder(r.Body))
+ if err != nil {
+ gobError(w, err)
+ return
+ }
+ writeGob(w, resp)
+ }
+}
+
+func gobRespStreamMiddleware[Request any](next func(*encoder, *Request) error) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/gob")
+ if !tokenAuth(w, r) {
+ return
+ }
+ req := new(Request)
+ if err := gob.NewDecoder(r.Body).Decode(req); err != nil {
+ gobError(w, fmt.Errorf("%w: read body: %s", api.BadRequest, err))
+ return
+ }
+ gobEncoder := gob.NewEncoder(w)
+ if err := next(&encoder{gobEncoder}, req); err != nil {
+ gobError(w, err)
+ return
+ }
+ }
+}
+
func main() {
flag.Parse()
@@ -335,6 +575,10 @@ func main() {
http.HandleFunc("POST /login", login)
http.HandleFunc("GET /notepad", notepad)
http.HandleFunc("POST /notepad/autosave", autosave)
+ http.HandleFunc("POST /api/login", apiLogin)
+ http.HandleFunc("GET /api/list-notes", gobReqRespMiddleware(listNotes))
+ http.HandleFunc("POST /api/create-note", gobReqStreamMiddleware(createNote))
+ http.HandleFunc("GET /api/read-note", gobRespStreamMiddleware(readNote))
http.HandleFunc("GET /static/", static)
http.HandleFunc("GET /favicon.ico", favicon)
http.HandleFunc("GET /{$}", index)