diff options
| author | Rose Hogenson <rosehogenson@posteo.net> | 2025-10-05 18:00:06 -0700 |
|---|---|---|
| committer | Rose Hogenson <rosehogenson@posteo.net> | 2025-10-05 18:00:06 -0700 |
| commit | d89b325294201d95498ae9d08f102e12ac2f2876 (patch) | |
| tree | ffe65e33d0d01e811a5323d5e40c47564ccb8f2d /roseh.moe.go | |
| parent | 624985afaaa8b94e4d9685bcdc935d467f9e7060 (diff) | |
| download | roseh.moe-d89b325294201d95498ae9d08f102e12ac2f2876.tar.zst | |
Implement a "magic wormhole" file-sharing solution
Diffstat (limited to 'roseh.moe.go')
| -rw-r--r-- | roseh.moe.go | 752 |
1 files changed, 93 insertions, 659 deletions
diff --git a/roseh.moe.go b/roseh.moe.go index 8c7e176..e9d0750 100644 --- a/roseh.moe.go +++ b/roseh.moe.go @@ -1,78 +1,30 @@ package main import ( - "bufio" - "bytes" "crypto/rand" - "crypto/subtle" "embed" - "encoding/base64" "encoding/binary" - "encoding/gob" - "encoding/hex" - "errors" "flag" "fmt" "html/template" "io" "log" "net/http" - "net/url" - "os" - "path/filepath" - "strconv" "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") notepadDir = flag.String("notepad", "notepad", "directory to save user notes") + selfURL = flag.String("self-url", "http://localhost:42069", "base URL of the server") serverStartTime = time.Now() ) var ( - notepadPassword cryptoutil.PasswordHash - secretKey cryptoutil.HMACKey - - encryptionKeyMu sync.Mutex - encryptionKey cryptoutil.EncryptionKey -) - -func loadSecrets() error { - secrets, err := os.ReadFile(*secretsFile) - if err != nil { - return err - } - for _, line := range bytes.Split(bytes.TrimSuffix(secrets, []byte("\n")), []byte("\n")) { - if pw, ok := bytes.CutPrefix(line, []byte("notepad-password=")); ok { - if hex.DecodedLen(len(pw)) != cryptoutil.PasswordHashSize { - return fmt.Errorf("invalid PBKDF2-SHA256 hash") - } - notepadPassword = make(cryptoutil.PasswordHash, 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 { - if hex.DecodedLen(len(key)) != cryptoutil.HMACKeySize { - return fmt.Errorf("invalid HMAC-SHA256 key") - } - secretKey = make(cryptoutil.HMACKey, hex.DecodedLen(len(key))) - if _, err := hex.Decode(secretKey, key); err != nil { - return err - } - } - } - return nil -} - -var ( //go:embed templates/404.html.template notFoundString string notFoundTemplate = template.Must(template.New("notFound").Parse(notFoundString)) @@ -137,660 +89,142 @@ func favicon(w http.ResponseWriter, r *http.Request) { serveStaticFile(w, r, "static/favicon.ico") } -type authToken struct { - Issued time.Time -} - -func makeToken() (string, error) { - buf := new(bytes.Buffer) - if err := gob.NewEncoder(buf).Encode(authToken{Issued: time.Now()}); err != nil { - return "", err - } - return base64.RawStdEncoding.EncodeToString(secretKey.Sign(buf.Bytes(), "auth")), nil -} - -func checkToken(token string) bool { - authCookie, err := base64.RawStdEncoding.DecodeString(token) - if err != nil { - return false - } - msg, ok := secretKey.Verify(authCookie, "auth") - if !ok { - return false - } - var t authToken - if err := gob.NewDecoder(bytes.NewReader(msg)).Decode(&t); err != nil { - return false - } - return time.Since(t.Issued) < cookieExpiration -} - -const cookieExpiration = 180 * 24 * time.Hour - -func attachCookie(w http.ResponseWriter) error { - token, err := makeToken() - if err != nil { - return err - } - http.SetCookie(w, &http.Cookie{ - Name: "auth", - Value: token, - Path: "/", - Expires: time.Now().Add(cookieExpiration), - Secure: true, - HttpOnly: true, - SameSite: http.SameSiteStrictMode, - Partitioned: true, - }) - return nil -} - -func cookieAuth(w http.ResponseWriter, r *http.Request) (string, bool) { - const csrfTokenLen = 32 - - cookie, err := r.Cookie("auth") - if err != nil { - return "", false - } - if !checkToken(cookie.Value) { - return "", false - } - attachCookie(w) - - if csrfToken, err := r.Cookie("csrf-token"); err == nil { - return csrfToken.Value, true - } - - buf := make([]byte, csrfTokenLen) - rand.Read(buf) - csrfToken := base64.RawStdEncoding.EncodeToString(buf) - http.SetCookie(w, &http.Cookie{ - Name: "csrf-token", - Value: csrfToken, - Path: "/", - Secure: true, - HttpOnly: true, - SameSite: http.SameSiteStrictMode, - Partitioned: true, - }) - 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 - loginTemplate = template.Must(template.Must(outlineTemplate.Clone()).New("body").Parse(loginString)).Lookup("outline") -) - -type loginTemplateArgs struct { - Error bool - Redirect string -} - -func executeLoginTemplate(w io.Writer, redirect string) { - if err := loginTemplate.Execute(w, loginTemplateArgs{Redirect: base64.RawURLEncoding.EncodeToString(secretKey.Sign([]byte(redirect), "redirect"))}); err != nil { - log.Printf("Warning: login: %s", err) - } -} - -func login(w http.ResponseWriter, r *http.Request) { - rawKey, err := notepadPassword.CheckPassword(r.FormValue("password")) - if err != nil { - if err := loginTemplate.Execute(w, loginTemplateArgs{Error: true, Redirect: r.FormValue("redirect")}); err != nil { - log.Printf("Warning: login: %s", err) - } - return - } - if err := setEncryptionKey(rawKey); err != nil { - http.Error(w, fmt.Sprintf("Error: %s", err), http.StatusInternalServerError) - return - } - attachCookie(w) - redirect := "/" - if b, err := base64.RawURLEncoding.DecodeString(r.FormValue("redirect")); err == nil { - if r, ok := secretKey.Verify(b, "redirect"); ok { - redirect = string(r) - } - } - http.Redirect(w, r, redirect, http.StatusSeeOther) -} - -func readNotepad(key cryptoutil.EncryptionKey) (string, error) { - f, err := os.Open(*notepadDir + "/notepad") - if err != nil { - return "", err - } - defer f.Close() - notepad, err := io.ReadAll(key.NewReader(f, cryptoutil.WithAdditionalData([]byte("notepad")))) - if err != nil { - return "", err - } - return string(notepad), nil -} - -var ( - //go:embed templates/note.html.template - notepadString string - notepadTemplate = template.Must(template.Must(outlineTemplate.Clone()).New("body").Parse(notepadString)).Lookup("outline") -) - -type notepadTemplateArgs struct { - Content string - CSRFToken string -} - -func notepad(w http.ResponseWriter, r *http.Request) { - csrfToken, ok := cookieAuth(w, r) - if !ok { - executeLoginTemplate(w, "/notepad") - return - } - encryptionKeyMu.Lock() - key := encryptionKey - encryptionKeyMu.Unlock() - if key == nil { - executeLoginTemplate(w, "/notepad") - return - } - currentContent, err := readNotepad(key) - if err != nil { - currentContent = fmt.Sprintf("Error reading notepad file: %s", err) - } - if err := notepadTemplate.Execute(w, notepadTemplateArgs{Content: currentContent, CSRFToken: csrfToken}); err != nil { - log.Printf("Warning: notepad: %s", err) - } -} - -func saveNote(w http.ResponseWriter, r *http.Request) error { - if csrfToken, err := r.Cookie("csrf-token"); err != nil || subtle.ConstantTimeCompare([]byte(csrfToken.Value), []byte(r.FormValue("csrf-token"))) == 0 { - return fmt.Errorf("bad CSRF token") - } - if _, ok := cookieAuth(w, r); !ok { - return fmt.Errorf("not logged in") - } - encryptionKeyMu.Lock() - key := encryptionKey - encryptionKeyMu.Unlock() - if key == nil { - return errors.New("not logged in") - } - f, err := os.CreateTemp(*notepadDir, "notepad") - if err != nil { - return err - } - defer f.Close() - encryptingWriter := key.NewWriter(f, cryptoutil.WithAdditionalData([]byte("notepad"))) - if _, err := encryptingWriter.Write([]byte(r.FormValue("content"))); err != nil { - os.Remove(f.Name()) - return err - } - if err := encryptingWriter.Close(); err != nil { - os.Remove(f.Name()) - return err - } - if err := f.Close(); err != nil { - os.Remove(f.Name()) - return err - } - return os.Rename(f.Name(), *notepadDir+"/notepad") -} - -func autosave(w http.ResponseWriter, r *http.Request) { - msg := "Saved ✓" - if err := saveNote(w, r); err != nil { - msg = fmt.Sprintf("Failed to save: %s", err) - } - if err := notepadTemplate.ExecuteTemplate(w, "saveIndicator", msg); err != nil { - log.Printf("Warning: autosave: %s", err) - } -} - -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 -} - -func dumpFile(w io.Writer, key cryptoutil.EncryptionKey, name string) error { - f, err := os.Open(name) - if err != nil { - return err - } - defer f.Close() - _, err = io.Copy(w, key.NewReader(f, cryptoutil.WithAdditionalData([]byte("chunks/"+filepath.Base(name))))) - return err -} - -func chunkName(prefix string, i int64) string { - return fmt.Sprintf("%s%013s", prefix, strconv.FormatUint(uint64(i), 36)) -} - var ( //go:embed wordlist.txt wordListString string wordList = strings.Split(strings.TrimSuffix(wordListString, "\n"), "\n") + + //go:embed templates/wormhole.html.template + wormholeTemplateString string + wormholeTemplate = template.Must(template.Must(outlineTemplate.Clone()).New("body").Parse(wormholeTemplateString)).Lookup("outline") ) -func newNoteName() (*os.File, error) { - 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) - } - return f, nil - } +type wormholeTemplateArgs struct { + Self string + Hole string } -func createNote(req *api.CreateNoteRequest) (*api.CreateNoteResponse, error) { - type continuationToken struct { - Prefix string - I int64 - } - - encryptionKeyMu.Lock() - key := encryptionKey - encryptionKeyMu.Unlock() - if key == nil { - return nil, fmt.Errorf("%w: need login", api.PermissionDenied) - } - - var contToken continuationToken - if len(req.ContinuationToken) > 0 { - msg, ok := secretKey.Verify(req.ContinuationToken, "continuation-token") - if !ok { - return nil, fmt.Errorf("%w: invalid continuation token", api.InvalidArgument) - } - if err := gob.NewDecoder(bytes.NewReader(msg)).Decode(&contToken); err != nil { - return nil, fmt.Errorf("bad continuation token") - } - } else { - token := make([]byte, 64) - rand.Read(token) - contToken.Prefix = base64.RawURLEncoding.EncodeToString(token) - contToken.I = 0 - } - - if req.More { - if err := os.MkdirAll(*notepadDir+"/chunks", 0700); err != nil { - return nil, err - } - name := chunkName(contToken.Prefix, contToken.I) - f, err := os.OpenFile(*notepadDir+"/chunks/"+name, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) - if err != nil { - if errors.Is(err, os.ErrExist) { - return nil, fmt.Errorf("%w: continuation token expired", api.InvalidArgument) - } - return nil, err - } - defer f.Close() - if contToken.I > 0 { - _, err := os.Stat(*notepadDir + "/chunks/" + chunkName(contToken.Prefix, 0)) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("%w: continuation token expired", api.InvalidArgument) - } - return nil, err - } - } - encryptingWriter := key.NewWriter(f, cryptoutil.WithAdditionalData([]byte("chunks/"+name))) - if contToken.I == 0 { - if _, err := encryptingWriter.Write(binary.AppendVarint(nil, int64(len(req.FileName)))); err != nil { - return nil, err - } - if _, err := io.WriteString(encryptingWriter, req.FileName); 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 := f.Close(); err != nil { - return nil, err - } - contToken.I++ - buf := new(bytes.Buffer) - if err := gob.NewEncoder(buf).Encode(contToken); err != nil { - return nil, err - } - return &api.CreateNoteResponse{ContinuationToken: secretKey.Sign(buf.Bytes(), "continuation-token")}, nil - } - if err := os.MkdirAll(*notepadDir+"/notes", 0700); err != nil { - return nil, err - } - finalNote, err := newNoteName() - if err != nil { - return nil, err - } - defer finalNote.Close() - writer := key.NewWriter(finalNote, cryptoutil.WithAdditionalData([]byte("notes/"+filepath.Base(finalNote.Name())))) - for i := range contToken.I { - if err := dumpFile(writer, key, *notepadDir+"/chunks/"+chunkName(contToken.Prefix, i)); err != nil { - return nil, err - } - } - if _, err := writer.Write(req.Chunk); err != nil { - return nil, err - } - if err := writer.Close(); err != nil { - return nil, err +func wormhole(w http.ResponseWriter, r *http.Request) { + buf := make([]byte, 20) + rand.Read(buf) + words := make([]string, 10) + for i := range words { + words[i] = wordList[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff] } - if err := finalNote.Close(); err != nil { - return nil, err + if err := wormholeTemplate.Execute(w, wormholeTemplateArgs{Self: *selfURL, Hole: strings.Join(words, "-")}); err != nil { + log.Printf("Warning: wormhole: %s", err) } - for i := range contToken.I { - if err := os.Remove(*notepadDir + "/chunks/" + chunkName(contToken.Prefix, i)); err != nil { - log.Printf("Warning: createNote: failed to delete chunk: %s", err) - } - } - return &api.CreateNoteResponse{Name: filepath.Base(finalNote.Name())}, nil } -type readNoteResponseWriter struct { - fileName string - stream *encoder +type wormholeConn struct { + done chan struct{} + w http.ResponseWriter } -func (w *readNoteResponseWriter) Write(buf []byte) (int, error) { - if err := w.stream.send(&api.ReadNoteResponseStream{FileName: w.fileName, Chunk: buf}); err != nil { - return 0, err - } - w.fileName = "" - return len(buf), nil -} +var wormholeConnsMu sync.Mutex +var wormholeConns = make(map[string]*wormholeConn) -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) - } - reader := bufio.NewReader(key.NewReader(f, cryptoutil.WithAdditionalData([]byte("notes/"+req.Note)))) - nameLen, err := binary.ReadVarint(reader) - if err != nil { - return err - } - fileName := make([]byte, nameLen) - if _, err := io.ReadFull(reader, fileName); err != nil { - return err - } - _, err = io.Copy(&readNoteResponseWriter{fileName: string(fileName), stream: stream}, reader) - return err -} +var wormholeNotifyMu sync.Mutex +var wormholeNotify = make(map[string]chan struct{}) -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 +func wormholeSend(w http.ResponseWriter, r *http.Request) { + reader, err := r.MultipartReader() + if err != nil { + http.Error(w, "not a multipart/form-data request", http.StatusBadRequest) + return } - 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) + for { + part, err := reader.NextPart() if err != nil { - gobError(w, err) - return + break } - 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 + if part.FormName() != "file" { + continue } - req := new(Request) - if err := gob.NewDecoder(r.Body).Decode(req); err != nil { - gobError(w, fmt.Errorf("%w: read body: %s", api.BadRequest, err)) + hole := r.PathValue("hole") + wormholeConnsMu.Lock() + conn := wormholeConns[hole] + delete(wormholeConns, hole) + wormholeConnsMu.Unlock() + if conn == nil { + http.Error(w, "no such connection", http.StatusBadRequest) return } - gobEncoder := gob.NewEncoder(w) - if err := next(&encoder{gobEncoder}, req); err != nil { - gobError(w, err) + defer close(conn.done) + conn.w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+part.FileName()) + if _, err := io.Copy(conn.w, part); err != nil { + http.Error(w, fmt.Sprintf("Error during copy: %s", err), http.StatusServiceUnavailable) return } - } -} - -var ( - //go:embed templates/upload.html.template - uploadTemplateString string - uploadTemplate = template.Must(template.Must(outlineTemplate.Clone()).New("body").Parse(uploadTemplateString)).Lookup("outline") -) - -type uploadTemplateArgs struct { - CSRFToken string -} - -func uploadPage(w http.ResponseWriter, r *http.Request) { - csrfToken, ok := cookieAuth(w, r) - if !ok { - executeLoginTemplate(w, "/upload") - return - } - encryptionKeyMu.Lock() - key := encryptionKey - encryptionKeyMu.Unlock() - if key == nil { - executeLoginTemplate(w, "/upload") + fmt.Fprintf(w, "uploaded!") return } - if err := uploadTemplate.Execute(w, uploadTemplateArgs{CSRFToken: csrfToken}); err != nil { - log.Printf("Warning: upload: %s", err) - } + http.Error(w, "file not found", http.StatusBadRequest) } -func uploadNote(w http.ResponseWriter, r *http.Request) { - if csrfToken, err := r.Cookie("csrf-token"); err != nil || subtle.ConstantTimeCompare([]byte(csrfToken.Value), []byte(r.FormValue("csrf-token"))) == 0 { - http.Error(w, "bad CSRF token", http.StatusForbidden) - return +func wormholeRecv(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + hole := r.PathValue("hole") + conn := &wormholeConn{ + done: make(chan struct{}), + w: w, } - if _, ok := cookieAuth(w, r); !ok { - executeLoginTemplate(w, "/upload") + wormholeConnsMu.Lock() + wormholeConns[hole] = conn + wormholeConnsMu.Unlock() + defer func() { + wormholeConnsMu.Lock() + delete(wormholeConns, hole) + wormholeConnsMu.Unlock() + }() + wormholeNotifyMu.Lock() + notify := wormholeNotify[hole] + wormholeNotifyMu.Unlock() + if notify == nil { + http.Error(w, "no such connection", http.StatusBadRequest) return } - encryptionKeyMu.Lock() - key := encryptionKey - encryptionKeyMu.Unlock() - if key == nil { - executeLoginTemplate(w, "/upload") + select { + case notify <- struct{}{}: + default: + http.Error(w, "connection not ready", http.StatusBadRequest) return } - noteFile, err := newNoteName() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer noteFile.Close() - contentReader, metadata, err := r.FormFile("content") - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - writer := key.NewWriter(noteFile, cryptoutil.WithAdditionalData([]byte("notes/"+filepath.Base(noteFile.Name())))) - if _, err := writer.Write(binary.AppendVarint(nil, int64(len(metadata.Filename)))); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if _, err := io.WriteString(writer, metadata.Filename); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if _, err := io.Copy(writer, contentReader); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if err := writer.Close(); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if err := noteFile.Close(); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + select { + case <-ctx.Done(): + case <-conn.done: } - fmt.Fprintf(w, "upload! pog\n%s", filepath.Base(noteFile.Name())) } -func readNoteContent(w http.ResponseWriter, r *http.Request) { - if _, ok := cookieAuth(w, r); !ok { - executeLoginTemplate(w, "/note/"+r.PathValue("note")) +func wormholeReady(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + hole := r.PathValue("hole") + notify := make(chan struct{}) + wormholeNotifyMu.Lock() + wormholeNotify[hole] = notify + wormholeNotifyMu.Unlock() + defer func() { + wormholeNotifyMu.Lock() + delete(wormholeNotify, hole) + wormholeNotifyMu.Unlock() + }() + w.Header().Set("Content-Type", "text/event-stream") + select { + case <-ctx.Done(): return + case <-notify: } - f, err := os.OpenInRoot(*notepadDir+"/notes", r.PathValue("note")) - if err != nil { - notFound(w, r) - return - } - defer f.Close() - encryptionKeyMu.Lock() - key := encryptionKey - encryptionKeyMu.Unlock() - if key == nil { - executeLoginTemplate(w, "/note/"+r.PathValue("note")) - return - } - reader := bufio.NewReader(key.NewReader(f, cryptoutil.WithAdditionalData([]byte("notes/"+r.PathValue("note"))))) - nameLen, err := binary.ReadVarint(reader) - if err != nil { - http.Error(w, "invalid file", http.StatusInternalServerError) - return - } - fileName := make([]byte, nameLen) - if _, err := io.ReadFull(reader, fileName); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.QueryEscape(string(fileName))) - io.Copy(w, reader) + fmt.Fprintf(w, "event: ready\ndata:\n\n") } func main() { flag.Parse() - if err := loadSecrets(); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - http.HandleFunc("GET /pong", pong) - http.HandleFunc("POST /login", login) - http.HandleFunc("GET /notepad", notepad) - http.HandleFunc("POST /notepad/autosave", autosave) - http.HandleFunc("GET /upload", uploadPage) - http.HandleFunc("POST /upload", uploadNote) - http.HandleFunc("GET /notes/{note}", readNoteContent) - http.HandleFunc("POST /api/login", apiLogin) - http.HandleFunc("GET /api/list-notes", gobReqRespMiddleware(listNotes)) - http.HandleFunc("POST /api/create-note", gobReqRespMiddleware(createNote)) - http.HandleFunc("GET /api/read-note", gobRespStreamMiddleware(readNote)) + http.HandleFunc("GET /wormhole", wormhole) + http.HandleFunc("POST /wormhole/{hole}", wormholeSend) + http.HandleFunc("GET /wormhole/{hole}", wormholeRecv) + http.HandleFunc("GET /wormhole/{hole}/ready", wormholeReady) http.HandleFunc("GET /static/", static) http.HandleFunc("GET /favicon.ico", favicon) http.HandleFunc("GET /{$}", index) |
