diff --git a/.gitignore b/.gitignore index 391c53e..31a0442 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ # Binary jaf +yaf +yaf.conf +jaf.conf diff --git a/config.go b/config.go index d7a88d2..2c50253 100644 --- a/config.go +++ b/config.go @@ -23,6 +23,7 @@ type Config struct { ExifAllowedIds []uint16 ExifAllowedPaths []string ExifAbortOnError bool + FileExpiration bool } func ConfigFromFile(filePath string) (*Config, error) { @@ -46,6 +47,7 @@ func ConfigFromFile(filePath string) (*Config, error) { ExifAllowedIds: []uint16{}, ExifAllowedPaths: []string{}, ExifAbortOnError: true, + FileExpiration: true, } scanner := bufio.NewScanner(file) @@ -143,6 +145,13 @@ func ConfigFromFile(filePath string) (*Config, error) { } retval.ExifAbortOnError = parsed + case "FileExpiration": + parsed, err := strconv.ParseBool(val) + if err != nil { + return nil, err + } + + retval.FileExpiration = parsed default: return nil, errors.Errorf("unexpected config key: \"%s\"", key) } diff --git a/config_test.go b/config_test.go index bb431e5..29d8464 100644 --- a/config_test.go +++ b/config_test.go @@ -38,4 +38,5 @@ func TestConfigFromFile(t *testing.T) { assertEqualSlice(config.ExifAllowedIds, []uint16{0x0112, 274}, t) assertEqualSlice(config.ExifAllowedPaths, []string{"IFD/Orientation"}, t) assertEqual(config.ExifAbortOnError, true, t) + assertEqual(config.FileExpiration, true, t) } diff --git a/dist/apple-touch-icon.png b/dist/apple-touch-icon.png new file mode 100644 index 0000000..d593798 Binary files /dev/null and b/dist/apple-touch-icon.png differ diff --git a/dist/favicon.ico b/dist/favicon.ico new file mode 100644 index 0000000..f73028a Binary files /dev/null and b/dist/favicon.ico differ diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..6b3f394 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,68 @@ + + + + + + yaf - yet another fileshare + + + + + +
+yaf.ee - yet another (temporary) fileshare
+- all uploads will be deleted after 7 days
+- maximum file size is 50Mb
+
+
+
+
+ + +
+
+
+
+from your terminal as POST: 
+  $ curl -L -F "file=@/home/noury/xd.txt" i.yaf.ee/upload
+
+from chatterino:
+  Request URL: https://i.yaf.ee/upload
+  Form field: file
+  leave the rest empty
+
+from dankchat:
+  Upload url: https://i.yaf.ee/upload
+  Form field: file
+  leave the rest empty
+
+from sharenix:
+  "Services": [
+    {
+      "Name": "i.yaf.ee",
+      "RequestType": "POST",
+      "RequestURL": "https://i.yaf.ee/upload",
+      "FileFormName": "file"
+    }
+  ]
+
+--------------------------------------------------------------------------------------
+i tried to build this site as a minimalistic and privacy conscious filesharing host mostly 
+meant for sharing screenshots with your friends without having to stumble onto your old
+cringy screenshots 7 years later when someone looks through old logs.
+
+terms of service:
+1. you agree that this is beautiful web design.
+2. do not upload anything that is either:
+  2.1 illegal in luxembourg 
+  2.2 will annoy my hosting provider buyvm.net
+
+privacy policy:
+- i dont store logs
+
+this site and its storage are hosted by https://buyvm.net in a datacenter located in luxembourg. 
+--------------------------------------------------------------------------------------
+if you need to contact me or want your file removed send an email to noury[at]yaf.ee
+source: https://git.sr.ht/~noury/yaf
+
+ diff --git a/dist/og.png b/dist/og.png new file mode 100644 index 0000000..26a5bb5 Binary files /dev/null and b/dist/og.png differ diff --git a/dist/robots.txt b/dist/robots.txt new file mode 100644 index 0000000..e98abc4 --- /dev/null +++ b/dist/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /* diff --git a/dist/style.css b/dist/style.css new file mode 100644 index 0000000..d4c1e15 --- /dev/null +++ b/dist/style.css @@ -0,0 +1,4 @@ +body { + background: #222; + color: #F2F2F2; +} diff --git a/example.conf b/example.conf index e7bb885..c290cec 100644 --- a/example.conf +++ b/example.conf @@ -8,3 +8,4 @@ ScrubExif: true ExifAllowedIds: 0x0112 274 ExifAllowedPaths: IFD/Orientation ExifAbortOnError: true +FileExpiration: true diff --git a/fileexpiration/file_expiration.go b/fileexpiration/file_expiration.go new file mode 100644 index 0000000..52dbed0 --- /dev/null +++ b/fileexpiration/file_expiration.go @@ -0,0 +1,39 @@ +package fileexpiration + +import ( + "fmt" + "io/ioutil" + "os" + "regexp" + "time" +) + +var ( + deleteIgnoreRegexp = regexp.MustCompile("index\\.html|favicon\\.ico") +) + +func DeleteExpired(fd string, maxAge time.Duration) { + files, err := ioutil.ReadDir(fd) + if err != nil { + return + } + + for _, file := range files { + fname := file.Name() + + if file.IsDir() || deleteIgnoreRegexp.MatchString(fname) { + continue + } + + if time.Since(file.ModTime()) > maxAge { + err := os.Remove(fd + fname) + + if err != nil { + fmt.Println(err) + continue + } + + fmt.Printf("Removed %s \n", fname) + } + } +} diff --git a/go.mod b/go.mod index 5c58d77..43c6561 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/leon-richardt/jaf +module git.sr.ht/~noury/yaf -go 1.19 +go 1.20 require ( github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e @@ -9,6 +9,8 @@ require ( github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d github.com/gabriel-vasile/mimetype v1.4.1 github.com/go-errors/errors v1.1.1 + github.com/julienschmidt/httprouter v1.3.0 + github.com/leon-richardt/jaf v0.3.1 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e ) diff --git a/go.sum b/go.sum index 4889d1a..c829824 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,10 @@ github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgR github.com/golang/geo v0.0.0-20200319012246-673a6f80352d h1:C/hKUcHT483btRbeGkrRjJz+Zbcj8audldIi9tRJDCc= github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/leon-richardt/jaf v0.3.1 h1:af6p+xr7KX9OxwuiUHQSG8yVJQppugpW+iS+ptIQ0bM= +github.com/leon-richardt/jaf v0.3.1/go.mod h1:rvBGLF1s8u3XNUTEqGeOSYVoEnIIwI/pKeTEdMdKtJk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= diff --git a/jaf.go b/jaf.go index 10c03a8..7472949 100644 --- a/jaf.go +++ b/jaf.go @@ -4,23 +4,25 @@ import ( "flag" "fmt" "log" - "math/rand" "net/http" "time" + "git.sr.ht/~noury/yaf/fileexpiration" + "github.com/julienschmidt/httprouter" "github.com/leon-richardt/jaf/exifscrubber" ) -const allowedChars = "0123456789ABCDEFGHIJKLMNOPQRTSUVWXYZabcdefghijklmnopqrstuvwxyz" - -var config Config +const ( + allowedChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + maxAge = time.Hour * 24 * 7 // 7 days +) type parameters struct { configFile string } func parseParams() *parameters { - configFile := flag.String("configFile", "jaf.conf", "path to config file") + configFile := flag.String("configFile", "yaf.conf", "path to config file") flag.Parse() retval := ¶meters{} @@ -29,9 +31,6 @@ func parseParams() *parameters { } func main() { - rand.Seed(time.Now().UnixNano()) - log.SetPrefix("jaf > ") - params := parseParams() // Read config @@ -39,6 +38,8 @@ func main() { if err != nil { log.Fatalf("could not parse config file: %s\n", err.Error()) } + fd := config.FileDir + fmt.Println(fd) handler := uploadHandler{ config: config, @@ -49,14 +50,19 @@ func main() { handler.exifScrubber = &scrubber } - // Start server - uploadServer := &http.Server{ - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - Addr: fmt.Sprintf(":%d", config.Port), + if config.FileExpiration { + fmt.Println("FILE EXPIRATION ENABLED") + go func() { + for { + <-time.After(time.Hour * 2) + fileexpiration.DeleteExpired(fd, maxAge) + } + }() } + router := httprouter.New() log.Printf("starting jaf on port %d\n", config.Port) - http.Handle("/upload", &handler) - uploadServer.ListenAndServe() + router.HandlerFunc(http.MethodPost, "/upload", handler.PostUpload) + router.HandlerFunc(http.MethodPost, "/uploadweb", handler.PostUploadRedirect) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", config.Port), router)) } diff --git a/uploadhandler.go b/uploadhandler.go index a536dea..89dfa6b 100644 --- a/uploadhandler.go +++ b/uploadhandler.go @@ -17,9 +17,72 @@ type uploadHandler struct { exifScrubber *exifscrubber.ExifScrubber } -func (handler *uploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (handler *uploadHandler) PostUploadRedirect(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() + // Limit size to 50Mb + r.Body = http.MaxBytesReader(w, r.Body, 50*1024*1024) + uploadFile, header, err := r.FormFile("file") + if err != nil { + http.Error(w, "could not read uploaded file: "+err.Error(), http.StatusBadRequest) + log.Println(" could not read uploaded file: " + err.Error()) + return + } + + fileData, err := io.ReadAll(uploadFile) + uploadFile.Close() + if err != nil { + http.Error(w, "could not read attached file: "+err.Error(), http.StatusInternalServerError) + log.Println(" could not read attached file: " + err.Error()) + return + } + + // Scrub EXIF, if requested and detectable by us + if handler.config.ScrubExif { + scrubbedData, err := handler.exifScrubber.ScrubExif(fileData[:]) + + if err == nil { + // If scrubbing was successful, update what to write to file + fileData = scrubbedData + } else { + // Unknown file types (not PNG or JPEG) are allowed to contain EXIF, as we don't know + // how to handle them. Handling of other errors depends on configuration. + if err != exifscrubber.ErrUnknownFileType { + if handler.config.ExifAbortOnError { + log.Printf("could not scrub EXIF from file, aborting upload: %s", err.Error()) + http.Error( + w, + "could not scrub EXIF from file: "+err.Error(), + http.StatusInternalServerError, + ) + return + } + + // An error occured but we are configured to proceed with the upload anyway + log.Printf( + "could not scrub EXIF from file but proceeding with upload as configured: %s", + err.Error(), + ) + } + } + } + + link, err := generateLink(handler, fileData[:], header.Filename) + if err != nil { + http.Error(w, "could not save file: "+err.Error(), http.StatusInternalServerError) + log.Println(" could not save file: " + err.Error()) + return + } + + // Implicitly means code 200 + http.Redirect(w, r, link, http.StatusFound) +} + +func (handler *uploadHandler) PostUpload(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + // Limit size to 50Mb + r.Body = http.MaxBytesReader(w, r.Body, 50*1024*1024) uploadFile, header, err := r.FormFile("file") if err != nil { http.Error(w, "could not read uploaded file: "+err.Error(), http.StatusBadRequest)