From: www Date: Sun, 29 Sep 2024 21:29:49 +0000 (+0000) Subject: Mirrored from marisa.git X-Git-Url: https://git.chaotic.ninja/gitweb/yakumo_izuru/?a=commitdiff_plain;h=refs%2Fheads%2Fmaster;p=marisa.git Mirrored from marisa.git git-svn-id: https://svn.chaotic.ninja/svn/marisa-yakumo.izuru@1 53897a95-702d-234f-8f73-912f772065ac --- 028e57451f1d7f9df1c27d5ae2ff100f93010a22 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 0000000..9aed90e Binary files /dev/null and b/branches/master/example/static/favicon.ico differ diff --git a/branches/master/example/static/marisa.css b/branches/master/example/static/marisa.css new file mode 100644 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 0000000..4636a72 Binary files /dev/null and b/branches/master/example/static/marisa.png differ diff --git a/branches/master/example/static/robots.txt b/branches/master/example/static/robots.txt new file mode 100644 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 0000000..9aed90e Binary files /dev/null and b/branches/origin-master/example/static/favicon.ico differ diff --git a/branches/origin-master/example/static/marisa.css b/branches/origin-master/example/static/marisa.css new file mode 100644 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 0000000..4636a72 Binary files /dev/null and b/branches/origin-master/example/static/marisa.png differ diff --git a/branches/origin-master/example/static/robots.txt b/branches/origin-master/example/static/robots.txt new file mode 100644 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 0000000..9aed90e Binary files /dev/null and b/branches/origin/example/static/favicon.ico differ diff --git a/branches/origin/example/static/marisa.css b/branches/origin/example/static/marisa.css new file mode 100644 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 0000000..4636a72 Binary files /dev/null and b/branches/origin/example/static/marisa.png differ diff --git a/branches/origin/example/static/robots.txt b/branches/origin/example/static/robots.txt new file mode 100644 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 0000000..9aed90e Binary files /dev/null and b/trunk/example/static/favicon.ico differ diff --git a/trunk/example/static/marisa.css b/trunk/example/static/marisa.css new file mode 100644 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 0000000..4636a72 Binary files /dev/null and b/trunk/example/static/marisa.png differ diff --git a/trunk/example/static/robots.txt b/trunk/example/static/robots.txt new file mode 100644 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) +}