summaryrefslogtreecommitdiffstats
path: root/roseh.moe.go
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2025-10-05 18:00:06 -0700
committerRose Hogenson <rosehogenson@posteo.net>2025-10-05 18:00:06 -0700
commitd89b325294201d95498ae9d08f102e12ac2f2876 (patch)
treeffe65e33d0d01e811a5323d5e40c47564ccb8f2d /roseh.moe.go
parent624985afaaa8b94e4d9685bcdc935d467f9e7060 (diff)
downloadroseh.moe-d89b325294201d95498ae9d08f102e12ac2f2876.tar.zst
Implement a "magic wormhole" file-sharing solution
Diffstat (limited to 'roseh.moe.go')
-rw-r--r--roseh.moe.go752
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)