From c2ca26a918b7054199234e8459d84ee2fd260759 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Tue, 30 Sep 2025 21:50:36 -0700 Subject: Allow appending to notes Right now it's n^2 which is a big problem (to be fixed later) --- roseh.moe.go | 138 +++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 100 insertions(+), 38 deletions(-) (limited to 'roseh.moe.go') diff --git a/roseh.moe.go b/roseh.moe.go index f9c5478..76b4dc1 100644 --- a/roseh.moe.go +++ b/roseh.moe.go @@ -133,12 +133,16 @@ func favicon(w http.ResponseWriter, r *http.Request) { serveStaticFile(w, r, "static/favicon.ico") } +type authToken struct { + Issued time.Time +} + func makeToken() (string, error) { - nowBytes, err := time.Now().MarshalBinary() - if err != nil { + buf := new(bytes.Buffer) + if err := gob.NewEncoder(buf).Encode(authToken{Issued: time.Now()}); err != nil { return "", err } - return base64.RawStdEncoding.EncodeToString(secretKey.Sign(nowBytes)), nil + return base64.RawStdEncoding.EncodeToString(secretKey.Sign(buf.Bytes(), "auth")), nil } func checkToken(token string) bool { @@ -146,15 +150,15 @@ func checkToken(token string) bool { if err != nil { return false } - msg, ok := secretKey.Verify(authCookie) + msg, ok := secretKey.Verify(authCookie, "auth") if !ok { return false } - var t time.Time - if err := t.UnmarshalBinary(msg); err != nil { + var t authToken + if err := gob.NewDecoder(bytes.NewReader(msg)).Decode(&t); err != nil { return false } - return time.Since(t) < cookieExpiration + return time.Since(t.Issued) < cookieExpiration } const cookieExpiration = 180 * 24 * time.Hour @@ -402,13 +406,84 @@ func listNotes(req *api.ListNotesRequest) (*api.ListNotesResponse, error) { return &api.ListNotesResponse{Notes: names}, nil } +type countingWriter struct { + w io.Writer + count int64 +} + +func (w *countingWriter) Write(buf []byte) (int, error) { + n, err := w.w.Write(buf) + w.count += int64(n) + return n, err +} + +type continuationToken struct { + Name string + Size int64 +} + +func appendNote(req *api.CreateNoteRequest, key cryptoutil.EncryptionKey) (*api.CreateNoteResponse, error) { + msg, ok := secretKey.Verify(req.ContinuationToken, "continuation-token") + if !ok { + return nil, fmt.Errorf("%w: bad continuation token", api.InvalidArgument) + } + var token continuationToken + if err := gob.NewDecoder(bytes.NewReader(msg)).Decode(&token); err != nil { + return nil, fmt.Errorf("failed to decode continuation token: %s", err) + } + var err error + oldNote, err := os.Open(*notepadDir + "/notes/" + token.Name) + if err != nil { + return nil, err + } + defer oldNote.Close() + stat, err := oldNote.Stat() + if err != nil { + return nil, err + } + if token.Size != stat.Size() { + return nil, fmt.Errorf("%w: continuation token expired", api.InvalidArgument) + } + newNote, err := os.CreateTemp(*notepadDir+"/notes", token.Name) + if err != nil { + return nil, err + } + defer newNote.Close() + additionalData := []byte("notes/" + token.Name) + cw := &countingWriter{w: newNote} + encryptingWriter := key.NewWriter(cw, additionalData) + if _, err := io.Copy(encryptingWriter, key.NewReader(oldNote, additionalData)); err != nil { + return nil, err + } + if _, err := encryptingWriter.Write(req.Chunk); err != nil { + return nil, err + } + if err := encryptingWriter.Close(); err != nil { + return nil, err + } + if err := newNote.Close(); err != nil { + return nil, err + } + if err := os.Rename(newNote.Name(), *notepadDir+"/notes/"+token.Name); err != nil { + return nil, err + } + buf := new(bytes.Buffer) + if err := gob.NewEncoder(buf).Encode(continuationToken{Name: token.Name, Size: cw.count}); err != nil { + return nil, err + } + return &api.CreateNoteResponse{ + Name: token.Name, + ContinuationToken: secretKey.Sign(buf.Bytes(), "continuation-token"), + }, nil +} + var ( //go:embed wordlist.txt wordListString string wordList = strings.Split(strings.TrimSuffix(wordListString, "\n"), "\n") ) -func createNote(stream *gob.Decoder) (*api.CreateNoteResponse, error) { +func createNote(req *api.CreateNoteRequest) (*api.CreateNoteResponse, error) { encryptionKeyMu.Lock() key := encryptionKey encryptionKeyMu.Unlock() @@ -417,6 +492,9 @@ func createNote(stream *gob.Decoder) (*api.CreateNoteResponse, error) { } var name string var f *os.File + if len(req.ContinuationToken) > 0 { + return appendNote(req, key) + } for n := 1; ; n++ { buf := make([]byte, 2*n) rand.Read(buf) @@ -433,21 +511,13 @@ func createNote(stream *gob.Decoder) (*api.CreateNoteResponse, error) { } return nil, fmt.Errorf("create note: create note file: %s", err) } + defer f.Close() break } - defer f.Close() - encryptingWriter := key.NewWriter(f, []byte("notes/"+name)) - 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) - } + cw := &countingWriter{w: f} + encryptingWriter := key.NewWriter(cw, []byte("notes/"+name)) + if _, err := encryptingWriter.Write(req.Chunk); err != nil { + return nil, err } if err := encryptingWriter.Close(); err != nil { return nil, err @@ -455,7 +525,14 @@ func createNote(stream *gob.Decoder) (*api.CreateNoteResponse, error) { if err := f.Close(); err != nil { return nil, fmt.Errorf("create note: write file: %s", err) } - return &api.CreateNoteResponse{Name: name}, nil + token := new(bytes.Buffer) + if err := gob.NewEncoder(token).Encode(continuationToken{Name: name, Size: cw.count}); err != nil { + return nil, err + } + return &api.CreateNoteResponse{ + Name: name, + ContinuationToken: secretKey.Sign(token.Bytes(), "continuation-token"), + }, nil } type readNoteResponseWriter struct { @@ -521,21 +598,6 @@ func gobReqRespMiddleware[Request, Response any](next func(*Request) (*Response, } } -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") @@ -569,7 +631,7 @@ func main() { 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("POST /api/create-note", gobReqRespMiddleware(createNote)) http.HandleFunc("GET /api/read-note", gobRespStreamMiddleware(readNote)) http.HandleFunc("GET /static/", static) http.HandleFunc("GET /favicon.ico", favicon) -- cgit v1.3.1