]> Git repositories of Izuru Yakumo - marisa.git/commitdiff
Mirrored from marisa.git master
authorwww <www@53897a95-702d-234f-8f73-912f772065ac>
Sun, 29 Sep 2024 21:29:49 +0000 (21:29 +0000)
committerwww <www@53897a95-702d-234f-8f73-912f772065ac>
Sun, 29 Sep 2024 21:29:49 +0000 (21:29 +0000)
git-svn-id: https://svn.chaotic.ninja/svn/marisa-yakumo.izuru@1 53897a95-702d-234f-8f73-912f772065ac

116 files changed:
branches/master/.gitignore [new file with mode: 0644]
branches/master/COPYING [new file with mode: 0644]
branches/master/LICENSE [new file with mode: 0644]
branches/master/Makefile [new file with mode: 0644]
branches/master/README.md [new file with mode: 0644]
branches/master/cmd/marisa-trash/main.go [new file with mode: 0644]
branches/master/cmd/marisa/main.go [new file with mode: 0644]
branches/master/cmd/marisa/parseconfig.go [new file with mode: 0644]
branches/master/cmd/marisa/servetemplate.go [new file with mode: 0644]
branches/master/cmd/marisa/uploader.go [new file with mode: 0644]
branches/master/cmd/marisa/uploaderdelete.go [new file with mode: 0644]
branches/master/cmd/marisa/uploaderget.go [new file with mode: 0644]
branches/master/cmd/marisa/uploaderpost.go [new file with mode: 0644]
branches/master/cmd/marisa/uploaderput.go [new file with mode: 0644]
branches/master/cmd/marisa/usergroupids.go [new file with mode: 0644]
branches/master/cmd/marisa/writefile.go [new file with mode: 0644]
branches/master/cmd/marisa/writemeta.go [new file with mode: 0644]
branches/master/example/marisa.conf [new file with mode: 0644]
branches/master/example/static/favicon.ico [new file with mode: 0644]
branches/master/example/static/marisa.css [new file with mode: 0644]
branches/master/example/static/marisa.png [new file with mode: 0644]
branches/master/example/static/robots.txt [new file with mode: 0644]
branches/master/example/templates/index.html [new file with mode: 0644]
branches/master/go.mod [new file with mode: 0644]
branches/master/go.sum [new file with mode: 0644]
branches/master/marisa-trash.1 [new file with mode: 0644]
branches/master/marisa.1 [new file with mode: 0644]
branches/master/marisa.conf.5 [new file with mode: 0644]
branches/master/version.go [new file with mode: 0644]
branches/origin-master/.gitignore [new file with mode: 0644]
branches/origin-master/COPYING [new file with mode: 0644]
branches/origin-master/LICENSE [new file with mode: 0644]
branches/origin-master/Makefile [new file with mode: 0644]
branches/origin-master/README.md [new file with mode: 0644]
branches/origin-master/cmd/marisa-trash/main.go [new file with mode: 0644]
branches/origin-master/cmd/marisa/main.go [new file with mode: 0644]
branches/origin-master/cmd/marisa/parseconfig.go [new file with mode: 0644]
branches/origin-master/cmd/marisa/servetemplate.go [new file with mode: 0644]
branches/origin-master/cmd/marisa/uploader.go [new file with mode: 0644]
branches/origin-master/cmd/marisa/uploaderdelete.go [new file with mode: 0644]
branches/origin-master/cmd/marisa/uploaderget.go [new file with mode: 0644]
branches/origin-master/cmd/marisa/uploaderpost.go [new file with mode: 0644]
branches/origin-master/cmd/marisa/uploaderput.go [new file with mode: 0644]
branches/origin-master/cmd/marisa/usergroupids.go [new file with mode: 0644]
branches/origin-master/cmd/marisa/writefile.go [new file with mode: 0644]
branches/origin-master/cmd/marisa/writemeta.go [new file with mode: 0644]
branches/origin-master/example/marisa.conf [new file with mode: 0644]
branches/origin-master/example/static/favicon.ico [new file with mode: 0644]
branches/origin-master/example/static/marisa.css [new file with mode: 0644]
branches/origin-master/example/static/marisa.png [new file with mode: 0644]
branches/origin-master/example/static/robots.txt [new file with mode: 0644]
branches/origin-master/example/templates/index.html [new file with mode: 0644]
branches/origin-master/go.mod [new file with mode: 0644]
branches/origin-master/go.sum [new file with mode: 0644]
branches/origin-master/marisa-trash.1 [new file with mode: 0644]
branches/origin-master/marisa.1 [new file with mode: 0644]
branches/origin-master/marisa.conf.5 [new file with mode: 0644]
branches/origin-master/version.go [new file with mode: 0644]
branches/origin/.gitignore [new file with mode: 0644]
branches/origin/COPYING [new file with mode: 0644]
branches/origin/LICENSE [new file with mode: 0644]
branches/origin/Makefile [new file with mode: 0644]
branches/origin/README.md [new file with mode: 0644]
branches/origin/cmd/marisa-trash/main.go [new file with mode: 0644]
branches/origin/cmd/marisa/main.go [new file with mode: 0644]
branches/origin/cmd/marisa/parseconfig.go [new file with mode: 0644]
branches/origin/cmd/marisa/servetemplate.go [new file with mode: 0644]
branches/origin/cmd/marisa/uploader.go [new file with mode: 0644]
branches/origin/cmd/marisa/uploaderdelete.go [new file with mode: 0644]
branches/origin/cmd/marisa/uploaderget.go [new file with mode: 0644]
branches/origin/cmd/marisa/uploaderpost.go [new file with mode: 0644]
branches/origin/cmd/marisa/uploaderput.go [new file with mode: 0644]
branches/origin/cmd/marisa/usergroupids.go [new file with mode: 0644]
branches/origin/cmd/marisa/writefile.go [new file with mode: 0644]
branches/origin/cmd/marisa/writemeta.go [new file with mode: 0644]
branches/origin/example/marisa.conf [new file with mode: 0644]
branches/origin/example/static/favicon.ico [new file with mode: 0644]
branches/origin/example/static/marisa.css [new file with mode: 0644]
branches/origin/example/static/marisa.png [new file with mode: 0644]
branches/origin/example/static/robots.txt [new file with mode: 0644]
branches/origin/example/templates/index.html [new file with mode: 0644]
branches/origin/go.mod [new file with mode: 0644]
branches/origin/go.sum [new file with mode: 0644]
branches/origin/marisa-trash.1 [new file with mode: 0644]
branches/origin/marisa.1 [new file with mode: 0644]
branches/origin/marisa.conf.5 [new file with mode: 0644]
branches/origin/version.go [new file with mode: 0644]
trunk/.gitignore [new file with mode: 0644]
trunk/COPYING [new file with mode: 0644]
trunk/LICENSE [new file with mode: 0644]
trunk/Makefile [new file with mode: 0644]
trunk/README.md [new file with mode: 0644]
trunk/cmd/marisa-trash/main.go [new file with mode: 0644]
trunk/cmd/marisa/main.go [new file with mode: 0644]
trunk/cmd/marisa/parseconfig.go [new file with mode: 0644]
trunk/cmd/marisa/servetemplate.go [new file with mode: 0644]
trunk/cmd/marisa/uploader.go [new file with mode: 0644]
trunk/cmd/marisa/uploaderdelete.go [new file with mode: 0644]
trunk/cmd/marisa/uploaderget.go [new file with mode: 0644]
trunk/cmd/marisa/uploaderpost.go [new file with mode: 0644]
trunk/cmd/marisa/uploaderput.go [new file with mode: 0644]
trunk/cmd/marisa/usergroupids.go [new file with mode: 0644]
trunk/cmd/marisa/writefile.go [new file with mode: 0644]
trunk/cmd/marisa/writemeta.go [new file with mode: 0644]
trunk/example/marisa.conf [new file with mode: 0644]
trunk/example/static/favicon.ico [new file with mode: 0644]
trunk/example/static/marisa.css [new file with mode: 0644]
trunk/example/static/marisa.png [new file with mode: 0644]
trunk/example/static/robots.txt [new file with mode: 0644]
trunk/example/templates/index.html [new file with mode: 0644]
trunk/go.mod [new file with mode: 0644]
trunk/go.sum [new file with mode: 0644]
trunk/marisa-trash.1 [new file with mode: 0644]
trunk/marisa.1 [new file with mode: 0644]
trunk/marisa.conf.5 [new file with mode: 0644]
trunk/version.go [new file with mode: 0644]

