feature (plg_handler_site): discoverability by admin

This commit is contained in:
MickaelK 2025-09-10 16:21:58 +10:00
parent 32526f8bde
commit 6f2404d69a
5 changed files with 192 additions and 9 deletions

View file

@ -31,6 +31,7 @@ import (
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_editor_wopi" _ "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_console"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp" _ "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_ascii"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_c" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_c"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_license" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_license"

View file

@ -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()
}

View file

@ -3,6 +3,7 @@ package plg_handler_site
import ( import (
"io" "io"
"net/http" "net/http"
"os"
"strings" "strings"
. "github.com/mickael-kerjean/filestash/server/common" . "github.com/mickael-kerjean/filestash/server/common"
@ -15,17 +16,29 @@ import (
func init() { func init() {
Hooks.Register.HttpEndpoint(func(r *mux.Router, app *App) error { Hooks.Register.HttpEndpoint(func(r *mux.Router, app *App) error {
if PluginEnable() == false {
return nil
}
r.PathPrefix("/public/{share}/").HandlerFunc(NewMiddlewareChain( r.PathPrefix("/public/{share}/").HandlerFunc(NewMiddlewareChain(
publicHandler, SiteHandler,
[]Middleware{SessionStart, SecureHeaders}, []Middleware{SessionStart, SecureHeaders, cors},
*app, *app,
)).Methods("GET", "HEAD") )).Methods("GET", "HEAD")
r.HandleFunc("/public/", NewMiddlewareChain(
SharesListHandler,
[]Middleware{SecureHeaders, basicAdmin},
*app,
)).Methods("GET")
return nil return nil
}) })
} }
func publicHandler(app *App, w http.ResponseWriter, r *http.Request) { func SiteHandler(app *App, w http.ResponseWriter, r *http.Request) {
if app.Backend == nil { if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
} else if app.Backend == nil {
SendErrorResult(w, ErrNotFound) SendErrorResult(w, ErrNotFound)
return return
} else if model.CanRead(app) == false { } else if model.CanRead(app) == false {
@ -47,12 +60,53 @@ func publicHandler(app *App, w http.ResponseWriter, r *http.Request) {
return return
} }
} }
f, err := app.Backend.Cat(path) if f, err := app.Backend.Cat(path); err == nil {
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", GetMimeType(path)) w.Header().Set("Content-Type", GetMimeType(path))
io.Copy(w, f) io.Copy(w, f)
f.Close() 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 {
SendErrorResult(w, err)
return
}
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)
}
} }

View file

@ -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)
})
}

View file

@ -0,0 +1,24 @@
package plg_handler_site
import "html/template"
var TmplAutoindex = template.Must(template.New("autoindex").Parse(`
<html>
<head>
<title>Index of {{ .Base }}</title>
</head>
<body>
<h1>Index of {{ .Base }}</h1><pre><a href="../">../</a><br>
{{- range .Files -}}
<a href="{{if .IsDir}}{{printf "./%s/" .Name}}{{else}}{{printf "./%s" .Name}}{{end}}">
{{- if .IsDir -}}
{{ printf "%-40.40s" (printf "%s/" .Name) }}
{{- else -}}
{{ printf "%-40.40s" .Name }}
{{- end -}}
</a> {{ (.ModTime).Format "2006-01-02 15:04:05" }} {{ printf "%8d" .Size }}<br>
{{- end }}
<hr>
</pre>
</body>
</html>`))