summaryrefslogtreecommitdiffstats
path: root/internal/hole/hole.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/hole/hole.go')
-rw-r--r--internal/hole/hole.go278
1 files changed, 278 insertions, 0 deletions
diff --git a/internal/hole/hole.go b/internal/hole/hole.go
new file mode 100644
index 0000000..3749f9b
--- /dev/null
+++ b/internal/hole/hole.go
@@ -0,0 +1,278 @@
+// Copyright 2026 Rose Hogenson <r@roseh.moe>
+//
+// This file is part of roseh.moe.
+//
+// roseh.moe is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// roseh.moe is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty
+// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public
+// License along with roseh.moe. If not, see
+// <https://www.gnu.org/licenses/>.
+
+package hole
+
+import (
+ "archive/tar"
+ "cmp"
+ "compress/gzip"
+ "crypto/hkdf"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "log"
+ "mime/multipart"
+ "os"
+ "strings"
+ "time"
+
+ "roseh.moe/pkg/oae2"
+ "roseh.moe/pkg/wordlist"
+)
+
+var HoleTempDir string
+
+func makeHole() string {
+ const nWords = 10
+ buf := make([]byte, 2*nWords)
+ rand.Read(buf)
+ words := make([]string, nWords)
+ for i := range words {
+ words[i] = wordlist.Words[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff]
+ }
+ return strings.Join(words, "-")
+}
+
+const blockSize = 4 * 1024 * 1024
+
+type holeFileWriter struct {
+ *oae2.Writer
+ f *os.File
+}
+
+func (w *holeFileWriter) Close() error {
+ return cmp.Or(w.Writer.Close(), w.f.Close())
+}
+
+func writeHoleFile(filename string) (_ string, _ *holeFileWriter, err error) {
+ hole := makeHole()
+ primaryKey, err := hkdf.Key(sha256.New, []byte(hole), nil, "", 32)
+ if err != nil {
+ return "", nil, err
+ }
+ f, err := os.Create(fmt.Sprintf("%s/%x", HoleTempDir, primaryKey))
+ if err != nil {
+ return "", nil, err
+ }
+ defer func() {
+ if err != nil {
+ f.Close()
+ }
+ }()
+ w := oae2.NewWriter(f, []byte(hole), blockSize, nil)
+ buf := make([]byte, 8)
+ binary.BigEndian.PutUint64(buf, uint64(len(filename)))
+ if _, err := w.Write(buf); err != nil {
+ return "", nil, err
+ }
+ if _, err := io.WriteString(w, filename); err != nil {
+ return "", nil, err
+ }
+ return hole, &holeFileWriter{w, f}, nil
+}
+
+type offsetReader struct {
+ io.ReadSeeker
+ offset int
+}
+
+func (r *offsetReader) Seek(offset int64, whence int) (int64, error) {
+ if whence == io.SeekStart {
+ offset += int64(r.offset)
+ }
+ n, err := r.ReadSeeker.Seek(offset, whence)
+ return n - int64(r.offset), err
+}
+
+type HoleFileReader struct {
+ *offsetReader
+ F *os.File
+}
+
+func (r *HoleFileReader) Close() error {
+ return r.F.Close()
+}
+
+var ErrNoHole = errors.New("no such hole")
+
+func Open(hole string) (_ string, _ *HoleFileReader, err error) {
+ primaryKey, err := hkdf.Key(sha256.New, []byte(hole), nil, "", 32)
+ if err != nil {
+ return "", nil, err
+ }
+ f, err := os.Open(fmt.Sprintf("%s/%x", HoleTempDir, primaryKey))
+ if err != nil {
+ if errors.Is(err, fs.ErrNotExist) {
+ return "", nil, ErrNoHole
+ }
+ return "", nil, err
+ }
+ defer func() {
+ if err != nil {
+ f.Close()
+ }
+ }()
+ r := oae2.NewReader(f, []byte(hole), blockSize, nil)
+ buf := make([]byte, 8)
+ if _, err := io.ReadFull(r, buf); err != nil {
+ return "", nil, err
+ }
+ fileNameLen := binary.BigEndian.Uint64(buf)
+ fileName := make([]byte, fileNameLen)
+ if _, err := io.ReadFull(r, fileName); err != nil {
+ return "", nil, err
+ }
+ return string(fileName), &HoleFileReader{&offsetReader{r, 8 + int(fileNameLen)}, f}, nil
+}
+
+func writeHoleFileToTarFile(tarWriter *tar.Writer, hole string, now time.Time) error {
+ fileName, f, err := Open(hole)
+ if err != nil {
+ return fmt.Errorf("read back hole file: %s", err)
+ }
+ defer f.Close()
+ size, err := f.Seek(0, io.SeekEnd)
+ if err != nil {
+ return fmt.Errorf("read back hole file: %s", err)
+ }
+ if _, err := f.Seek(0, io.SeekStart); err != nil {
+ return fmt.Errorf("read back hole file: %s", err)
+ }
+ if err := tarWriter.WriteHeader(&tar.Header{Name: "wormhole-files/" + fileName, Mode: 0644, Size: size, ModTime: now, Format: tar.FormatPAX}); err != nil {
+ return err
+ }
+ if _, err := io.Copy(tarWriter, f); err != nil {
+ return fmt.Errorf("write file to tar file: %s", err)
+ }
+ return nil
+}
+
+var ErrNoFile = errors.New("no file")
+
+func Upload(reader *multipart.Reader) (string, error) {
+ var holes []string
+ for {
+ part, err := reader.NextPart()
+ if err != nil {
+ break
+ }
+ if part.FormName() != "file" || part.FileName() == "" {
+ continue
+ }
+ hole, err := WriteSingleFile(part.FileName(), part)
+ if err != nil {
+ return "", err
+ }
+ holes = append(holes, hole)
+ }
+ if len(holes) == 0 {
+ return "", ErrNoFile
+ }
+ if len(holes) == 1 {
+ return holes[0], nil
+ }
+ tarFileHole, tarHoleWriter, err := writeHoleFile("wormhole-files.tar.gz")
+ if err != nil {
+ return "", fmt.Errorf("make tar file: %s", err)
+ }
+ defer tarHoleWriter.Close()
+ gzipWriter, err := gzip.NewWriterLevel(tarHoleWriter, gzip.BestSpeed)
+ if err != nil {
+ return "", err
+ }
+ tarWriter := tar.NewWriter(gzipWriter)
+ now := time.Now()
+ if err := tarWriter.WriteHeader(&tar.Header{Name: "wormhole-files/", Mode: 0755, ModTime: now, Format: tar.FormatPAX}); err != nil {
+ return "", fmt.Errorf("create tar file main directory: %s", err)
+ }
+ for _, hole := range holes {
+ if err := writeHoleFileToTarFile(tarWriter, hole, now); err != nil {
+ return "", err
+ }
+ }
+ if err := tarWriter.Close(); err != nil {
+ return "", fmt.Errorf("write tar file: %s", err)
+ }
+ if err := gzipWriter.Close(); err != nil {
+ return "", fmt.Errorf("gzip tar file: %s", err)
+ }
+ if err := tarHoleWriter.Close(); err != nil {
+ return "", fmt.Errorf("write tar hole file: %s", err)
+ }
+ return tarFileHole, nil
+}
+
+func WriteSingleFile(filename string, r io.Reader) (string, error) {
+ hole, holeWriter, err := writeHoleFile(filename)
+ if err != nil {
+ return "", fmt.Errorf("write hole file: %s", err)
+ }
+ defer holeWriter.Close()
+ if _, err := io.Copy(holeWriter, r); err != nil {
+ return "", fmt.Errorf("write hole file: %s", err)
+ }
+ if err := holeWriter.Close(); err != nil {
+ return "", fmt.Errorf("write hole file: %s", err)
+ }
+ return hole, nil
+}
+
+func Clean() error {
+ log.Printf("Cleaning hole...")
+ holeDir, err := os.Open(HoleTempDir)
+ if err != nil {
+ return err
+ }
+ 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("Error: clean hole: %s", err)
+ continue
+ }
+ if !stat.ModTime().Before(deadline) {
+ continue
+ }
+ if err := os.Remove(HoleTempDir + "/" + file.Name()); err != nil {
+ log.Printf("Error: clean hole: %s", err)
+ continue
+ }
+ }
+ if err != nil {
+ if err != io.EOF {
+ return err
+ }
+ return nil
+ }
+ }
+}
+
+func Init() error {
+ if err := os.MkdirAll(HoleTempDir, 0700); err != nil {
+ return err
+ }
+ return nil
+}