diff --git a/flake.nix b/flake.nix index 4d3552e..3d29ef7 100644 --- a/flake.nix +++ b/flake.nix @@ -15,10 +15,10 @@ packages = rec { slenpaste = pkgs.buildGoModule { pname = "slenpaste"; - version = "0.1.2"; + version = "0.1.3"; src = ./.; goPackagePath = "github.com/slendidev/slenpaste"; - vendorHash = null; + vendorHash = "sha256-MUvodL6K71SCfxu51T/Ka2/w32Kz+IXem1bYqXQLSaU="; }; default = slenpaste; }; @@ -55,6 +55,11 @@ default = false; description = "Whether to expire on first view"; }; + options.services.slenpaste.https = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to use https:// in generated URLs"; + }; config = lib.mkIf config.services.slenpaste.enable { systemd.services.slenpaste = { @@ -68,6 +73,7 @@ -listen "${config.services.slenpaste.listen}" \ -expire "${config.services.slenpaste.expireDur}" \ ${lib.optionalString config.services.slenpaste.expireOnView "-expire-on-view=false"} + ${lib.optionalString config.services.slenpaste.https "-https"} ''; Restart = "on-failure"; }; diff --git a/go.mod b/go.mod index eb0d1a9..2636f89 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/slendidev/slenpaste go 1.24.2 + +require golang.org/x/time v0.11.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6980d24 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= diff --git a/main.go b/main.go index 4736393..07e0170 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,8 @@ import ( "path/filepath" "strings" "time" + "sync" + "golang.org/x/time/rate" ) var ( @@ -20,6 +22,9 @@ var ( staticDir string expireDur time.Duration expireOnView bool + limiters = make(map[string]*rate.Limiter) + limMu sync.Mutex + useHTTPS bool ) type meta struct { @@ -27,6 +32,32 @@ type meta struct { ExpireOnView bool `json:"expire_on_view"` } +func getLimiter(ip string) *rate.Limiter { + limMu.Lock() + defer limMu.Unlock() + lim, ok := limiters[ip] + if !ok { + lim = rate.NewLimiter(1, 5) // 1 req/sec, burst of 5 + limiters[ip] = lim + } + return lim +} + +func rateLimitMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ip := r.RemoteAddr + if i := strings.LastIndex(ip, ":"); i != -1 { + ip = ip[:i] + } + lim := getLimiter(ip) + if !lim.Allow() { + http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) + return + } + next(w, r) + } +} + func randomID(n int) string { letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") b := make([]rune, n) @@ -140,7 +171,11 @@ func uploadHandler(w http.ResponseWriter, r *http.Request) { _ = os.WriteFile(path+".json", metaBytes, 0644) } - fmt.Fprintf(w, "http://%s/%s\n", domain, filename) + scheme := "http" + if useHTTPS { + scheme = "https" + } + fmt.Fprintf(w, "%s://%s/%s\n", scheme, domain, filename) } func viewHandler(w http.ResponseWriter, r *http.Request) { @@ -192,15 +227,16 @@ func main() { flag.StringVar(&staticDir, "static", "static", "directory to save pastes") flag.DurationVar(&expireDur, "expire", 0, "time after which paste expires (e.g. 5m, 1h)") flag.BoolVar(&expireOnView, "expire-on-view", false, "delete paste after it's viewed once") + flag.BoolVar(&useHTTPS, "https", false, "use https:// in generated URLs") flag.Parse() - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.HandleFunc("/", rateLimitMiddleware(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { uploadHandler(w, r) } else { viewHandler(w, r) } - }) + })) fmt.Printf("slenpaste running at http://%s, storing in %s\n", listenAddr, staticDir) http.ListenAndServe(listenAddr, nil)