diff --git a/branches/master/.gitignore b/branches/master/.gitignore
new file mode 100644 (file)
index 0000000..97a6b1f
--- /dev/null
@@ -0,0 +1,2 @@
+/marisa
+/marisa-trash
diff --git a/branches/master/COPYING b/branches/master/COPYING
new file mode 100644 (file)
index 0000000..196d9bc
--- /dev/null
@@ -0,0 +1,14 @@
+Copyright (c) 2021 Willy Goiffon <contact@z3bra.org>
+Copyright (c) 2023-present Izuru Yakumo <postmaster@chaotic.ninja>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/branches/master/LICENSE b/branches/master/LICENSE
new file mode 100644 (file)
index 0000000..196d9bc
--- /dev/null
@@ -0,0 +1,14 @@
+Copyright (c) 2021 Willy Goiffon <contact@z3bra.org>
+Copyright (c) 2023-present Izuru Yakumo <postmaster@chaotic.ninja>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/branches/master/Makefile b/branches/master/Makefile
new file mode 100644 (file)
index 0000000..aac2610
--- /dev/null
@@ -0,0 +1,25 @@
+GO ?= go
+GOFLAGS ?= -v -ldflags "-w -X `go list`.Version=${VERSION} -X `go list`.Commit=${COMMIT} -X `go list`.Build=${BUILD}"
+CGO ?= 0
+
+VERSION = `git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION"`
+COMMIT = `git rev-parse --short HEAD || echo "$COMMIT"`
+BRANCH = `git rev-parse --abbrev-ref HEAD`
+BUILD = `git show -s --pretty=format:%cI`
+
+PREFIX ?= /usr/local
+
+all: marisa marisa-trash
+
+marisa:
+       CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa
+marisa-trash:
+       CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa-trash
+clean:
+       rm -f marisa marisa-trash
+install:
+       install -Dm0755 marisa ${PREFIX}/bin/marisa
+       install -Dm0755 marisa-trash ${PREFIX}/bin/marisa-trash
+       install -Dm0644 marisa.1 ${PREFIX}/share/man/man1/marisa.1
+       install -Dm0644 marisa.conf.5 ${PREFIX}/share/man/man5/marisa.conf.5
+.PHONY: marisa marisa-trash
diff --git a/branches/master/README.md b/branches/master/README.md
new file mode 100644 (file)
index 0000000..e487c7d
--- /dev/null
@@ -0,0 +1,36 @@
+marisa
+======
+HTTP based File upload system.
+
+Features
+--------
++ Link expiration
++ Mimetype support
++ Random filenames
++ Multiple file uploads
++ Javascript not needed
++ Privilege drop
++ chroot(2) support
++ FastCGI support
+
+Usage
+-----
+Refer to the marisa(1) manual page for details and examples.
+
+       marisa [-v] [-f marisa.conf]
+
+Configuration is done through its configuration file, marisa.conf(5).
+The format is that of the INI file format.
+
+Uploading files is done via PUT and POST requests. Multiple files can
+be sent via POST requests.
+
+       curl -T file.png http://domain.tld
+       curl -F file=file.png -F expiry=3600 http://domain.tld
+
+Installation
+------------
+Edit the `config.mk` file to match your setup, then run the following:
+
+        $ (b)make
+        # (b)make install
diff --git a/branches/master/cmd/marisa-trash/main.go b/branches/master/cmd/marisa-trash/main.go
new file mode 100644 (file)
index 0000000..e010dd0
--- /dev/null
@@ -0,0 +1,101 @@
+package main
+
+import (
+       "log"
+       "flag"
+       "os"
+       "time"
+       "path/filepath"
+       "encoding/json"
+       
+       "github.com/dustin/go-humanize"
+)
+
+type metadata struct {
+       Filename string
+       Size int64
+       Expiry int64
+}
+
+var conf struct {
+       filepath string
+       metapath string
+}
+
+var verbose bool
+var count int64
+var deleted int64
+var size int64
+
+func readmeta(filename string) (metadata, error) {
+       j, err := os.ReadFile(filename)
+       if err != nil {
+               return metadata{}, err
+       }
+
+       var meta metadata
+       err = json.Unmarshal(j, &meta)
+       if err != nil {
+               return metadata{}, err
+       }
+
+       return meta, nil
+}
+
+func checkexpiry(path string, info os.FileInfo, err error) error {
+       if filepath.Ext(path) != ".json" {
+               return nil
+       }
+       meta, err := readmeta(path)
+       if err != nil {
+               log.Fatal(err)
+       }
+
+
+       count++
+
+       now := time.Now().Unix()
+       if verbose {
+               log.Printf("now: %s, expiry: %s\n", now, meta.Expiry);
+       }
+
+       if meta.Expiry > 0 && now >= meta.Expiry {
+               if verbose {
+                       expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
+                       log.Printf("%s/%s: expired %s\n", conf.filepath, meta.Filename, expiration)
+               }
+               if err = os.Remove(conf.filepath + "/" + meta.Filename); err != nil {
+                       log.Fatal(err)
+               }
+               if err = os.Remove(path); err != nil {
+                       log.Fatal(err)
+               }
+               deleted++
+               return nil
+       } else {
+               if verbose {
+                       expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
+                       log.Printf("%s/%s: expire in %s\n", conf.filepath, meta.Filename, expiration)
+               }
+               size += meta.Size
+       }
+
+       return nil
+}
+
+func main() {
+       flag.BoolVar(&verbose,         "v", false, "Verbose logging")
+       flag.StringVar(&conf.filepath, "f", "./files", "Directory containing files")
+       flag.StringVar(&conf.metapath, "m", "./meta", "Directory containing metadata")
+
+       flag.Parse()
+
+       err := filepath.Walk(conf.metapath, checkexpiry)
+       if err != nil {
+               log.Fatal(err)
+       }
+
+       if verbose && count > 0 {
+               log.Printf("%d/%d file(s) deleted (remaining: %s)", deleted, count, humanize.IBytes(uint64(size)))
+       }
+}
diff --git a/branches/master/cmd/marisa/main.go b/branches/master/cmd/marisa/main.go
new file mode 100644 (file)
index 0000000..0f74d7e
--- /dev/null
@@ -0,0 +1,141 @@
+package main
+
+import (
+       "flag"
+       "log"
+       "net"
+       "net/http"
+       "net/http/fcgi"
+       "os"
+       "os/signal"
+       "syscall"
+
+       "marisa.chaotic.ninja/marisa"
+)
+
+type templatedata struct {
+       Links   []string
+       Size    string
+       Maxsize string
+}
+
+type metadata struct {
+       Filename string
+       Size     int64
+       Expiry   int64
+}
+
+var conf struct {
+       user     string
+       group    string
+       chroot   string
+       listen   string
+       baseuri  string
+       rootdir  string
+       tmplpath string
+       filepath string
+       metapath string
+       filectx  string
+       maxsize  int64
+       expiry   int64
+}
+
+var verbose bool
+
+func main() {
+       var err error
+       var configfile string
+       var listener net.Listener
+
+       /* default values */
+       conf.listen = "127.0.0.1:8080"
+       conf.baseuri = "http://127.0.0.1:8080"
+       conf.rootdir = "static"
+       conf.tmplpath = "templates"
+       conf.filepath = "files"
+       conf.metapath = "meta"
+       conf.filectx = "/f/"
+       conf.maxsize = 34359738368
+       conf.expiry = 86400
+
+       flag.StringVar(&configfile, "f", "", "Configuration file")
+       flag.BoolVar(&verbose, "v", false, "Verbose logging")
+       flag.Parse()
+
+       if configfile != "" {
+               if verbose {
+                       log.Printf("Reading configuration %s", configfile)
+               }
+               parseconfig(configfile)
+       }
+
+       if conf.chroot != "" {
+               if verbose {
+                       log.Printf("Changing root to %s", conf.chroot)
+               }
+               syscall.Chroot(conf.chroot)
+       }
+
+       if conf.listen[0] == '/' {
+               /* Remove any stale socket */
+               os.Remove(conf.listen)
+               if listener, err = net.Listen("unix", conf.listen); err != nil {
+                       log.Fatal(err)
+               }
+               defer listener.Close()
+
+               /*
+                * Ensure unix socket is removed on exit.
+                * Note: this might not work when dropping privileges…
+                */
+               defer os.Remove(conf.listen)
+               sigs := make(chan os.Signal, 1)
+               signal.Notify(sigs, os.Interrupt, os.Kill, syscall.SIGTERM)
+               go func() {
+                       _ = <-sigs
+                       listener.Close()
+                       if err = os.Remove(conf.listen); err != nil {
+                               log.Fatal(err)
+                       }
+                       os.Exit(0)
+               }()
+       } else {
+               if listener, err = net.Listen("tcp", conf.listen); err != nil {
+                       log.Fatal(err)
+               }
+               defer listener.Close()
+       }
+
+       if conf.user != "" {
+               if verbose {
+                       log.Printf("Dropping privileges to %s", conf.user)
+               }
+               uid, gid, err := usergroupids(conf.user, conf.group)
+               if err != nil {
+                       log.Fatal(err)
+               }
+
+               if listener.Addr().Network() == "unix" {
+                       os.Chown(conf.listen, uid, gid)
+               }
+
+               syscall.Setuid(uid)
+               syscall.Setgid(gid)
+       }
+
+       http.HandleFunc("/", uploader)
+       http.Handle(conf.filectx, http.StripPrefix(conf.filectx, http.FileServer(http.Dir(conf.filepath))))
+
+       if verbose {
+               log.Printf("Starting marisa %v\n", marisa.FullVersion())
+               log.Printf("Listening on %s", conf.listen)
+       }
+
+       if listener.Addr().Network() == "unix" {
+               err = fcgi.Serve(listener, nil)
+               log.Fatal(err) /* NOTREACHED */
+       }
+
+       err = http.Serve(listener, nil)
+       log.Fatal(err) /* NOTREACHED */
+}
diff --git a/branches/master/cmd/marisa/parseconfig.go b/branches/master/cmd/marisa/parseconfig.go
new file mode 100644 (file)
index 0000000..d08cd83
--- /dev/null
@@ -0,0 +1,27 @@
+package main
+
+import (
+       "gopkg.in/ini.v1"
+)
+
+func parseconfig(file string) error {
+        cfg, err := ini.Load(file)
+        if err != nil {
+                return err
+        }
+
+        conf.listen = cfg.Section("marisa").Key("listen").String()
+        conf.user = cfg.Section("marisa").Key("user").String()
+        conf.group = cfg.Section("marisa").Key("group").String()
+        conf.baseuri = cfg.Section("www").Key("baseuri").String()
+        conf.filepath = cfg.Section("www").Key("filepath").String()
+        conf.metapath = cfg.Section("www").Key("metapath").String()
+        conf.filectx = cfg.Section("www").Key("filectx").String()
+        conf.rootdir = cfg.Section("www").Key("rootdir").String()
+        conf.chroot = cfg.Section("marisa").Key("chroot").String()
+        conf.tmplpath = cfg.Section("www").Key("tmplpath").String()
+        conf.maxsize, _ = cfg.Section("www").Key("maxsize").Int64()
+        conf.expiry, _ = cfg.Section("www").Key("expiry").Int64()
+
+        return nil
+}
diff --git a/branches/master/cmd/marisa/servetemplate.go b/branches/master/cmd/marisa/servetemplate.go
new file mode 100644 (file)
index 0000000..f092f76
--- /dev/null
@@ -0,0 +1,25 @@
+package main
+
+import (
+       "fmt"
+       "html/template"
+       "log"
+       "net/http"
+)
+
+func servetemplate(w http.ResponseWriter, f string, d templatedata) {
+        t, err := template.ParseFiles(conf.tmplpath + "/" + f)
+        if err != nil {
+                http.Error(w, "Internal error", http.StatusInternalServerError)
+                return
+        }
+
+        if verbose {
+                log.Printf("Serving template %s", t.Name())
+        }
+
+        err = t.Execute(w, d)
+        if err != nil {
+                fmt.Println(err)
+        }
+}
diff --git a/branches/master/cmd/marisa/uploader.go b/branches/master/cmd/marisa/uploader.go
new file mode 100644 (file)
index 0000000..f071449
--- /dev/null
@@ -0,0 +1,23 @@
+package main
+
+import (
+       "log"
+       "net/http"
+)
+
+func uploader(w http.ResponseWriter, r *http.Request) {
+        if verbose {
+                log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
+        }
+
+        switch r.Method {
+        case "DELETE":
+                uploaderDelete(w, r)
+        case "POST":
+                uploaderPost(w, r)
+        case "PUT":
+                uploaderPut(w, r)
+        case "GET":
+                uploaderGet(w, r)
+        }
+}
diff --git a/branches/master/cmd/marisa/uploaderdelete.go b/branches/master/cmd/marisa/uploaderdelete.go
new file mode 100644 (file)
index 0000000..1369e6f
--- /dev/null
@@ -0,0 +1,28 @@
+package main
+
+import (
+       "log"
+       "net/http"
+       "os"
+)
+
+func uploaderDelete(w http.ResponseWriter, r *http.Request) {
+        // r.URL.Path is sanitized regarding "." and ".."
+        filename := r.URL.Path
+        filepath := conf.filepath + filename
+
+        if verbose {
+                log.Printf("Deleting file %s", filepath)
+        }
+
+        f, err := os.Open(filepath)
+        if err != nil {
+                http.NotFound(w, r)
+                return
+        }
+        f.Close()
+
+        // Force file expiration
+        writemeta(filepath, 0)
+        w.WriteHeader(http.StatusNoContent)
+}
diff --git a/branches/master/cmd/marisa/uploaderget.go b/branches/master/cmd/marisa/uploaderget.go
new file mode 100644 (file)
index 0000000..4b5defa
--- /dev/null
@@ -0,0 +1,24 @@
+package main
+
+import (
+       "log"
+       "net/http"
+
+       "github.com/dustin/go-humanize"
+)
+
+func uploaderGet(w http.ResponseWriter, r *http.Request) {
+        // r.URL.Path is sanitized regarding "." and ".."
+        filename := r.URL.Path
+        if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+                data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
+                servetemplate(w, "/index.html", data)
+                return
+        }
+
+        if verbose {
+                log.Printf("Serving file %s", conf.rootdir+filename)
+        }
+
+        http.ServeFile(w, r, conf.rootdir+filename)
+}
diff --git a/branches/master/cmd/marisa/uploaderpost.go b/branches/master/cmd/marisa/uploaderpost.go
new file mode 100644 (file)
index 0000000..9ecb6ae
--- /dev/null
@@ -0,0 +1,71 @@
+package main
+
+import (
+       "encoding/json"
+       "io/ioutil"
+       "net/http"
+       "os"
+       "path"
+       "path/filepath"
+       "strconv"
+
+       "github.com/dustin/go-humanize"
+)
+
+func uploaderPost(w http.ResponseWriter, r *http.Request) {
+        /* read 32Mb at a time */
+        r.ParseMultipartForm(32 << 20)
+
+        links := []string{}
+        for _, h := range r.MultipartForm.File["file"] {
+                if h.Size > conf.maxsize {
+                        http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
+                        return
+                }
+
+                post, err := h.Open()
+                if err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        return
+                }
+                defer post.Close()
+
+                tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename))
+                f, err := os.Create(tmp.Name())
+                if err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        return
+                }
+                defer f.Close()
+
+                if err = writefile(f, post, h.Size); err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        defer os.Remove(tmp.Name())
+                        return
+                }
+                expiry, err := strconv.Atoi(r.PostFormValue("expiry"))
+                if err != nil || expiry < 0 {
+                        expiry = int(conf.expiry)
+                }
+                writemeta(tmp.Name(), int64(expiry))
+
+                link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
+                links = append(links, link)
+        }
+
+        switch r.PostFormValue("output") {
+        case "html":
+                data := templatedata{
+                        Maxsize: humanize.IBytes(uint64(conf.maxsize)),
+                        Links:   links,
+                }
+                servetemplate(w, "/index.html", data)
+        case "json":
+                data, _ := json.Marshal(links)
+                w.Write(data)
+        default:
+                for _, link := range links {
+                        w.Write([]byte(link + "\r\n"))
+                }
+        }
+}
diff --git a/branches/master/cmd/marisa/uploaderput.go b/branches/master/cmd/marisa/uploaderput.go
new file mode 100644 (file)
index 0000000..51723e5
--- /dev/null
@@ -0,0 +1,40 @@
+package main
+
+import (
+       "fmt"
+       "io/ioutil"
+       "log"
+       "net/http"
+       "os"
+       "path"
+       "path/filepath"
+)
+
+func uploaderPut(w http.ResponseWriter, r *http.Request) {
+        /* limit upload size */
+        if r.ContentLength > conf.maxsize {
+                http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
+        }
+
+        tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path))
+        f, err := os.Create(tmp.Name())
+        if err != nil {
+                fmt.Println(err)
+                return
+        }
+        defer f.Close()
+
+        if verbose {
+                log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name())
+        }
+
+        if err = writefile(f, r.Body, r.ContentLength); err != nil {
+                http.Error(w, "Internal error", http.StatusInternalServerError)
+                defer os.Remove(tmp.Name())
+                return
+        }
+        writemeta(tmp.Name(), conf.expiry)
+
+        resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
+        w.Write([]byte(resp + "\r\n"))
+}
diff --git a/branches/master/cmd/marisa/usergroupids.go b/branches/master/cmd/marisa/usergroupids.go
new file mode 100644 (file)
index 0000000..758581c
--- /dev/null
@@ -0,0 +1,26 @@
+package main
+
+import (
+       "os/user"
+       "strconv"
+)
+
+func usergroupids(username string, groupname string) (int, int, error) {
+        u, err := user.Lookup(username)
+        if err != nil {
+                return -1, -1, err
+        }
+
+        uid, _ := strconv.Atoi(u.Uid)
+        gid, _ := strconv.Atoi(u.Gid)
+
+        if conf.group != "" {
+                g, err := user.LookupGroup(groupname)
+                if err != nil {
+                        return uid, -1, err
+                }
+                gid, _ = strconv.Atoi(g.Gid)
+        }
+
+        return uid, gid, nil
+}
diff --git a/branches/master/cmd/marisa/writefile.go b/branches/master/cmd/marisa/writefile.go
new file mode 100644 (file)
index 0000000..d5367ec
--- /dev/null
@@ -0,0 +1,38 @@
+package main
+
+import (
+       "io"
+       "os"
+)
+
+func writefile(f *os.File, s io.ReadCloser, contentlength int64) error {
+        buffer := make([]byte, 4096)
+        eof := false
+        sz := int64(0)
+
+        defer f.Sync()
+
+        for !eof {
+                n, err := s.Read(buffer)
+                if err != nil && err != io.EOF {
+                        return err
+                } else if err == io.EOF {
+                        eof = true
+                }
+
+                /* ensure we don't write more than expected */
+                r := int64(n)
+                if sz+r > contentlength {
+                        r = contentlength - sz
+                        eof = true
+                }
+
+                _, err = f.Write(buffer[:r])
+                if err != nil {
+                        return err
+                }
+                sz += r
+        }
+
+        return nil
+}
diff --git a/branches/master/cmd/marisa/writemeta.go b/branches/master/cmd/marisa/writemeta.go
new file mode 100644 (file)
index 0000000..c63d9c8
--- /dev/null
@@ -0,0 +1,46 @@
+package main
+
+import (
+       "encoding/json"
+       "log"
+       "os"
+       "path/filepath"
+       "time"
+)
+
+func writemeta(filename string, expiry int64) error {
+
+        f, _ := os.Open(filename)
+        stat, _ := f.Stat()
+        size := stat.Size()
+        f.Close()
+
+        if expiry < 0 {
+                expiry = conf.expiry
+        }
+
+        meta := metadata{
+                Filename: filepath.Base(filename),
+                Size:     size,
+                Expiry:   time.Now().Unix() + expiry,
+        }
+
+        if verbose {
+                log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json")
+        }
+
+        f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json")
+        if err != nil {
+                return err
+        }
+        defer f.Close()
+
+        j, err := json.Marshal(meta)
+        if err != nil {
+                return err
+        }
+
+       _, err = f.Write(j)
+
+       return err
+}
diff --git a/branches/master/example/marisa.conf b/branches/master/example/marisa.conf
new file mode 100644 (file)
index 0000000..334cf7d
--- /dev/null
@@ -0,0 +1,35 @@
+[marisa]
+# TCP or Unix socket to listen on.
+# When the Unix socket is used, the content will be served through FastCGI
+# listen = /var/run/marisa.sock
+# listen = 127.0.0.1:9000
+
+# Drop privilege to the user and group specified.
+# When only the user is specified, the default group of the user
+# will be used.
+# user = www
+# group = www
+
+# Change the root directory to the following directory.
+# When a chroot(2) is set, all paths must be given according to it.
+# Note: the configuration file is read before it happens
+# chroot =
+[www]
+# baseuri = http://127.0.0.1:9000
+
+# Path to the resources used by the server, must take into account
+# the chroot is set
+# rootdir = ./static
+# tmplpath = ./templates
+# filepath = ./files
+# metapath = ./meta
+
+# URI context that files will be served on
+# filectx = /f/
+
+# Maximum per-file upload size (in bytes)
+# maxsize = 536870912 # 512 MiB
+
+# Default expiration time (in seconds).
+# An expiration time of 0 seconds means no expiration.
+# expiry = 86400 # 24 hours
diff --git a/branches/master/example/static/favicon.ico b/branches/master/example/static/favicon.ico
new file mode 100644 (file)
index 0000000..9aed90e
Binary files /dev/null and b/branches/master/example/static/favicon.ico differ
diff --git a/branches/master/example/static/marisa.css b/branches/master/example/static/marisa.css
new file mode 100644 (file)
index 0000000..ed5156d
--- /dev/null
@@ -0,0 +1,15 @@
+body {
+    background-color: #282c37;
+    color: #f8f8f2;
+    font-family: sans-serif;
+    text-align: center;
+}
+a {
+    color: #272822;
+}
+a:hover, a:link {
+    color: #e6db74;
+}
+a:visited {
+    color: #66d9ef;
+ }
diff --git a/branches/master/example/static/marisa.png b/branches/master/example/static/marisa.png
new file mode 100644 (file)
index 0000000..4636a72
Binary files /dev/null and b/branches/master/example/static/marisa.png differ
diff --git a/branches/master/example/static/robots.txt b/branches/master/example/static/robots.txt
new file mode 100644 (file)
index 0000000..c6742d8
--- /dev/null
@@ -0,0 +1,2 @@
+User-Agent: *
+Disallow: /
diff --git a/branches/master/example/templates/index.html b/branches/master/example/templates/index.html
new file mode 100644 (file)
index 0000000..2444493
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML PUBLIC "//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <link rel="icon" href="/favicon.ico">
+    <link rel="stylesheet" href="/marisa.css">
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <meta name="author" content="Izuru Yakumo">
+    <meta name="viewport" content="width=device-width">
+    <title>Marisa</title>
+  </head>
+  <body>
+    <table>
+      <thead>
+       <img class="logo" src="/marisa.png">
+       <br>
+       <h1>Marisa</h1>
+      </thead>
+      <tbody>
+       <form enctype="multipart/form-data" method="POST">
+         <input class="file" name="file" type="file"><br>
+         <input name="output" type="hidden" value="html"><br>
+         <input type="submit"><br>
+         <label for="expiry">Destroy after</label>
+         <select name="expiry">
+           <option value="900">15 minutes</option>
+           <option value="3600">1 hour</option>
+           <option value="28800">8 hours</option>
+           <option value="86400">1 day</option>
+           <option value="604800">1 week</option>
+         </select>
+       </form>
+       <p>
+         File size limited to {{.Maxsize}}.
+       </p>
+      </tbody>
+      <tfoot>
+       {{if .Links}}
+       <tr>
+         {{range .Links}}<td><a href="{{.}}">{{.}}</a></td>{{end}}
+       </tr>
+       {{end}}
+      </tfoot>
+    </table>
+  </body>
+</html>
diff --git a/branches/master/go.mod b/branches/master/go.mod
new file mode 100644 (file)
index 0000000..d989832
--- /dev/null
@@ -0,0 +1,10 @@
+module marisa.chaotic.ninja/marisa
+
+go 1.17
+
+require (
+       github.com/dustin/go-humanize v1.0.0
+       gopkg.in/ini.v1 v1.63.2
+)
+
+require github.com/stretchr/testify v1.8.4 // indirect
diff --git a/branches/master/go.sum b/branches/master/go.sum
new file mode 100644 (file)
index 0000000..56d5331
--- /dev/null
@@ -0,0 +1,20 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c=
+gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/branches/master/marisa-trash.1 b/branches/master/marisa-trash.1
new file mode 100644 (file)
index 0000000..31a9ff8
--- /dev/null
@@ -0,0 +1,44 @@
+.Dd $Mdocdate$
+.Dt MARISA-TRASH 1
+.Os
+.Sh NAME
+.Nm marisa-trash
+.Nd Purge expired share files
+.Sh SYNOPSIS
+.Nm marisa-trash
+.Op Fl v
+.Op Fl f Ar files
+.Op Fl m Ar metadata
+.Sh DESCRIPTION
+Upon each run,
+.Nm
+will check expiration times for files in the
+.Pa metadata
+directory, and delete the according file in the 
+.Pa files
+directory if the expiration time has passed.
+.Pp
+.Nm
+is best run as a
+.Xr cron 8
+job, as the same user as the
+.Xr marisa 1
+daemon.
+.Bl -tag -width Ds
+.It Fl v
+Turn on verbose logging to
+.Pa stderr
+.It Fl f Ar files
+Set the location of actual files to
+.Pa files
+.It Fl m Ar metadata
+Lookup metadata files in directory
+.Pa metadata
+.El
+.Sh SEE ALSO
+.Xr marisa 1
+.Sh AUTHOR
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
diff --git a/branches/master/marisa.1 b/branches/master/marisa.1
new file mode 100644 (file)
index 0000000..a45428e
--- /dev/null
@@ -0,0 +1,43 @@
+.Dd $Mdocdate$
+.Dt MARISA 1
+.Os
+.Sh NAME
+.Nm marisa
+.Nd HTTP based file upload system
+.Sh SYNOPSIS
+.Nm marisa
+.Op Fl v
+.Op Fl f Ar file
+.Sh DESCRIPTION
+.Nm
+is an HTTP server that permits temporary file uploads using PUT and
+POST requests.
+.Pp
+Files uploaded are saved in a single directory and given random names
+while retaining their original extension.
+A configurable expiration time is set for each file, that can be used
+to cleanup expired files thanks to
+.Xr marisa-trash 1 .
+.Bl -tag -width Ds
+.It Fl v
+Turn on verbose logging to
+.Pa stderr
+.It Fl f Ar file
+Load configuration from
+.Pa file
+.El
+.Sh SEE ALSO
+.Xr marisa-trash 1 ,
+.Xr marisa.conf 5
+.Sh AUTHORS
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
+.Sh BUGS
+If you upload a file through the browser, and refresh the
+page, the file will get constantly reuploaded, which may
+exhaust the server's storage at some point.
+.Pp
+This shouldn't happen with a CLI, such as
+.Xr curl 1
diff --git a/branches/master/marisa.conf.5 b/branches/master/marisa.conf.5
new file mode 100644 (file)
index 0000000..03c4d6e
--- /dev/null
@@ -0,0 +1,94 @@
+.Dd $Mdocdate$
+.Dt MARISA.CONF 5
+.Os
+.Sh NAME
+.Nm marisa.conf
+.Nd marisa configuration file format
+.Sh DESCRIPTION
+.Nm
+is the configuration file for the HTTP file sharing system,
+.Xr marisa 1 .
+.Sh CONFIGURATION
+Here are the settings that can be set:
+.Bl -tag -width Ds
+.It Ic listen Ar socket
+Have the program listen on
+.Ar socket .
+This socket can be specified either as a TCP socket:
+.Ar host:port
+or as a Unix socket:
+.Ar /path/to/marisa.sock .
+When using Unix sockets, the program will serve content using the
+.Em FastCGI
+protocol.
+.It Ic user Ar user
+Username that the program will drop privileges to upon startup. When
+using Unix sockets, the owner of the socket will be changed to this user.
+.It Ic group Ar group
+Group that the program will drop privileges to upon startup (require that
+.Ic user
+is set). When using Unix sockets, the owner group of the socket will be
+changed to this group.
+.It Ic chroot Pa dir
+Directory to chroot into upon startup. When specified, all other path
+must be set within the chroot directory.
+.It Ic baseuri Ar uri
+Base URI to use when constructing hyper links.
+.It Ic rootdir Pa dir
+Directory containing static files.
+.It Ic tmplpath Pa dir
+Directory containing template files.
+.It Ic filepath Pa dir
+Directory where uploaded files must be written to.
+.It Ic metapath Pa dir
+Directory where metadata for uploaded files will be saved.
+.It Ic filectx Pa context
+URI context to use for serving files.
+.It Ic maxsize Ar size
+Maximum size per file to accept for uploads.
+.It Ic expiry Ar time
+Default expiration time to set for uploads.
+.El
+.Sh EXAMPLE
+Configuration suitable for use with
+.Xr httpd 8
+using fastcgi:
+.Bd -literal -offset indent
+listen      = /run/marisa.sock
+baseuri     = https://domain.tld
+user        = www
+group       = daemon
+chroot      = /var/www
+rootdir     = /htdocs/static
+filepath    = /htdocs/files
+metapath    = /htdocs/meta
+tmplpath    = /htdocs/templates
+filectx     = /d/
+maxsize     = 10737418240 # 10 Gib
+expiry      = 86400       # 24 hours
+.Ed
+
+Mathing
+.Xr httpd.conf 5
+configuration:
+.Bd -literal -offset indent
+server "domain.tld" {
+       listen on * tls port 443
+       connection { max request body 10737418240 }
+       location "*" {
+               fastcgi socket "/run/marisa.sock"
+       }
+}
+types { include "/usr/share/misc/mime.types" }
+.Ed
+
+.Sh SEE ALSO
+.Xr marisa 1 ,
+.Xr marisa-trash 1 ,
+.Xr httpd 8,
+.Xr httpd.conf 5
+.Sh AUTHORS
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
diff --git a/branches/master/version.go b/branches/master/version.go
new file mode 100644 (file)
index 0000000..611fd43
--- /dev/null
@@ -0,0 +1,18 @@
+package marisa
+
+import (
+       "fmt"
+)
+
+var (
+       // Version release version
+       Version = "0.0.1"
+
+       // Commit will be overwritten automatically by the build system
+       Commit = "HEAD"
+)
+
+// FullVersion display the full version and build
+func FullVersion() string {
+       return fmt.Sprintf("%s@%s", Version, Commit)
+}
diff --git a/branches/origin-master/.gitignore b/branches/origin-master/.gitignore
new file mode 100644 (file)
index 0000000..97a6b1f
--- /dev/null
@@ -0,0 +1,2 @@
+/marisa
+/marisa-trash
diff --git a/branches/origin-master/COPYING b/branches/origin-master/COPYING
new file mode 100644 (file)
index 0000000..196d9bc
--- /dev/null
@@ -0,0 +1,14 @@
+Copyright (c) 2021 Willy Goiffon <contact@z3bra.org>
+Copyright (c) 2023-present Izuru Yakumo <postmaster@chaotic.ninja>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/branches/origin-master/LICENSE b/branches/origin-master/LICENSE
new file mode 100644 (file)
index 0000000..196d9bc
--- /dev/null
@@ -0,0 +1,14 @@
+Copyright (c) 2021 Willy Goiffon <contact@z3bra.org>
+Copyright (c) 2023-present Izuru Yakumo <postmaster@chaotic.ninja>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/branches/origin-master/Makefile b/branches/origin-master/Makefile
new file mode 100644 (file)
index 0000000..aac2610
--- /dev/null
@@ -0,0 +1,25 @@
+GO ?= go
+GOFLAGS ?= -v -ldflags "-w -X `go list`.Version=${VERSION} -X `go list`.Commit=${COMMIT} -X `go list`.Build=${BUILD}"
+CGO ?= 0
+
+VERSION = `git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION"`
+COMMIT = `git rev-parse --short HEAD || echo "$COMMIT"`
+BRANCH = `git rev-parse --abbrev-ref HEAD`
+BUILD = `git show -s --pretty=format:%cI`
+
+PREFIX ?= /usr/local
+
+all: marisa marisa-trash
+
+marisa:
+       CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa
+marisa-trash:
+       CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa-trash
+clean:
+       rm -f marisa marisa-trash
+install:
+       install -Dm0755 marisa ${PREFIX}/bin/marisa
+       install -Dm0755 marisa-trash ${PREFIX}/bin/marisa-trash
+       install -Dm0644 marisa.1 ${PREFIX}/share/man/man1/marisa.1
+       install -Dm0644 marisa.conf.5 ${PREFIX}/share/man/man5/marisa.conf.5
+.PHONY: marisa marisa-trash
diff --git a/branches/origin-master/README.md b/branches/origin-master/README.md
new file mode 100644 (file)
index 0000000..e487c7d
--- /dev/null
@@ -0,0 +1,36 @@
+marisa
+======
+HTTP based File upload system.
+
+Features
+--------
++ Link expiration
++ Mimetype support
++ Random filenames
++ Multiple file uploads
++ Javascript not needed
++ Privilege drop
++ chroot(2) support
++ FastCGI support
+
+Usage
+-----
+Refer to the marisa(1) manual page for details and examples.
+
+       marisa [-v] [-f marisa.conf]
+
+Configuration is done through its configuration file, marisa.conf(5).
+The format is that of the INI file format.
+
+Uploading files is done via PUT and POST requests. Multiple files can
+be sent via POST requests.
+
+       curl -T file.png http://domain.tld
+       curl -F file=file.png -F expiry=3600 http://domain.tld
+
+Installation
+------------
+Edit the `config.mk` file to match your setup, then run the following:
+
+        $ (b)make
+        # (b)make install
diff --git a/branches/origin-master/cmd/marisa-trash/main.go b/branches/origin-master/cmd/marisa-trash/main.go
new file mode 100644 (file)
index 0000000..e010dd0
--- /dev/null
@@ -0,0 +1,101 @@
+package main
+
+import (
+       "log"
+       "flag"
+       "os"
+       "time"
+       "path/filepath"
+       "encoding/json"
+       
+       "github.com/dustin/go-humanize"
+)
+
+type metadata struct {
+       Filename string
+       Size int64
+       Expiry int64
+}
+
+var conf struct {
+       filepath string
+       metapath string
+}
+
+var verbose bool
+var count int64
+var deleted int64
+var size int64
+
+func readmeta(filename string) (metadata, error) {
+       j, err := os.ReadFile(filename)
+       if err != nil {
+               return metadata{}, err
+       }
+
+       var meta metadata
+       err = json.Unmarshal(j, &meta)
+       if err != nil {
+               return metadata{}, err
+       }
+
+       return meta, nil
+}
+
+func checkexpiry(path string, info os.FileInfo, err error) error {
+       if filepath.Ext(path) != ".json" {
+               return nil
+       }
+       meta, err := readmeta(path)
+       if err != nil {
+               log.Fatal(err)
+       }
+
+
+       count++
+
+       now := time.Now().Unix()
+       if verbose {
+               log.Printf("now: %s, expiry: %s\n", now, meta.Expiry);
+       }
+
+       if meta.Expiry > 0 && now >= meta.Expiry {
+               if verbose {
+                       expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
+                       log.Printf("%s/%s: expired %s\n", conf.filepath, meta.Filename, expiration)
+               }
+               if err = os.Remove(conf.filepath + "/" + meta.Filename); err != nil {
+                       log.Fatal(err)
+               }
+               if err = os.Remove(path); err != nil {
+                       log.Fatal(err)
+               }
+               deleted++
+               return nil
+       } else {
+               if verbose {
+                       expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
+                       log.Printf("%s/%s: expire in %s\n", conf.filepath, meta.Filename, expiration)
+               }
+               size += meta.Size
+       }
+
+       return nil
+}
+
+func main() {
+       flag.BoolVar(&verbose,         "v", false, "Verbose logging")
+       flag.StringVar(&conf.filepath, "f", "./files", "Directory containing files")
+       flag.StringVar(&conf.metapath, "m", "./meta", "Directory containing metadata")
+
+       flag.Parse()
+
+       err := filepath.Walk(conf.metapath, checkexpiry)
+       if err != nil {
+               log.Fatal(err)
+       }
+
+       if verbose && count > 0 {
+               log.Printf("%d/%d file(s) deleted (remaining: %s)", deleted, count, humanize.IBytes(uint64(size)))
+       }
+}
diff --git a/branches/origin-master/cmd/marisa/main.go b/branches/origin-master/cmd/marisa/main.go
new file mode 100644 (file)
index 0000000..0f74d7e
--- /dev/null
@@ -0,0 +1,141 @@
+package main
+
+import (
+       "flag"
+       "log"
+       "net"
+       "net/http"
+       "net/http/fcgi"
+       "os"
+       "os/signal"
+       "syscall"
+
+       "marisa.chaotic.ninja/marisa"
+)
+
+type templatedata struct {
+       Links   []string
+       Size    string
+       Maxsize string
+}
+
+type metadata struct {
+       Filename string
+       Size     int64
+       Expiry   int64
+}
+
+var conf struct {
+       user     string
+       group    string
+       chroot   string
+       listen   string
+       baseuri  string
+       rootdir  string
+       tmplpath string
+       filepath string
+       metapath string
+       filectx  string
+       maxsize  int64
+       expiry   int64
+}
+
+var verbose bool
+
+func main() {
+       var err error
+       var configfile string
+       var listener net.Listener
+
+       /* default values */
+       conf.listen = "127.0.0.1:8080"
+       conf.baseuri = "http://127.0.0.1:8080"
+       conf.rootdir = "static"
+       conf.tmplpath = "templates"
+       conf.filepath = "files"
+       conf.metapath = "meta"
+       conf.filectx = "/f/"
+       conf.maxsize = 34359738368
+       conf.expiry = 86400
+
+       flag.StringVar(&configfile, "f", "", "Configuration file")
+       flag.BoolVar(&verbose, "v", false, "Verbose logging")
+       flag.Parse()
+
+       if configfile != "" {
+               if verbose {
+                       log.Printf("Reading configuration %s", configfile)
+               }
+               parseconfig(configfile)
+       }
+
+       if conf.chroot != "" {
+               if verbose {
+                       log.Printf("Changing root to %s", conf.chroot)
+               }
+               syscall.Chroot(conf.chroot)
+       }
+
+       if conf.listen[0] == '/' {
+               /* Remove any stale socket */
+               os.Remove(conf.listen)
+               if listener, err = net.Listen("unix", conf.listen); err != nil {
+                       log.Fatal(err)
+               }
+               defer listener.Close()
+
+               /*
+                * Ensure unix socket is removed on exit.
+                * Note: this might not work when dropping privileges…
+                */
+               defer os.Remove(conf.listen)
+               sigs := make(chan os.Signal, 1)
+               signal.Notify(sigs, os.Interrupt, os.Kill, syscall.SIGTERM)
+               go func() {
+                       _ = <-sigs
+                       listener.Close()
+                       if err = os.Remove(conf.listen); err != nil {
+                               log.Fatal(err)
+                       }
+                       os.Exit(0)
+               }()
+       } else {
+               if listener, err = net.Listen("tcp", conf.listen); err != nil {
+                       log.Fatal(err)
+               }
+               defer listener.Close()
+       }
+
+       if conf.user != "" {
+               if verbose {
+                       log.Printf("Dropping privileges to %s", conf.user)
+               }
+               uid, gid, err := usergroupids(conf.user, conf.group)
+               if err != nil {
+                       log.Fatal(err)
+               }
+
+               if listener.Addr().Network() == "unix" {
+                       os.Chown(conf.listen, uid, gid)
+               }
+
+               syscall.Setuid(uid)
+               syscall.Setgid(gid)
+       }
+
+       http.HandleFunc("/", uploader)
+       http.Handle(conf.filectx, http.StripPrefix(conf.filectx, http.FileServer(http.Dir(conf.filepath))))
+
+       if verbose {
+               log.Printf("Starting marisa %v\n", marisa.FullVersion())
+               log.Printf("Listening on %s", conf.listen)
+       }
+
+       if listener.Addr().Network() == "unix" {
+               err = fcgi.Serve(listener, nil)
+               log.Fatal(err) /* NOTREACHED */
+       }
+
+       err = http.Serve(listener, nil)
+       log.Fatal(err) /* NOTREACHED */
+}
diff --git a/branches/origin-master/cmd/marisa/parseconfig.go b/branches/origin-master/cmd/marisa/parseconfig.go
new file mode 100644 (file)
index 0000000..d08cd83
--- /dev/null
@@ -0,0 +1,27 @@
+package main
+
+import (
+       "gopkg.in/ini.v1"
+)
+
+func parseconfig(file string) error {
+        cfg, err := ini.Load(file)
+        if err != nil {
+                return err
+        }
+
+        conf.listen = cfg.Section("marisa").Key("listen").String()
+        conf.user = cfg.Section("marisa").Key("user").String()
+        conf.group = cfg.Section("marisa").Key("group").String()
+        conf.baseuri = cfg.Section("www").Key("baseuri").String()
+        conf.filepath = cfg.Section("www").Key("filepath").String()
+        conf.metapath = cfg.Section("www").Key("metapath").String()
+        conf.filectx = cfg.Section("www").Key("filectx").String()
+        conf.rootdir = cfg.Section("www").Key("rootdir").String()
+        conf.chroot = cfg.Section("marisa").Key("chroot").String()
+        conf.tmplpath = cfg.Section("www").Key("tmplpath").String()
+        conf.maxsize, _ = cfg.Section("www").Key("maxsize").Int64()
+        conf.expiry, _ = cfg.Section("www").Key("expiry").Int64()
+
+        return nil
+}
diff --git a/branches/origin-master/cmd/marisa/servetemplate.go b/branches/origin-master/cmd/marisa/servetemplate.go
new file mode 100644 (file)
index 0000000..f092f76
--- /dev/null
@@ -0,0 +1,25 @@
+package main
+
+import (
+       "fmt"
+       "html/template"
+       "log"
+       "net/http"
+)
+
+func servetemplate(w http.ResponseWriter, f string, d templatedata) {
+        t, err := template.ParseFiles(conf.tmplpath + "/" + f)
+        if err != nil {
+                http.Error(w, "Internal error", http.StatusInternalServerError)
+                return
+        }
+
+        if verbose {
+                log.Printf("Serving template %s", t.Name())
+        }
+
+        err = t.Execute(w, d)
+        if err != nil {
+                fmt.Println(err)
+        }
+}
diff --git a/branches/origin-master/cmd/marisa/uploader.go b/branches/origin-master/cmd/marisa/uploader.go
new file mode 100644 (file)
index 0000000..f071449
--- /dev/null
@@ -0,0 +1,23 @@
+package main
+
+import (
+       "log"
+       "net/http"
+)
+
+func uploader(w http.ResponseWriter, r *http.Request) {
+        if verbose {
+                log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
+        }
+
+        switch r.Method {
+        case "DELETE":
+                uploaderDelete(w, r)
+        case "POST":
+                uploaderPost(w, r)
+        case "PUT":
+                uploaderPut(w, r)
+        case "GET":
+                uploaderGet(w, r)
+        }
+}
diff --git a/branches/origin-master/cmd/marisa/uploaderdelete.go b/branches/origin-master/cmd/marisa/uploaderdelete.go
new file mode 100644 (file)
index 0000000..1369e6f
--- /dev/null
@@ -0,0 +1,28 @@
+package main
+
+import (
+       "log"
+       "net/http"
+       "os"
+)
+
+func uploaderDelete(w http.ResponseWriter, r *http.Request) {
+        // r.URL.Path is sanitized regarding "." and ".."
+        filename := r.URL.Path
+        filepath := conf.filepath + filename
+
+        if verbose {
+                log.Printf("Deleting file %s", filepath)
+        }
+
+        f, err := os.Open(filepath)
+        if err != nil {
+                http.NotFound(w, r)
+                return
+        }
+        f.Close()
+
+        // Force file expiration
+        writemeta(filepath, 0)
+        w.WriteHeader(http.StatusNoContent)
+}
diff --git a/branches/origin-master/cmd/marisa/uploaderget.go b/branches/origin-master/cmd/marisa/uploaderget.go
new file mode 100644 (file)
index 0000000..4b5defa
--- /dev/null
@@ -0,0 +1,24 @@
+package main
+
+import (
+       "log"
+       "net/http"
+
+       "github.com/dustin/go-humanize"
+)
+
+func uploaderGet(w http.ResponseWriter, r *http.Request) {
+        // r.URL.Path is sanitized regarding "." and ".."
+        filename := r.URL.Path
+        if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+                data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
+                servetemplate(w, "/index.html", data)
+                return
+        }
+
+        if verbose {
+                log.Printf("Serving file %s", conf.rootdir+filename)
+        }
+
+        http.ServeFile(w, r, conf.rootdir+filename)
+}
diff --git a/branches/origin-master/cmd/marisa/uploaderpost.go b/branches/origin-master/cmd/marisa/uploaderpost.go
new file mode 100644 (file)
index 0000000..9ecb6ae
--- /dev/null
@@ -0,0 +1,71 @@
+package main
+
+import (
+       "encoding/json"
+       "io/ioutil"
+       "net/http"
+       "os"
+       "path"
+       "path/filepath"
+       "strconv"
+
+       "github.com/dustin/go-humanize"
+)
+
+func uploaderPost(w http.ResponseWriter, r *http.Request) {
+        /* read 32Mb at a time */
+        r.ParseMultipartForm(32 << 20)
+
+        links := []string{}
+        for _, h := range r.MultipartForm.File["file"] {
+                if h.Size > conf.maxsize {
+                        http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
+                        return
+                }
+
+                post, err := h.Open()
+                if err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        return
+                }
+                defer post.Close()
+
+                tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename))
+                f, err := os.Create(tmp.Name())
+                if err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        return
+                }
+                defer f.Close()
+
+                if err = writefile(f, post, h.Size); err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        defer os.Remove(tmp.Name())
+                        return
+                }
+                expiry, err := strconv.Atoi(r.PostFormValue("expiry"))
+                if err != nil || expiry < 0 {
+                        expiry = int(conf.expiry)
+                }
+                writemeta(tmp.Name(), int64(expiry))
+
+                link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
+                links = append(links, link)
+        }
+
+        switch r.PostFormValue("output") {
+        case "html":
+                data := templatedata{
+                        Maxsize: humanize.IBytes(uint64(conf.maxsize)),
+                        Links:   links,
+                }
+                servetemplate(w, "/index.html", data)
+        case "json":
+                data, _ := json.Marshal(links)
+                w.Write(data)
+        default:
+                for _, link := range links {
+                        w.Write([]byte(link + "\r\n"))
+                }
+        }
+}
diff --git a/branches/origin-master/cmd/marisa/uploaderput.go b/branches/origin-master/cmd/marisa/uploaderput.go
new file mode 100644 (file)
index 0000000..51723e5
--- /dev/null
@@ -0,0 +1,40 @@
+package main
+
+import (
+       "fmt"
+       "io/ioutil"
+       "log"
+       "net/http"
+       "os"
+       "path"
+       "path/filepath"
+)
+
+func uploaderPut(w http.ResponseWriter, r *http.Request) {
+        /* limit upload size */
+        if r.ContentLength > conf.maxsize {
+                http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
+        }
+
+        tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path))
+        f, err := os.Create(tmp.Name())
+        if err != nil {
+                fmt.Println(err)
+                return
+        }
+        defer f.Close()
+
+        if verbose {
+                log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name())
+        }
+
+        if err = writefile(f, r.Body, r.ContentLength); err != nil {
+                http.Error(w, "Internal error", http.StatusInternalServerError)
+                defer os.Remove(tmp.Name())
+                return
+        }
+        writemeta(tmp.Name(), conf.expiry)
+
+        resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
+        w.Write([]byte(resp + "\r\n"))
+}
diff --git a/branches/origin-master/cmd/marisa/usergroupids.go b/branches/origin-master/cmd/marisa/usergroupids.go
new file mode 100644 (file)
index 0000000..758581c
--- /dev/null
@@ -0,0 +1,26 @@
+package main
+
+import (
+       "os/user"
+       "strconv"
+)
+
+func usergroupids(username string, groupname string) (int, int, error) {
+        u, err := user.Lookup(username)
+        if err != nil {
+                return -1, -1, err
+        }
+
+        uid, _ := strconv.Atoi(u.Uid)
+        gid, _ := strconv.Atoi(u.Gid)
+
+        if conf.group != "" {
+                g, err := user.LookupGroup(groupname)
+                if err != nil {
+                        return uid, -1, err
+                }
+                gid, _ = strconv.Atoi(g.Gid)
+        }
+
+        return uid, gid, nil
+}
diff --git a/branches/origin-master/cmd/marisa/writefile.go b/branches/origin-master/cmd/marisa/writefile.go
new file mode 100644 (file)
index 0000000..d5367ec
--- /dev/null
@@ -0,0 +1,38 @@
+package main
+
+import (
+       "io"
+       "os"
+)
+
+func writefile(f *os.File, s io.ReadCloser, contentlength int64) error {
+        buffer := make([]byte, 4096)
+        eof := false
+        sz := int64(0)
+
+        defer f.Sync()
+
+        for !eof {
+                n, err := s.Read(buffer)
+                if err != nil && err != io.EOF {
+                        return err
+                } else if err == io.EOF {
+                        eof = true
+                }
+
+                /* ensure we don't write more than expected */
+                r := int64(n)
+                if sz+r > contentlength {
+                        r = contentlength - sz
+                        eof = true
+                }
+
+                _, err = f.Write(buffer[:r])
+                if err != nil {
+                        return err
+                }
+                sz += r
+        }
+
+        return nil
+}
diff --git a/branches/origin-master/cmd/marisa/writemeta.go b/branches/origin-master/cmd/marisa/writemeta.go
new file mode 100644 (file)
index 0000000..c63d9c8
--- /dev/null
@@ -0,0 +1,46 @@
+package main
+
+import (
+       "encoding/json"
+       "log"
+       "os"
+       "path/filepath"
+       "time"
+)
+
+func writemeta(filename string, expiry int64) error {
+
+        f, _ := os.Open(filename)
+        stat, _ := f.Stat()
+        size := stat.Size()
+        f.Close()
+
+        if expiry < 0 {
+                expiry = conf.expiry
+        }
+
+        meta := metadata{
+                Filename: filepath.Base(filename),
+                Size:     size,
+                Expiry:   time.Now().Unix() + expiry,
+        }
+
+        if verbose {
+                log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json")
+        }
+
+        f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json")
+        if err != nil {
+                return err
+        }
+        defer f.Close()
+
+        j, err := json.Marshal(meta)
+        if err != nil {
+                return err
+        }
+
+       _, err = f.Write(j)
+
+       return err
+}
diff --git a/branches/origin-master/example/marisa.conf b/branches/origin-master/example/marisa.conf
new file mode 100644 (file)
index 0000000..334cf7d
--- /dev/null
@@ -0,0 +1,35 @@
+[marisa]
+# TCP or Unix socket to listen on.
+# When the Unix socket is used, the content will be served through FastCGI
+# listen = /var/run/marisa.sock
+# listen = 127.0.0.1:9000
+
+# Drop privilege to the user and group specified.
+# When only the user is specified, the default group of the user
+# will be used.
+# user = www
+# group = www
+
+# Change the root directory to the following directory.
+# When a chroot(2) is set, all paths must be given according to it.
+# Note: the configuration file is read before it happens
+# chroot =
+[www]
+# baseuri = http://127.0.0.1:9000
+
+# Path to the resources used by the server, must take into account
+# the chroot is set
+# rootdir = ./static
+# tmplpath = ./templates
+# filepath = ./files
+# metapath = ./meta
+
+# URI context that files will be served on
+# filectx = /f/
+
+# Maximum per-file upload size (in bytes)
+# maxsize = 536870912 # 512 MiB
+
+# Default expiration time (in seconds).
+# An expiration time of 0 seconds means no expiration.
+# expiry = 86400 # 24 hours
diff --git a/branches/origin-master/example/static/favicon.ico b/branches/origin-master/example/static/favicon.ico
new file mode 100644 (file)
index 0000000..9aed90e
Binary files /dev/null and b/branches/origin-master/example/static/favicon.ico differ
diff --git a/branches/origin-master/example/static/marisa.css b/branches/origin-master/example/static/marisa.css
new file mode 100644 (file)
index 0000000..ed5156d
--- /dev/null
@@ -0,0 +1,15 @@
+body {
+    background-color: #282c37;
+    color: #f8f8f2;
+    font-family: sans-serif;
+    text-align: center;
+}
+a {
+    color: #272822;
+}
+a:hover, a:link {
+    color: #e6db74;
+}
+a:visited {
+    color: #66d9ef;
+ }
diff --git a/branches/origin-master/example/static/marisa.png b/branches/origin-master/example/static/marisa.png
new file mode 100644 (file)
index 0000000..4636a72
Binary files /dev/null and b/branches/origin-master/example/static/marisa.png differ
diff --git a/branches/origin-master/example/static/robots.txt b/branches/origin-master/example/static/robots.txt
new file mode 100644 (file)
index 0000000..c6742d8
--- /dev/null
@@ -0,0 +1,2 @@
+User-Agent: *
+Disallow: /
diff --git a/branches/origin-master/example/templates/index.html b/branches/origin-master/example/templates/index.html
new file mode 100644 (file)
index 0000000..2444493
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML PUBLIC "//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <link rel="icon" href="/favicon.ico">
+    <link rel="stylesheet" href="/marisa.css">
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <meta name="author" content="Izuru Yakumo">
+    <meta name="viewport" content="width=device-width">
+    <title>Marisa</title>
+  </head>
+  <body>
+    <table>
+      <thead>
+       <img class="logo" src="/marisa.png">
+       <br>
+       <h1>Marisa</h1>
+      </thead>
+      <tbody>
+       <form enctype="multipart/form-data" method="POST">
+         <input class="file" name="file" type="file"><br>
+         <input name="output" type="hidden" value="html"><br>
+         <input type="submit"><br>
+         <label for="expiry">Destroy after</label>
+         <select name="expiry">
+           <option value="900">15 minutes</option>
+           <option value="3600">1 hour</option>
+           <option value="28800">8 hours</option>
+           <option value="86400">1 day</option>
+           <option value="604800">1 week</option>
+         </select>
+       </form>
+       <p>
+         File size limited to {{.Maxsize}}.
+       </p>
+      </tbody>
+      <tfoot>
+       {{if .Links}}
+       <tr>
+         {{range .Links}}<td><a href="{{.}}">{{.}}</a></td>{{end}}
+       </tr>
+       {{end}}
+      </tfoot>
+    </table>
+  </body>
+</html>
diff --git a/branches/origin-master/go.mod b/branches/origin-master/go.mod
new file mode 100644 (file)
index 0000000..d989832
--- /dev/null
@@ -0,0 +1,10 @@
+module marisa.chaotic.ninja/marisa
+
+go 1.17
+
+require (
+       github.com/dustin/go-humanize v1.0.0
+       gopkg.in/ini.v1 v1.63.2
+)
+
+require github.com/stretchr/testify v1.8.4 // indirect
diff --git a/branches/origin-master/go.sum b/branches/origin-master/go.sum
new file mode 100644 (file)
index 0000000..56d5331
--- /dev/null
@@ -0,0 +1,20 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c=
+gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/branches/origin-master/marisa-trash.1 b/branches/origin-master/marisa-trash.1
new file mode 100644 (file)
index 0000000..31a9ff8
--- /dev/null
@@ -0,0 +1,44 @@
+.Dd $Mdocdate$
+.Dt MARISA-TRASH 1
+.Os
+.Sh NAME
+.Nm marisa-trash
+.Nd Purge expired share files
+.Sh SYNOPSIS
+.Nm marisa-trash
+.Op Fl v
+.Op Fl f Ar files
+.Op Fl m Ar metadata
+.Sh DESCRIPTION
+Upon each run,
+.Nm
+will check expiration times for files in the
+.Pa metadata
+directory, and delete the according file in the 
+.Pa files
+directory if the expiration time has passed.
+.Pp
+.Nm
+is best run as a
+.Xr cron 8
+job, as the same user as the
+.Xr marisa 1
+daemon.
+.Bl -tag -width Ds
+.It Fl v
+Turn on verbose logging to
+.Pa stderr
+.It Fl f Ar files
+Set the location of actual files to
+.Pa files
+.It Fl m Ar metadata
+Lookup metadata files in directory
+.Pa metadata
+.El
+.Sh SEE ALSO
+.Xr marisa 1
+.Sh AUTHOR
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
diff --git a/branches/origin-master/marisa.1 b/branches/origin-master/marisa.1
new file mode 100644 (file)
index 0000000..a45428e
--- /dev/null
@@ -0,0 +1,43 @@
+.Dd $Mdocdate$
+.Dt MARISA 1
+.Os
+.Sh NAME
+.Nm marisa
+.Nd HTTP based file upload system
+.Sh SYNOPSIS
+.Nm marisa
+.Op Fl v
+.Op Fl f Ar file
+.Sh DESCRIPTION
+.Nm
+is an HTTP server that permits temporary file uploads using PUT and
+POST requests.
+.Pp
+Files uploaded are saved in a single directory and given random names
+while retaining their original extension.
+A configurable expiration time is set for each file, that can be used
+to cleanup expired files thanks to
+.Xr marisa-trash 1 .
+.Bl -tag -width Ds
+.It Fl v
+Turn on verbose logging to
+.Pa stderr
+.It Fl f Ar file
+Load configuration from
+.Pa file
+.El
+.Sh SEE ALSO
+.Xr marisa-trash 1 ,
+.Xr marisa.conf 5
+.Sh AUTHORS
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
+.Sh BUGS
+If you upload a file through the browser, and refresh the
+page, the file will get constantly reuploaded, which may
+exhaust the server's storage at some point.
+.Pp
+This shouldn't happen with a CLI, such as
+.Xr curl 1
diff --git a/branches/origin-master/marisa.conf.5 b/branches/origin-master/marisa.conf.5
new file mode 100644 (file)
index 0000000..03c4d6e
--- /dev/null
@@ -0,0 +1,94 @@
+.Dd $Mdocdate$
+.Dt MARISA.CONF 5
+.Os
+.Sh NAME
+.Nm marisa.conf
+.Nd marisa configuration file format
+.Sh DESCRIPTION
+.Nm
+is the configuration file for the HTTP file sharing system,
+.Xr marisa 1 .
+.Sh CONFIGURATION
+Here are the settings that can be set:
+.Bl -tag -width Ds
+.It Ic listen Ar socket
+Have the program listen on
+.Ar socket .
+This socket can be specified either as a TCP socket:
+.Ar host:port
+or as a Unix socket:
+.Ar /path/to/marisa.sock .
+When using Unix sockets, the program will serve content using the
+.Em FastCGI
+protocol.
+.It Ic user Ar user
+Username that the program will drop privileges to upon startup. When
+using Unix sockets, the owner of the socket will be changed to this user.
+.It Ic group Ar group
+Group that the program will drop privileges to upon startup (require that
+.Ic user
+is set). When using Unix sockets, the owner group of the socket will be
+changed to this group.
+.It Ic chroot Pa dir
+Directory to chroot into upon startup. When specified, all other path
+must be set within the chroot directory.
+.It Ic baseuri Ar uri
+Base URI to use when constructing hyper links.
+.It Ic rootdir Pa dir
+Directory containing static files.
+.It Ic tmplpath Pa dir
+Directory containing template files.
+.It Ic filepath Pa dir
+Directory where uploaded files must be written to.
+.It Ic metapath Pa dir
+Directory where metadata for uploaded files will be saved.
+.It Ic filectx Pa context
+URI context to use for serving files.
+.It Ic maxsize Ar size
+Maximum size per file to accept for uploads.
+.It Ic expiry Ar time
+Default expiration time to set for uploads.
+.El
+.Sh EXAMPLE
+Configuration suitable for use with
+.Xr httpd 8
+using fastcgi:
+.Bd -literal -offset indent
+listen      = /run/marisa.sock
+baseuri     = https://domain.tld
+user        = www
+group       = daemon
+chroot      = /var/www
+rootdir     = /htdocs/static
+filepath    = /htdocs/files
+metapath    = /htdocs/meta
+tmplpath    = /htdocs/templates
+filectx     = /d/
+maxsize     = 10737418240 # 10 Gib
+expiry      = 86400       # 24 hours
+.Ed
+
+Mathing
+.Xr httpd.conf 5
+configuration:
+.Bd -literal -offset indent
+server "domain.tld" {
+       listen on * tls port 443
+       connection { max request body 10737418240 }
+       location "*" {
+               fastcgi socket "/run/marisa.sock"
+       }
+}
+types { include "/usr/share/misc/mime.types" }
+.Ed
+
+.Sh SEE ALSO
+.Xr marisa 1 ,
+.Xr marisa-trash 1 ,
+.Xr httpd 8,
+.Xr httpd.conf 5
+.Sh AUTHORS
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
diff --git a/branches/origin-master/version.go b/branches/origin-master/version.go
new file mode 100644 (file)
index 0000000..611fd43
--- /dev/null
@@ -0,0 +1,18 @@
+package marisa
+
+import (
+       "fmt"
+)
+
+var (
+       // Version release version
+       Version = "0.0.1"
+
+       // Commit will be overwritten automatically by the build system
+       Commit = "HEAD"
+)
+
+// FullVersion display the full version and build
+func FullVersion() string {
+       return fmt.Sprintf("%s@%s", Version, Commit)
+}
diff --git a/branches/origin/.gitignore b/branches/origin/.gitignore
new file mode 100644 (file)
index 0000000..97a6b1f
--- /dev/null
@@ -0,0 +1,2 @@
+/marisa
+/marisa-trash
diff --git a/branches/origin/COPYING b/branches/origin/COPYING
new file mode 100644 (file)
index 0000000..196d9bc
--- /dev/null
@@ -0,0 +1,14 @@
+Copyright (c) 2021 Willy Goiffon <contact@z3bra.org>
+Copyright (c) 2023-present Izuru Yakumo <postmaster@chaotic.ninja>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/branches/origin/LICENSE b/branches/origin/LICENSE
new file mode 100644 (file)
index 0000000..196d9bc
--- /dev/null
@@ -0,0 +1,14 @@
+Copyright (c) 2021 Willy Goiffon <contact@z3bra.org>
+Copyright (c) 2023-present Izuru Yakumo <postmaster@chaotic.ninja>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/branches/origin/Makefile b/branches/origin/Makefile
new file mode 100644 (file)
index 0000000..aac2610
--- /dev/null
@@ -0,0 +1,25 @@
+GO ?= go
+GOFLAGS ?= -v -ldflags "-w -X `go list`.Version=${VERSION} -X `go list`.Commit=${COMMIT} -X `go list`.Build=${BUILD}"
+CGO ?= 0
+
+VERSION = `git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION"`
+COMMIT = `git rev-parse --short HEAD || echo "$COMMIT"`
+BRANCH = `git rev-parse --abbrev-ref HEAD`
+BUILD = `git show -s --pretty=format:%cI`
+
+PREFIX ?= /usr/local
+
+all: marisa marisa-trash
+
+marisa:
+       CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa
+marisa-trash:
+       CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa-trash
+clean:
+       rm -f marisa marisa-trash
+install:
+       install -Dm0755 marisa ${PREFIX}/bin/marisa
+       install -Dm0755 marisa-trash ${PREFIX}/bin/marisa-trash
+       install -Dm0644 marisa.1 ${PREFIX}/share/man/man1/marisa.1
+       install -Dm0644 marisa.conf.5 ${PREFIX}/share/man/man5/marisa.conf.5
+.PHONY: marisa marisa-trash
diff --git a/branches/origin/README.md b/branches/origin/README.md
new file mode 100644 (file)
index 0000000..e487c7d
--- /dev/null
@@ -0,0 +1,36 @@
+marisa
+======
+HTTP based File upload system.
+
+Features
+--------
++ Link expiration
++ Mimetype support
++ Random filenames
++ Multiple file uploads
++ Javascript not needed
++ Privilege drop
++ chroot(2) support
++ FastCGI support
+
+Usage
+-----
+Refer to the marisa(1) manual page for details and examples.
+
+       marisa [-v] [-f marisa.conf]
+
+Configuration is done through its configuration file, marisa.conf(5).
+The format is that of the INI file format.
+
+Uploading files is done via PUT and POST requests. Multiple files can
+be sent via POST requests.
+
+       curl -T file.png http://domain.tld
+       curl -F file=file.png -F expiry=3600 http://domain.tld
+
+Installation
+------------
+Edit the `config.mk` file to match your setup, then run the following:
+
+        $ (b)make
+        # (b)make install
diff --git a/branches/origin/cmd/marisa-trash/main.go b/branches/origin/cmd/marisa-trash/main.go
new file mode 100644 (file)
index 0000000..e010dd0
--- /dev/null
@@ -0,0 +1,101 @@
+package main
+
+import (
+       "log"
+       "flag"
+       "os"
+       "time"
+       "path/filepath"
+       "encoding/json"
+       
+       "github.com/dustin/go-humanize"
+)
+
+type metadata struct {
+       Filename string
+       Size int64
+       Expiry int64
+}
+
+var conf struct {
+       filepath string
+       metapath string
+}
+
+var verbose bool
+var count int64
+var deleted int64
+var size int64
+
+func readmeta(filename string) (metadata, error) {
+       j, err := os.ReadFile(filename)
+       if err != nil {
+               return metadata{}, err
+       }
+
+       var meta metadata
+       err = json.Unmarshal(j, &meta)
+       if err != nil {
+               return metadata{}, err
+       }
+
+       return meta, nil
+}
+
+func checkexpiry(path string, info os.FileInfo, err error) error {
+       if filepath.Ext(path) != ".json" {
+               return nil
+       }
+       meta, err := readmeta(path)
+       if err != nil {
+               log.Fatal(err)
+       }
+
+
+       count++
+
+       now := time.Now().Unix()
+       if verbose {
+               log.Printf("now: %s, expiry: %s\n", now, meta.Expiry);
+       }
+
+       if meta.Expiry > 0 && now >= meta.Expiry {
+               if verbose {
+                       expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
+                       log.Printf("%s/%s: expired %s\n", conf.filepath, meta.Filename, expiration)
+               }
+               if err = os.Remove(conf.filepath + "/" + meta.Filename); err != nil {
+                       log.Fatal(err)
+               }
+               if err = os.Remove(path); err != nil {
+                       log.Fatal(err)
+               }
+               deleted++
+               return nil
+       } else {
+               if verbose {
+                       expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
+                       log.Printf("%s/%s: expire in %s\n", conf.filepath, meta.Filename, expiration)
+               }
+               size += meta.Size
+       }
+
+       return nil
+}
+
+func main() {
+       flag.BoolVar(&verbose,         "v", false, "Verbose logging")
+       flag.StringVar(&conf.filepath, "f", "./files", "Directory containing files")
+       flag.StringVar(&conf.metapath, "m", "./meta", "Directory containing metadata")
+
+       flag.Parse()
+
+       err := filepath.Walk(conf.metapath, checkexpiry)
+       if err != nil {
+               log.Fatal(err)
+       }
+
+       if verbose && count > 0 {
+               log.Printf("%d/%d file(s) deleted (remaining: %s)", deleted, count, humanize.IBytes(uint64(size)))
+       }
+}
diff --git a/branches/origin/cmd/marisa/main.go b/branches/origin/cmd/marisa/main.go
new file mode 100644 (file)
index 0000000..0f74d7e
--- /dev/null
@@ -0,0 +1,141 @@
+package main
+
+import (
+       "flag"
+       "log"
+       "net"
+       "net/http"
+       "net/http/fcgi"
+       "os"
+       "os/signal"
+       "syscall"
+
+       "marisa.chaotic.ninja/marisa"
+)
+
+type templatedata struct {
+       Links   []string
+       Size    string
+       Maxsize string
+}
+
+type metadata struct {
+       Filename string
+       Size     int64
+       Expiry   int64
+}
+
+var conf struct {
+       user     string
+       group    string
+       chroot   string
+       listen   string
+       baseuri  string
+       rootdir  string
+       tmplpath string
+       filepath string
+       metapath string
+       filectx  string
+       maxsize  int64
+       expiry   int64
+}
+
+var verbose bool
+
+func main() {
+       var err error
+       var configfile string
+       var listener net.Listener
+
+       /* default values */
+       conf.listen = "127.0.0.1:8080"
+       conf.baseuri = "http://127.0.0.1:8080"
+       conf.rootdir = "static"
+       conf.tmplpath = "templates"
+       conf.filepath = "files"
+       conf.metapath = "meta"
+       conf.filectx = "/f/"
+       conf.maxsize = 34359738368
+       conf.expiry = 86400
+
+       flag.StringVar(&configfile, "f", "", "Configuration file")
+       flag.BoolVar(&verbose, "v", false, "Verbose logging")
+       flag.Parse()
+
+       if configfile != "" {
+               if verbose {
+                       log.Printf("Reading configuration %s", configfile)
+               }
+               parseconfig(configfile)
+       }
+
+       if conf.chroot != "" {
+               if verbose {
+                       log.Printf("Changing root to %s", conf.chroot)
+               }
+               syscall.Chroot(conf.chroot)
+       }
+
+       if conf.listen[0] == '/' {
+               /* Remove any stale socket */
+               os.Remove(conf.listen)
+               if listener, err = net.Listen("unix", conf.listen); err != nil {
+                       log.Fatal(err)
+               }
+               defer listener.Close()
+
+               /*
+                * Ensure unix socket is removed on exit.
+                * Note: this might not work when dropping privileges…
+                */
+               defer os.Remove(conf.listen)
+               sigs := make(chan os.Signal, 1)
+               signal.Notify(sigs, os.Interrupt, os.Kill, syscall.SIGTERM)
+               go func() {
+                       _ = <-sigs
+                       listener.Close()
+                       if err = os.Remove(conf.listen); err != nil {
+                               log.Fatal(err)
+                       }
+                       os.Exit(0)
+               }()
+       } else {
+               if listener, err = net.Listen("tcp", conf.listen); err != nil {
+                       log.Fatal(err)
+               }
+               defer listener.Close()
+       }
+
+       if conf.user != "" {
+               if verbose {
+                       log.Printf("Dropping privileges to %s", conf.user)
+               }
+               uid, gid, err := usergroupids(conf.user, conf.group)
+               if err != nil {
+                       log.Fatal(err)
+               }
+
+               if listener.Addr().Network() == "unix" {
+                       os.Chown(conf.listen, uid, gid)
+               }
+
+               syscall.Setuid(uid)
+               syscall.Setgid(gid)
+       }
+
+       http.HandleFunc("/", uploader)
+       http.Handle(conf.filectx, http.StripPrefix(conf.filectx, http.FileServer(http.Dir(conf.filepath))))
+
+       if verbose {
+               log.Printf("Starting marisa %v\n", marisa.FullVersion())
+               log.Printf("Listening on %s", conf.listen)
+       }
+
+       if listener.Addr().Network() == "unix" {
+               err = fcgi.Serve(listener, nil)
+               log.Fatal(err) /* NOTREACHED */
+       }
+
+       err = http.Serve(listener, nil)
+       log.Fatal(err) /* NOTREACHED */
+}
diff --git a/branches/origin/cmd/marisa/parseconfig.go b/branches/origin/cmd/marisa/parseconfig.go
new file mode 100644 (file)
index 0000000..d08cd83
--- /dev/null
@@ -0,0 +1,27 @@
+package main
+
+import (
+       "gopkg.in/ini.v1"
+)
+
+func parseconfig(file string) error {
+        cfg, err := ini.Load(file)
+        if err != nil {
+                return err
+        }
+
+        conf.listen = cfg.Section("marisa").Key("listen").String()
+        conf.user = cfg.Section("marisa").Key("user").String()
+        conf.group = cfg.Section("marisa").Key("group").String()
+        conf.baseuri = cfg.Section("www").Key("baseuri").String()
+        conf.filepath = cfg.Section("www").Key("filepath").String()
+        conf.metapath = cfg.Section("www").Key("metapath").String()
+        conf.filectx = cfg.Section("www").Key("filectx").String()
+        conf.rootdir = cfg.Section("www").Key("rootdir").String()
+        conf.chroot = cfg.Section("marisa").Key("chroot").String()
+        conf.tmplpath = cfg.Section("www").Key("tmplpath").String()
+        conf.maxsize, _ = cfg.Section("www").Key("maxsize").Int64()
+        conf.expiry, _ = cfg.Section("www").Key("expiry").Int64()
+
+        return nil
+}
diff --git a/branches/origin/cmd/marisa/servetemplate.go b/branches/origin/cmd/marisa/servetemplate.go
new file mode 100644 (file)
index 0000000..f092f76
--- /dev/null
@@ -0,0 +1,25 @@
+package main
+
+import (
+       "fmt"
+       "html/template"
+       "log"
+       "net/http"
+)
+
+func servetemplate(w http.ResponseWriter, f string, d templatedata) {
+        t, err := template.ParseFiles(conf.tmplpath + "/" + f)
+        if err != nil {
+                http.Error(w, "Internal error", http.StatusInternalServerError)
+                return
+        }
+
+        if verbose {
+                log.Printf("Serving template %s", t.Name())
+        }
+
+        err = t.Execute(w, d)
+        if err != nil {
+                fmt.Println(err)
+        }
+}
diff --git a/branches/origin/cmd/marisa/uploader.go b/branches/origin/cmd/marisa/uploader.go
new file mode 100644 (file)
index 0000000..f071449
--- /dev/null
@@ -0,0 +1,23 @@
+package main
+
+import (
+       "log"
+       "net/http"
+)
+
+func uploader(w http.ResponseWriter, r *http.Request) {
+        if verbose {
+                log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
+        }
+
+        switch r.Method {
+        case "DELETE":
+                uploaderDelete(w, r)
+        case "POST":
+                uploaderPost(w, r)
+        case "PUT":
+                uploaderPut(w, r)
+        case "GET":
+                uploaderGet(w, r)
+        }
+}
diff --git a/branches/origin/cmd/marisa/uploaderdelete.go b/branches/origin/cmd/marisa/uploaderdelete.go
new file mode 100644 (file)
index 0000000..1369e6f
--- /dev/null
@@ -0,0 +1,28 @@
+package main
+
+import (
+       "log"
+       "net/http"
+       "os"
+)
+
+func uploaderDelete(w http.ResponseWriter, r *http.Request) {
+        // r.URL.Path is sanitized regarding "." and ".."
+        filename := r.URL.Path
+        filepath := conf.filepath + filename
+
+        if verbose {
+                log.Printf("Deleting file %s", filepath)
+        }
+
+        f, err := os.Open(filepath)
+        if err != nil {
+                http.NotFound(w, r)
+                return
+        }
+        f.Close()
+
+        // Force file expiration
+        writemeta(filepath, 0)
+        w.WriteHeader(http.StatusNoContent)
+}
diff --git a/branches/origin/cmd/marisa/uploaderget.go b/branches/origin/cmd/marisa/uploaderget.go
new file mode 100644 (file)
index 0000000..4b5defa
--- /dev/null
@@ -0,0 +1,24 @@
+package main
+
+import (
+       "log"
+       "net/http"
+
+       "github.com/dustin/go-humanize"
+)
+
+func uploaderGet(w http.ResponseWriter, r *http.Request) {
+        // r.URL.Path is sanitized regarding "." and ".."
+        filename := r.URL.Path
+        if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+                data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
+                servetemplate(w, "/index.html", data)
+                return
+        }
+
+        if verbose {
+                log.Printf("Serving file %s", conf.rootdir+filename)
+        }
+
+        http.ServeFile(w, r, conf.rootdir+filename)
+}
diff --git a/branches/origin/cmd/marisa/uploaderpost.go b/branches/origin/cmd/marisa/uploaderpost.go
new file mode 100644 (file)
index 0000000..9ecb6ae
--- /dev/null
@@ -0,0 +1,71 @@
+package main
+
+import (
+       "encoding/json"
+       "io/ioutil"
+       "net/http"
+       "os"
+       "path"
+       "path/filepath"
+       "strconv"
+
+       "github.com/dustin/go-humanize"
+)
+
+func uploaderPost(w http.ResponseWriter, r *http.Request) {
+        /* read 32Mb at a time */
+        r.ParseMultipartForm(32 << 20)
+
+        links := []string{}
+        for _, h := range r.MultipartForm.File["file"] {
+                if h.Size > conf.maxsize {
+                        http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
+                        return
+                }
+
+                post, err := h.Open()
+                if err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        return
+                }
+                defer post.Close()
+
+                tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename))
+                f, err := os.Create(tmp.Name())
+                if err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        return
+                }
+                defer f.Close()
+
+                if err = writefile(f, post, h.Size); err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        defer os.Remove(tmp.Name())
+                        return
+                }
+                expiry, err := strconv.Atoi(r.PostFormValue("expiry"))
+                if err != nil || expiry < 0 {
+                        expiry = int(conf.expiry)
+                }
+                writemeta(tmp.Name(), int64(expiry))
+
+                link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
+                links = append(links, link)
+        }
+
+        switch r.PostFormValue("output") {
+        case "html":
+                data := templatedata{
+                        Maxsize: humanize.IBytes(uint64(conf.maxsize)),
+                        Links:   links,
+                }
+                servetemplate(w, "/index.html", data)
+        case "json":
+                data, _ := json.Marshal(links)
+                w.Write(data)
+        default:
+                for _, link := range links {
+                        w.Write([]byte(link + "\r\n"))
+                }
+        }
+}
diff --git a/branches/origin/cmd/marisa/uploaderput.go b/branches/origin/cmd/marisa/uploaderput.go
new file mode 100644 (file)
index 0000000..51723e5
--- /dev/null
@@ -0,0 +1,40 @@
+package main
+
+import (
+       "fmt"
+       "io/ioutil"
+       "log"
+       "net/http"
+       "os"
+       "path"
+       "path/filepath"
+)
+
+func uploaderPut(w http.ResponseWriter, r *http.Request) {
+        /* limit upload size */
+        if r.ContentLength > conf.maxsize {
+                http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
+        }
+
+        tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path))
+        f, err := os.Create(tmp.Name())
+        if err != nil {
+                fmt.Println(err)
+                return
+        }
+        defer f.Close()
+
+        if verbose {
+                log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name())
+        }
+
+        if err = writefile(f, r.Body, r.ContentLength); err != nil {
+                http.Error(w, "Internal error", http.StatusInternalServerError)
+                defer os.Remove(tmp.Name())
+                return
+        }
+        writemeta(tmp.Name(), conf.expiry)
+
+        resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
+        w.Write([]byte(resp + "\r\n"))
+}
diff --git a/branches/origin/cmd/marisa/usergroupids.go b/branches/origin/cmd/marisa/usergroupids.go
new file mode 100644 (file)
index 0000000..758581c
--- /dev/null
@@ -0,0 +1,26 @@
+package main
+
+import (
+       "os/user"
+       "strconv"
+)
+
+func usergroupids(username string, groupname string) (int, int, error) {
+        u, err := user.Lookup(username)
+        if err != nil {
+                return -1, -1, err
+        }
+
+        uid, _ := strconv.Atoi(u.Uid)
+        gid, _ := strconv.Atoi(u.Gid)
+
+        if conf.group != "" {
+                g, err := user.LookupGroup(groupname)
+                if err != nil {
+                        return uid, -1, err
+                }
+                gid, _ = strconv.Atoi(g.Gid)
+        }
+
+        return uid, gid, nil
+}
diff --git a/branches/origin/cmd/marisa/writefile.go b/branches/origin/cmd/marisa/writefile.go
new file mode 100644 (file)
index 0000000..d5367ec
--- /dev/null
@@ -0,0 +1,38 @@
+package main
+
+import (
+       "io"
+       "os"
+)
+
+func writefile(f *os.File, s io.ReadCloser, contentlength int64) error {
+        buffer := make([]byte, 4096)
+        eof := false
+        sz := int64(0)
+
+        defer f.Sync()
+
+        for !eof {
+                n, err := s.Read(buffer)
+                if err != nil && err != io.EOF {
+                        return err
+                } else if err == io.EOF {
+                        eof = true
+                }
+
+                /* ensure we don't write more than expected */
+                r := int64(n)
+                if sz+r > contentlength {
+                        r = contentlength - sz
+                        eof = true
+                }
+
+                _, err = f.Write(buffer[:r])
+                if err != nil {
+                        return err
+                }
+                sz += r
+        }
+
+        return nil
+}
diff --git a/branches/origin/cmd/marisa/writemeta.go b/branches/origin/cmd/marisa/writemeta.go
new file mode 100644 (file)
index 0000000..c63d9c8
--- /dev/null
@@ -0,0 +1,46 @@
+package main
+
+import (
+       "encoding/json"
+       "log"
+       "os"
+       "path/filepath"
+       "time"
+)
+
+func writemeta(filename string, expiry int64) error {
+
+        f, _ := os.Open(filename)
+        stat, _ := f.Stat()
+        size := stat.Size()
+        f.Close()
+
+        if expiry < 0 {
+                expiry = conf.expiry
+        }
+
+        meta := metadata{
+                Filename: filepath.Base(filename),
+                Size:     size,
+                Expiry:   time.Now().Unix() + expiry,
+        }
+
+        if verbose {
+                log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json")
+        }
+
+        f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json")
+        if err != nil {
+                return err
+        }
+        defer f.Close()
+
+        j, err := json.Marshal(meta)
+        if err != nil {
+                return err
+        }
+
+       _, err = f.Write(j)
+
+       return err
+}
diff --git a/branches/origin/example/marisa.conf b/branches/origin/example/marisa.conf
new file mode 100644 (file)
index 0000000..334cf7d
--- /dev/null
@@ -0,0 +1,35 @@
+[marisa]
+# TCP or Unix socket to listen on.
+# When the Unix socket is used, the content will be served through FastCGI
+# listen = /var/run/marisa.sock
+# listen = 127.0.0.1:9000
+
+# Drop privilege to the user and group specified.
+# When only the user is specified, the default group of the user
+# will be used.
+# user = www
+# group = www
+
+# Change the root directory to the following directory.
+# When a chroot(2) is set, all paths must be given according to it.
+# Note: the configuration file is read before it happens
+# chroot =
+[www]
+# baseuri = http://127.0.0.1:9000
+
+# Path to the resources used by the server, must take into account
+# the chroot is set
+# rootdir = ./static
+# tmplpath = ./templates
+# filepath = ./files
+# metapath = ./meta
+
+# URI context that files will be served on
+# filectx = /f/
+
+# Maximum per-file upload size (in bytes)
+# maxsize = 536870912 # 512 MiB
+
+# Default expiration time (in seconds).
+# An expiration time of 0 seconds means no expiration.
+# expiry = 86400 # 24 hours
diff --git a/branches/origin/example/static/favicon.ico b/branches/origin/example/static/favicon.ico
new file mode 100644 (file)
index 0000000..9aed90e
Binary files /dev/null and b/branches/origin/example/static/favicon.ico differ
diff --git a/branches/origin/example/static/marisa.css b/branches/origin/example/static/marisa.css
new file mode 100644 (file)
index 0000000..ed5156d
--- /dev/null
@@ -0,0 +1,15 @@
+body {
+    background-color: #282c37;
+    color: #f8f8f2;
+    font-family: sans-serif;
+    text-align: center;
+}
+a {
+    color: #272822;
+}
+a:hover, a:link {
+    color: #e6db74;
+}
+a:visited {
+    color: #66d9ef;
+ }
diff --git a/branches/origin/example/static/marisa.png b/branches/origin/example/static/marisa.png
new file mode 100644 (file)
index 0000000..4636a72
Binary files /dev/null and b/branches/origin/example/static/marisa.png differ
diff --git a/branches/origin/example/static/robots.txt b/branches/origin/example/static/robots.txt
new file mode 100644 (file)
index 0000000..c6742d8
--- /dev/null
@@ -0,0 +1,2 @@
+User-Agent: *
+Disallow: /
diff --git a/branches/origin/example/templates/index.html b/branches/origin/example/templates/index.html
new file mode 100644 (file)
index 0000000..2444493
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML PUBLIC "//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <link rel="icon" href="/favicon.ico">
+    <link rel="stylesheet" href="/marisa.css">
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <meta name="author" content="Izuru Yakumo">
+    <meta name="viewport" content="width=device-width">
+    <title>Marisa</title>
+  </head>
+  <body>
+    <table>
+      <thead>
+       <img class="logo" src="/marisa.png">
+       <br>
+       <h1>Marisa</h1>
+      </thead>
+      <tbody>
+       <form enctype="multipart/form-data" method="POST">
+         <input class="file" name="file" type="file"><br>
+         <input name="output" type="hidden" value="html"><br>
+         <input type="submit"><br>
+         <label for="expiry">Destroy after</label>
+         <select name="expiry">
+           <option value="900">15 minutes</option>
+           <option value="3600">1 hour</option>
+           <option value="28800">8 hours</option>
+           <option value="86400">1 day</option>
+           <option value="604800">1 week</option>
+         </select>
+       </form>
+       <p>
+         File size limited to {{.Maxsize}}.
+       </p>
+      </tbody>
+      <tfoot>
+       {{if .Links}}
+       <tr>
+         {{range .Links}}<td><a href="{{.}}">{{.}}</a></td>{{end}}
+       </tr>
+       {{end}}
+      </tfoot>
+    </table>
+  </body>
+</html>
diff --git a/branches/origin/go.mod b/branches/origin/go.mod
new file mode 100644 (file)
index 0000000..d989832
--- /dev/null
@@ -0,0 +1,10 @@
+module marisa.chaotic.ninja/marisa
+
+go 1.17
+
+require (
+       github.com/dustin/go-humanize v1.0.0
+       gopkg.in/ini.v1 v1.63.2
+)
+
+require github.com/stretchr/testify v1.8.4 // indirect
diff --git a/branches/origin/go.sum b/branches/origin/go.sum
new file mode 100644 (file)
index 0000000..56d5331
--- /dev/null
@@ -0,0 +1,20 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c=
+gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/branches/origin/marisa-trash.1 b/branches/origin/marisa-trash.1
new file mode 100644 (file)
index 0000000..31a9ff8
--- /dev/null
@@ -0,0 +1,44 @@
+.Dd $Mdocdate$
+.Dt MARISA-TRASH 1
+.Os
+.Sh NAME
+.Nm marisa-trash
+.Nd Purge expired share files
+.Sh SYNOPSIS
+.Nm marisa-trash
+.Op Fl v
+.Op Fl f Ar files
+.Op Fl m Ar metadata
+.Sh DESCRIPTION
+Upon each run,
+.Nm
+will check expiration times for files in the
+.Pa metadata
+directory, and delete the according file in the 
+.Pa files
+directory if the expiration time has passed.
+.Pp
+.Nm
+is best run as a
+.Xr cron 8
+job, as the same user as the
+.Xr marisa 1
+daemon.
+.Bl -tag -width Ds
+.It Fl v
+Turn on verbose logging to
+.Pa stderr
+.It Fl f Ar files
+Set the location of actual files to
+.Pa files
+.It Fl m Ar metadata
+Lookup metadata files in directory
+.Pa metadata
+.El
+.Sh SEE ALSO
+.Xr marisa 1
+.Sh AUTHOR
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
diff --git a/branches/origin/marisa.1 b/branches/origin/marisa.1
new file mode 100644 (file)
index 0000000..a45428e
--- /dev/null
@@ -0,0 +1,43 @@
+.Dd $Mdocdate$
+.Dt MARISA 1
+.Os
+.Sh NAME
+.Nm marisa
+.Nd HTTP based file upload system
+.Sh SYNOPSIS
+.Nm marisa
+.Op Fl v
+.Op Fl f Ar file
+.Sh DESCRIPTION
+.Nm
+is an HTTP server that permits temporary file uploads using PUT and
+POST requests.
+.Pp
+Files uploaded are saved in a single directory and given random names
+while retaining their original extension.
+A configurable expiration time is set for each file, that can be used
+to cleanup expired files thanks to
+.Xr marisa-trash 1 .
+.Bl -tag -width Ds
+.It Fl v
+Turn on verbose logging to
+.Pa stderr
+.It Fl f Ar file
+Load configuration from
+.Pa file
+.El
+.Sh SEE ALSO
+.Xr marisa-trash 1 ,
+.Xr marisa.conf 5
+.Sh AUTHORS
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
+.Sh BUGS
+If you upload a file through the browser, and refresh the
+page, the file will get constantly reuploaded, which may
+exhaust the server's storage at some point.
+.Pp
+This shouldn't happen with a CLI, such as
+.Xr curl 1
diff --git a/branches/origin/marisa.conf.5 b/branches/origin/marisa.conf.5
new file mode 100644 (file)
index 0000000..03c4d6e
--- /dev/null
@@ -0,0 +1,94 @@
+.Dd $Mdocdate$
+.Dt MARISA.CONF 5
+.Os
+.Sh NAME
+.Nm marisa.conf
+.Nd marisa configuration file format
+.Sh DESCRIPTION
+.Nm
+is the configuration file for the HTTP file sharing system,
+.Xr marisa 1 .
+.Sh CONFIGURATION
+Here are the settings that can be set:
+.Bl -tag -width Ds
+.It Ic listen Ar socket
+Have the program listen on
+.Ar socket .
+This socket can be specified either as a TCP socket:
+.Ar host:port
+or as a Unix socket:
+.Ar /path/to/marisa.sock .
+When using Unix sockets, the program will serve content using the
+.Em FastCGI
+protocol.
+.It Ic user Ar user
+Username that the program will drop privileges to upon startup. When
+using Unix sockets, the owner of the socket will be changed to this user.
+.It Ic group Ar group
+Group that the program will drop privileges to upon startup (require that
+.Ic user
+is set). When using Unix sockets, the owner group of the socket will be
+changed to this group.
+.It Ic chroot Pa dir
+Directory to chroot into upon startup. When specified, all other path
+must be set within the chroot directory.
+.It Ic baseuri Ar uri
+Base URI to use when constructing hyper links.
+.It Ic rootdir Pa dir
+Directory containing static files.
+.It Ic tmplpath Pa dir
+Directory containing template files.
+.It Ic filepath Pa dir
+Directory where uploaded files must be written to.
+.It Ic metapath Pa dir
+Directory where metadata for uploaded files will be saved.
+.It Ic filectx Pa context
+URI context to use for serving files.
+.It Ic maxsize Ar size
+Maximum size per file to accept for uploads.
+.It Ic expiry Ar time
+Default expiration time to set for uploads.
+.El
+.Sh EXAMPLE
+Configuration suitable for use with
+.Xr httpd 8
+using fastcgi:
+.Bd -literal -offset indent
+listen      = /run/marisa.sock
+baseuri     = https://domain.tld
+user        = www
+group       = daemon
+chroot      = /var/www
+rootdir     = /htdocs/static
+filepath    = /htdocs/files
+metapath    = /htdocs/meta
+tmplpath    = /htdocs/templates
+filectx     = /d/
+maxsize     = 10737418240 # 10 Gib
+expiry      = 86400       # 24 hours
+.Ed
+
+Mathing
+.Xr httpd.conf 5
+configuration:
+.Bd -literal -offset indent
+server "domain.tld" {
+       listen on * tls port 443
+       connection { max request body 10737418240 }
+       location "*" {
+               fastcgi socket "/run/marisa.sock"
+       }
+}
+types { include "/usr/share/misc/mime.types" }
+.Ed
+
+.Sh SEE ALSO
+.Xr marisa 1 ,
+.Xr marisa-trash 1 ,
+.Xr httpd 8,
+.Xr httpd.conf 5
+.Sh AUTHORS
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
diff --git a/branches/origin/version.go b/branches/origin/version.go
new file mode 100644 (file)
index 0000000..611fd43
--- /dev/null
@@ -0,0 +1,18 @@
+package marisa
+
+import (
+       "fmt"
+)
+
+var (
+       // Version release version
+       Version = "0.0.1"
+
+       // Commit will be overwritten automatically by the build system
+       Commit = "HEAD"
+)
+
+// FullVersion display the full version and build
+func FullVersion() string {
+       return fmt.Sprintf("%s@%s", Version, Commit)
+}
diff --git a/trunk/.gitignore b/trunk/.gitignore
new file mode 100644 (file)
index 0000000..97a6b1f
--- /dev/null
@@ -0,0 +1,2 @@
+/marisa
+/marisa-trash
diff --git a/trunk/COPYING b/trunk/COPYING
new file mode 100644 (file)
index 0000000..196d9bc
--- /dev/null
@@ -0,0 +1,14 @@
+Copyright (c) 2021 Willy Goiffon <contact@z3bra.org>
+Copyright (c) 2023-present Izuru Yakumo <postmaster@chaotic.ninja>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/trunk/LICENSE b/trunk/LICENSE
new file mode 100644 (file)
index 0000000..196d9bc
--- /dev/null
@@ -0,0 +1,14 @@
+Copyright (c) 2021 Willy Goiffon <contact@z3bra.org>
+Copyright (c) 2023-present Izuru Yakumo <postmaster@chaotic.ninja>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/trunk/Makefile b/trunk/Makefile
new file mode 100644 (file)
index 0000000..aac2610
--- /dev/null
@@ -0,0 +1,25 @@
+GO ?= go
+GOFLAGS ?= -v -ldflags "-w -X `go list`.Version=${VERSION} -X `go list`.Commit=${COMMIT} -X `go list`.Build=${BUILD}"
+CGO ?= 0
+
+VERSION = `git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION"`
+COMMIT = `git rev-parse --short HEAD || echo "$COMMIT"`
+BRANCH = `git rev-parse --abbrev-ref HEAD`
+BUILD = `git show -s --pretty=format:%cI`
+
+PREFIX ?= /usr/local
+
+all: marisa marisa-trash
+
+marisa:
+       CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa
+marisa-trash:
+       CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa-trash
+clean:
+       rm -f marisa marisa-trash
+install:
+       install -Dm0755 marisa ${PREFIX}/bin/marisa
+       install -Dm0755 marisa-trash ${PREFIX}/bin/marisa-trash
+       install -Dm0644 marisa.1 ${PREFIX}/share/man/man1/marisa.1
+       install -Dm0644 marisa.conf.5 ${PREFIX}/share/man/man5/marisa.conf.5
+.PHONY: marisa marisa-trash
diff --git a/trunk/README.md b/trunk/README.md
new file mode 100644 (file)
index 0000000..e487c7d
--- /dev/null
@@ -0,0 +1,36 @@
+marisa
+======
+HTTP based File upload system.
+
+Features
+--------
++ Link expiration
++ Mimetype support
++ Random filenames
++ Multiple file uploads
++ Javascript not needed
++ Privilege drop
++ chroot(2) support
++ FastCGI support
+
+Usage
+-----
+Refer to the marisa(1) manual page for details and examples.
+
+       marisa [-v] [-f marisa.conf]
+
+Configuration is done through its configuration file, marisa.conf(5).
+The format is that of the INI file format.
+
+Uploading files is done via PUT and POST requests. Multiple files can
+be sent via POST requests.
+
+       curl -T file.png http://domain.tld
+       curl -F file=file.png -F expiry=3600 http://domain.tld
+
+Installation
+------------
+Edit the `config.mk` file to match your setup, then run the following:
+
+        $ (b)make
+        # (b)make install
diff --git a/trunk/cmd/marisa-trash/main.go b/trunk/cmd/marisa-trash/main.go
new file mode 100644 (file)
index 0000000..e010dd0
--- /dev/null
@@ -0,0 +1,101 @@
+package main
+
+import (
+       "log"
+       "flag"
+       "os"
+       "time"
+       "path/filepath"
+       "encoding/json"
+       
+       "github.com/dustin/go-humanize"
+)
+
+type metadata struct {
+       Filename string
+       Size int64
+       Expiry int64
+}
+
+var conf struct {
+       filepath string
+       metapath string
+}
+
+var verbose bool
+var count int64
+var deleted int64
+var size int64
+
+func readmeta(filename string) (metadata, error) {
+       j, err := os.ReadFile(filename)
+       if err != nil {
+               return metadata{}, err
+       }
+
+       var meta metadata
+       err = json.Unmarshal(j, &meta)
+       if err != nil {
+               return metadata{}, err
+       }
+
+       return meta, nil
+}
+
+func checkexpiry(path string, info os.FileInfo, err error) error {
+       if filepath.Ext(path) != ".json" {
+               return nil
+       }
+       meta, err := readmeta(path)
+       if err != nil {
+               log.Fatal(err)
+       }
+
+
+       count++
+
+       now := time.Now().Unix()
+       if verbose {
+               log.Printf("now: %s, expiry: %s\n", now, meta.Expiry);
+       }
+
+       if meta.Expiry > 0 && now >= meta.Expiry {
+               if verbose {
+                       expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
+                       log.Printf("%s/%s: expired %s\n", conf.filepath, meta.Filename, expiration)
+               }
+               if err = os.Remove(conf.filepath + "/" + meta.Filename); err != nil {
+                       log.Fatal(err)
+               }
+               if err = os.Remove(path); err != nil {
+                       log.Fatal(err)
+               }
+               deleted++
+               return nil
+       } else {
+               if verbose {
+                       expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
+                       log.Printf("%s/%s: expire in %s\n", conf.filepath, meta.Filename, expiration)
+               }
+               size += meta.Size
+       }
+
+       return nil
+}
+
+func main() {
+       flag.BoolVar(&verbose,         "v", false, "Verbose logging")
+       flag.StringVar(&conf.filepath, "f", "./files", "Directory containing files")
+       flag.StringVar(&conf.metapath, "m", "./meta", "Directory containing metadata")
+
+       flag.Parse()
+
+       err := filepath.Walk(conf.metapath, checkexpiry)
+       if err != nil {
+               log.Fatal(err)
+       }
+
+       if verbose && count > 0 {
+               log.Printf("%d/%d file(s) deleted (remaining: %s)", deleted, count, humanize.IBytes(uint64(size)))
+       }
+}
diff --git a/trunk/cmd/marisa/main.go b/trunk/cmd/marisa/main.go
new file mode 100644 (file)
index 0000000..0f74d7e
--- /dev/null
@@ -0,0 +1,141 @@
+package main
+
+import (
+       "flag"
+       "log"
+       "net"
+       "net/http"
+       "net/http/fcgi"
+       "os"
+       "os/signal"
+       "syscall"
+
+       "marisa.chaotic.ninja/marisa"
+)
+
+type templatedata struct {
+       Links   []string
+       Size    string
+       Maxsize string
+}
+
+type metadata struct {
+       Filename string
+       Size     int64
+       Expiry   int64
+}
+
+var conf struct {
+       user     string
+       group    string
+       chroot   string
+       listen   string
+       baseuri  string
+       rootdir  string
+       tmplpath string
+       filepath string
+       metapath string
+       filectx  string
+       maxsize  int64
+       expiry   int64
+}
+
+var verbose bool
+
+func main() {
+       var err error
+       var configfile string
+       var listener net.Listener
+
+       /* default values */
+       conf.listen = "127.0.0.1:8080"
+       conf.baseuri = "http://127.0.0.1:8080"
+       conf.rootdir = "static"
+       conf.tmplpath = "templates"
+       conf.filepath = "files"
+       conf.metapath = "meta"
+       conf.filectx = "/f/"
+       conf.maxsize = 34359738368
+       conf.expiry = 86400
+
+       flag.StringVar(&configfile, "f", "", "Configuration file")
+       flag.BoolVar(&verbose, "v", false, "Verbose logging")
+       flag.Parse()
+
+       if configfile != "" {
+               if verbose {
+                       log.Printf("Reading configuration %s", configfile)
+               }
+               parseconfig(configfile)
+       }
+
+       if conf.chroot != "" {
+               if verbose {
+                       log.Printf("Changing root to %s", conf.chroot)
+               }
+               syscall.Chroot(conf.chroot)
+       }
+
+       if conf.listen[0] == '/' {
+               /* Remove any stale socket */
+               os.Remove(conf.listen)
+               if listener, err = net.Listen("unix", conf.listen); err != nil {
+                       log.Fatal(err)
+               }
+               defer listener.Close()
+
+               /*
+                * Ensure unix socket is removed on exit.
+                * Note: this might not work when dropping privileges…
+                */
+               defer os.Remove(conf.listen)
+               sigs := make(chan os.Signal, 1)
+               signal.Notify(sigs, os.Interrupt, os.Kill, syscall.SIGTERM)
+               go func() {
+                       _ = <-sigs
+                       listener.Close()
+                       if err = os.Remove(conf.listen); err != nil {
+                               log.Fatal(err)
+                       }
+                       os.Exit(0)
+               }()
+       } else {
+               if listener, err = net.Listen("tcp", conf.listen); err != nil {
+                       log.Fatal(err)
+               }
+               defer listener.Close()
+       }
+
+       if conf.user != "" {
+               if verbose {
+                       log.Printf("Dropping privileges to %s", conf.user)
+               }
+               uid, gid, err := usergroupids(conf.user, conf.group)
+               if err != nil {
+                       log.Fatal(err)
+               }
+
+               if listener.Addr().Network() == "unix" {
+                       os.Chown(conf.listen, uid, gid)
+               }
+
+               syscall.Setuid(uid)
+               syscall.Setgid(gid)
+       }
+
+       http.HandleFunc("/", uploader)
+       http.Handle(conf.filectx, http.StripPrefix(conf.filectx, http.FileServer(http.Dir(conf.filepath))))
+
+       if verbose {
+               log.Printf("Starting marisa %v\n", marisa.FullVersion())
+               log.Printf("Listening on %s", conf.listen)
+       }
+
+       if listener.Addr().Network() == "unix" {
+               err = fcgi.Serve(listener, nil)
+               log.Fatal(err) /* NOTREACHED */
+       }
+
+       err = http.Serve(listener, nil)
+       log.Fatal(err) /* NOTREACHED */
+}
diff --git a/trunk/cmd/marisa/parseconfig.go b/trunk/cmd/marisa/parseconfig.go
new file mode 100644 (file)
index 0000000..d08cd83
--- /dev/null
@@ -0,0 +1,27 @@
+package main
+
+import (
+       "gopkg.in/ini.v1"
+)
+
+func parseconfig(file string) error {
+        cfg, err := ini.Load(file)
+        if err != nil {
+                return err
+        }
+
+        conf.listen = cfg.Section("marisa").Key("listen").String()
+        conf.user = cfg.Section("marisa").Key("user").String()
+        conf.group = cfg.Section("marisa").Key("group").String()
+        conf.baseuri = cfg.Section("www").Key("baseuri").String()
+        conf.filepath = cfg.Section("www").Key("filepath").String()
+        conf.metapath = cfg.Section("www").Key("metapath").String()
+        conf.filectx = cfg.Section("www").Key("filectx").String()
+        conf.rootdir = cfg.Section("www").Key("rootdir").String()
+        conf.chroot = cfg.Section("marisa").Key("chroot").String()
+        conf.tmplpath = cfg.Section("www").Key("tmplpath").String()
+        conf.maxsize, _ = cfg.Section("www").Key("maxsize").Int64()
+        conf.expiry, _ = cfg.Section("www").Key("expiry").Int64()
+
+        return nil
+}
diff --git a/trunk/cmd/marisa/servetemplate.go b/trunk/cmd/marisa/servetemplate.go
new file mode 100644 (file)
index 0000000..f092f76
--- /dev/null
@@ -0,0 +1,25 @@
+package main
+
+import (
+       "fmt"
+       "html/template"
+       "log"
+       "net/http"
+)
+
+func servetemplate(w http.ResponseWriter, f string, d templatedata) {
+        t, err := template.ParseFiles(conf.tmplpath + "/" + f)
+        if err != nil {
+                http.Error(w, "Internal error", http.StatusInternalServerError)
+                return
+        }
+
+        if verbose {
+                log.Printf("Serving template %s", t.Name())
+        }
+
+        err = t.Execute(w, d)
+        if err != nil {
+                fmt.Println(err)
+        }
+}
diff --git a/trunk/cmd/marisa/uploader.go b/trunk/cmd/marisa/uploader.go
new file mode 100644 (file)
index 0000000..f071449
--- /dev/null
@@ -0,0 +1,23 @@
+package main
+
+import (
+       "log"
+       "net/http"
+)
+
+func uploader(w http.ResponseWriter, r *http.Request) {
+        if verbose {
+                log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
+        }
+
+        switch r.Method {
+        case "DELETE":
+                uploaderDelete(w, r)
+        case "POST":
+                uploaderPost(w, r)
+        case "PUT":
+                uploaderPut(w, r)
+        case "GET":
+                uploaderGet(w, r)
+        }
+}
diff --git a/trunk/cmd/marisa/uploaderdelete.go b/trunk/cmd/marisa/uploaderdelete.go
new file mode 100644 (file)
index 0000000..1369e6f
--- /dev/null
@@ -0,0 +1,28 @@
+package main
+
+import (
+       "log"
+       "net/http"
+       "os"
+)
+
+func uploaderDelete(w http.ResponseWriter, r *http.Request) {
+        // r.URL.Path is sanitized regarding "." and ".."
+        filename := r.URL.Path
+        filepath := conf.filepath + filename
+
+        if verbose {
+                log.Printf("Deleting file %s", filepath)
+        }
+
+        f, err := os.Open(filepath)
+        if err != nil {
+                http.NotFound(w, r)
+                return
+        }
+        f.Close()
+
+        // Force file expiration
+        writemeta(filepath, 0)
+        w.WriteHeader(http.StatusNoContent)
+}
diff --git a/trunk/cmd/marisa/uploaderget.go b/trunk/cmd/marisa/uploaderget.go
new file mode 100644 (file)
index 0000000..4b5defa
--- /dev/null
@@ -0,0 +1,24 @@
+package main
+
+import (
+       "log"
+       "net/http"
+
+       "github.com/dustin/go-humanize"
+)
+
+func uploaderGet(w http.ResponseWriter, r *http.Request) {
+        // r.URL.Path is sanitized regarding "." and ".."
+        filename := r.URL.Path
+        if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+                data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
+                servetemplate(w, "/index.html", data)
+                return
+        }
+
+        if verbose {
+                log.Printf("Serving file %s", conf.rootdir+filename)
+        }
+
+        http.ServeFile(w, r, conf.rootdir+filename)
+}
diff --git a/trunk/cmd/marisa/uploaderpost.go b/trunk/cmd/marisa/uploaderpost.go
new file mode 100644 (file)
index 0000000..9ecb6ae
--- /dev/null
@@ -0,0 +1,71 @@
+package main
+
+import (
+       "encoding/json"
+       "io/ioutil"
+       "net/http"
+       "os"
+       "path"
+       "path/filepath"
+       "strconv"
+
+       "github.com/dustin/go-humanize"
+)
+
+func uploaderPost(w http.ResponseWriter, r *http.Request) {
+        /* read 32Mb at a time */
+        r.ParseMultipartForm(32 << 20)
+
+        links := []string{}
+        for _, h := range r.MultipartForm.File["file"] {
+                if h.Size > conf.maxsize {
+                        http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
+                        return
+                }
+
+                post, err := h.Open()
+                if err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        return
+                }
+                defer post.Close()
+
+                tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename))
+                f, err := os.Create(tmp.Name())
+                if err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        return
+                }
+                defer f.Close()
+
+                if err = writefile(f, post, h.Size); err != nil {
+                        http.Error(w, "Internal error", http.StatusInternalServerError)
+                        defer os.Remove(tmp.Name())
+                        return
+                }
+                expiry, err := strconv.Atoi(r.PostFormValue("expiry"))
+                if err != nil || expiry < 0 {
+                        expiry = int(conf.expiry)
+                }
+                writemeta(tmp.Name(), int64(expiry))
+
+                link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
+                links = append(links, link)
+        }
+
+        switch r.PostFormValue("output") {
+        case "html":
+                data := templatedata{
+                        Maxsize: humanize.IBytes(uint64(conf.maxsize)),
+                        Links:   links,
+                }
+                servetemplate(w, "/index.html", data)
+        case "json":
+                data, _ := json.Marshal(links)
+                w.Write(data)
+        default:
+                for _, link := range links {
+                        w.Write([]byte(link + "\r\n"))
+                }
+        }
+}
diff --git a/trunk/cmd/marisa/uploaderput.go b/trunk/cmd/marisa/uploaderput.go
new file mode 100644 (file)
index 0000000..51723e5
--- /dev/null
@@ -0,0 +1,40 @@
+package main
+
+import (
+       "fmt"
+       "io/ioutil"
+       "log"
+       "net/http"
+       "os"
+       "path"
+       "path/filepath"
+)
+
+func uploaderPut(w http.ResponseWriter, r *http.Request) {
+        /* limit upload size */
+        if r.ContentLength > conf.maxsize {
+                http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
+        }
+
+        tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path))
+        f, err := os.Create(tmp.Name())
+        if err != nil {
+                fmt.Println(err)
+                return
+        }
+        defer f.Close()
+
+        if verbose {
+                log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name())
+        }
+
+        if err = writefile(f, r.Body, r.ContentLength); err != nil {
+                http.Error(w, "Internal error", http.StatusInternalServerError)
+                defer os.Remove(tmp.Name())
+                return
+        }
+        writemeta(tmp.Name(), conf.expiry)
+
+        resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
+        w.Write([]byte(resp + "\r\n"))
+}
diff --git a/trunk/cmd/marisa/usergroupids.go b/trunk/cmd/marisa/usergroupids.go
new file mode 100644 (file)
index 0000000..758581c
--- /dev/null
@@ -0,0 +1,26 @@
+package main
+
+import (
+       "os/user"
+       "strconv"
+)
+
+func usergroupids(username string, groupname string) (int, int, error) {
+        u, err := user.Lookup(username)
+        if err != nil {
+                return -1, -1, err
+        }
+
+        uid, _ := strconv.Atoi(u.Uid)
+        gid, _ := strconv.Atoi(u.Gid)
+
+        if conf.group != "" {
+                g, err := user.LookupGroup(groupname)
+                if err != nil {
+                        return uid, -1, err
+                }
+                gid, _ = strconv.Atoi(g.Gid)
+        }
+
+        return uid, gid, nil
+}
diff --git a/trunk/cmd/marisa/writefile.go b/trunk/cmd/marisa/writefile.go
new file mode 100644 (file)
index 0000000..d5367ec
--- /dev/null
@@ -0,0 +1,38 @@
+package main
+
+import (
+       "io"
+       "os"
+)
+
+func writefile(f *os.File, s io.ReadCloser, contentlength int64) error {
+        buffer := make([]byte, 4096)
+        eof := false
+        sz := int64(0)
+
+        defer f.Sync()
+
+        for !eof {
+                n, err := s.Read(buffer)
+                if err != nil && err != io.EOF {
+                        return err
+                } else if err == io.EOF {
+                        eof = true
+                }
+
+                /* ensure we don't write more than expected */
+                r := int64(n)
+                if sz+r > contentlength {
+                        r = contentlength - sz
+                        eof = true
+                }
+
+                _, err = f.Write(buffer[:r])
+                if err != nil {
+                        return err
+                }
+                sz += r
+        }
+
+        return nil
+}
diff --git a/trunk/cmd/marisa/writemeta.go b/trunk/cmd/marisa/writemeta.go
new file mode 100644 (file)
index 0000000..c63d9c8
--- /dev/null
@@ -0,0 +1,46 @@
+package main
+
+import (
+       "encoding/json"
+       "log"
+       "os"
+       "path/filepath"
+       "time"
+)
+
+func writemeta(filename string, expiry int64) error {
+
+        f, _ := os.Open(filename)
+        stat, _ := f.Stat()
+        size := stat.Size()
+        f.Close()
+
+        if expiry < 0 {
+                expiry = conf.expiry
+        }
+
+        meta := metadata{
+                Filename: filepath.Base(filename),
+                Size:     size,
+                Expiry:   time.Now().Unix() + expiry,
+        }
+
+        if verbose {
+                log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json")
+        }
+
+        f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json")
+        if err != nil {
+                return err
+        }
+        defer f.Close()
+
+        j, err := json.Marshal(meta)
+        if err != nil {
+                return err
+        }
+
+       _, err = f.Write(j)
+
+       return err
+}
diff --git a/trunk/example/marisa.conf b/trunk/example/marisa.conf
new file mode 100644 (file)
index 0000000..334cf7d
--- /dev/null
@@ -0,0 +1,35 @@
+[marisa]
+# TCP or Unix socket to listen on.
+# When the Unix socket is used, the content will be served through FastCGI
+# listen = /var/run/marisa.sock
+# listen = 127.0.0.1:9000
+
+# Drop privilege to the user and group specified.
+# When only the user is specified, the default group of the user
+# will be used.
+# user = www
+# group = www
+
+# Change the root directory to the following directory.
+# When a chroot(2) is set, all paths must be given according to it.
+# Note: the configuration file is read before it happens
+# chroot =
+[www]
+# baseuri = http://127.0.0.1:9000
+
+# Path to the resources used by the server, must take into account
+# the chroot is set
+# rootdir = ./static
+# tmplpath = ./templates
+# filepath = ./files
+# metapath = ./meta
+
+# URI context that files will be served on
+# filectx = /f/
+
+# Maximum per-file upload size (in bytes)
+# maxsize = 536870912 # 512 MiB
+
+# Default expiration time (in seconds).
+# An expiration time of 0 seconds means no expiration.
+# expiry = 86400 # 24 hours
diff --git a/trunk/example/static/favicon.ico b/trunk/example/static/favicon.ico
new file mode 100644 (file)
index 0000000..9aed90e
Binary files /dev/null and b/trunk/example/static/favicon.ico differ
diff --git a/trunk/example/static/marisa.css b/trunk/example/static/marisa.css
new file mode 100644 (file)
index 0000000..ed5156d
--- /dev/null
@@ -0,0 +1,15 @@
+body {
+    background-color: #282c37;
+    color: #f8f8f2;
+    font-family: sans-serif;
+    text-align: center;
+}
+a {
+    color: #272822;
+}
+a:hover, a:link {
+    color: #e6db74;
+}
+a:visited {
+    color: #66d9ef;
+ }
diff --git a/trunk/example/static/marisa.png b/trunk/example/static/marisa.png
new file mode 100644 (file)
index 0000000..4636a72
Binary files /dev/null and b/trunk/example/static/marisa.png differ
diff --git a/trunk/example/static/robots.txt b/trunk/example/static/robots.txt
new file mode 100644 (file)
index 0000000..c6742d8
--- /dev/null
@@ -0,0 +1,2 @@
+User-Agent: *
+Disallow: /
diff --git a/trunk/example/templates/index.html b/trunk/example/templates/index.html
new file mode 100644 (file)
index 0000000..2444493
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML PUBLIC "//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <link rel="icon" href="/favicon.ico">
+    <link rel="stylesheet" href="/marisa.css">
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <meta name="author" content="Izuru Yakumo">
+    <meta name="viewport" content="width=device-width">
+    <title>Marisa</title>
+  </head>
+  <body>
+    <table>
+      <thead>
+       <img class="logo" src="/marisa.png">
+       <br>
+       <h1>Marisa</h1>
+      </thead>
+      <tbody>
+       <form enctype="multipart/form-data" method="POST">
+         <input class="file" name="file" type="file"><br>
+         <input name="output" type="hidden" value="html"><br>
+         <input type="submit"><br>
+         <label for="expiry">Destroy after</label>
+         <select name="expiry">
+           <option value="900">15 minutes</option>
+           <option value="3600">1 hour</option>
+           <option value="28800">8 hours</option>
+           <option value="86400">1 day</option>
+           <option value="604800">1 week</option>
+         </select>
+       </form>
+       <p>
+         File size limited to {{.Maxsize}}.
+       </p>
+      </tbody>
+      <tfoot>
+       {{if .Links}}
+       <tr>
+         {{range .Links}}<td><a href="{{.}}">{{.}}</a></td>{{end}}
+       </tr>
+       {{end}}
+      </tfoot>
+    </table>
+  </body>
+</html>
diff --git a/trunk/go.mod b/trunk/go.mod
new file mode 100644 (file)
index 0000000..d989832
--- /dev/null
@@ -0,0 +1,10 @@
+module marisa.chaotic.ninja/marisa
+
+go 1.17
+
+require (
+       github.com/dustin/go-humanize v1.0.0
+       gopkg.in/ini.v1 v1.63.2
+)
+
+require github.com/stretchr/testify v1.8.4 // indirect
diff --git a/trunk/go.sum b/trunk/go.sum
new file mode 100644 (file)
index 0000000..56d5331
--- /dev/null
@@ -0,0 +1,20 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c=
+gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/trunk/marisa-trash.1 b/trunk/marisa-trash.1
new file mode 100644 (file)
index 0000000..31a9ff8
--- /dev/null
@@ -0,0 +1,44 @@
+.Dd $Mdocdate$
+.Dt MARISA-TRASH 1
+.Os
+.Sh NAME
+.Nm marisa-trash
+.Nd Purge expired share files
+.Sh SYNOPSIS
+.Nm marisa-trash
+.Op Fl v
+.Op Fl f Ar files
+.Op Fl m Ar metadata
+.Sh DESCRIPTION
+Upon each run,
+.Nm
+will check expiration times for files in the
+.Pa metadata
+directory, and delete the according file in the 
+.Pa files
+directory if the expiration time has passed.
+.Pp
+.Nm
+is best run as a
+.Xr cron 8
+job, as the same user as the
+.Xr marisa 1
+daemon.
+.Bl -tag -width Ds
+.It Fl v
+Turn on verbose logging to
+.Pa stderr
+.It Fl f Ar files
+Set the location of actual files to
+.Pa files
+.It Fl m Ar metadata
+Lookup metadata files in directory
+.Pa metadata
+.El
+.Sh SEE ALSO
+.Xr marisa 1
+.Sh AUTHOR
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
diff --git a/trunk/marisa.1 b/trunk/marisa.1
new file mode 100644 (file)
index 0000000..a45428e
--- /dev/null
@@ -0,0 +1,43 @@
+.Dd $Mdocdate$
+.Dt MARISA 1
+.Os
+.Sh NAME
+.Nm marisa
+.Nd HTTP based file upload system
+.Sh SYNOPSIS
+.Nm marisa
+.Op Fl v
+.Op Fl f Ar file
+.Sh DESCRIPTION
+.Nm
+is an HTTP server that permits temporary file uploads using PUT and
+POST requests.
+.Pp
+Files uploaded are saved in a single directory and given random names
+while retaining their original extension.
+A configurable expiration time is set for each file, that can be used
+to cleanup expired files thanks to
+.Xr marisa-trash 1 .
+.Bl -tag -width Ds
+.It Fl v
+Turn on verbose logging to
+.Pa stderr
+.It Fl f Ar file
+Load configuration from
+.Pa file
+.El
+.Sh SEE ALSO
+.Xr marisa-trash 1 ,
+.Xr marisa.conf 5
+.Sh AUTHORS
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
+.Sh BUGS
+If you upload a file through the browser, and refresh the
+page, the file will get constantly reuploaded, which may
+exhaust the server's storage at some point.
+.Pp
+This shouldn't happen with a CLI, such as
+.Xr curl 1
diff --git a/trunk/marisa.conf.5 b/trunk/marisa.conf.5
new file mode 100644 (file)
index 0000000..03c4d6e
--- /dev/null
@@ -0,0 +1,94 @@
+.Dd $Mdocdate$
+.Dt MARISA.CONF 5
+.Os
+.Sh NAME
+.Nm marisa.conf
+.Nd marisa configuration file format
+.Sh DESCRIPTION
+.Nm
+is the configuration file for the HTTP file sharing system,
+.Xr marisa 1 .
+.Sh CONFIGURATION
+Here are the settings that can be set:
+.Bl -tag -width Ds
+.It Ic listen Ar socket
+Have the program listen on
+.Ar socket .
+This socket can be specified either as a TCP socket:
+.Ar host:port
+or as a Unix socket:
+.Ar /path/to/marisa.sock .
+When using Unix sockets, the program will serve content using the
+.Em FastCGI
+protocol.
+.It Ic user Ar user
+Username that the program will drop privileges to upon startup. When
+using Unix sockets, the owner of the socket will be changed to this user.
+.It Ic group Ar group
+Group that the program will drop privileges to upon startup (require that
+.Ic user
+is set). When using Unix sockets, the owner group of the socket will be
+changed to this group.
+.It Ic chroot Pa dir
+Directory to chroot into upon startup. When specified, all other path
+must be set within the chroot directory.
+.It Ic baseuri Ar uri
+Base URI to use when constructing hyper links.
+.It Ic rootdir Pa dir
+Directory containing static files.
+.It Ic tmplpath Pa dir
+Directory containing template files.
+.It Ic filepath Pa dir
+Directory where uploaded files must be written to.
+.It Ic metapath Pa dir
+Directory where metadata for uploaded files will be saved.
+.It Ic filectx Pa context
+URI context to use for serving files.
+.It Ic maxsize Ar size
+Maximum size per file to accept for uploads.
+.It Ic expiry Ar time
+Default expiration time to set for uploads.
+.El
+.Sh EXAMPLE
+Configuration suitable for use with
+.Xr httpd 8
+using fastcgi:
+.Bd -literal -offset indent
+listen      = /run/marisa.sock
+baseuri     = https://domain.tld
+user        = www
+group       = daemon
+chroot      = /var/www
+rootdir     = /htdocs/static
+filepath    = /htdocs/files
+metapath    = /htdocs/meta
+tmplpath    = /htdocs/templates
+filectx     = /d/
+maxsize     = 10737418240 # 10 Gib
+expiry      = 86400       # 24 hours
+.Ed
+
+Mathing
+.Xr httpd.conf 5
+configuration:
+.Bd -literal -offset indent
+server "domain.tld" {
+       listen on * tls port 443
+       connection { max request body 10737418240 }
+       location "*" {
+               fastcgi socket "/run/marisa.sock"
+       }
+}
+types { include "/usr/share/misc/mime.types" }
+.Ed
+
+.Sh SEE ALSO
+.Xr marisa 1 ,
+.Xr marisa-trash 1 ,
+.Xr httpd 8,
+.Xr httpd.conf 5
+.Sh AUTHORS
+.An Willy Goiffon Aq Mt dev@z3bra.org
+.Pp
+"Borrowed" by
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
diff --git a/trunk/version.go b/trunk/version.go
new file mode 100644 (file)
index 0000000..611fd43
--- /dev/null
@@ -0,0 +1,18 @@
+package marisa
+
+import (
+       "fmt"
+)
+
+var (
+       // Version release version
+       Version = "0.0.1"
+
+       // Commit will be overwritten automatically by the build system
+       Commit = "HEAD"
+)
+
+// FullVersion display the full version and build
+func FullVersion() string {
+       return fmt.Sprintf("%s@%s", Version, Commit)
+}