From 6f2404d69a082442fc23fe87277f65b671982ef7 Mon Sep 17 00:00:00 2001 From: MickaelK Date: Wed, 10 Sep 2025 16:21:58 +1000 Subject: [PATCH] feature (plg_handler_site): discoverability by admin --- server/plugin/index.go | 1 + server/plugin/plg_handler_site/config.go | 56 +++++++++++++++ server/plugin/plg_handler_site/index.go | 72 +++++++++++++++++--- server/plugin/plg_handler_site/middleware.go | 48 +++++++++++++ server/plugin/plg_handler_site/template.go | 24 +++++++ 5 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 server/plugin/plg_handler_site/config.go create mode 100644 server/plugin/plg_handler_site/middleware.go create mode 100644 server/plugin/plg_handler_site/template.go diff --git a/server/plugin/index.go b/server/plugin/index.go index e60f7d99..0bead7a6 100644 --- a/server/plugin/index.go +++ b/server/plugin/index.go @@ -31,6 +31,7 @@ import ( _ "github.com/mickael-kerjean/filestash/server/plugin/plg_editor_wopi" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_console" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp" + _ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_site" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_ascii" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_c" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_license" diff --git a/server/plugin/plg_handler_site/config.go b/server/plugin/plg_handler_site/config.go new file mode 100644 index 00000000..11f2d322 --- /dev/null +++ b/server/plugin/plg_handler_site/config.go @@ -0,0 +1,56 @@ +package plg_handler_site + +import ( + . "github.com/mickael-kerjean/filestash/server/common" +) + +func init() { + Hooks.Register.Onload(func() { + PluginEnable() + PluginParamAutoindex() + PluginParamCORSAllowOrigins() + }) + +} + +var PluginEnable = func() bool { + return Config.Get("features.site.enable").Schema(func(f *FormElement) *FormElement { + if f == nil { + f = &FormElement{} + } + f.Name = "enable" + f.Type = "enable" + f.Target = []string{"site_autoindex", "site_cors_allow_origins"} + f.Description = "Enable/Disable the creation of site via shared links. Sites will be made available under /public/{shareID}/" + f.Default = false + return f + }).Bool() +} + +var PluginParamAutoindex = func() bool { + return Config.Get("features.site.autoindex").Schema(func(f *FormElement) *FormElement { + if f == nil { + f = &FormElement{} + } + f.Id = "site_autoindex" + f.Name = "autoindex" + f.Type = "boolean" + f.Description = "Enables or disables automatic directory listing when no index file is present." + f.Default = false + return f + }).Bool() +} + +var PluginParamCORSAllowOrigins = func() string { + return Config.Get("features.site.cors_allow_origins").Schema(func(f *FormElement) *FormElement { + if f == nil { + f = &FormElement{} + } + f.Id = "site_cors_allow_origins" + f.Name = "cors_allow_origins" + f.Type = "text" + f.Placeholder = "* or https://example.com, https://app.example.com" + f.Description = "List of allowed origins for CORS. Use '*' to allow all origins, or provide a comma-separated list." + return f + }).String() +} diff --git a/server/plugin/plg_handler_site/index.go b/server/plugin/plg_handler_site/index.go index e5b5f8ea..5bcc938c 100644 --- a/server/plugin/plg_handler_site/index.go +++ b/server/plugin/plg_handler_site/index.go @@ -3,6 +3,7 @@ package plg_handler_site import ( "io" "net/http" + "os" "strings" . "github.com/mickael-kerjean/filestash/server/common" @@ -15,17 +16,29 @@ import ( func init() { Hooks.Register.HttpEndpoint(func(r *mux.Router, app *App) error { + if PluginEnable() == false { + return nil + } r.PathPrefix("/public/{share}/").HandlerFunc(NewMiddlewareChain( - publicHandler, - []Middleware{SessionStart, SecureHeaders}, + SiteHandler, + []Middleware{SessionStart, SecureHeaders, cors}, *app, )).Methods("GET", "HEAD") + + r.HandleFunc("/public/", NewMiddlewareChain( + SharesListHandler, + []Middleware{SecureHeaders, basicAdmin}, + *app, + )).Methods("GET") return nil }) } -func publicHandler(app *App, w http.ResponseWriter, r *http.Request) { - if app.Backend == nil { +func SiteHandler(app *App, w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } else if app.Backend == nil { SendErrorResult(w, ErrNotFound) return } else if model.CanRead(app) == false { @@ -47,12 +60,53 @@ func publicHandler(app *App, w http.ResponseWriter, r *http.Request) { return } } - f, err := app.Backend.Cat(path) + if f, err := app.Backend.Cat(path); err == nil { + w.Header().Set("Content-Type", GetMimeType(path)) + io.Copy(w, f) + f.Close() + return + } else if err == ErrNotFound && PluginParamAutoindex() { + if files, err := app.Backend.Ls(strings.TrimSuffix(path, "index.html")); err == nil { + if strings.HasSuffix(r.URL.Path, "/") == false { + http.Redirect(w, r, r.URL.Path+"/", http.StatusSeeOther) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := TmplAutoindex.Execute(w, map[string]any{ + "Base": r.URL.Path, + "Files": files, + }); err != nil { + SendErrorResult(w, err) + } + return + } + } + SendErrorResult(w, ErrNotFound) +} + +func SharesListHandler(app *App, w http.ResponseWriter, r *http.Request) { + shares, err := model.ShareAll() if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + SendErrorResult(w, err) return } - w.Header().Set("Content-Type", GetMimeType(path)) - io.Copy(w, f) - f.Close() + files := make([]os.FileInfo, len(shares)) + for i, share := range shares { + t := int64(-1) + if share.Expire != nil { + t = *share.Expire + } + files[i] = File{ + FName: share.Id, + FType: "directory", + FTime: t, + } + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := TmplAutoindex.Execute(w, map[string]any{ + "Base": r.URL.Path, + "Files": files, + }); err != nil { + SendErrorResult(w, err) + } } diff --git a/server/plugin/plg_handler_site/middleware.go b/server/plugin/plg_handler_site/middleware.go new file mode 100644 index 00000000..91b0cd7d --- /dev/null +++ b/server/plugin/plg_handler_site/middleware.go @@ -0,0 +1,48 @@ +package plg_handler_site + +import ( + "net/http" + "strings" + + . "github.com/mickael-kerjean/filestash/server/common" + + "golang.org/x/crypto/bcrypt" +) + +func cors(fn HandlerFunc) HandlerFunc { + return HandlerFunc(func(ctx *App, w http.ResponseWriter, r *http.Request) { + if allowed := PluginParamCORSAllowOrigins(); allowed != "" { + w.Header().Add("Vary", "Origin") + w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS") + switch allowed { + case "*": + w.Header().Set("Access-Control-Allow-Origin", "*") + default: + origin := r.Header.Get("Origin") + for _, o := range strings.Split(allowed, ",") { + if strings.TrimSpace(o) == origin { + w.Header().Set("Access-Control-Allow-Origin", origin) + break + } + } + } + } + fn(ctx, w, r) + }) +} + +func basicAdmin(fn HandlerFunc) HandlerFunc { + return HandlerFunc(func(ctx *App, w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok || user != "admin" { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } else if err := bcrypt.CompareHashAndPassword([]byte(Config.Get("auth.admin").String()), []byte(pass)); err != nil { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + fn(ctx, w, r) + }) +} diff --git a/server/plugin/plg_handler_site/template.go b/server/plugin/plg_handler_site/template.go new file mode 100644 index 00000000..f429e1fe --- /dev/null +++ b/server/plugin/plg_handler_site/template.go @@ -0,0 +1,24 @@ +package plg_handler_site + +import "html/template" + +var TmplAutoindex = template.Must(template.New("autoindex").Parse(` + + + Index of {{ .Base }} + + +

Index of {{ .Base }}

../
+ {{- range .Files -}} + + {{- if .IsDir -}} + {{ printf "%-40.40s" (printf "%s/" .Name) }} + {{- else -}} + {{ printf "%-40.40s" .Name }} + {{- end -}} + {{ (.ModTime).Format "2006-01-02 15:04:05" }} {{ printf "%8d" .Size }}
+ {{- end }} +
+
+ +`))