From 028e57451f1d7f9df1c27d5ae2ff100f93010a22 Mon Sep 17 00:00:00 2001 From: www Date: Sun, 29 Sep 2024 21:29:49 +0000 Subject: [PATCH 1/1] Mirrored from marisa.git git-svn-id: https://svn.chaotic.ninja/svn/marisa-yakumo.izuru@1 53897a95-702d-234f-8f73-912f772065ac --- branches/master/.gitignore | 2 + branches/master/COPYING | 14 ++ branches/master/LICENSE | 14 ++ branches/master/Makefile | 25 ++++ branches/master/README.md | 36 +++++ branches/master/cmd/marisa-trash/main.go | 101 +++++++++++++ branches/master/cmd/marisa/main.go | 141 ++++++++++++++++++ branches/master/cmd/marisa/parseconfig.go | 27 ++++ branches/master/cmd/marisa/servetemplate.go | 25 ++++ branches/master/cmd/marisa/uploader.go | 23 +++ branches/master/cmd/marisa/uploaderdelete.go | 28 ++++ branches/master/cmd/marisa/uploaderget.go | 24 +++ branches/master/cmd/marisa/uploaderpost.go | 71 +++++++++ branches/master/cmd/marisa/uploaderput.go | 40 +++++ branches/master/cmd/marisa/usergroupids.go | 26 ++++ branches/master/cmd/marisa/writefile.go | 38 +++++ branches/master/cmd/marisa/writemeta.go | 46 ++++++ branches/master/example/marisa.conf | 35 +++++ branches/master/example/static/favicon.ico | Bin 0 -> 46686 bytes branches/master/example/static/marisa.css | 15 ++ branches/master/example/static/marisa.png | Bin 0 -> 25282 bytes branches/master/example/static/robots.txt | 2 + branches/master/example/templates/index.html | 45 ++++++ branches/master/go.mod | 10 ++ branches/master/go.sum | 20 +++ branches/master/marisa-trash.1 | 44 ++++++ branches/master/marisa.1 | 43 ++++++ branches/master/marisa.conf.5 | 94 ++++++++++++ branches/master/version.go | 18 +++ branches/origin-master/.gitignore | 2 + branches/origin-master/COPYING | 14 ++ branches/origin-master/LICENSE | 14 ++ branches/origin-master/Makefile | 25 ++++ branches/origin-master/README.md | 36 +++++ .../origin-master/cmd/marisa-trash/main.go | 101 +++++++++++++ branches/origin-master/cmd/marisa/main.go | 141 ++++++++++++++++++ .../origin-master/cmd/marisa/parseconfig.go | 27 ++++ .../origin-master/cmd/marisa/servetemplate.go | 25 ++++ branches/origin-master/cmd/marisa/uploader.go | 23 +++ .../cmd/marisa/uploaderdelete.go | 28 ++++ .../origin-master/cmd/marisa/uploaderget.go | 24 +++ .../origin-master/cmd/marisa/uploaderpost.go | 71 +++++++++ .../origin-master/cmd/marisa/uploaderput.go | 40 +++++ .../origin-master/cmd/marisa/usergroupids.go | 26 ++++ .../origin-master/cmd/marisa/writefile.go | 38 +++++ .../origin-master/cmd/marisa/writemeta.go | 46 ++++++ branches/origin-master/example/marisa.conf | 35 +++++ .../origin-master/example/static/favicon.ico | Bin 0 -> 46686 bytes .../origin-master/example/static/marisa.css | 15 ++ .../origin-master/example/static/marisa.png | Bin 0 -> 25282 bytes .../origin-master/example/static/robots.txt | 2 + .../example/templates/index.html | 45 ++++++ branches/origin-master/go.mod | 10 ++ branches/origin-master/go.sum | 20 +++ branches/origin-master/marisa-trash.1 | 44 ++++++ branches/origin-master/marisa.1 | 43 ++++++ branches/origin-master/marisa.conf.5 | 94 ++++++++++++ branches/origin-master/version.go | 18 +++ branches/origin/.gitignore | 2 + branches/origin/COPYING | 14 ++ branches/origin/LICENSE | 14 ++ branches/origin/Makefile | 25 ++++ branches/origin/README.md | 36 +++++ branches/origin/cmd/marisa-trash/main.go | 101 +++++++++++++ branches/origin/cmd/marisa/main.go | 141 ++++++++++++++++++ branches/origin/cmd/marisa/parseconfig.go | 27 ++++ branches/origin/cmd/marisa/servetemplate.go | 25 ++++ branches/origin/cmd/marisa/uploader.go | 23 +++ branches/origin/cmd/marisa/uploaderdelete.go | 28 ++++ branches/origin/cmd/marisa/uploaderget.go | 24 +++ branches/origin/cmd/marisa/uploaderpost.go | 71 +++++++++ branches/origin/cmd/marisa/uploaderput.go | 40 +++++ branches/origin/cmd/marisa/usergroupids.go | 26 ++++ branches/origin/cmd/marisa/writefile.go | 38 +++++ branches/origin/cmd/marisa/writemeta.go | 46 ++++++ branches/origin/example/marisa.conf | 35 +++++ branches/origin/example/static/favicon.ico | Bin 0 -> 46686 bytes branches/origin/example/static/marisa.css | 15 ++ branches/origin/example/static/marisa.png | Bin 0 -> 25282 bytes branches/origin/example/static/robots.txt | 2 + branches/origin/example/templates/index.html | 45 ++++++ branches/origin/go.mod | 10 ++ branches/origin/go.sum | 20 +++ branches/origin/marisa-trash.1 | 44 ++++++ branches/origin/marisa.1 | 43 ++++++ branches/origin/marisa.conf.5 | 94 ++++++++++++ branches/origin/version.go | 18 +++ trunk/.gitignore | 2 + trunk/COPYING | 14 ++ trunk/LICENSE | 14 ++ trunk/Makefile | 25 ++++ trunk/README.md | 36 +++++ trunk/cmd/marisa-trash/main.go | 101 +++++++++++++ trunk/cmd/marisa/main.go | 141 ++++++++++++++++++ trunk/cmd/marisa/parseconfig.go | 27 ++++ trunk/cmd/marisa/servetemplate.go | 25 ++++ trunk/cmd/marisa/uploader.go | 23 +++ trunk/cmd/marisa/uploaderdelete.go | 28 ++++ trunk/cmd/marisa/uploaderget.go | 24 +++ trunk/cmd/marisa/uploaderpost.go | 71 +++++++++ trunk/cmd/marisa/uploaderput.go | 40 +++++ trunk/cmd/marisa/usergroupids.go | 26 ++++ trunk/cmd/marisa/writefile.go | 38 +++++ trunk/cmd/marisa/writemeta.go | 46 ++++++ trunk/example/marisa.conf | 35 +++++ trunk/example/static/favicon.ico | Bin 0 -> 46686 bytes trunk/example/static/marisa.css | 15 ++ trunk/example/static/marisa.png | Bin 0 -> 25282 bytes trunk/example/static/robots.txt | 2 + trunk/example/templates/index.html | 45 ++++++ trunk/go.mod | 10 ++ trunk/go.sum | 20 +++ trunk/marisa-trash.1 | 44 ++++++ trunk/marisa.1 | 43 ++++++ trunk/marisa.conf.5 | 94 ++++++++++++ trunk/version.go | 18 +++ 116 files changed, 4028 insertions(+) create mode 100644 branches/master/.gitignore create mode 100644 branches/master/COPYING create mode 100644 branches/master/LICENSE create mode 100644 branches/master/Makefile create mode 100644 branches/master/README.md create mode 100644 branches/master/cmd/marisa-trash/main.go create mode 100644 branches/master/cmd/marisa/main.go create mode 100644 branches/master/cmd/marisa/parseconfig.go create mode 100644 branches/master/cmd/marisa/servetemplate.go create mode 100644 branches/master/cmd/marisa/uploader.go create mode 100644 branches/master/cmd/marisa/uploaderdelete.go create mode 100644 branches/master/cmd/marisa/uploaderget.go create mode 100644 branches/master/cmd/marisa/uploaderpost.go create mode 100644 branches/master/cmd/marisa/uploaderput.go create mode 100644 branches/master/cmd/marisa/usergroupids.go create mode 100644 branches/master/cmd/marisa/writefile.go create mode 100644 branches/master/cmd/marisa/writemeta.go create mode 100644 branches/master/example/marisa.conf create mode 100644 branches/master/example/static/favicon.ico create mode 100644 branches/master/example/static/marisa.css create mode 100644 branches/master/example/static/marisa.png create mode 100644 branches/master/example/static/robots.txt create mode 100644 branches/master/example/templates/index.html create mode 100644 branches/master/go.mod create mode 100644 branches/master/go.sum create mode 100644 branches/master/marisa-trash.1 create mode 100644 branches/master/marisa.1 create mode 100644 branches/master/marisa.conf.5 create mode 100644 branches/master/version.go create mode 100644 branches/origin-master/.gitignore create mode 100644 branches/origin-master/COPYING create mode 100644 branches/origin-master/LICENSE create mode 100644 branches/origin-master/Makefile create mode 100644 branches/origin-master/README.md create mode 100644 branches/origin-master/cmd/marisa-trash/main.go create mode 100644 branches/origin-master/cmd/marisa/main.go create mode 100644 branches/origin-master/cmd/marisa/parseconfig.go create mode 100644 branches/origin-master/cmd/marisa/servetemplate.go create mode 100644 branches/origin-master/cmd/marisa/uploader.go create mode 100644 branches/origin-master/cmd/marisa/uploaderdelete.go create mode 100644 branches/origin-master/cmd/marisa/uploaderget.go create mode 100644 branches/origin-master/cmd/marisa/uploaderpost.go create mode 100644 branches/origin-master/cmd/marisa/uploaderput.go create mode 100644 branches/origin-master/cmd/marisa/usergroupids.go create mode 100644 branches/origin-master/cmd/marisa/writefile.go create mode 100644 branches/origin-master/cmd/marisa/writemeta.go create mode 100644 branches/origin-master/example/marisa.conf create mode 100644 branches/origin-master/example/static/favicon.ico create mode 100644 branches/origin-master/example/static/marisa.css create mode 100644 branches/origin-master/example/static/marisa.png create mode 100644 branches/origin-master/example/static/robots.txt create mode 100644 branches/origin-master/example/templates/index.html create mode 100644 branches/origin-master/go.mod create mode 100644 branches/origin-master/go.sum create mode 100644 branches/origin-master/marisa-trash.1 create mode 100644 branches/origin-master/marisa.1 create mode 100644 branches/origin-master/marisa.conf.5 create mode 100644 branches/origin-master/version.go create mode 100644 branches/origin/.gitignore create mode 100644 branches/origin/COPYING create mode 100644 branches/origin/LICENSE create mode 100644 branches/origin/Makefile create mode 100644 branches/origin/README.md create mode 100644 branches/origin/cmd/marisa-trash/main.go create mode 100644 branches/origin/cmd/marisa/main.go create mode 100644 branches/origin/cmd/marisa/parseconfig.go create mode 100644 branches/origin/cmd/marisa/servetemplate.go create mode 100644 branches/origin/cmd/marisa/uploader.go create mode 100644 branches/origin/cmd/marisa/uploaderdelete.go create mode 100644 branches/origin/cmd/marisa/uploaderget.go create mode 100644 branches/origin/cmd/marisa/uploaderpost.go create mode 100644 branches/origin/cmd/marisa/uploaderput.go create mode 100644 branches/origin/cmd/marisa/usergroupids.go create mode 100644 branches/origin/cmd/marisa/writefile.go create mode 100644 branches/origin/cmd/marisa/writemeta.go create mode 100644 branches/origin/example/marisa.conf create mode 100644 branches/origin/example/static/favicon.ico create mode 100644 branches/origin/example/static/marisa.css create mode 100644 branches/origin/example/static/marisa.png create mode 100644 branches/origin/example/static/robots.txt create mode 100644 branches/origin/example/templates/index.html create mode 100644 branches/origin/go.mod create mode 100644 branches/origin/go.sum create mode 100644 branches/origin/marisa-trash.1 create mode 100644 branches/origin/marisa.1 create mode 100644 branches/origin/marisa.conf.5 create mode 100644 branches/origin/version.go create mode 100644 trunk/.gitignore create mode 100644 trunk/COPYING create mode 100644 trunk/LICENSE create mode 100644 trunk/Makefile create mode 100644 trunk/README.md create mode 100644 trunk/cmd/marisa-trash/main.go create mode 100644 trunk/cmd/marisa/main.go create mode 100644 trunk/cmd/marisa/parseconfig.go create mode 100644 trunk/cmd/marisa/servetemplate.go create mode 100644 trunk/cmd/marisa/uploader.go create mode 100644 trunk/cmd/marisa/uploaderdelete.go create mode 100644 trunk/cmd/marisa/uploaderget.go create mode 100644 trunk/cmd/marisa/uploaderpost.go create mode 100644 trunk/cmd/marisa/uploaderput.go create mode 100644 trunk/cmd/marisa/usergroupids.go create mode 100644 trunk/cmd/marisa/writefile.go create mode 100644 trunk/cmd/marisa/writemeta.go create mode 100644 trunk/example/marisa.conf create mode 100644 trunk/example/static/favicon.ico create mode 100644 trunk/example/static/marisa.css create mode 100644 trunk/example/static/marisa.png create mode 100644 trunk/example/static/robots.txt create mode 100644 trunk/example/templates/index.html create mode 100644 trunk/go.mod create mode 100644 trunk/go.sum create mode 100644 trunk/marisa-trash.1 create mode 100644 trunk/marisa.1 create mode 100644 trunk/marisa.conf.5 create mode 100644 trunk/version.go diff --git a/branches/master/.gitignore b/branches/master/.gitignore new file mode 100644 index 0000000..97a6b1f --- /dev/null +++ b/branches/master/.gitignore @@ -0,0 +1,2 @@ +/marisa +/marisa-trash diff --git a/branches/master/COPYING b/branches/master/COPYING new file mode 100644 index 0000000..196d9bc --- /dev/null +++ b/branches/master/COPYING @@ -0,0 +1,14 @@ +Copyright (c) 2021 Willy Goiffon +Copyright (c) 2023-present Izuru Yakumo + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/branches/master/LICENSE b/branches/master/LICENSE new file mode 100644 index 0000000..196d9bc --- /dev/null +++ b/branches/master/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2021 Willy Goiffon +Copyright (c) 2023-present Izuru Yakumo + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/branches/master/Makefile b/branches/master/Makefile new file mode 100644 index 0000000..aac2610 --- /dev/null +++ b/branches/master/Makefile @@ -0,0 +1,25 @@ +GO ?= go +GOFLAGS ?= -v -ldflags "-w -X `go list`.Version=${VERSION} -X `go list`.Commit=${COMMIT} -X `go list`.Build=${BUILD}" +CGO ?= 0 + +VERSION = `git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION"` +COMMIT = `git rev-parse --short HEAD || echo "$COMMIT"` +BRANCH = `git rev-parse --abbrev-ref HEAD` +BUILD = `git show -s --pretty=format:%cI` + +PREFIX ?= /usr/local + +all: marisa marisa-trash + +marisa: + CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa +marisa-trash: + CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa-trash +clean: + rm -f marisa marisa-trash +install: + install -Dm0755 marisa ${PREFIX}/bin/marisa + install -Dm0755 marisa-trash ${PREFIX}/bin/marisa-trash + install -Dm0644 marisa.1 ${PREFIX}/share/man/man1/marisa.1 + install -Dm0644 marisa.conf.5 ${PREFIX}/share/man/man5/marisa.conf.5 +.PHONY: marisa marisa-trash diff --git a/branches/master/README.md b/branches/master/README.md new file mode 100644 index 0000000..e487c7d --- /dev/null +++ b/branches/master/README.md @@ -0,0 +1,36 @@ +marisa +====== +HTTP based File upload system. + +Features +-------- ++ Link expiration ++ Mimetype support ++ Random filenames ++ Multiple file uploads ++ Javascript not needed ++ Privilege drop ++ chroot(2) support ++ FastCGI support + +Usage +----- +Refer to the marisa(1) manual page for details and examples. + + marisa [-v] [-f marisa.conf] + +Configuration is done through its configuration file, marisa.conf(5). +The format is that of the INI file format. + +Uploading files is done via PUT and POST requests. Multiple files can +be sent via POST requests. + + curl -T file.png http://domain.tld + curl -F file=file.png -F expiry=3600 http://domain.tld + +Installation +------------ +Edit the `config.mk` file to match your setup, then run the following: + + $ (b)make + # (b)make install diff --git a/branches/master/cmd/marisa-trash/main.go b/branches/master/cmd/marisa-trash/main.go new file mode 100644 index 0000000..e010dd0 --- /dev/null +++ b/branches/master/cmd/marisa-trash/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "log" + "flag" + "os" + "time" + "path/filepath" + "encoding/json" + + "github.com/dustin/go-humanize" +) + +type metadata struct { + Filename string + Size int64 + Expiry int64 +} + +var conf struct { + filepath string + metapath string +} + +var verbose bool +var count int64 +var deleted int64 +var size int64 + +func readmeta(filename string) (metadata, error) { + j, err := os.ReadFile(filename) + if err != nil { + return metadata{}, err + } + + var meta metadata + err = json.Unmarshal(j, &meta) + if err != nil { + return metadata{}, err + } + + return meta, nil +} + +func checkexpiry(path string, info os.FileInfo, err error) error { + if filepath.Ext(path) != ".json" { + return nil + } + meta, err := readmeta(path) + if err != nil { + log.Fatal(err) + } + + + count++ + + now := time.Now().Unix() + if verbose { + log.Printf("now: %s, expiry: %s\n", now, meta.Expiry); + } + + if meta.Expiry > 0 && now >= meta.Expiry { + if verbose { + expiration := humanize.Time(time.Unix(meta.Expiry, 0)) + log.Printf("%s/%s: expired %s\n", conf.filepath, meta.Filename, expiration) + } + if err = os.Remove(conf.filepath + "/" + meta.Filename); err != nil { + log.Fatal(err) + } + if err = os.Remove(path); err != nil { + log.Fatal(err) + } + deleted++ + return nil + } else { + if verbose { + expiration := humanize.Time(time.Unix(meta.Expiry, 0)) + log.Printf("%s/%s: expire in %s\n", conf.filepath, meta.Filename, expiration) + } + size += meta.Size + } + + return nil +} + +func main() { + flag.BoolVar(&verbose, "v", false, "Verbose logging") + flag.StringVar(&conf.filepath, "f", "./files", "Directory containing files") + flag.StringVar(&conf.metapath, "m", "./meta", "Directory containing metadata") + + flag.Parse() + + err := filepath.Walk(conf.metapath, checkexpiry) + if err != nil { + log.Fatal(err) + } + + if verbose && count > 0 { + log.Printf("%d/%d file(s) deleted (remaining: %s)", deleted, count, humanize.IBytes(uint64(size))) + } +} diff --git a/branches/master/cmd/marisa/main.go b/branches/master/cmd/marisa/main.go new file mode 100644 index 0000000..0f74d7e --- /dev/null +++ b/branches/master/cmd/marisa/main.go @@ -0,0 +1,141 @@ +package main + +import ( + "flag" + "log" + "net" + "net/http" + "net/http/fcgi" + "os" + "os/signal" + "syscall" + + "marisa.chaotic.ninja/marisa" +) + +type templatedata struct { + Links []string + Size string + Maxsize string +} + +type metadata struct { + Filename string + Size int64 + Expiry int64 +} + +var conf struct { + user string + group string + chroot string + listen string + baseuri string + rootdir string + tmplpath string + filepath string + metapath string + filectx string + maxsize int64 + expiry int64 +} + +var verbose bool + +func main() { + var err error + var configfile string + var listener net.Listener + + /* default values */ + conf.listen = "127.0.0.1:8080" + conf.baseuri = "http://127.0.0.1:8080" + conf.rootdir = "static" + conf.tmplpath = "templates" + conf.filepath = "files" + conf.metapath = "meta" + conf.filectx = "/f/" + conf.maxsize = 34359738368 + conf.expiry = 86400 + + flag.StringVar(&configfile, "f", "", "Configuration file") + flag.BoolVar(&verbose, "v", false, "Verbose logging") + flag.Parse() + + if configfile != "" { + if verbose { + log.Printf("Reading configuration %s", configfile) + } + parseconfig(configfile) + } + + if conf.chroot != "" { + if verbose { + log.Printf("Changing root to %s", conf.chroot) + } + syscall.Chroot(conf.chroot) + } + + if conf.listen[0] == '/' { + /* Remove any stale socket */ + os.Remove(conf.listen) + if listener, err = net.Listen("unix", conf.listen); err != nil { + log.Fatal(err) + } + defer listener.Close() + + /* + * Ensure unix socket is removed on exit. + * Note: this might not work when dropping privileges… + */ + defer os.Remove(conf.listen) + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt, os.Kill, syscall.SIGTERM) + go func() { + _ = <-sigs + listener.Close() + if err = os.Remove(conf.listen); err != nil { + log.Fatal(err) + } + os.Exit(0) + }() + } else { + if listener, err = net.Listen("tcp", conf.listen); err != nil { + log.Fatal(err) + } + defer listener.Close() + } + + if conf.user != "" { + if verbose { + log.Printf("Dropping privileges to %s", conf.user) + } + uid, gid, err := usergroupids(conf.user, conf.group) + if err != nil { + log.Fatal(err) + } + + if listener.Addr().Network() == "unix" { + os.Chown(conf.listen, uid, gid) + } + + syscall.Setuid(uid) + syscall.Setgid(gid) + } + + http.HandleFunc("/", uploader) + http.Handle(conf.filectx, http.StripPrefix(conf.filectx, http.FileServer(http.Dir(conf.filepath)))) + + if verbose { + log.Printf("Starting marisa %v\n", marisa.FullVersion()) + log.Printf("Listening on %s", conf.listen) + } + + if listener.Addr().Network() == "unix" { + err = fcgi.Serve(listener, nil) + log.Fatal(err) /* NOTREACHED */ + } + + err = http.Serve(listener, nil) + log.Fatal(err) /* NOTREACHED */ +} diff --git a/branches/master/cmd/marisa/parseconfig.go b/branches/master/cmd/marisa/parseconfig.go new file mode 100644 index 0000000..d08cd83 --- /dev/null +++ b/branches/master/cmd/marisa/parseconfig.go @@ -0,0 +1,27 @@ +package main + +import ( + "gopkg.in/ini.v1" +) + +func parseconfig(file string) error { + cfg, err := ini.Load(file) + if err != nil { + return err + } + + conf.listen = cfg.Section("marisa").Key("listen").String() + conf.user = cfg.Section("marisa").Key("user").String() + conf.group = cfg.Section("marisa").Key("group").String() + conf.baseuri = cfg.Section("www").Key("baseuri").String() + conf.filepath = cfg.Section("www").Key("filepath").String() + conf.metapath = cfg.Section("www").Key("metapath").String() + conf.filectx = cfg.Section("www").Key("filectx").String() + conf.rootdir = cfg.Section("www").Key("rootdir").String() + conf.chroot = cfg.Section("marisa").Key("chroot").String() + conf.tmplpath = cfg.Section("www").Key("tmplpath").String() + conf.maxsize, _ = cfg.Section("www").Key("maxsize").Int64() + conf.expiry, _ = cfg.Section("www").Key("expiry").Int64() + + return nil +} diff --git a/branches/master/cmd/marisa/servetemplate.go b/branches/master/cmd/marisa/servetemplate.go new file mode 100644 index 0000000..f092f76 --- /dev/null +++ b/branches/master/cmd/marisa/servetemplate.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "html/template" + "log" + "net/http" +) + +func servetemplate(w http.ResponseWriter, f string, d templatedata) { + t, err := template.ParseFiles(conf.tmplpath + "/" + f) + if err != nil { + http.Error(w, "Internal error", http.StatusInternalServerError) + return + } + + if verbose { + log.Printf("Serving template %s", t.Name()) + } + + err = t.Execute(w, d) + if err != nil { + fmt.Println(err) + } +} diff --git a/branches/master/cmd/marisa/uploader.go b/branches/master/cmd/marisa/uploader.go new file mode 100644 index 0000000..f071449 --- /dev/null +++ b/branches/master/cmd/marisa/uploader.go @@ -0,0 +1,23 @@ +package main + +import ( + "log" + "net/http" +) + +func uploader(w http.ResponseWriter, r *http.Request) { + if verbose { + log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto) + } + + switch r.Method { + case "DELETE": + uploaderDelete(w, r) + case "POST": + uploaderPost(w, r) + case "PUT": + uploaderPut(w, r) + case "GET": + uploaderGet(w, r) + } +} diff --git a/branches/master/cmd/marisa/uploaderdelete.go b/branches/master/cmd/marisa/uploaderdelete.go new file mode 100644 index 0000000..1369e6f --- /dev/null +++ b/branches/master/cmd/marisa/uploaderdelete.go @@ -0,0 +1,28 @@ +package main + +import ( + "log" + "net/http" + "os" +) + +func uploaderDelete(w http.ResponseWriter, r *http.Request) { + // r.URL.Path is sanitized regarding "." and ".." + filename := r.URL.Path + filepath := conf.filepath + filename + + if verbose { + log.Printf("Deleting file %s", filepath) + } + + f, err := os.Open(filepath) + if err != nil { + http.NotFound(w, r) + return + } + f.Close() + + // Force file expiration + writemeta(filepath, 0) + w.WriteHeader(http.StatusNoContent) +} diff --git a/branches/master/cmd/marisa/uploaderget.go b/branches/master/cmd/marisa/uploaderget.go new file mode 100644 index 0000000..4b5defa --- /dev/null +++ b/branches/master/cmd/marisa/uploaderget.go @@ -0,0 +1,24 @@ +package main + +import ( + "log" + "net/http" + + "github.com/dustin/go-humanize" +) + +func uploaderGet(w http.ResponseWriter, r *http.Request) { + // r.URL.Path is sanitized regarding "." and ".." + filename := r.URL.Path + if r.URL.Path == "/" || r.URL.Path == "/index.html" { + data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))} + servetemplate(w, "/index.html", data) + return + } + + if verbose { + log.Printf("Serving file %s", conf.rootdir+filename) + } + + http.ServeFile(w, r, conf.rootdir+filename) +} diff --git a/branches/master/cmd/marisa/uploaderpost.go b/branches/master/cmd/marisa/uploaderpost.go new file mode 100644 index 0000000..9ecb6ae --- /dev/null +++ b/branches/master/cmd/marisa/uploaderpost.go @@ -0,0 +1,71 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "os" + "path" + "path/filepath" + "strconv" + + "github.com/dustin/go-humanize" +) + +func uploaderPost(w http.ResponseWriter, r *http.Request) { + /* read 32Mb at a time */ + r.ParseMultipartForm(32 << 20) + + links := []string{} + for _, h := range r.MultipartForm.File["file"] { + if h.Size > conf.maxsize { + http.Error(w, "File is too big", http.StatusRequestEntityTooLarge) + return + } + + post, err := h.Open() + if err != nil { + http.Error(w, "Internal error", http.StatusInternalServerError) + return + } + defer post.Close() + + tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename)) + f, err := os.Create(tmp.Name()) + if err != nil { + http.Error(w, "Internal error", http.StatusInternalServerError) + return + } + defer f.Close() + + if err = writefile(f, post, h.Size); err != nil { + http.Error(w, "Internal error", http.StatusInternalServerError) + defer os.Remove(tmp.Name()) + return + } + expiry, err := strconv.Atoi(r.PostFormValue("expiry")) + if err != nil || expiry < 0 { + expiry = int(conf.expiry) + } + writemeta(tmp.Name(), int64(expiry)) + + link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name()) + links = append(links, link) + } + + switch r.PostFormValue("output") { + case "html": + data := templatedata{ + Maxsize: humanize.IBytes(uint64(conf.maxsize)), + Links: links, + } + servetemplate(w, "/index.html", data) + case "json": + data, _ := json.Marshal(links) + w.Write(data) + default: + for _, link := range links { + w.Write([]byte(link + "\r\n")) + } + } +} diff --git a/branches/master/cmd/marisa/uploaderput.go b/branches/master/cmd/marisa/uploaderput.go new file mode 100644 index 0000000..51723e5 --- /dev/null +++ b/branches/master/cmd/marisa/uploaderput.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path" + "path/filepath" +) + +func uploaderPut(w http.ResponseWriter, r *http.Request) { + /* limit upload size */ + if r.ContentLength > conf.maxsize { + http.Error(w, "File is too big", http.StatusRequestEntityTooLarge) + } + + tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path)) + f, err := os.Create(tmp.Name()) + if err != nil { + fmt.Println(err) + return + } + defer f.Close() + + if verbose { + log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name()) + } + + if err = writefile(f, r.Body, r.ContentLength); err != nil { + http.Error(w, "Internal error", http.StatusInternalServerError) + defer os.Remove(tmp.Name()) + return + } + writemeta(tmp.Name(), conf.expiry) + + resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name()) + w.Write([]byte(resp + "\r\n")) +} diff --git a/branches/master/cmd/marisa/usergroupids.go b/branches/master/cmd/marisa/usergroupids.go new file mode 100644 index 0000000..758581c --- /dev/null +++ b/branches/master/cmd/marisa/usergroupids.go @@ -0,0 +1,26 @@ +package main + +import ( + "os/user" + "strconv" +) + +func usergroupids(username string, groupname string) (int, int, error) { + u, err := user.Lookup(username) + if err != nil { + return -1, -1, err + } + + uid, _ := strconv.Atoi(u.Uid) + gid, _ := strconv.Atoi(u.Gid) + + if conf.group != "" { + g, err := user.LookupGroup(groupname) + if err != nil { + return uid, -1, err + } + gid, _ = strconv.Atoi(g.Gid) + } + + return uid, gid, nil +} diff --git a/branches/master/cmd/marisa/writefile.go b/branches/master/cmd/marisa/writefile.go new file mode 100644 index 0000000..d5367ec --- /dev/null +++ b/branches/master/cmd/marisa/writefile.go @@ -0,0 +1,38 @@ +package main + +import ( + "io" + "os" +) + +func writefile(f *os.File, s io.ReadCloser, contentlength int64) error { + buffer := make([]byte, 4096) + eof := false + sz := int64(0) + + defer f.Sync() + + for !eof { + n, err := s.Read(buffer) + if err != nil && err != io.EOF { + return err + } else if err == io.EOF { + eof = true + } + + /* ensure we don't write more than expected */ + r := int64(n) + if sz+r > contentlength { + r = contentlength - sz + eof = true + } + + _, err = f.Write(buffer[:r]) + if err != nil { + return err + } + sz += r + } + + return nil +} diff --git a/branches/master/cmd/marisa/writemeta.go b/branches/master/cmd/marisa/writemeta.go new file mode 100644 index 0000000..c63d9c8 --- /dev/null +++ b/branches/master/cmd/marisa/writemeta.go @@ -0,0 +1,46 @@ +package main + +import ( + "encoding/json" + "log" + "os" + "path/filepath" + "time" +) + +func writemeta(filename string, expiry int64) error { + + f, _ := os.Open(filename) + stat, _ := f.Stat() + size := stat.Size() + f.Close() + + if expiry < 0 { + expiry = conf.expiry + } + + meta := metadata{ + Filename: filepath.Base(filename), + Size: size, + Expiry: time.Now().Unix() + expiry, + } + + if verbose { + log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json") + } + + f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json") + if err != nil { + return err + } + defer f.Close() + + j, err := json.Marshal(meta) + if err != nil { + return err + } + + _, err = f.Write(j) + + return err +} diff --git a/branches/master/example/marisa.conf b/branches/master/example/marisa.conf new file mode 100644 index 0000000..334cf7d --- /dev/null +++ b/branches/master/example/marisa.conf @@ -0,0 +1,35 @@ +[marisa] +# TCP or Unix socket to listen on. +# When the Unix socket is used, the content will be served through FastCGI +# listen = /var/run/marisa.sock +# listen = 127.0.0.1:9000 + +# Drop privilege to the user and group specified. +# When only the user is specified, the default group of the user +# will be used. +# user = www +# group = www + +# Change the root directory to the following directory. +# When a chroot(2) is set, all paths must be given according to it. +# Note: the configuration file is read before it happens +# chroot = +[www] +# baseuri = http://127.0.0.1:9000 + +# Path to the resources used by the server, must take into account +# the chroot is set +# rootdir = ./static +# tmplpath = ./templates +# filepath = ./files +# metapath = ./meta + +# URI context that files will be served on +# filectx = /f/ + +# Maximum per-file upload size (in bytes) +# maxsize = 536870912 # 512 MiB + +# Default expiration time (in seconds). +# An expiration time of 0 seconds means no expiration. +# expiry = 86400 # 24 hours diff --git a/branches/master/example/static/favicon.ico b/branches/master/example/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9aed90e0ee800ddfe89900b127ec509beaae6945 GIT binary patch literal 46686 zcmbSU4_s4K`#-bLhKYqW6AO)5Xcu!9+AK6MXQ9o)0%I1|#YA7QSy*6BLz{_(wy9Xd zoQ5_N3(Q$)XBHaTW})vWr=iV41#=eG#aadS`#yIlrdaRichIrjoqNvn|9Q@Ho^xSL zfgiV<;q@x^z( z0<5B92vu(Gl;_RKTSusiO!gSED{vK>2i zumuYiu;k=q)~#DNcJSaq_SRc(u_vE=lJ)D?k2N(lF{jhX^78W7J@?$h_UzfimMmGq z#*ZJ*R4Ntw{`>FQJMX;1rcIm11`Zs^g25njxm>KEpn#1UHHuwz)m7}PufAe$yzvH` zIB_ED)29zRdGaJHFE3}$J@*_NI&>)8wQCn!v}h62>-Fr~Yp-R84i80Vb5^wLZ0zWeTDUAlB( zwY9Zu#flZoU@)-g=xFxC4?nQ?-+!My`|Pu9$dDndy1JSb78bGxAAFEqbImpE(4j+Y z_3G6uGc%LL#>TQ=e))xc^wCG`#TQ>>BSws1ojP@5H8nNNYPGV|)Ku21S1)$#*fI9* zyYI4`oE)anXxN{B{>eW5^i%fo%P+J0@4ugQ?b?<3d_GoEQo=GaGFVJZ4EyoNAK3>V ze88T6{&_ZR*f7Ymn-vumv4b?nHIBW&&3wJa+uiwT0je*N`V_VLFbv$=EU zvXLW4vd*15Gq2anii?X`dU`s$>86|5i4!N-d+)u+X3Utu;^X7lUw{3@KKtx5ws7G> zHg@b-*1daoR##WY>~=d7MUmZl>#gkPpMPc_e)u7qHER|dK72S+C=~3=FTZ3Ki-kS< z=%eh08*X4nj~->~*RN;U+1YH+ph4`9KmK5!eDVpKKYu@vXU=3=t(JZM`RDAl*Ir`}J@gRk(W3|R`~9r6w3JPmGKJlC z+imR3nKNwLwryC>m#mMvRYetten zNJwB-quOfFT0K@{z^^uFJsyKr5l|>RT7$ysP-{I3wbf`<7_0^bzEY?`4ujSh(0U9; zHLe5{_)cLkT0IJ@)?hFiAd>&w6jrO2{uzw8L*cPnJ(!bTw77_AJqiO}tmg+(7#x_0 z0J~gMXw~>J2;64lLc#STwB?eQOM=L`*S~cBtABjFaM_PH{4sFsH;+9s?uN}D-1+nm z?6*V7`F?}JRS}_0iW)dqWpY{lC2DPw%I|VD2ek{q>wj%-wwcAu3udYm3P)1FsQG#b z7bGd~DK~pw;!2~E7sYXID~BzR8y9&w5B7yRcHATmn7OFda#2tUkx#ec0#LBw#Y=Z2 zR@Viy7o1u@ZrrjfkG;3$+5J;u-g~Xv!}=(b&Z+k3)Cz57ryHyVO=@Eq#4EySG&Mb& z&fF*e4G%Zl&(d$JQF}Z(jn(dR20P!O#B{kaEQZaI1kka`CGk?*IjBVuFzpN%&V9b* zH@*SfS}m7EUU0RUP~bLi2CfM8BsZow)AL7`FM%4eX<}2?mqj zp-~t;>Zs0VMlKB$=H^yKsMXD}^ER`S77O{ee`)EgZ(W2+VMtIdj?Gn6ec=b?l3v_w zVKHD07a}w~H3cjRHGxZYyYD!U)+J~FP#_|xt++M$MI*Q)6@evn$s&Y{3!=d5QUqb@ zq{W+NPXGSSVb{bki+j~KCt0r{Df+m@p18&&tzYZe`0lgUU;D`sqo&?jbrWn_%)ev7 zEUZ^%R9vmunt9)Sb35A|E5-tZ9(Bvv$@GV_BpWX&k~GO7c!3HR6ArBC0mv@XBmfTJ zwH}*P{fb`nD!HxND{*h3C_+U@$Xe<}uKaoDBg=-Zx#9Lj-*sc75IDFky{uKPIGw)P z@9~5y>T60)-2DLi@~t)N9B#lSxQGfN;q54{oQ%rq8m(>J7jF`}^SuP`o79DN;( zGrP4Jc!<$1AtXn{l1f*RLdC@pcP@P;27n_9D=%1<$@9Drt4ew)O$C=^J&8|%;8p+i z;`Pfv8TjtbmG?g}+nw&lT>tSW`=g*BzAUTG=W`avs}KC~5yKbh^zv_LxGijX*o3jM z0iSA|z`c2spH1ko;Kp~B=%n?Gon)&9my5hPTPD5?w6}rty-l*pcFYkV0Y&$CimoXE zwg6qqD}dn=f=Wplj~RHZi8xk&)4ku1x@F(Kr(c~$5=@|eawap$wlHTKwOEUPg zH0tilZtDh#!4Ij(KS`Hm^W%kvO{a~4VS8o9yA~X~=euS>3VWo>>lmNC-sDt~2|b4j z=MCNSAbFQ>08{dKzEJT*ut2g2JT)wvOObVwP*V-bimW2Rlfno6G{y44fC2BU`xtVS z?si)&Hw7Zh;h9=Rvm@ZwXBt$s&CV#FX7BXJMp4X~4s7IR0Xi~9#3lmKLR3hVcpWxH&03bE<tKW8lAe{Zi>*@BYY8A&cf;pfA^u!fb_5k zKp7YiI%vQFxPX`L4fRn0YmLqs?56~+r9%O5g$MhjP6a6ii5oVgvyTdGWNGAvi`V@L zDDe5*rHjxxhX*_jDFT^cV4q>ntHUpshG&QI)H0DLDs8{GXK()S=-fN6{t3XG#1C`- zXlia&^f8-UakZYzq)dmg!ml;Pl{g#$b!FCyu5T?Q*wGItG0SE`23X00_DW@@tI=qv z)$g%!o78Xs{))Gh@wFw82RQSy3;N&Y?)G~D;`Hk5Fi+!-mP$gw59GNBR%s}|n#<>parP&`NsETF^}v7(J*_~lVh7dfngaB-f; zZAxb$ys4(bYr~yh#4aL_+yC2buMYhE*V(eXfVP|2bK_&JDs8~&P-Ph%T9ewWHQ9|0 zd%&Ua>EawdO?@z8?;RW9JsF!FVh!=c!fs56uv%SJ2ETPU*9J=`f%%XFDYXYhcH)U0 zd~g_-ijKbd+Us|0AEJqjPqxmQvCTsY4Bz|@co>Nnq(u-a7%jp@f4Jz^M ztu+EaRS9;+kMV-xyycm{a&_;lUY8lADhUr{X2uz{`DTwJso9_pFL7A= zWNIB1eH5z9X1&30uXpVE{2oFioyB~b>B!V(X6CCM;kDZ|yu3e=4~!5;gYD5>k|mxz zp5i;%eGp{Syb!pV{Q+*rihdCu6jqU7AzDHI=#72Et$nyH+UD%UVMn;*`tCaqKVxY# zqd+D-y(I3{tu-38#^BKzd(6pp9a(mVHNtAv)}(}GY_nKyEvZM$uePd!0cRf%7q(V%@WCrdyMo|C zR5GL?*#SUh(9Qu2stSoben~tD|KGj|^|%-hpxXe(f-q#GM4o->;fk zgjZwT)JNMV};Zai%0eXfWN>NC4jejCe+bO!YQ(B@-lwpsL6{ zXV9esc7#y#T%aE!PbtEUqJV{z;N|60SO4<$D=P$^bxnD8e<|hE;5)XHTWf3SZ1V@< znxLcmfbOln6l112;RsnJw9VUE*+YKw)@AV#uLFHSDGe0@RFD@X3sORv4BiEJ z!2AF#i8u62g(h8u1U_Db2b`+206tZ+fxEcVJHH31V73|({G)kZm~ByGM$b`*cSB)0 zz+JUi)zzmf$xU(W`|^vln!={55Q4}apKYFf zIwBD4L$0)n&gF8r!FlBB5DQ|XpSax65s5S{OZAi*+t z!NemY$ZJsOW#32+vgDyB*dt{=-ExQroCq=sjD6(Lm;xrN#(_+pDWdWl%M~xR+vWLD zS!$2MuJH$)KEJwV*~uwSd}grQoqnrEZEys2#t7%I7MbIMSXM!Ki zxYWI-gYQFlXaS|jQ$cvA1{_E&E{EL}&b*fgehak1^7e)iv*lRQ)BkI5ggu7H3XOy( z{i7tS$J30s#N2FD=U2To>$`SdxtB$l{Bb27Ypofvu)ge`-|p%*=y#o78y;uXn@gP9 zIEQ*#1*tE@sG&S0OIZ={#FxQ?3rfl6*mYSv3=|jI%DrOwE*L3YZ9G})>z-F~M7kJV z@Jgx*E@WEVmKR^`S(SShpi&f0nEdJRKxACLxROkVBRu!`Xz{I($kEqN`+N>prqxsH z(kkKtLvMZgTDRo}omCOuY`~f&pl>#p-7$i|mt6v&2TE0ikekclLD6G*tMYRCVMN+Z z$;)lL*~)FhJEy|_#UDIO3ws)nl&ol?7`x%q??%>Dukq=8y+Rs-^|t$7yFusFsx*GT z!enqoT{)j;|8!okKJaBJwEhU4%HuRROfT)fY0#%^mC@;J_Up7-RnSy1CjRX87+%(3 z2rkWMue!Wk0bQ?PThVeU!(_xgYK$TB7O7<|FJ7{iL^1s8?zjYpPM(&&+?#R#=1fG_ zw)HzIoBBR;4<*}To_NXqOjv|QXHt}z0tTNws&T_{D_>9c4D4X+2Th7IV6CdclEiMc zpMDMNTKAi(j4G!t#TTJXsZwpt9{pJ~EpEunhe8fr6-DO+!=+6k`QSZDA&M9E2;pJf zR1Y^9$!nA2UJXK53I@Ct#l2n_F4^h8uk(!YgHF&=TB#U-rE<+Z&*ei&;O-SAW-R&3;ZI7MHR%a#hs4aaHMIOKM^WG$gUP`=#@J2S^ z{?8D>VHF}x99X=|m}NIvb2TX^*feLep}C|sP+QxqP(&PbueaTpJU@ho+kM>cGuPG{ z72$f7(OjZe)DL;u!rojtf7`h6y>lv@){Vnc=Jlemy`1I&Y>+Td)j#Mtw6|ygQni=C z!zys!=q?90&Fvxay6Zk!kP1PBk8QIH#AcghUPMSCEG?RbY^cGc(gzgw$?g+DtIlb4 z`TP!C)cneBEYF_h7(}iRvbCx;*<(~im{iyxaTuKjeOA++EB6n`@R{u@hfCv}+9UV$ zrwbrYGPP(PfvX@_=Orqn|CpD)!w(Y9+Z3cw`1rRbf0X^XI0oVnD_lpOY)n3XH)L4&Bz+wE#_zL zRpo^vyM>Jqcg(!;)Bp2{E+IV5We%@(IV#Kw!{W?h^;0j~gN@AwL#KH&&i=J3w@rRS z)YImYO$s$a4CxyZ18gcGB;JNZ!6nd9Uzrdd18~lRj?KH`mac=tfWLe>G%me*xpE@n z2a2x6281g8mPYE->Y&M2Yb+l4ljYVC&q5*Yys@^Ddz$_n)==8!hh;UMQ-Ba)`V2G=xCl^gKCoD>_D_nM+N2|#~ZYx4jkn`(_!H(TS z4~%^8$6m|MO-CfN6r&K523T0!4gx!T0BS(q-NJKPrjxhqY$E-v8h)FJOB|&Ut=&A`_F8M`x!!!D6sX*~lkq zc!HF-alT6tr#IJ{k?P6R8!KvkzK0G7=KH!lzVeE08?xoe@Iq4uQ_p=m_dCi4fe`tk z8K59ksa@>Ob--^KQt0`->puRjK#q82Y|-~ZeUkXz)Hdw{m=bTPo<6bO72(jU45sEN z?elL@)|RrqZud`j_>rrqv+h9rmCjD_W zQsci+sSBNsK!n}w&({AY=X+^Cjbg{^kH+#o(ceL^EQh%?+ROu3!P&;I%P?6D$RTs zSe`13Z)`Xws?1(H=u(wbYaONcY z{K^>iiPCFp*dk>_B4xp)=CMAzz1bKrxSA8jBcqOFj&a@>Ncg@4G&`Q1!NfKI;$tF-zumy&_MQA%oxhK^yUO=Amm>a`8AqbLI(g=c^D-LaoLY~jEN$$Y58U$` zYTIbN3x!XYw{0JI^}gB9EDlRag`9yVlhuDzW31%%T!elN zVKa5PDZjptaQ0Fw1Der92ISyLs{WN93Y6IV9luLIA>WIggw|KbC(CI82y}G!JSlbh zgyLME&YzMp{I18Zd;gx3F_{gOWxMYh%g&t0DhrcLUs4I!f{X)s|A0XV(l&nl4|vE?B0S)v zcYj1ypK?vY5GoSrwA}+ zL>+SZcA3SJ8g9%+HdLw{ou$y5eZOSLJ1iU(Iv5&16o7}`EU1!+e94;QLRyFLK-@0B zx$v7y-lGSRMQT|vZKWMvOA>-8_ToHk-HIzFrubI=xoX4gZWcBb#W{QH7d0dsgbLd6 zIq5!8U*s)JLu>?al*YjeZp>=j!nOI_lug5hjaK_zjrOHOA-ss5WC_QsQk2Uw@UeAI zNZ?3UF55zQScwMzQ&TE!um=_IMnIgon2CTx(r%j-2+ zkh~fz1^WYr8gW?=wJ?8x1`0;Oe&?uk@_zVnee60>Q2zfQK*OAQYDs=Cd1a^oMF0Z>Bx@cbJVp`Pdr8!t%60#y$yA_E+a zSPQW6ua@NQ{`85vx~*Cja|2hVQF%n<0i|gnIY!hnTsgl#NI)x6qC}9DX4c)k)SP{8F4!of4F%A+#&>Dm-eVd}MbawJN zog{j=tGPbQ8PH@|oyohWW^6e;OqVJzJ@aNwg}zkoz7gdJP?LY*hzwo(K2FDcxdcn7 zt9JtStq|zrJAzt!`yIVzkGlC6#=b0Xz}k^!-l~6{4SGBMp19*|RWaBI3Lq^bwNeC= zjtDIMkR;UeJ~<|GyjwjPK}ddrHPTNKEV@s;+TA)0L+iA}vwcy>5x)f}#f^GxbAcICM4AW9;y$`7QXZ)+nNxFKD^Ns3uIh+Ep=X_Jwf=(1U$a2PiWRE?tdT_UgEvK6dkh^MJ&z(8doj!Eyjr_W0p%fJ{ z51d-O0b5p^QKIoqON5lPS8ZqxR|jf;d@{-D`)Wx!kv1%6`;MZ(iTB{{xsWy)Vyvwvc_0{| zQZR85|CJZQG3-ONP*GcGw`%PP%Wm=2IMg+(*B|gv?QNoUz!#oI6IIag&fRgJOt8zL zfw|EBg&&uy7?sbVqGc0kizOma3iu3N)rapyEzoVqKyjg~51}~UZuvAPGdnG|3Sly2 zo(KvhM$Op1&9rWGvspr8ewI%Wm(uvFH7Z~>+B3#o_vEbJnN5c2XHu1QgCW3NwDp?@ z3ILVMxBm%`j`#@{NGJLre2~YsRToa42J3xudQ7?{lZgLF7UWstpX{&86q3@Alfotw zD#fHc!Uu0F22oUeGNw7@kV;jXZyb_l%=A|n^j)?Z5*+>B+j&z@WJ!brDD6f%Ox%b_4^5@j{}J=jQem{hKz6=7;a|0A8oX;Dvtb6x z9O2>iiffKMAFhs;GdD=M%L8N2y3`ghx9$kMAVleta zO8(^QSo|((Fm*R+g8@&V?;5AFb!n&s3@%(QL*5GUB$*WW$ohX0tbfLX*5WoKxD)di ztadfgE|A=xjE=;Qez^!lgX9P~y+_WSzPF(k#*}I*SGTyPChm1&{_1`zd)&|S*LHV0 zU=XmVaIIhTCwf_C1DTWTskhkv?@>Ov_x6!r?Vi7B3GLvF8Ek6{r({`(bI^VGXDdB} zVD7R_{_65}7pjKc5a)@`Sgpyx(?EobS32-Q0%!~xmG+*RvpyLDxQh~3h}^FkY~XoE zuvJpjNz2o!bpu}M|NI>Xv4{m-ZN6_&(u6D#`o8apO;S;sP^_3!=u4K;x4}=MOSWrW8a!5@I}n#dE`X66;XT4OLryJDshyF zd;--VS8Ww|Z)J0;Y#Y|A$-2nH2;L-mi+{ETUPwb;Az8|fP}OVjRe}YafI`Y z*O4Z|Wd{t9w@f>}mcn|m7fRs7ky){Ymvg5bHzjXcv18{QuRcIEzb8?$3AzqH&>B`( zIU2Rk#0-o|8HF)ej~A3xV(+_OXQGF}{Q(uLA*vMy`vl2P%eW`s`ugyKk=w3@R>!bz zC9-Z2QFuNM8i36*geY^{$mc9g%-e;J$on5ff}Mgk^e)dtXHqQmuGx$GF4Jhp4)MT! z6l3P+b6$*NdE_9dDg)H933)tuE@>Yw29>c`=ghldOLf@d2R^MKToG}N-wW4~8ZAk! zs~);}`Q39@k<&;YG-&ol5>SB81+|$#7O`ljVdBh^e$0gILv>i#Nc<2sWx*_pYnqUK zKzG7~C#moo?>n-;m%#(zQr<<%9-vg#`66U)ATYV5dpkt5+k@2KdMsX=S)oFtCA5FSL8F5#M*)$d@bJU8Z# zo*o4t0B=X^2%*-2eo!m_1yP>rrh3t+j zU|=O}o?u+5_~~(g1vW~Ic&~izRvgY)f9*57hX?DmM#;1iC}Oey@QE!)?ikxAn`iiC z0-_ps?uZ^MRN3-|7^EBxLtoJ3kC*ZTa*Ob9*C*urcw`P$U2^ZnJN6Q3%5Ki)UAK}3 zAfg78HC&h{XPkRX0I1^9cb4A1d&ZhQ)nsD;FMfqK_Go1aXThV#b^(#|FmT$ROCY4Y z<=x-zo@@-)HdZF4p}WLSe}Ch#BN?RLIZh}EV61vC!kePX*>Sp}i6rlW2&oO1j`4sp z;QQ#ObMYHI-J6V0w!ssM*J2v+FmgEN2_#j38WfJlbSNi9RsXI$IN-a7{(Rr`MJS2} zC-DL?AWkS75(yqM%c3;@W2!S2rRsyxWPC(%?38Ctug&UTsl+ZI%=V0*#um$wTjEG; z)soeGBqOmbqbgNuX&!c(3N!y_e$a3;d(ym^Jm3d5fuF@i8Th0g2zn$wLV8YLBB{uw zd>8s3jjhGPy7xo3-}*C3!zfUp20a#~#GtFhSPYPH1396Qx6Vw2gK&Walr9$d*@wgR zc9o`jF>sd}o*4!#FuJs>gtqi;!@4$wseQg2C*FHKoz`BqKG8!n5>V|8vt1WKm+;@a zBhj1pMt0a?GPX;R5fyk(6`?`akxm^IHbUh`^A!oa<=#=RwNhLh5@0eHHeQ^m0fwO; zum=*gap99GJaREm6~Qh5ziyvTbx2WOE<_5v&u+j=)-XdZ)lGh6ezMDKbY=Fo$}UB8 zEpp1E%lyU&1dlwr_F_B;3ZZm6!YN>ZWnFkCKfRZ)--Y1|pND)%wyZ-m0O6UJhlmh0 zZNqi^gx))cJ%X~ib~FT>*;N~H%#;*Oo{bjiTxk95wa$~UKZqb5!y)x{HqJSzDV0ZM z3)!7$=|>xRS<(2i$I8|H%_{<}uEjG_P@@NfW6Ms%9vz7^8eWMS^j36Uj0eO5)k$Lr zPBM9U;_#N~4SC(G@u~M1+<-WE1{P{EngvufF*>p6NVM&4gJ<~>%+zjA2=+o92^670 z93cZuN>TXs$yczAV^b=@4qQJq)4$8e6ut-D1?;&5AzXz7}{|O9)w>12y`Jliee>Gmo!_`QKG==NZAI7 zW76ay5cOb-R64E68Y89rc;rqBfdH<|cf18sbC(jda{g`z`x#J z+Z$)BY*KcR<|G}=lDPKRE~H$;PlYSOwWH5o+w00(-yYChlHa4xkXc=q?0oNV&+)!k zY@dRwm_m6eKge87#SXVDI;jyt`G5v)Ujaf!?t$f`=%;!PrX%p;321AQDok4a?n~jE zrxK7d)S=sb@l@Ic*2#h;CyHcgl@6_;EKpe;K*mwUK_gq&`nw|*LP8WVGo+?GQQR{Y z+p71kPa3>-#E*Snp8dcQS6Gd~Y;x-L5rtZ3l}6Rrnzo=f;3Ot!6Yvkk97;=kX{f*n!p%x z@Uln;yM37O`wlME9DQvRTd<^rfS_vfaHi?fusZf{dW1G4}t1!Df>P)}U zkXho<`)jR+@RDE${Gibsjer^JJHKBlUJSV?w+|0a)WgscX?Y1E;4DF{0r+5u_gFl7 zT#N_61-(h6663R@8;SrW{50T{%{`7a7Lk?ZVs>eulAlWD1=$!v{%%KJ{DU~_?WRNG z*kC|Ol$$+#)|eJBp%8{&m`zxqp)btvwPFxgECpWW%y1l1-} zRKSoDVfWip47v{ZfoUK-kw;*HXc5$TlCl~PoXF$xZz3F#wX^}nzIiK(hJ&I+@0C3X z%1avADT0e$ zu)E`|_F5dDR>dhiX0tUAhr-+1aDzG15p*_|bi@zHh*Fe-E5T@z*FH=0tSP{W=X4Qh z-yg740J^Q2+#Lh^Yz9+oTJRt-2c$(ZQI9nq#qa! zq8fEJgTf`7u zCDo7?lE)}3;|*nbyeJStQIZlqg?68`;L$a1JU+zzW<U1@5bc!?jBX$DLZ#p1KrGKCZi@duWkp`=6JJOoG31ThYpQ#UyX zsl)f#U(vHDfC_OZ1T_5ukQgQO`NCIHZqhJVIO2?`F^dg#63;^R0%`*~yqiATGTTC> zz$n>tf0y99;bH8Vysq^HvK&zv1y!D`uzIzpwdc-vSMFVGMW`uH(~+JN5NJZtk(oGe zi?PLsV-FFz6I2AMB+UfK*@uh48?c~qS~TD!$p<^|N-7g0u`pQdkr_jdsy6~Y6}9f1*Ss-ewpFVS+Hhj*GUzgdP@Ca zO5iSB8kd}#l%iSN(i`E&F%8cfdRU3s3R9^ZL+XtHBEAdli$KPmYk(<0LY5g|%R)tF zhz375)`h=uJ2-0SglhsN;=&d~!I7VlnTx?6gkHRa8We0c;#}N4cn%6g%(5+*+2_d1 zI8VLmvFYo36oR-U!DuoMG4UmS@cf*ZL*#Q_MuOKytBR2au)+}>$7)*5UVgVs$K61P*0g1n$3cDAG-|U2)4|T!SXy z{?3(Hfi23Qk;-dOY42@_CSgw~Jk+}geMqOU*Z{a4HsA(bA8G}*=u+d)1*7X+G#Kcx zX7lXL*m#p&AWm8V5BAaTQDaZWr2jIx9>W0z>V|{TM4~n_0nW?v11M9B&8K~I9EJP< zF7jXt1VWyChYp5I9ft9Q*qxbRfB^w2CGu zjtH4?Hi7yjfG+|!gi)j*@$q>00UWsO&WRUn!?Alma(|OXk2I3y6>Nhn$y!M8ofq57 z@q<`EXYHz=x<7IGf)VquV@_EkmxBK!1KIT9hOi=XswK6A7GmXjdJQ(Q7CU8Rd1 z`HxU%JT(Y8;=fibtvT*&7;;w6dDIBl2|_Tj9yDpq1=O8x2JzK%No}YGEVZ<_p)RAt zvKI6pfaOVPa2 zgl(_?h6gq@@^0TFE{gkb2_7rc{YbLdsg}$V5XlBi=43CAziKb6*r}C>xFLCE4W+Ms z*1gz6V4j1weMWlVS%`M>H7tRJ;0roltpiF$YlU*;Zuum@O^kqzLb!`EP4~x(52tVb z9LKRQ9q`=H|AGez!H5yC1d}FyjzkAnj5I{X8@0G%SiiL&J+S%RGKc3c;tmCDP^qlaO z(V_8Wt9ERnWcLawK5m0givJHSoJg>M&8YO-k%<%M;?r#R^YVD|t(S~^c{~6jjAPTc zO|l@^mQFf67@0osB>o`f;@ZUD%k^t?<;sS_5p*z53Xd)yZaMkGQ~QRQ=*dqPH;Jc4 z8B``{7Nx*tslv9G=t$*SCB-yCEa!nivUVf5g6VZzux{h2cziXgfFJgS0)&f4rO%he z1CwB-;r$v<2Y@fYb8+rSp1M65(H)*Ha-$c?f0?RKy;)uL<$|^Tbt8u- z?tPPLMrAvB;jF2Vu1xi!1M#rVa@rkz@UKYEy~afHKMkdllq4&;ELhN!t3e!_g8U(h zWxqy&JUt7mgLWA_LJ)ZQiJ|1sfv%0OZsft=Q($ui@yWsDgYc9Dc=pHzi^FX~!>%`1 z;#zW0=-c4U>!9oAAN;K@ClXV7I&YJb!ofr!e5uc13-{`>tBZ|kb2M__dq0`BFu$|YNGz4 zMR!;=dDr}z)@HCQ>!#O`(Wxoqzphz6abmCemF1AYa}%9=18NAnVi9)X1e#5cd=g=Z zdd)0esui+wshhy()A?em&Y}S8+IU#Z@(1u11#0Q+Q5^86Na5VT?Z4$q9r!_FxR7^W z0C5u55|H#fe(HAspJ41fbngADh64g?F(nojc#K1J(^mg^E6}rLaiVEQDDXHmq{@Og}zYpD}KhMJLIfW zJN)O~b&Lm)!2aAP6_6*$r@YcbQj;t9ox%xGb6pFsegiu>rQk=QNh~-F6D!%%GEU$B z$$;+RO~vK|d~nj%{&XkB18t2~J3_LC+pOiqbN0V%s&;D6*h-Pc%LpR*@-sGJ(GmZZ zsu#)Nl6s_LK>{{zBHpB@KbSQ2OExMXcRDxd`QIJmLF*L0^*u84cqdCFZN2cs`Dc*B zT3jmfrG2DS4Q6o`N#$oNMOw(!m%_hmdYq1{b4EuFv<==tXtd!W`wL836Xf1{Bi{jG zkI&9NRxmD>3t3Id--i_eOZp-ASw7}rs0P&fiBg%4Ur-g`o2%Rv1wHPvd_bj5=fOY! zrei#4$d<1QC>|w5NX$TT*ehY&e4HklDG0fHL@$*Q=_3WPC+dOlBkKUNUU>GDj*kxG z<0p|)pyAb)=q1O;D+AnS?#zdyC_Eua_-CP%uG{~$CP+3ZJwa)DbAL+>1Syzc9?pf;Gf@259wGmG>qJWP+4(m z;L1)TH_a=x@r>oLSTSel7BTfpc#pSZ{(I`v+Jda)SH7X6r-U@&ctOKd7q6oz&sKd) z@`X#D&<%w3#%anb_@go7c?r&VF-_1=GlZE$C9zBS+rf)mCDQg zr=x`A_0&}sKcIuU7I9aDREAoxj57s&hBeau9}aXq(T~85Zxu$y+!p=GOM_Fz+Ap)Y ztz;!hWa{|&I!a|J`Hiv@d}(XcH|{HG*FxZGdXQ<%!DZiMoY=g}p0ewON@TsznS8

