diff options
| author | Rose Hogenson <rosehogenson@posteo.net> | 2025-12-29 18:56:30 -0800 |
|---|---|---|
| committer | Rose Hogenson <rosehogenson@posteo.net> | 2025-12-29 18:56:30 -0800 |
| commit | 54ea1b0f4a841e91daa3ac08d155a8f0109c920d (patch) | |
| tree | 8c5a005af0244ca1291dcf5f6a5c2b33d59a0b11 /roseh.moe.go | |
| parent | 2a9faab3de7648344dcc1973a71ada8125c0afa3 (diff) | |
| download | roseh.moe-54ea1b0f4a841e91daa3ac08d155a8f0109c920d.tar.zst | |
Make wormhole a little bit simpler
Diffstat (limited to 'roseh.moe.go')
| -rw-r--r-- | roseh.moe.go | 227 |
1 files changed, 144 insertions, 83 deletions
diff --git a/roseh.moe.go b/roseh.moe.go index 993282a..b1f659e 100644 --- a/roseh.moe.go +++ b/roseh.moe.go @@ -1,7 +1,7 @@ package main import ( - "context" + "crypto/hkdf" "crypto/hmac" "crypto/rand" "crypto/sha1" @@ -10,6 +10,7 @@ import ( "embed" "encoding/base64" "encoding/binary" + "errors" "flag" "fmt" "html/template" @@ -27,6 +28,7 @@ import ( "github.com/skip2/go-qrcode" "roseh.moe/pkg/ccl" + "roseh.moe/pkg/oae2" "roseh.moe/pkg/roseh.moe/internal/config" "roseh.moe/pkg/roseh.moe/internal/pwhash" "roseh.moe/pkg/wordlist" @@ -39,6 +41,7 @@ var ( secretsFile = flag.String("secrets", "secrets.ccl", "secrets file") configFile = flag.String("config", "config.ccl", "configuration file (ccl format https://pkg.go.dev/roseh.moe/pkg/ccl)") https = flag.String("https", "", "directory containing cert.pem and key.pem, or empty string to use unencrypted http") + holeTempDir = flag.String("hole-temp-dir", "hole", "directory to store temporary files for the wormhole") serverStartTime = time.Now() ) @@ -187,18 +190,32 @@ func favicon(w http.ResponseWriter, r *http.Request) { serveStaticFile(w, r, "static/favicon.ico") } +func qrHandler(w http.ResponseWriter, r *http.Request) { + sig, err := base64.RawURLEncoding.DecodeString(r.FormValue("s")) + if err != nil { + http.Error(w, fmt.Sprintf("Bad signature: %s", err), http.StatusBadRequest) + return + } + text := r.FormValue("t") + if !verify([]byte(text), sig, "qr") { + http.Error(w, "Bad signature", http.StatusBadRequest) + return + } + qr, err := qrcode.Encode(text, qrcode.Medium, 200) + if err != nil { + http.Error(w, fmt.Sprintf("failed to encode QR code: %s", err), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "image/png") + w.Write(qr) +} + var ( //go:embed templates/wormhole.html.template wormholeTemplateString string wormholeTemplate = template.Must(template.Must(outlineTemplate.Clone()).New("body").Parse(wormholeTemplateString)).Lookup("outline") ) -type wormholeTemplateArgs struct { - Self string - Hole string - Sig string -} - func makeHole() string { const nWords = 10 buf := make([]byte, 2*nWords) @@ -210,59 +227,72 @@ func makeHole() string { return strings.Join(words, "-") } -func newWormhole(w http.ResponseWriter, r *http.Request) { - hole := makeHole() - http.Redirect(w, r, "/wormhole/"+hole+"/upload?s="+base64.RawURLEncoding.EncodeToString(mac([]byte(hole), "hole")), http.StatusSeeOther) -} +const blockSize = 4 * 1024 * 1024 -func wormhole(w http.ResponseWriter, r *http.Request) { - if err := wormholeTemplate.Execute(w, wormholeTemplateArgs{Self: *selfURL, Hole: r.PathValue("hole"), Sig: r.FormValue("s")}); err != nil { - log.Printf("Warning: wormhole: %s", err) +func writeHoleFile(part *multipart.Part) (string, error) { + hole := makeHole() + key, err := hkdf.Key(sha256.New, []byte(hole), nil, "", 48) + if err != nil { + return "", err } + primaryKey, encryptionKey := key[:32], key[32:] + f, err := os.Create(fmt.Sprintf("%s/%x", *holeTempDir, primaryKey)) + if err != nil { + return "", err + } + defer f.Close() + w := oae2.NewWriter(f, encryptionKey, blockSize) + fileName := part.FileName() + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, uint64(len(fileName))) + if _, err := w.Write(buf); err != nil { + return "", err + } + if _, err := io.WriteString(w, fileName); err != nil { + return "", err + } + if _, err := io.Copy(w, part); err != nil { + return "", err + } + if err := w.Close(); err != nil { + return "", err + } + return hole, f.Close() } -type wormholeConn struct { - done chan struct{} - r *multipart.Part - w http.ResponseWriter -} - -var wormholeConnsMu sync.Mutex -var wormholeConns = make(map[string]wormholeConn) +var errNoHole = errors.New("no such hole") -func (c wormholeConn) wormholeCopy(ctx context.Context, hole string) error { - wormholeConnsMu.Lock() - prevConn, ok := wormholeConns[hole] - if !ok { - wormholeConns[hole] = c - wormholeConnsMu.Unlock() - defer func() { - wormholeConnsMu.Lock() - delete(wormholeConns, hole) - wormholeConnsMu.Unlock() - }() - select { - case <-ctx.Done(): - return ctx.Err() - case <-c.done: - return nil - } +func readHoleFile(w http.ResponseWriter, hole string) error { + key, err := hkdf.Key(sha256.New, []byte(hole), nil, "", 48) + if err != nil { + return err } - wormholeConnsMu.Unlock() - if c.w == nil { - c.w = prevConn.w - } else { - c.r = prevConn.r + primaryKey, encryptionKey := key[:32], key[32:] + f, err := os.Open(fmt.Sprintf("%s/%x", *holeTempDir, primaryKey)) + if err != nil { + return errNoHole } - if c.w == nil || c.r == nil { - return fmt.Errorf("connection already registered") + defer f.Close() + r := oae2.NewReader(f, encryptionKey, blockSize) + buf := make([]byte, 8) + if _, err := io.ReadFull(r, buf); err != nil { + return err } - defer close(prevConn.done) - c.w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+c.r.FileName()) - if _, err := io.Copy(c.w, c.r); err != nil { - return fmt.Errorf("copy: %s", err) + fileNameLen := binary.BigEndian.Uint64(buf) + buf = make([]byte, fileNameLen) + if _, err := io.ReadFull(r, buf); err != nil { + return err + } + fileName := string(buf) + w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+fileName) + _, err = io.Copy(w, r) + return err +} + +func wormhole(w http.ResponseWriter, r *http.Request) { + if err := wormholeTemplate.Execute(w, nil); err != nil { + log.Printf("Warning: wormhole: %s", err) } - return nil } var ( @@ -271,8 +301,12 @@ var ( wormholeSuccessTemplate = template.Must(template.Must(outlineTemplate.Clone()).New("body").Parse(wormholeSuccessTemplateString)).Lookup("outline") ) -func wormholeSend(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() +type wormholeSuccessTemplateArgs struct { + URL string + QR string +} + +func wormholeUpload(w http.ResponseWriter, r *http.Request) { reader, err := r.MultipartReader() if err != nil { http.Error(w, fmt.Sprintf("Not a multipart/form-data request: %s", err), http.StatusBadRequest) @@ -286,11 +320,17 @@ func wormholeSend(w http.ResponseWriter, r *http.Request) { if part.FormName() != "file" { continue } - hole := r.PathValue("hole") - if err := (wormholeConn{done: make(chan struct{}), r: part}).wormholeCopy(ctx, hole); err != nil { - http.Error(w, fmt.Sprintf("Error during copy: %s", err), http.StatusServiceUnavailable) + hole, err := writeHoleFile(part) + if err != nil { + log.Printf("Warning: failed to open hole file: %s", err) + http.Error(w, "Internal error", http.StatusInternalServerError) + return } - if err := wormholeSuccessTemplate.Execute(w, nil); err != nil { + downloadURL := *selfURL + "/wormhole/" + hole + if err := wormholeSuccessTemplate.Execute(w, wormholeSuccessTemplateArgs{ + URL: downloadURL, + QR: "/qr.png?t=" + url.QueryEscape(downloadURL) + "&s=" + base64.RawURLEncoding.EncodeToString(mac([]byte(downloadURL), "qr")), + }); err != nil { log.Printf("Warning: wormholeSend: %s", err) } return @@ -298,31 +338,51 @@ func wormholeSend(w http.ResponseWriter, r *http.Request) { http.Error(w, "file not found", http.StatusBadRequest) } -func wormholeRecv(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - hole := r.PathValue("hole") - if err := (wormholeConn{done: make(chan struct{}), w: w}).wormholeCopy(ctx, hole); err != nil { - http.Error(w, fmt.Sprintf("Error during copy: %s", err), http.StatusServiceUnavailable) +func wormholeDownload(w http.ResponseWriter, r *http.Request) { + if err := readHoleFile(w, r.PathValue("hole")); err != nil { + if err == errNoHole { + notFound(w, r) + return + } + log.Printf("Warning: read hole file: %s", err) + return } } -func wormholeQR(w http.ResponseWriter, r *http.Request) { - sig, err := base64.RawURLEncoding.DecodeString(r.FormValue("s")) - if err != nil { - http.Error(w, fmt.Sprintf("Bad signature: %s", err), http.StatusBadRequest) - return - } - if !verify([]byte(r.PathValue("hole")), sig, "hole") { - http.Error(w, "Bad signature", http.StatusBadRequest) - return - } - qr, err := qrcode.Encode(*selfURL+"/wormhole/"+r.PathValue("hole"), qrcode.Medium, 200) - if err != nil { - http.Error(w, fmt.Sprintf("failed to encode QR code: %s", err), http.StatusInternalServerError) - return +func cleanHole() { + for range time.NewTicker(time.Hour).C { + log.Printf("Cleaning hole...") + holeDir, err := os.Open(*holeTempDir) + if err != nil { + log.Printf("Warning: clean hole: %s", err) + continue + } + defer holeDir.Close() + deadline := time.Now().Add(-time.Hour) + for { + files, err := holeDir.ReadDir(1024) + for _, file := range files { + stat, err := file.Info() + if err != nil { + log.Printf("Warning: clean hole: %s", err) + continue + } + if !stat.ModTime().Before(deadline) { + continue + } + if err := os.Remove(*holeTempDir + "/" + file.Name()); err != nil { + log.Printf("Warning: clean hole: %s", err) + continue + } + } + if err != nil { + if err != io.EOF { + log.Printf("Warning: clean hole: %s", err) + } + break + } + } } - w.Header().Set("Content-Type", "image/png") - w.Write(qr) } const macSize = sha256.Size @@ -605,13 +665,14 @@ func main() { log.Fatal(err) } + go cleanHole() + mux := http.NewServeMux() mux.HandleFunc("GET /pong", pong) - mux.HandleFunc("GET /wormhole", newWormhole) - mux.HandleFunc("GET /wormhole/{hole}/upload", wormhole) - mux.HandleFunc("POST /wormhole/{hole}/upload", wormholeSend) - mux.HandleFunc("GET /wormhole/{hole}", wormholeRecv) - mux.HandleFunc("GET /wormhole/{hole}/qr.png", wormholeQR) + mux.HandleFunc("GET /qr.png", qrHandler) + mux.HandleFunc("GET /wormhole", wormhole) + mux.HandleFunc("POST /wormhole", wormholeUpload) + mux.HandleFunc("GET /wormhole/{hole}", wormholeDownload) mux.HandleFunc("GET /login", serveLogin) mux.HandleFunc("POST /login", login) mux.HandleFunc("GET /notepad", notepad) |
