--- /dev/null
+/marisa
+/marisa-trash
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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)))
+ }
+}
--- /dev/null
+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 */
+}
--- /dev/null
+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
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)
+}
--- /dev/null
+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)
+}
--- /dev/null
+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"))
+ }
+ }
+}
--- /dev/null
+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"))
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+}
--- /dev/null
+[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
--- /dev/null
+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;
+ }
--- /dev/null
+User-Agent: *
+Disallow: /
--- /dev/null
+<!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>
--- /dev/null
+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
--- /dev/null
+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=
--- /dev/null
+.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
--- /dev/null
+.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
--- /dev/null
+.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
--- /dev/null
+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)
+}
--- /dev/null
+/marisa
+/marisa-trash
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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)))
+ }
+}
--- /dev/null
+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 */
+}
--- /dev/null
+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
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)
+}
--- /dev/null
+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)
+}
--- /dev/null
+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"))
+ }
+ }
+}
--- /dev/null
+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"))
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+}
--- /dev/null
+[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
--- /dev/null
+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;
+ }
--- /dev/null
+User-Agent: *
+Disallow: /
--- /dev/null
+<!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>
--- /dev/null
+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
--- /dev/null
+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=
--- /dev/null
+.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
--- /dev/null
+.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
--- /dev/null
+.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
--- /dev/null
+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)
+}
--- /dev/null
+/marisa
+/marisa-trash
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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)))
+ }
+}
--- /dev/null
+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 */
+}
--- /dev/null
+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
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)
+}
--- /dev/null
+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)
+}
--- /dev/null
+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"))
+ }
+ }
+}
--- /dev/null
+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"))
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+}
--- /dev/null
+[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
--- /dev/null
+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;
+ }
--- /dev/null
+User-Agent: *
+Disallow: /
--- /dev/null
+<!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>
--- /dev/null
+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
--- /dev/null
+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=
--- /dev/null
+.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
--- /dev/null
+.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
--- /dev/null
+.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
--- /dev/null
+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)
+}
--- /dev/null
+/marisa
+/marisa-trash
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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)))
+ }
+}
--- /dev/null
+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 */
+}
--- /dev/null
+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
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)
+}
--- /dev/null
+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)
+}
--- /dev/null
+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"))
+ }
+ }
+}
--- /dev/null
+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"))
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+}
--- /dev/null
+[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
--- /dev/null
+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;
+ }
--- /dev/null
+User-Agent: *
+Disallow: /
--- /dev/null
+<!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>
--- /dev/null
+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
--- /dev/null
+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=
--- /dev/null
+.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
--- /dev/null
+.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
--- /dev/null
+.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
--- /dev/null
+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)
+}