UhO*dznGZ)60*{Dfb>iV<0agn6;0wnOZBRsgIS_g^0&}@co!u|#5p7gmLFm|p} zNaCSzrKa)c0M-%moTROay~-qPCTmhKz%b;cd~r_N6t>RpHV^zPA9!T8CFTkCiZtQj zgp};1cwPdpUD+bxL^B91HPW-%=%|k6p4s#88Kn6FvF_}d9}S2?(dmaR8fU`O5p96| zjUniKQCtG`6~02IQr=Vj8`4Av3X;Z8hX?Fb6u}!#VcEd!kJ!#>4Bf8cMK9R?WX+C%c2@(XZoV`hIr&jZv9p{xK@p!Ce!*wp5%%BvO0_ zXO4sqJ=;2ls*J$oH^GjbJKqjok#m8&sOl9GlE{nCoW`< zBbLeg96oNddz7kaV&z7VH2eS}tL#!rvq(`mfDcPW#)`dJ3Opd3pIO-V6IL09;o=)9 zT92!w>~X!U~3x=an~5 z))9BvG&r}y%QC>cx`|%ykL0bBrLEyg#g%OKz1Q{l9UHIi2gAblRjI=8Oa0y3;b_s8iMzARTP{aisFR|N$50__vl%vc=Bt+ ze2B__X!VvnUsr{V96^Lts*K=Rd<(qhIr4w7Scnw6!Qjz^j!`6L44H zv0vZ`w|_ULkU$*s_s|V(*g@|F=?+@cUx+SsIM;xO@T^bD-Jva(rl!C?aoZl84-9oD zmzYpL26&VsCd6N{I2x_=qeyq1k7w-FMx%B?-9INxDXiaJWa}@xG|(Yf6ir4=da{ih zwBP(ExQU6R#UyD@tb+bO6{X2nn40}&-HN-Ti68Avp4+BAj|V=3uu}LwA{248qXxm@ z8yE2M;ejmP&k9hB_@r=>LnJ?BqpmLN1E6enoa*-SMP=Lwp$VHV33<&s!tD)!0Oc|U z&QlJ71$cfF&vWr=97dH&pTqhI?4lK6`ampwXt>^Rh%sN%Q|(928i+sI(#LFWRv$!} z_XTt&G>|cD=Lar;Lww|3@&lJ^Ad@Zj{&L>Gu$gx^;z^k`LJ^+B4m^k%XmJTD+WrL9 z@u953=B!5=doNYlCng*Hg$>976Zc$Ez+Ff!=NYJ>r~8(;aoSBzTv^zsH3$;OlPv&m z;6c*aQ#%HoiFKx^O<56H;j0c3l~e!D_F)PFyfS)06{@7n$b{7~{mS4-BS5q#iL>`TMYXLLdg05D~d&dCnY!vqWB zK~8};8bvB>6c!uet*xJ6h&jL%3s^4eYdn7Ak2lCh4y_;0t6R#%|IWb)dgKIcuTkNqHB%S z;(H~Ns5shM$@zE>*#}e{LNT@$sq4`{x*Ce^V=&hI;8HnNz1I+fkihuK19nUdh`?=-pMSQ9Apq9Un0Ff z$u@s@8@$TH&diUukdK zhJypXy6ME7coJsIB%qy>M4E@$Mcc-PAk2{{#gZaPqDq@rFR6Q|=HqlKu3#|8o7k`v zD_O{)Gtq^?MZTDx2Mtb;H^1^OR6JAS%*MBNBfPLZ_{MU)xgoH@J!M|d?~e%UyFm6> z1Qhv}FaU26}-KtV)EbVEJsnY7=JPB+w2hCh;}sX`sM9-KrSFwl%10Q z5fW&GFCuB;k{q zw5VPZP4j3<`j1Y|4^c=*OtUKLksR}OVf!uE$V~sBAM$gOA&qP^u-=QDHAO0q@5W!O zNw@seg|YkaH)kA*Kyc&?qAg}XgP4~8aP4{h2!VehH^edOAU6teVLk*~e&1O*TMEmB z%OvPfRDg|&m&JGN4u}&A(UIhGUUpM0eH}$;!LG+;VBbu#MfRXsL;nhv$d4@o5JPI| zNlW4TnPm!_{WMmgbdw4iiV;NRy}$v8SZ?{8{$d3?c@GLshK4z;CY_?{#atpBQJk2Z z`_h#c)V(4eu3G@Rj|t928uShif5k%L*w2#|pBgZC+Rj%8TsLuy)sFcQ#fj1hdR$%gxdw@6eGR zDV$10(RVMHKn_0UrQhVODLf#>!oG2ule8+cv(rk7?Lsv7iig>M+v5^Y6Gw|FJ&zeM zRHy+I+%f&_hlX_@;_^jiM`qalwZlJp_xYYxBywOX^)OHP_AB(qt!XvUIlxE6IY7Eo zD&DH5O_apF*jfyY*kdW*MSIBTy*C%XuSVX}{YwG1j!(MipjbtJ0OFOZzmfmS4pqca zWgHv}JrwYZvN*j_QJb0jiCrFECRTK>-(EehAAsD(r=p;XLbUcqj=|gIcr#+4^IWo} zeq8UIIALG^QV;E+w7n#NPZp*m_L2 zb=FbL*#h4IB_}RW1T6ozSgyQ&LV+=%P;H7zxFM1b0%A~sfA`$Z;ph250G9tud#Y_s zeg}0HeIF+y8GVjRB-s%Mr4}4b5mK-Cxhwpe9Au+X=h}S&SCsIzHcw}>|m!30MhGZ}c2PPGhr$IGj4?D+;9&jQ+dra*;#->Ypy9iK+6 zi_-Wia!?Le_5qSQNP$5jN`Jayo$ovzm_{lb*^UNbBQr%j6Qnoi@W6YUZz(pJ$vxz! z1@7;Mj5eJ};bQWGPo=6(i9O)&utFm2@ULpMyrno5D-z<|X++(rw4)1+PRt8_iEwI* zY@cw!b^})5$)Am!+r*nTP<|Rwquc$`Hzav7E+;qX!l%FW)SR-#;blvYBXcCvW6PiK zjt;i_bI{kiQHES*2)!fl`P#j-m2+Ne5M1&qoZ!W=D9P5avL$iOH0K{ z-1-!A-~0IUn~s|8fvmILVfrzLX2bxTqYPYt2hWw&ybWNQ9^LY3y?uc{A#t(?P-B96 zdhqE$366gq91miHq={1Af>{kYs5B*e5W=HoJ&XYNE>UAJuh0`m^0*S)fv{GkuQ5i? zE0X1n;%F{>u!4JUa6f(DaSDH_LD?w;MuVKWpTgte`^LtGMKl|n{&}}x68!AAdvcr~ z4_zj(F%y9uRGh;hbP=%e)v#3rQRo|5M`4`@|0Sf#+n_~53v9MvzmcFRR{Ka7tjSRq zks)d8&tuo4u1*ry&7)BwrUG0WPe!Q1BVTi~_i^47(2(&$^YSOhqEa#aeWbbM#R$O< zQ-#{EZDdZFQ+i%nEI|;_4d2_FUQJ@tJsNV0&DGxt*@i?DR^Kgqe$a(+Bhq zHCinZ*YMnY zB)UpJp09dO5|XvzrW>GDX;6 zJm?aC>gvg_5(0sU^Z6^lESdD@0S;lo#Tu8KMQkH40Y+k?hx-LxfuD_^N)rWF;^H?N z$b}{f%2S63G#U>7a3WBCI7rswW@vwPC!At|p464{y3g)90&78AM8tJqLb6zW7{ANp ztu@)^5+cM83K!jM`5P9?w^_ATy(-S($@Dj0NBdY)uL`fKaH-GRU1G1@NaJ15P?TXM zXl1syT?hDL3ig|2y9y?H`=-$GqwoP(Z4>N{=V8)l7zh$jO;ygcS94idZ1&zipG{Df)cjvAnM+57!3y~fq8!vbO@`!3)8sxi%O^ z^-w$+fP_xgU!G!sSn-KP+TlEL4ju?8Goa?tbpROl4xMB~@P?>md$t7c@;*ZdLhWyV zwiFNJn9eMZhq=@Nqt>J|D%9cQQ0(z&j})~+rE5ROf((@P5XNe61UvotAV7B>4O}91 zE)J(tqcFEP>u9rXYT!-D{7^lK1|CUL1Y-c~DJ6kaLkm{h${AwiIIq1wkH&@*9)0`L zdcaRp;Xmvj{oo$(@BJ+_z7>yjKu{%z?J`1m0cMBq|G)OGKD?jK?gZvW2+vgA-|r+%3iz?lF8}QF^iYzUbMHOp{eI7R&sU}5KqL0} zmXs>DO>&1mSS))U+7Wl_oj7tBr3o-W2-`INNF@Sa8x}%B!X=V0SR-9~(dEI=2fd=! zw+ho}7Ngluc*Gn9z(khBasH* zU5-jeYM&PJL=%-xU6Uq%+Msx$S{?C9N`n>sknVANea{?x)2i`!BPqFJ!P3kGtV2Hg zN>5}4Zb7`EzK6LF3n~>GiND5OA}ArGw!uP(@EYHYH1Zis*k@kwJjO^HvDj9>bbCU1 z@Gz7-Ppm=|7-@a?`~i8GK643M>)=5eldj6!4x^_s&pAKl50)A|{WV#U_qVrR^V}#$ zq`yX;(sRO8cQPJu7Y>p*%?>;U8U_NgdlexEtR=F!)n~DYUKLx2a?V~FXClJkLUh6c zzBhZczTLA7aw~Rtn7won?LNyv2@o>QdZJ=6ci)q+77*2@xuW-xR&!vqwry_Lx>fG5 z-7DcdCw%hSMV9&S zu?TK~97o_nhOz?+GiyGMzO{Ra2M(n(DkI+V)ffgH|MT*&-kXvumRoAmxrL8j+m3O)Qr5-ho29=tO9x1D7;~U2&8cn(w zP9P_c$%+Hm`tRgalGvo+P3VHDPf%m92Ej6!Zc%Y6DLg@oV2Ujo1ccXOx+8| zzZFXeMxp`3RX7Yl`UX@(H;34J>?E%+JBIit)tj=I4PmJ6W~nVraYVXV&y3&0|973U*5ub${;pe zGvv*Q))PP~KYM0C4g^}KLQ2l6wBu<6?4Vo$O&ga#3KUQ~OF`_qVJAbt*bw2aM4Kdu z2Ya>(#eF$SqsnRvhe8gM*B!H3mTDgP&jOp;p0Zb@Ysn4GCn1rLcu~ixcW~OUZC*9= z#iluA3q83N+nWG3Ij0wXkaG9c*`r~TaxPU&n){OCtxk55sS8!T>XC9uK=^YZlMqT^ zut6~oG7IO^gB_;^nSpPNNKcxy+TGOeHwK$*wk(TUD%Dw@{I{I38=QX6kT?Ts9hC{9 zoS4w!Iwg-!5N}9TG~??B=iGCAZxL1jlBU3=qO;5}f3N4wd*WDQj-FoN=-;qy)aYYu zdUti*NiPrmk&7M($uMwu30$C%A(MpAUihm4e4^f63{s0u2+#mOz>vu;Sq?0NgpTWU zYNak_+P>c7eKyPENw?cc&aWB&I57bU6#jh)+alx8$UmR^$8F*1ecApR4Oi!b)W%&I zxgP&l`#Xja)~u z_Ab$z0wUl|SHq=ogGc3dI;|Z_ySqbaiY{B0+pZbo#g_O45hYO)w0Aqc4`ALtH3`wz z|Hn2i#%2kD>=t_qF8U1RSaW}OTNAw`bXHz){^(l~#S#0{pYe-gdKq8QoeW;ERU*m; zLGVXI#dg8p6tkgSn7i4sGSRoCbfIU(PWqunI1aTsqH*Bx3WvkJYfNw4S?V{X@E|0< z2e4A}q$Iku-z!^?hWzs=O%O0FIl_DG)pBbDe$#b#?;fnDfWW}PsLt4bJvu>XX1w}f zQC>a6_a*D+f%IZo0$(G)8N$IeUJA&4kH3WJD``DYTa6P!R1!?XIijZjMh0x!Qfk zMFNsztD^)N8*jv~zoZbCcr95NzBnF8L>e(rqHUS`IA+JvM0}Cpqp($!s|LlQLJ!WA zw&|Pv4pT5_m{z2d!Zi+9VS+taUeU52M;6pF5gKGle3q+Hy*y0YtyPiA7$4<(835@G(UXeujlBL?BqzN2`Nj6vk` z7$T!&M1m(E$j2+X?HX^q&}4VlX*4afb*de1dj#7YB=I0~<=CzyMn8@Y%sXl;9K+oX zWqF{iw%neT@$&2EPK;2NnlbMWP>Iw%{aVZu;3^Zr8QE2R+sYP>Qpn_bwMzWg1`e04rrd7skJy{O_kZ}W47ti<~@dMz>K1(&)?DKmyh06=F2YR}r zYQ<-yuCM*(XwbTB)&-ZGr8qW0Y&Q(?!9M$tAu53Eo1Du}^yzEbR!qS_^p31pq#YbY zam)ut(EwY~@J~#(MZ{}|=;J0Cf*{r;#rR^6Q8M{)B7xsxeP_OUHvEZl2tQyBt1=+O zKb=@xH})1YKBXJ*wKs>F8{^&O{zowd6IMr~uQPqN{;`P-zk32h&M>4j%f7zZXukkf z5MWy`_(;b>ki~z>G#=uZFVSIhz&x+(I(DGVzWwCFARb)X`DI(3oefXembGrfTKS9<}BB8Ur%<2cTab5fpx_yF~v{zEL(Nt|(?S1Rku!rYU&Bj?t)8+(a}M553?R; z>+ijH9gHlL1z@t%j_>F_i9vRG_j&9D3N+X7gt?8N;PPoE%87P!P+MedD;6{KsVrPXV+ZHI@o!%c?u0 z9q#EpkqVEku_#ee%R3*w`eHfd19C!)^5s7ii+Z=xzA>BgW- zp{)y8JH6Ef)!ErGT^PF;fu?57Ck?wn?arN;JC1$`%K|oazdUf+SKvX3_Oh-@=n7pY z8z?_q4>n)@auJm2l;GpxcpWt#EfZqjjo^qQjc0ub_DXkev;c>q4+lg&B5A4IqW+?3rBiWbC|R z?2EAHB1Prgr+^`?F)IC`#50Y16~{SA>sxl!ho<`sHk{k7ma>9MN32P2v*^@mdnNcB z{EXEma1b0n1sPGZRw=gB4K?SqdwYr-Go?p#hrWE6FT$@pRLM>TRwr!FU{lkV*{F1XF4#8lh1Pl66Il`(mVBA(?(P7^S zzf;LLX-Yo?{kvz7Oi*}y{tzCpq}7=qxU1PVV2x_Jn_Irwq0D&icb~f?r%WR14< zE@|6z#g(srfB_XJE$d#kNScl^@AR}POCpeiRo%jfT zi;uxv5fR&PbSoa_I$I>s_C;H9YbYn~w7bilrVfKa%84r@EouEoygsg*CLiQrEhiK^ zSUgz;9Vyo|J4g2ASS9K9Ck}itZwY2kqgTv<{94V=&qt*n(V|-K>S3=5ro*gN>E+$X zr>Ki2_$yf0lHfU!I5~0WD(nV0VrY<;c)-1P&9NwPj-lVyR^!n%_3J&QvBDwzz-zMa zPh%jt@VAKy0LjUTo9W3VWxme5_PpBe*-!jt{2zB>F(aOIE&GaUt*v;#mpA2$vlRQ6 z!@H}`?4bdno|KgOT0Ta&lN}0pWt^YBh`WBK5P(r&voo1+Lz_7k6KJ3s44g2DFfWuWn?MYJMS|D~6q0Y|k{6{97 zbMN@=-&Dafg93P}Z$=rk7dy9v@nFu_SGUmS9C@2gojt9-e4lwLctMy*ReVi4^W_%7 zd6q!A!XJK%N(U_eHCMPj$QAzBHo*!bp0Iw7WH6`@W5?c6DyPX&sPa4QC&(3w322UP zo;cxf6l1RBh4@ITiK`h=xjo?rZutGZ&z-sbPn*QlHzL6}k5-P}4xBKzY#NrdVOiK= zY^~{wKG%a{7<=>0Z}s)?+~kDy0YDJ-NpDsaKxnUSdq57gqCuIWtU^`xn;rl1KT#WNrgR`ReKiK zcedA7$5qb!Umw}Nf6K(*Kl<1_Pt^b$931$@7U6SU@e!&&P!6iyy&) zQWWkoJFutEHTR3S2gIEFD2Y+PhWj^u`r=m>dt-KXc3HH}Gv%Y7ZN_#Vf^qQPxDd8M z;L){iO-+kE%NX%kj2^^8H8w{~GL;&3rs5@nn+cAB$tBh5qL=?V;a_ik;)NOeK6rZ3 z6ld*;i7g`f;$G5T_MNHzbwUJ%@ zx4w8__a$Tttk5K^s}r)1>H8~}K6%qp%ux~yAlLW|k8|QT2X?UNVB$AcBjP?5CHx@H zgbzw#UC?B!iBC=OLr4)>OOQkhnn4`x$h`Xc=8B4f_P)+~%>NZU15Ese5U{BwxG3Zg z%Iw;Zg=quYmX8=`1b?t@)4VpumI z*A!-vol+kV9FhZb(>kRXba2I<<)L4)9ovnYZ@rjogMRcOl+wBk^BGx}PxJ4;5+X*h z3A^}__(&jwPhh~gKD^zq@?vkXN+WrKCJjW&;KwG4e!oWViD@F%ukTFT-Gto$G+;;) z2NB_8wUB?35m@0$4D5+Z0guQ7T;w9+RdN+3TxsQ8%yaO$rt9#kW;5oL2+S#w&vyW2 z;o`Id^Y_yvEsok3#sTx1g^U@`Zm8Cf~j{45D)k$W5ti)1U+`E4~>dSg>i zueXipPuX1n1_}zAvvb2?enbJ9nOXY&#SK4dL6 z4fU05Hqz(dEigqkVp=RVQ|51^%&+gl`6oyKI+5sH_yd^ni|nZUeB!RmOcnZn7`u_w zSU-VaqRr`bdpri%-B5>ReLc^qP87*vPe%&K(M+zhJI;j%rRduFc3&q50LG!FcoC2U zhJ#fFLWj<0vjc}1QAq$k=JFDLSX_#e;-aNHj^P_lI(W9-VSGB57{D4mB-_b zcpZ^~UH5VQAcY8&KTS#SGCT&;S!W){f)^*s*550bCh(B!pJ!tvT_0|_G&}F*j8LHs zf(^uooaj2^mZ|x*QG>;;@fahKu-@df;mlQ=I^uRnPL(>$Ek~`Pw>x||7a8)(dQMlO z_;tRv54~JRyQ^#o6f5t;oCJ}`4=smwY4MVY+K6E(_wjd^k}WV{4Lm;`KJwU{cSp5# zE5rH0V4KI^)Nk{&1$9lqnpg}vqMvW^Nd5gdX1b_q(N3S)DbCv#vVNKvGLm6LU+v^k ze(mzv!60q*1*3+PJ!Se}fG3qnWkevzI&I;WD~}PINe_Lu`+-P%d)eY>ysEagB_8dL zTdNvXs(7JOiSuUl1`I%94Bgb-Q=W7AyNF|$G_<6NxfiK&|8Eztz-ZlSGsjJcO~3X< zEH~6{`Yn|(nO5`&dooxDOc`FFi69u})==B%ov(bc;OLf#fBs_2(FeCbc3|wchrZqL z&b%9szyH*VQcJ1MWb$~dJsO+I)OOY3+gBgCn#Teu^?03D>U5=)*+8HmV}>^}4-Td3 zAf}uQCbf?4WJra$#xLZF30 zLYyMO#9MAG%uA~|E)!KJMVrqgb70hH=aM%dcYPJpr|*e&SI4a?cgP+pE+|_JAr{FT z(QEJz!?vd@ne-^hsZ^Rgrm%!=v&n|?O*WGxp%SMP|80@52|ts`DJl8q#5+!)7%ZGVG0aWV3-2K6d0z!Fa?GwFie4A k3Jgc#e|3`uU2Dy@&!vFvP literal 0 HcmV?d00001 diff --git a/branches/master/example/static/marisa.css b/branches/master/example/static/marisa.css new file mode 100644 index 0000000..ed5156d --- /dev/null +++ b/branches/master/example/static/marisa.css @@ -0,0 +1,15 @@ +body { + background-color: #282c37; + color: #f8f8f2; + font-family: sans-serif; + text-align: center; +} +a { + color: #272822; +} +a:hover, a:link { + color: #e6db74; +} +a:visited { + color: #66d9ef; + } diff --git a/branches/master/example/static/marisa.png b/branches/master/example/static/marisa.png new file mode 100644 index 0000000000000000000000000000000000000000..4636a72ad4874f1f4f2c058da3535880f0dbb0a9 GIT binary patch literal 25282 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

cm{X`hdF7YXOsPJ7C&~hxR_fJMhV~@qr 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 ZD=;uRFfj1ULhAqk002ovPDHLkV1l4|B&+}c literal 0 HcmV?d00001 diff --git a/branches/master/example/static/robots.txt b/branches/master/example/static/robots.txt new file mode 100644 index 0000000..c6742d8 --- /dev/null +++ b/branches/master/example/static/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: / diff --git a/branches/master/example/templates/index.html b/branches/master/example/templates/index.html new file mode 100644 index 0000000..2444493 --- /dev/null +++ b/branches/master/example/templates/index.html @@ -0,0 +1,45 @@ + + + + + + + + + Marisa + + + + + +
+

Marisa

+ + + +
+
+
+ + + +

+ File size limited to {{.Maxsize}}. +

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

cm{X`hdF7YXOsPJ7C&~hxR_fJMhV~@qr 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 ZD=;uRFfj1ULhAqk002ovPDHLkV1l4|B&+}c literal 0 HcmV?d00001 diff --git a/branches/origin-master/example/static/robots.txt b/branches/origin-master/example/static/robots.txt new file mode 100644 index 0000000..c6742d8 --- /dev/null +++ b/branches/origin-master/example/static/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: / diff --git a/branches/origin-master/example/templates/index.html b/branches/origin-master/example/templates/index.html new file mode 100644 index 0000000..2444493 --- /dev/null +++ b/branches/origin-master/example/templates/index.html @@ -0,0 +1,45 @@ + + + + + + + + + Marisa + + + + + +
+

Marisa

+ + + +
+
+
+ + + +

+ File size limited to {{.Maxsize}}. +

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

cm{X`hdF7YXOsPJ7C&~hxR_fJMhV~@qr zL1bbz$(YpPjkO~a20;V^rHbBw2Z)EX$4YV4AthlQv5>QtuDBoH7vge61VDu?Kmi!` z0`pF0BukWb)9;W#J^GqUez^&$Yi&^87XlZCg(okoA6yd$2=o(P6m;&uvZz#KP+tq4 zK+afO`0z+?V2RXvlX@(7aNuo>v?HQ?wX(31f;CyYnGopkP3%`+JWms*o6ev1?AOZ_ z;b26i&M7HVIWV$pQUBod?iU4)t%xEcIj&`xg&P8t2FXhVfj-3ASCq9t1&G6MI%b;nX*OOdis( zM2kZY_lv{^6z%oKlRp6|j8T9z6KL57P5@dCRIrolm$Evr^Z`VrxN$Jcmb$9S%F3$B zs;b(Cx;3YzPE9t~X;ovo&)KP0Ti?f9QC_(!koIN2WE>>iR52)0Yrt^~d4J}4ywTYw zR~sP!s|P9|P$FTfb)6iUkuB0QF1_D}Hk)^$9sT}Ye;|%M5fsKJS>d6cm3?~y!G&{g zhZ>qH05xbxbMz<7uO z#DL3|fk5{dC$Poy#Hfe+L@dBFHV*XMAx^}rcxY+k;A$G5j!cP4eYIZ!r7EQ_`z0uh z8dCHTC4r-aUsjPe0%8_Yz4@BT;*wFXyl`n!7{J2LB&h))YCJTx2BFHp`K{wMteNFA zF)?J&i;wR8a5urLivuVrm$h^{crpXOSt{jVfI98Vj5ddm&O)JBDwRsb&X`|1k$iT~ z`)u4TeQAWD&TW4OT8>f%c~RMZAgX}EY#I%)Cn!7m`@utH*5v1(_;toZBZ+36ARtnJ z2&||c5DW3R&df-nljfO}GwM6r2E7;Da>uK%-jb+-yaJ+}+gJsx{H=g}?@pvw#*nDY z?_ll}8#*hVLh6i0tHH5LmQ_0q`v4+1Me(=$j&L9V3v+~ z`jy|M#jv;hVCSd;G^VZo<7kC)gdy$iY4~j2a!q;I&7yU0mlmA%#a8QtiM<+yFzc(U zDaH*x#bWw|L<|&U)oflT;)ABJCRrxd%ps2JnAc< zwa?8gJaEKv8;HQDXE0)Vd5{K+9@=9+;qAyw@GvAo-mj>{qB+Hmj@Ep4OV_4{#(5Z2 z`2}yF(7D~zRR$87zKmNz1Ppsqm{hji)p(CW22_MvCwTCLM!Vpb6+a3!U@B4Z{$WS2 z5n|C^EQE(qOA-ucARgP^qp(pFDt!H0d*H(Z7eE=FjfzJgAMQ`_bD?2wNTi`ZBkZ&U~B{60^aWp>rk2xM~aHK$Drki*NsG` z1Y#efbIl=;6$1zEd3UvL1q0%R0ah!txdC|cs-NHaJ}0&My(5`p0w2`0d2y`?x6_D+ z<`?%>bcqy1=@CzG>}i{95?I(yLQfLxicfsI^tZqM^B9U)T%aAFn|~q5DN!a;RaF3q zP}w7jb3z7gzu85G6@=bKRP%fTI!>O-A&kI>0iI9v(?aJ(vhO}$>)io>YzD$C922$d zSzA$B^Zx5Me&yXd_w0BTB_EA8`45XG>ZARFDwk=W|MvbmdBx(OPnf|fx%fk9V7%D3 z_FefEe|vxNEC_wu@f)Y@cYv29)Teu6u;|v38iZa5~rZ@Zf+!t?y z5`(bLBvxl>ci;{6-2VKNH+?%H#1=;1+kd}V*mC5D^R%dS7ah#Q0~hoZ8eYa6A%~OSspv?NujdAu27T?-2ju6B?Sl1cAwW<=l0cpt>eM7AfkOR3lO6}9d_^=`Iw-h%pkZLTDSDyKfJO3udkcnRY=4Z zp5OnqS1(`Trk`v}knR6rwAZDEZe!+BeD9k=+%jg+DwJLu;uK;29qoLn~ z*p!~2hAVG?VP=G4)ri$liAXvwy=0Mg=3Snr9Z+e@%z)+M)&(DbBB=(L@P-h|xl-G* zK*g9p{pd^Mk4z7&u5C0v>g+6R*mQNj;TMl2h#1c#Jma~S&;1cVVl^FE1Av4fY;`@w z&MGvWf)a?39`}zcf?|@rA=$8D2z~eER~;T(@oXdJE`BBL3BiO0XJh+?->e9{hZsi$ z;)sQOP5)%L}08_KsX3nW8)C5#aJ@ZdRST;1t(zhSp81(^!gwQl& z2rK&LJ6OOO^7hE1-hO>*X72;)G=PB;g>g`fXTGz1z;$ErX4ME-QDa>(Y!9L2TZR(A zRE!xe0BsN`gA^oaJoh)_E0x&u09iAvm^}1Lw=$26H#B7H`bC|QcC6{x)Nok;FC@|P;`dbyu{T~2CyWp!l*t3DN}LR#W^p~rt|>5d z&vCDx%ib7d>pEQ`4u19g3KPTlSEpZp;;F}Kr4_^pZ@zHzJL~~G^WC!_i(>@MzXgJ(bZ z-vNovj62Y59lR&+7NfWFe2-Xu%eVWKyg#w$}x(0}ag8bnPt343J zuhsS60DglW{qC3d=?2@jdH*W=qzND>t7NbAeUwHe0K?t}am;>~p+r&iO#4Taxb5^g z7pdjhu6ku~sNn+bA6{{0kRkh~O^vs8zp0{{|#NM&zK^rx;k*%=9 zHmZ`tEfuQdm8}|%ZvaJ5PbO1K0LLWxYg)#ZS^Q(uj z7*Z925EsP&pt5UZ)uH)}w{f)i9M&csA{>RW_vjfOjL8I^Yps|~{ksrGKfbS%ls#BU z^M81h1xADimjfrdWYJ+Syp`?=sx<&cUVm(DIaR#8WpBKNr@r}G5U8z~i5Rr&c)M{e z;c+O)Ak%MvYI_s)Y?%xu4>raGwjwSj0WGo+g4IKV6@w`bp58kRDs6f9-KEaiWGda% zufC08R0}+?6)r^Bcp0%#2B|V$TI;O`0Yd>8B~zMuM)*P%Wy5qjQN3iuS7K|oqYQ&% z1pX67x&nIWo3P?{@NaEf&b~Y`Rfuy9h^8@o*|a8&aJasXgR z)ZZm!K+pzKILTh_l$8aMU+Bk2dnHxJ(XoYO{}@Z1&0UE4YN zS*Z9@fU4CQ3oCSOy|OaAlGwG&7zC!;1WB-QJcO4mLOMlBO4mJlNycj5Brg((v+*a~ z(_jF{Ef!&vFV#=@A|L={`ZmW|P1P}{ z+HK88Z$0(Y6OT`QY}&h9Kz1Pn#h!X*dF|++-PS+-!4KXZebgPtZaQbt2fshzt}a6G z-X3dzc=r+01Oo>BJF+z$K*Wrz1E^6LFIFFDCo!sZb?hsyUwimZ4PN+7z3Y}9edfEN zT3|;x_vwI$`mo;{LK26jz9CPQw|?xUw)?K zT0-^-qfR!qjl-sKuk~ER6DIuPirnoF4c7eeZ?H*6W50UT$EU8yyaRXSpgZFFAny{M zX!uDWZ%6`mSg^SK#I$hhhI7w8`3!@9znVn8|G%pa5)Z#a7Ayg?R8>WLmw1)8z9$-c zzwK|m%W27}f9cSV#J}*smiNR>eYv#gg)8TJ=Tz0JsMGfzEWI>e{Gz2A`#-Mp`r-6> z;!8R^k$Oxc&Hw863AO`piri4MYH8b_J4E!AUv$U{=f$hMKo$rIh8q))c@!s4f@rFFd8uQ#WtaSXI85{)al%=`-FZ9G;%st$N6n>X-c` z)}pSw?xn6+y~Nrzh$#eslmC&pxV*nR4H)C@tDgQQk`FDMv;6x%P_N#r`}E#q_KvwH zk=^r8usdW3P|PAzU;kbwY;qo7HSvvl zZ#@>i`rEH6(9wP1Q)cgJN47oG~9Xkg`=+qBs@ettNE-ey>f{-#9K=f&$a7`?@3N7f#NsXXSMt7)rf*?y_~W8OdPo2MXt|LYng)Tf8{q&ALX7zmT95G!FF z?6N{-{O_|5R)QeyVOEVkF@R5$icQ6ui;sVL6AI}C=QRG}WznNn|Ms$G-TMe(lZm`jnvd^B0psHKYn$?*}rb9zJ#xYFud7Wjr0!5n53+qKj*@R zC;?$H!oqOIS(2MJ{yF$4tdsTi0~`}LMYX>3U~<-31z;pH#&ZvFH15 zU*DPwZhr3NlrmsGg-7Kaq#Zz0@>KQpA5-9kz3~i&v))kYyZ*+YW7UbuPmbYIEuh&1 zMQ!!oMAoltC($L(KJj`-Fl#8n?8aP={Ts; z{NjH6)TJ}RKU(mvb*E3VUsbPOoh~?~pk7s-s(6jG^wm36TVL-fs^0xyiCVf{tZV+R zOaJI1EzDW@`O4JLA+^~7*Sx=M&6HQ)Q7t(y+ixmfL^mxIU9xHZ^k2`EW5sm}( zG=9>rehxs5H)4FybL}A)`%=%8E$?-7&-j-Ft80fUYS$IJ>Wzu)xHCWa+hN(6rOVg+ z;h}|JzGzB95pJu^Q2=07t?dvRsEI)n+TGBVEvG{)G}8iK($fZie9!EHA9LigBB5x# zK#8WV8wk-oG93h!C$gDb!!NNtBWlOBW9D@r9E$w#)DI?k1sP`%o4fbD(Ag~;XUtne zQ~#cTB2eK}>O-H7LYseJ+S8|K$FPyNM4i{(@b*9RA>v@`1GU^bft1nMU7hfc?lY!_ z{LHhxVPOgq`HXZS{9L$kfn)}a-9Q3W5L9!w{@PjT2UvA1=^TDAP&B~*j#{c-OR75K zu!5*~uaVR4Uqzw|-ds|Qb=e`_$GW;xBh?r0oBg_~TR3)fUUkh9J?WzUx7~D|I}nDpd(r$50&HLm#&kP% zUS(IPwIf6Jj1Y(qI{c?_Acj>uxNorJU&(`qhwN|u62YP}s|=aEc2n%6v^;S^QjieY zdvNaMZ#?qX$0U$#YBu%e!ooV;cjmlO&A1Bf88pBHATa9NkXCK7B;6ZXnj^5`m`KU! z##t;#9IHeWtJ2L`>y%506~Aiz*Qyz z6DsWqhzhAfg^Iruqw)kTSizT*!_YC@|z}{zOEd!FzWO2nZmfO*ed@q@q}J!1@7S;W!N9 z*?*O@wjzwG)xA15<-=5^M1YgQ8I;rNe4`<1ThnpOLZk_{`GIo zlC1j(!q6d&ir%<3`2=0j4VO9;99uef9CP`5|4)GQhNDk8?kvyjKk9nqq!?Cnf8W&a zf@B$M2m(SlaWT{d;)Tf!+8kAW@4J5azI7U4CpBP3l^NR=KJ`NQ<)dD+!rCE`Ra6Zr z=gbL!u;~9g`7LWkWdw1Bz%u3PO8od4s>zxWUOy?tl~9e2F;tv&BMEA0S4x*_JnwipNG z-e!*@-tln2kP7EM9(j4XGQ^H>z^D`M8+aini+}daJ`IbjQb^^X0jrq;)!xcM3OxnI ztB+WCqJ{^b?Rb|;*4DM79NEgAi(a+kkpKL7&A55Fr@uc#b=jpw^&;wv%f-txE48S9 zI>M{WIp4>skw%5?*0s4U>*9DPw~JN1>NdSaimGeRTIZzosgjC+u{BqeSXVW`q(LAe zvScU#Frt5IfjSZG^X;9;kzD~IqTab!JNNoxvHx!_eJrmc*Uc&4P;wus`qx*g`VHs4 z>a_d*oT`e%B`vDNvFgqsaoX2>7S+{XUZhx$-4cw|3G8ksq9)wAG zCr)<*$gWU}C}v|=Do>jHY-#;3&PWM8VAS0_$)4=>S!98Wy3-5;F_qLUeCtRkJl7TNVViD1co5#%(QSVzW>~P}Li_?G2*GT59)=2&OOJ4JlzQ0UWT~sQr zx#`I*>S9&jS@1i5hh8=IqCHoAtR?mFrVop%`tgFYTD!3t#6*UO8H|vbhP?TcG$!2g zK|$5|l4_(|oey4A)t3+Jom~RrHS*5u+as?lc=08#=RT`meXJ__z($qQzt^d#%da^l z)+pEGow`*^rrzD<)q9P-w)HsO`4Um5(RH)Sdsy@K%1C9)8!GBF(u=}GXv$qpz_1L1 z=6^1`IXxsr z@S@VASNu&yRAbdxBUSa0s+V$Uv+U$VoI2I+zdxyYQB~PIeVtc{o|Te16+bV*CYwM4 z1Ou`RV}aw|UiRhcEYgQx|MHrG^Xf#!si+g#N_Ow=vVc;8QbKQ8@ZU=I8ld=7|D`z4 zW&K21vDrXO7&1U<|CK*{Q`uQtOj@-<%B;>1?X+P80A+>BlH6%XiTZ$wxoU<68iz2V zJaN){QXH_MWQ4r+t}*HW(~B=%@%@X7^B`Qp}AaUXaHddLt%CfhytWR z_Z|KJu2F&lzvz|?Q(1IP%Sb3G41+bT3L!F0l*_;hC7~ol*?Vy&Ap0Gp4 z)YR4v3-JT&3B(Dja~HMkGqS2_NHs&jkUlk5pM=G%y8ZHz4(!dG`%a0DZ-nsu zqX)(o%Cx0F>&mXoP*G(`e>}L-03<9Rp>hd?f(W!T+#==Jx>bp4X9(YV8)L((jmm#L^Kz=IW27BJL*y5)fwhKM8Q0TRJh51}H2 zA~R{87zZaNBrZ-^LP+I%3!i=dKSy~Mc!0OW0;UhI{Pxm|Z?EN`uF6(r)9G|YMJ82Q zk*%t&sjO~PpJ}M?XLqKec$hgOhZq9fyNxHTYps*8=$|-v&&^yi0a#$zEf^^S+d%}# zVaX2d&3x!nAV)yNY$PzHj`l*aTRcSqQW8=FKPHufaRK}G_IaI+T|K7OKvV#-3xdb? z_X(whSM_R0nVvoA_bb*?_vY5Fp7zep&W`rZuGY@3w)W1B_ASNE_RhAAZU;aFu!!a# zVoGZ0NN$J-ns`5&5Zj_6qp--Gk(x$pzd!RCX~T| zZaQj1RQ<&H z=#&@+D#FGwX5IIPRo5-MwK*uxzV(uiy9rco!_+l|KSJ{UJL@E2q;_=krr@xXj;f6A zUPvVbOoo}+7>1~Ng~W+Oruy$M(T=EPJxOZ?v9p}!AXinhWwdq<$}o~K#iXjRyrh3^ z;JB#+P{NFtFTB5N2d@b$m3|qX-^gBw#Un;z3oMBzW)-W8e{tRyUmo+%y0!OBKJ@Z) zkDLt<90d7+nd2W=Zt&&VXO~nrEVa6L+2)EH&OU07oED>8oG+G&#X_M}j9szleLkO{ z*Z(l;w|+<8uNXkHB38jlD@XaR1a(Fh8kS2+0NSwd1gJrMBrGhN%Z|DJuW*JG6ouQw z24?PaIzyQNRtY4mZmU-@iZ~#&{ouh<`3yl$_vtKtEw#t}g9lqR0z}nP{e$mcwkHjF z=$0RsqS%&7srol&zo2d1=`aqIEy!!&S-lMlk`CXSH}JO&N;?1|4CO4!ITY~4Owlo- z7@)cs>q=Ultx@02l@_X)y^b`{N%6 zldc%=Q(3vgMM-!GLyjwc`^!@g1Anu%b-|k-rS_2bGkYiVsTyCb41GbX zN>-B$RJzjOVC9>YUm3^F>Q0E=3kse{0HRoHUA<@_pcr+F)Ea?E7@lANAQ2FNTrfEv zJo%e50T17|$&Cyu79#^NQ`s|}N+`%tpx)j2>)?CXfhrA`rzGZ0PB& zO>$WcU-gvquzv3?PC*Lp3J(y#G78Fy73o1O`njXdeHAum;%y-%4@xDAVNpa>%PLGJ zNq~VR^X&RY6tTI#usGf))y-{F9_^P1d(=iA$|#v%bbY^*Ci!>v|H8j6J@ec9b$#wH zxqj7wPXswiS8 z6vUwiW6RL!nf#FG=p#>Edu2e*Y|R?BKq|XztSL7s&~gGZ!v|P;LSeaQxGHC0Ksc>B zRiD^@*3!|{ugDnnVPq5a>l*hPeaEcJN3H9(bm*qKAt(R5_05mgCueT1=>LAYo@|?NvL}K{mvM0oV0Kn`S5oNrjawwAfv!xN;Q0G4MjHaAbMuMjXZt26PF`&6vo6GOWw zG2pR%3|LK6u|8{(2e(ffp6wqEjjN*R{Rd7s==5XvJ$7>UxUWrW?pGIr%N=dmTM&Hv zj=@kOrJ5~^%ILs;)q(LTGuqF2)mt}m>_URtryC(g0nyMX)>ILWe-q;e1?Nw^@6c(z zF9R^1Idqc;zPq%$x5H|0)4T^RIDXoBN6mG{Le)#4H+0wM5^GIis7KA%pQv$GEGnNd zUeEZ(yKnC?u%`j(`c4xC3On=30moLrSPum(yoDGj)RcCj#*=_s-aojE3=67rBERB6WezNe%I-MU90KVKP&y`sbRxDj@I-FyI1T*}rr7ip`Ya za*8hD=fC^YRdq;hu1>00XF=Eh<0r1hRHxq2Z5}xN6<76mz-lxpAyPUBCYQE13XNRl4S-=MS?a zTyyzx0oZta?{5<(m+F^}u9!ET1Yzf&)=j9P$kO2Z_end#LGfRX8eGa$4Gz0zPnEMK z@uyz<*8gtU5w#RPmdxds#VmyOOud$1FBc@%)=V5{iZ%b)l{tt(Ku06G|RYqDsl?#5w}ZS1O-AcxHCZRTsYS;))0X7-B;2 zA!^_eVBOhGe@!AzLOz^2?||nt{P@nf6GjiK9){}1#j{G*3x-I)Nx0#AFSOG>c~~Nk zQNO+M%K5A7&p6|#en=Qcgq?< zuF4xC?t(37wy=1NRpl->%9|*4)E)Z|Utc)=#vx1IesQ{~3=9#7K?H9^9{=8tzF4we zU9kQSUm7}QR4FVT{kgl7ZLbX)k*j|qJlI**gbZmIe01}y=a(lx8XJ%pq2T1%rySIr zQUfJtiFT421Z>#R(k*!+3fR= zbn42DQO6@*Vkhy;qyKy7CW+PiMFX=9<4*kj^UK;}C#tF~XP(?5v6Q^}&Ogj4sf&E1 z>hE6Gv-BSy&wk~$m$c|qKWQ23|y9z6aR~N@BZC8{^ zTRvQ%v8cvca$0EEu@*lVn+m!{~D zG$S?Skn?{wby53W1FmrDW6?+pH?QoDRlJkPY4=~X_}}(NZ+mlYr0U~$7ptfjjh(*z zro`p_%f{anr5C9G9guQ5ig)_ri>hniFDEM&)yLvB_66O1zIp+3B8=TpQTK6aeO~pk z)Bf7I`q?2>YgC=YPVah9BH!I3@=WH`Bl@3tegZd;S8_ z5KI7X86!r^?b}t&zAI?nnjfWtDu_kgRYOE70iacB1uHxlGI~{6W%z%KvzIA|`72M) zt0yJ3qx5Cee z>gLjy|M%f$6^dZpwpP3jr@uFy0T`P#bq*opgY&?+@G_nhgCD5e^QTR-EBi!P5sa+W0!#^PfH6Q)&Uy;AUx0h4<} z^rTeqDC+~1_Aef?J3w~f#0IDo*kU-Y>7LDX6=Y(&J;^*C%vYXTZHi=q5pU00H)oHu z(f99Dj)_)M7Z0#+@58~U$L+W|lPjlne?L26!ea+2yTC8{)9L{1$#u?o;sBbpsZk(jp!)XaBR&HL00gftN+AI?~#;;3C%dW_OE+am5bvFtxM0qNb2lH@DoRQk#0aLmTg1Q5S@zaJ#j1j{kFLIE%!|t| zBQ-+g8Qu^c+jlSl&I|Fz*%1X|b#})}~PXtzi@cPSMym0J6lMF-2 zo803+UTHxhLzYwfJ%_d)>x?tepQ}%K>GMh6G-o~!YT7^T7EJ^T1=*URZO0FKzq;jd zKkSGrTM9KkhyH8tnY8TlwWAWufV1!h?!L3LLPE;-rq5Q;ZUEUeTO$I&C=|LK&~jgY zbz%QsPPlIU2c1pt56lj0l+1<49rY0_)Do%(Eop>N=&nBc2;21cV+$~RzAEo~$2XJ&;f7a!`5tn2 z9Pu?ctDa#;y;*(_a{r9yIzS7L{&`8d3Z^^EM+ptDdHm?JTXpG&Hac=YA>oySjbnQtqFM`W&e(1i-_c{20H%R?aL^8OgKvAgp+N-OZ zWabf*;h|6^EhXas3is;w3(k0}KW<#G36ZZ{WorxCQ-9k1M-8uwHZ6$n`tqe&$ox~h z_4%y-^OsWIIOg5#b9bNbfSNH%tmbVxMnm=RP|l9LzTe?5U^qFMY&U_mSpY}Bc`kSh zo$;h@N7j)AQ8EskP}fzbbo)RkzTx0ynVPDKurBC)+&JmuwXz{-@T_eV2reKiYD2?|2iV#@XAS2@7kU*{f8@9B5#~REp!* zj+9$od@la}jGITrp=!Cv#1#MdvxpTjQUAxQwrG(|58(2ZpIiTIP1yK#@-DGt)!-x3 zJZWt}KCREb{{!|u1mbXa`}%+8T0}N}@T#&y5=P0}_ntcYsd-`7GlGgb^&rnokB_?W z^@@g78&AK~uh}wCNerA7die%6-YCS}+9b?2jRbtcc8}~hJ^uZTE|5grJT!pnLo2Rh z;PC}N2$Vuq4tFcrZMhNQEnGPg+nJd(zsLCIWY-oY zki75q_z2y3zxhZ)X>EUw18=MnO&Pu%L2P_bJp$S`ER)WbY&ul2pZnABe@%UGz_CLU zNvBl$&7dD3Zu|3=KZjcfBkgIuoFO5mFdvgCMXk1q1B zJr-0ojLyFH;urRMbM~6Sr*oY0(A@(80QP^ls_aJ60M2OW>}$tXz)IYnuz^qLTiw^R zjV$cij`6~Au)Nw9 zt<|~z`OXJZO=X88A+2;)7VaT;A6~aMr9Oy*LgP>NjjKCrVxzO4;WxtBC6lT)Ao4=3v-A#@I+H0R=|Bzb9>-O0~O9GIs8`+X*;6a-qkI zm+vExz_-+Cf-v{K3$Jc7XLX4ggD7Wwis3xyF&$dZbOkxf5& z=Bnf!C$9KPT>_G@lblIes;++Kz|kDUT))>BzH`Mn6PXgpMC=3@xT3Y3#`r&Am(|_! z0{}|!CWLn0zHsD?quCi*MhUZS|cyRc!SfMJ?HXfC>Z&5NBUcq0~-16O?Z!Q@p3z{8L(9<791-}~{#?L4&3OWA;q7-*r_MiE+$5hHg91D&g-ANd=si!7i9*0j03}7>$}?YKj$5#71mS;z zfwpSuLzP20gzBZge%$Q`v9T!2O$}mUgYkcNhMRN}upsjM84#-&Z$#<6=LY_v%0XDkK-LUO&iL*nN364^Af+S^PG?Sf=cD*q zUD@u82cAO`3J0tW+B4+xPpq2!{ftaocxq1^#jDb=*$9sFt(~0}?rR$^@x3uEY!4c+ zFh~`4ECdZwFc5QS;o?-&Af0 z6R~N1Q)e8t&w|4Diy%W2RmVc(dCRVG>VPc5O-$d$a7y?;I|G#9(iXQCfY+2R9|- zM5S$f>vO-I9g%nzW1t}Mp1|TD#Z^3l+G{;wM6(z({kbn&Q&W+suBce%f~xA0=~-Cw z#CdJOg=@&{1!_D_Gk7aJ%7QiIhXx~<<;EuG>D zq)T%sw^SitZVJO9fgIXob!h)siw{?Kq)8m>8&7>joGX8eZRp~*z4b=)o27ERL$ z76RT`x)qbwXR&ANMviuGZZMo$ij3^43RY?MT{jOa!FggR5GOqT-gV>h)+pcI(-92; zykTWy0a7v~)*L7K&G7kQ?ll*SANx9Rn?iQ>`3Vt z7I-kcC-TfO23|P+b2z~nkCYZxeqrOlz?Tv)ym1kCRaN#FW06eOZ3d(8^z$Q0HT4Z= zsoY2H(}(~iOE9P!vU~{p-5DgTpZnZbW|P6LkjuJ3NvL(ktA!GhiH<*A)e};&%0P+<+Tuz@@eU&1=c>{R(fxr+xO;f%(f7|x zwpoqB$i*(Od0UBlPuY9^?Cz@JZ>_ICb{h&4WAT9Owv7=0m~2vAAa$$%bOo8-&WpP` z5pQ6dfA`G4zWPptkSX47N|k>okDVKnhp|N%@44fNi{t8KGHsh0YbyTKzHEK6XsxH_ zZ=CzYoeJ+Wa{YS1TMS)W9PK+F_@%q74qQ`J8CF!MGS!)y@yot9*}OMxpGo7^2B{-! zwrvFJR`>Kh5`}7tDDV`0Y0z&=svzEVb0)UrQIh@-p7i^Re*VujKKL~Eau`;~!-yd1 z^VZ|OlLy8onMCcT_UzMdEz6vHB0D$oD{r6qg9T#YK6dxdWn#pdn)8j#*Iu^%+`}U= z6)?xb+!j%IbxMV_=b&b(E+t654ZK_X)`f`nL!Xq|naL$3h>Le0}&uGr1 zCQb^+XP&v~{g>`}!EE(p^@c;Bu%qYiZ<&grNocs>_3M{Xly4|z<~@DcA5-5ya>BDu zWKaLf=&sFV8xX(0*X!R^4Wq!BRo%aOY|L$yOWT=B6a-!AHnS-`vgHzcx0#*!Qumg@ zsfIUozdoo$2#cTWO2J4Q7pOrgadhp^u5S>#YbDq!xj^6pj#;m=`B}Cb2)m<}(-Z{Lx z6)7-l%4OIV2h>0H!?Ab8t+iX4Izo)BczEBzvdt%;a1w8y5?-Dh8yEqE5;53Z>(e4II8^xrIH0Z)3x-rLpqi)jap|H;?~QcPjK%zM4(HWyQ(b zThE{nH8u!8VXZ0@${xnUupjl~8@@9hep`y(r)h))AKh`)k9rb^{^r`JKfdsH1rJg$ z+xk*b;I#gEHEd&0&oieKjN=GJ@+(TEHVSO$TiQ$zgECU6GWYB|190Ax4}X97x~?Qh zY$Ti%jq0oJ`{L}&|0#Km)N|2lQ**9AZzEnZf>NmuYLzumQp3uIz}ocNJ77lm<--!opa1a>t8ePK^`x(!7l?`3o8GBUQJ{Z&@8kycR_U%in&;JLG#ghJZAn)b zbJXKCF2JU|tE17umB0FRH_BOfL+VKcAU?!~W1srV&tR3-Ex5d8n075%PnCs(V_qLv%9M&~Nu4Igkp0Is;Xk%wMZ$su zF~lY)746{sP4|~%$j_3Qo>aEPMJ4OCy6#X`N|gs9fd!%2wSni=y37xs8M>>_E&s^E zn?U!Te%grJ4mb^se}3hgXMF=+V7w7#_U83tPy6lB2PRY=Iqe{euc+ih>PIwkn^>(Sj4#Zrt*xq|n_^&`#kd$-@bO}`-@M^O}&Bu~ZIU9)C% zMTIUgTMC^$gh{r+$9!)f0k>d)pvn6iQBuT~f?P-bck;K8j4;9{#*_+X~i0 zh%c2&rDCDvOLAUKwyukhPP-_M<5Hnm=uS3vY}}iL$%9~3Spd5;b-s6*7|d}twJS*3 z!5MG8;c(DmgVFotBJ1(H!B-Rm7U2kiTXG=y;HgBf%{r!y-$tnFK~Ofu9-=^;qkt(P zQS-*LUf4Rgm{6f!MU_D6CHKwv1-yQP9;;xEKk~>2_Ep9 z-}I~mW!W_#ijd6QJ$DHw3#pPC6;9TywjONMmRPjn+8n2~E06uF2L^S5{V5R%F7)QXI8gRqsiXYY7ag%(9Pq zz+^odmgl|U#T?iKQq~v zM+E=^6WP49+ z_D1{Xvc&LYy)ufWyPs@a1&)6CtJLy+Xw7|l9WibDcyV~joIf~P{P6g1w_|msmTalD zIgGtl?0L}Op;e`hTx$ujcd<`#$p^mu3s1DTdynple6gr;ZNfQAa&% zK5c+R9!j#^sUxgnNYu<-bv|_0j>JCh3vYZ{ofTuegO%4m9(v9|RgsG;RD(n*5%w>I=KNc* zbmb7G%0Nn4lig4q^q;x8$vWeS2eefM*jUjZ6!d`5XG{o#h$K#QJpMg++NIZO`xnSt zZzWB{9&Bvxfx;!liEtyT7DQ*6ct9}gwR0Ecy8Czp8Gl{;=ds_c7D13IaKa~8V5>sg zwW$yUp-CiILlao|Omp?7w|{ozlM;0P0|~>%8e$Ef6KY+TTid>hIZ{n*?uC#4%|DqkJ?{3VgPSETde6^cA031&Z}FNxBI5;vwGmCELn14 z^O};>bs?8@ovCDZPbh^Vr-JrmTDru?5f@T%RdVz5kA1uuET-hAeLY#!JmRpFqt+#( zCqA<7u5w#uN+p-XcMF4w)L{MK+K_kWb}Rn#jb63HYI@oUKkdz3J`p8GRs?%4XSSc$X2Ikg?ea=G(|HQrh=Y&Fd$a3-A$eTqiZbiZ-U ziC4|o3>Nan_1sz|oJa)Lr0Iwi#YAxhJd9COxj2x_7jJNqDizAzV^u`eJ1;Os?pu8p zKD!B$jS>nLDpw4Wx1QPC_Ax2EJ9ba&KtmF70J`O&+Cid@{3yl6RN#%4o7Wj(C@;RN<{IoKCTbbNU{y6rVr?<$WKnuJ>EfOvNQrz% z?9J;xtr!yRQU6$vBPE45Vbt*BrVW6QlX8a&EhZC@l8F<6c%L@?;-2|0Jk#M=5SDt! zLx8yLZ$IoVrSjE1nq52kz4en{YOhN`LBp4;QU3bR{{3Yt1)87w$3oNa(Gy11*?7%U zm1{28ixsN$8IV{mg(T(4F>2wGuCQL}#P+P55J%>KGY*?Y4C5TBO=YC}~QZAY3B=JyEijy#*k8Hj6 zZ=d(#TkmuL>;daM*m~dFQcI=d4|i@dT1s~$y1X4t``vd86uI`HRVxPjL=^tv#_FEm z%t`+9lmtM&XLD;5&HLqbcQ^(qeE3T#)N@wJr66W$G_0Kwy z*AGoZmJ9%Idp1-I6*Q8pm_$AiL5vXodH;ZxVBo5E`d3WL0g;g8#@^J@5qs`P<`FwB zl@OTL0X4y}-V>1Z?FJSLsMLb9W7C;%Iz?Pr>Ea%d$y<$su4LdvGPhZ{#g*~?1 z-B?H%?zr)uEvT42%=PMx1akH#FK+JFn?f zFkbmizYc%ssE=jXs~bp-05ax*QjZUyp-mJBP_fx;g?W3Xv=H;txG3wAnZe5)t8S(2IzxnS8dQi=!tFP&pcI`$n4VMfkPloIrWe9UzEwKHvy#;1BEn?GQ&&UhX6FJA~EW{!-5 zbD;iIc=6?B3kxhPL`q8Af*A^+JUiH-9Dvc58-Eo%HgT{F_-Fzd#k}ghkDhYP!DH_I z`DF`Uw}OKo+Wh_$-)Gb=Fp=;XCsH25ZK>i9-=@QhhMn!Kwa&H;2)(U_3w)}Iyy?1V zKu>t!p6}Hjyy#U@5U1sI$GhbR`Jd8IUTPK_CY7wG05_c62 z-W(kG22AY#F#G2BhcCPC$n(aJ`7VMu^XK!!es8qbVC!vMoK9DD)a^QA)7)Xs0d{5h86J6WQom1v^#OF-?Zy{MEC8~b0Vw2Q%`5-`NWnHlJ zl@DjH>NlyWY0)$x8(WXvKw_Oeamcmp?Wx+oetz=n|7^J0Uid(>zb#mLGdu=9IK{qp zXmQR#WhGDnY+f?y<9LeSl`&6Hh`}sNRx1B>-i`Z14Ov206(mroka0qC&p{P4kN99( z&9`2j=D3t;TpQQiUA0Ma8CQ%deFe>UjMi_O@a@B@!tT`4g>csIHfPGQn{iFtA#v9k zKSAS}lUrBr-z=v2(1|`kHf-Byed)of#I7?Rl*(O8tq1vr2QK)@!Q?97 zwA5sb2pNU1j`-Q0{EG1(K5*<_?M=g0xNy|kxcSySTPt#jtR{`AduL^IH?dV^ZxIK(%}`K=H}HmH zW=}*=VUP-o@kU5kjR+HMy^)$I;fUR?U686l9K`=0iMK|yA#z^K0000GWmrjONl7XI z2mk;80000000000oIJTG0000bbVXQnWMOn=I%9HWVRU5xGB7eUEif`IF)&mzF*-0b zIyE*cFfckWFue@EOaK4?C3HntbYx+4WjbwdWNBu305UK#Gc7PREif}wFf=+bH##vf zD=;uRFfiK9??V6p04Q`tSaf7zbY(hpX>Db5bYX3905UK#G%YYPEio`uGBG+ZH99di ZD=;uRFfj1ULhAqk002ovPDHLkV1l4|B&+}c literal 0 HcmV?d00001 diff --git a/branches/origin/example/static/robots.txt b/branches/origin/example/static/robots.txt new file mode 100644 index 0000000..c6742d8 --- /dev/null +++ b/branches/origin/example/static/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: / diff --git a/branches/origin/example/templates/index.html b/branches/origin/example/templates/index.html new file mode 100644 index 0000000..2444493 --- /dev/null +++ b/branches/origin/example/templates/index.html @@ -0,0 +1,45 @@ + + + + + + + + + Marisa + + + + + +
+

Marisa

+ + + +
+
+
+ + + +

+ File size limited to {{.Maxsize}}. +

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

cm{X`hdF7YXOsPJ7C&~hxR_fJMhV~@qr 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 ZD=;uRFfj1ULhAqk002ovPDHLkV1l4|B&+}c literal 0 HcmV?d00001 diff --git a/trunk/example/static/robots.txt b/trunk/example/static/robots.txt new file mode 100644 index 0000000..c6742d8 --- /dev/null +++ b/trunk/example/static/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: / diff --git a/trunk/example/templates/index.html b/trunk/example/templates/index.html new file mode 100644 index 0000000..2444493 --- /dev/null +++ b/trunk/example/templates/index.html @@ -0,0 +1,45 @@ + + + + + + + + + Marisa + + + + + +
+

Marisa

+ + + +
+
+
+ + + +

+ File size limited to {{.Maxsize}}. +

+ + + {{if .Links}} + + {{range .Links}}{{end}} + + {{end}} + +
{{.}}
+ + diff --git a/trunk/go.mod b/trunk/go.mod new file mode 100644 index 0000000..d989832 --- /dev/null +++ b/trunk/go.mod @@ -0,0 +1,10 @@ +module marisa.chaotic.ninja/marisa + +go 1.17 + +require ( + github.com/dustin/go-humanize v1.0.0 + gopkg.in/ini.v1 v1.63.2 +) + +require github.com/stretchr/testify v1.8.4 // indirect diff --git a/trunk/go.sum b/trunk/go.sum new file mode 100644 index 0000000..56d5331 --- /dev/null +++ b/trunk/go.sum @@ -0,0 +1,20 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c= +gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/trunk/marisa-trash.1 b/trunk/marisa-trash.1 new file mode 100644 index 0000000..31a9ff8 --- /dev/null +++ b/trunk/marisa-trash.1 @@ -0,0 +1,44 @@ +.Dd $Mdocdate$ +.Dt MARISA-TRASH 1 +.Os +.Sh NAME +.Nm marisa-trash +.Nd Purge expired share files +.Sh SYNOPSIS +.Nm marisa-trash +.Op Fl v +.Op Fl f Ar files +.Op Fl m Ar metadata +.Sh DESCRIPTION +Upon each run, +.Nm +will check expiration times for files in the +.Pa metadata +directory, and delete the according file in the +.Pa files +directory if the expiration time has passed. +.Pp +.Nm +is best run as a +.Xr cron 8 +job, as the same user as the +.Xr marisa 1 +daemon. +.Bl -tag -width Ds +.It Fl v +Turn on verbose logging to +.Pa stderr +.It Fl f Ar files +Set the location of actual files to +.Pa files +.It Fl m Ar metadata +Lookup metadata files in directory +.Pa metadata +.El +.Sh SEE ALSO +.Xr marisa 1 +.Sh AUTHOR +.An Willy Goiffon Aq Mt dev@z3bra.org +.Pp +"Borrowed" by +.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja diff --git a/trunk/marisa.1 b/trunk/marisa.1 new file mode 100644 index 0000000..a45428e --- /dev/null +++ b/trunk/marisa.1 @@ -0,0 +1,43 @@ +.Dd $Mdocdate$ +.Dt MARISA 1 +.Os +.Sh NAME +.Nm marisa +.Nd HTTP based file upload system +.Sh SYNOPSIS +.Nm marisa +.Op Fl v +.Op Fl f Ar file +.Sh DESCRIPTION +.Nm +is an HTTP server that permits temporary file uploads using PUT and +POST requests. +.Pp +Files uploaded are saved in a single directory and given random names +while retaining their original extension. +A configurable expiration time is set for each file, that can be used +to cleanup expired files thanks to +.Xr marisa-trash 1 . +.Bl -tag -width Ds +.It Fl v +Turn on verbose logging to +.Pa stderr +.It Fl f Ar file +Load configuration from +.Pa file +.El +.Sh SEE ALSO +.Xr marisa-trash 1 , +.Xr marisa.conf 5 +.Sh AUTHORS +.An Willy Goiffon Aq Mt dev@z3bra.org +.Pp +"Borrowed" by +.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja +.Sh BUGS +If you upload a file through the browser, and refresh the +page, the file will get constantly reuploaded, which may +exhaust the server's storage at some point. +.Pp +This shouldn't happen with a CLI, such as +.Xr curl 1 diff --git a/trunk/marisa.conf.5 b/trunk/marisa.conf.5 new file mode 100644 index 0000000..03c4d6e --- /dev/null +++ b/trunk/marisa.conf.5 @@ -0,0 +1,94 @@ +.Dd $Mdocdate$ +.Dt MARISA.CONF 5 +.Os +.Sh NAME +.Nm marisa.conf +.Nd marisa configuration file format +.Sh DESCRIPTION +.Nm +is the configuration file for the HTTP file sharing system, +.Xr marisa 1 . +.Sh CONFIGURATION +Here are the settings that can be set: +.Bl -tag -width Ds +.It Ic listen Ar socket +Have the program listen on +.Ar socket . +This socket can be specified either as a TCP socket: +.Ar host:port +or as a Unix socket: +.Ar /path/to/marisa.sock . +When using Unix sockets, the program will serve content using the +.Em FastCGI +protocol. +.It Ic user Ar user +Username that the program will drop privileges to upon startup. When +using Unix sockets, the owner of the socket will be changed to this user. +.It Ic group Ar group +Group that the program will drop privileges to upon startup (require that +.Ic user +is set). When using Unix sockets, the owner group of the socket will be +changed to this group. +.It Ic chroot Pa dir +Directory to chroot into upon startup. When specified, all other path +must be set within the chroot directory. +.It Ic baseuri Ar uri +Base URI to use when constructing hyper links. +.It Ic rootdir Pa dir +Directory containing static files. +.It Ic tmplpath Pa dir +Directory containing template files. +.It Ic filepath Pa dir +Directory where uploaded files must be written to. +.It Ic metapath Pa dir +Directory where metadata for uploaded files will be saved. +.It Ic filectx Pa context +URI context to use for serving files. +.It Ic maxsize Ar size +Maximum size per file to accept for uploads. +.It Ic expiry Ar time +Default expiration time to set for uploads. +.El +.Sh EXAMPLE +Configuration suitable for use with +.Xr httpd 8 +using fastcgi: +.Bd -literal -offset indent +listen = /run/marisa.sock +baseuri = https://domain.tld +user = www +group = daemon +chroot = /var/www +rootdir = /htdocs/static +filepath = /htdocs/files +metapath = /htdocs/meta +tmplpath = /htdocs/templates +filectx = /d/ +maxsize = 10737418240 # 10 Gib +expiry = 86400 # 24 hours +.Ed + +Mathing +.Xr httpd.conf 5 +configuration: +.Bd -literal -offset indent +server "domain.tld" { + listen on * tls port 443 + connection { max request body 10737418240 } + location "*" { + fastcgi socket "/run/marisa.sock" + } +} +types { include "/usr/share/misc/mime.types" } +.Ed + +.Sh SEE ALSO +.Xr marisa 1 , +.Xr marisa-trash 1 , +.Xr httpd 8, +.Xr httpd.conf 5 +.Sh AUTHORS +.An Willy Goiffon Aq Mt dev@z3bra.org +.Pp +"Borrowed" by +.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja diff --git a/trunk/version.go b/trunk/version.go new file mode 100644 index 0000000..611fd43 --- /dev/null +++ b/trunk/version.go @@ -0,0 +1,18 @@ +package marisa + +import ( + "fmt" +) + +var ( + // Version release version + Version = "0.0.1" + + // Commit will be overwritten automatically by the build system + Commit = "HEAD" +) + +// FullVersion display the full version and build +func FullVersion() string { + return fmt.Sprintf("%s@%s", Version, Commit) +} -- 2.45.2