Skip to content

Commit 44b6fbb

Browse files
feature (plg_handler_site): make everything discoverable
1 parent 32526f8 commit 44b6fbb

File tree

5 files changed

+192
-9
lines changed

5 files changed

+192
-9
lines changed

server/plugin/index.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_editor_wopi"
3232
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_console"
3333
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp"
34+
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_site"
3435
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_ascii"
3536
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_c"
3637
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_license"
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package plg_handler_site
2+
3+
import (
4+
. "github.com/mickael-kerjean/filestash/server/common"
5+
)
6+
7+
func init() {
8+
Hooks.Register.Onload(func() {
9+
PluginEnable()
10+
PluginParamAutoindex()
11+
PluginParamCORSAllowOrigins()
12+
})
13+
14+
}
15+
16+
var PluginEnable = func() bool {
17+
return Config.Get("features.site.enable").Schema(func(f *FormElement) *FormElement {
18+
if f == nil {
19+
f = &FormElement{}
20+
}
21+
f.Name = "enable"
22+
f.Type = "enable"
23+
f.Target = []string{"site_autoindex", "site_cors_allow_origins"}
24+
f.Description = "Enable/Disable the creation of site via shared links. Sites will be made available under /public/{shareID}/"
25+
f.Default = false
26+
return f
27+
}).Bool()
28+
}
29+
30+
var PluginParamAutoindex = func() bool {
31+
return Config.Get("features.site.autoindex").Schema(func(f *FormElement) *FormElement {
32+
if f == nil {
33+
f = &FormElement{}
34+
}
35+
f.Id = "site_autoindex"
36+
f.Name = "autoindex"
37+
f.Type = "boolean"
38+
f.Description = "Enables or disables automatic directory listing when no index file is present."
39+
f.Default = false
40+
return f
41+
}).Bool()
42+
}
43+
44+
var PluginParamCORSAllowOrigins = func() string {
45+
return Config.Get("features.site.cors_allow_origins").Schema(func(f *FormElement) *FormElement {
46+
if f == nil {
47+
f = &FormElement{}
48+
}
49+
f.Id = "site_cors_allow_origins"
50+
f.Name = "cors_allow_origins"
51+
f.Type = "text"
52+
f.Placeholder = "* or https://example.com, https://app.example.com"
53+
f.Description = "List of allowed origins for CORS. Use '*' to allow all origins, or provide a comma-separated list."
54+
return f
55+
}).String()
56+
}

server/plugin/plg_handler_site/index.go

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package plg_handler_site
33
import (
44
"io"
55
"net/http"
6+
"os"
67
"strings"
78

89
. "github.com/mickael-kerjean/filestash/server/common"
@@ -15,17 +16,29 @@ import (
1516

1617
func init() {
1718
Hooks.Register.HttpEndpoint(func(r *mux.Router, app *App) error {
19+
if PluginEnable() == false {
20+
return nil
21+
}
1822
r.PathPrefix("/public/{share}/").HandlerFunc(NewMiddlewareChain(
19-
publicHandler,
20-
[]Middleware{SessionStart, SecureHeaders},
23+
SiteHandler,
24+
[]Middleware{SessionStart, SecureHeaders, cors},
2125
*app,
2226
)).Methods("GET", "HEAD")
27+
28+
r.HandleFunc("/public/", NewMiddlewareChain(
29+
SharesListHandler,
30+
[]Middleware{SecureHeaders, basicAdmin},
31+
*app,
32+
)).Methods("GET")
2333
return nil
2434
})
2535
}
2636

27-
func publicHandler(app *App, w http.ResponseWriter, r *http.Request) {
28-
if app.Backend == nil {
37+
func SiteHandler(app *App, w http.ResponseWriter, r *http.Request) {
38+
if r.Method == http.MethodOptions {
39+
w.WriteHeader(http.StatusNoContent)
40+
return
41+
} else if app.Backend == nil {
2942
SendErrorResult(w, ErrNotFound)
3043
return
3144
} else if model.CanRead(app) == false {
@@ -47,12 +60,53 @@ func publicHandler(app *App, w http.ResponseWriter, r *http.Request) {
4760
return
4861
}
4962
}
50-
f, err := app.Backend.Cat(path)
63+
if f, err := app.Backend.Cat(path); err == nil {
64+
w.Header().Set("Content-Type", GetMimeType(path))
65+
io.Copy(w, f)
66+
f.Close()
67+
return
68+
} else if err == ErrNotFound && PluginParamAutoindex() {
69+
if files, err := app.Backend.Ls(strings.TrimSuffix(path, "index.html")); err == nil {
70+
if strings.HasSuffix(r.URL.Path, "/") == false {
71+
http.Redirect(w, r, r.URL.Path+"/", http.StatusSeeOther)
72+
return
73+
}
74+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
75+
if err := TmplAutoindex.Execute(w, map[string]any{
76+
"Base": r.URL.Path,
77+
"Files": files,
78+
}); err != nil {
79+
SendErrorResult(w, err)
80+
}
81+
return
82+
}
83+
}
84+
SendErrorResult(w, ErrNotFound)
85+
}
86+
87+
func SharesListHandler(app *App, w http.ResponseWriter, r *http.Request) {
88+
shares, err := model.ShareAll()
5189
if err != nil {
52-
http.Error(w, err.Error(), http.StatusBadRequest)
90+
SendErrorResult(w, err)
5391
return
5492
}
55-
w.Header().Set("Content-Type", GetMimeType(path))
56-
io.Copy(w, f)
57-
f.Close()
93+
files := make([]os.FileInfo, len(shares))
94+
for i, share := range shares {
95+
t := int64(-1)
96+
if share.Expire != nil {
97+
t = *share.Expire
98+
}
99+
files[i] = File{
100+
FName: share.Id,
101+
FType: "directory",
102+
FTime: t,
103+
}
104+
}
105+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
106+
if err := TmplAutoindex.Execute(w, map[string]any{
107+
"Base": r.URL.Path,
108+
"Files": files,
109+
}); err != nil {
110+
SendErrorResult(w, err)
111+
}
58112
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package plg_handler_site
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
7+
. "github.com/mickael-kerjean/filestash/server/common"
8+
9+
"golang.org/x/crypto/bcrypt"
10+
)
11+
12+
func cors(fn HandlerFunc) HandlerFunc {
13+
return HandlerFunc(func(ctx *App, w http.ResponseWriter, r *http.Request) {
14+
if allowed := PluginParamCORSAllowOrigins(); allowed != "" {
15+
w.Header().Add("Vary", "Origin")
16+
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
17+
switch allowed {
18+
case "*":
19+
w.Header().Set("Access-Control-Allow-Origin", "*")
20+
default:
21+
origin := r.Header.Get("Origin")
22+
for _, o := range strings.Split(allowed, ",") {
23+
if strings.TrimSpace(o) == origin {
24+
w.Header().Set("Access-Control-Allow-Origin", origin)
25+
break
26+
}
27+
}
28+
}
29+
}
30+
fn(ctx, w, r)
31+
})
32+
}
33+
34+
func basicAdmin(fn HandlerFunc) HandlerFunc {
35+
return HandlerFunc(func(ctx *App, w http.ResponseWriter, r *http.Request) {
36+
user, pass, ok := r.BasicAuth()
37+
if !ok || user != "admin" {
38+
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
39+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
40+
return
41+
} else if err := bcrypt.CompareHashAndPassword([]byte(Config.Get("auth.admin").String()), []byte(pass)); err != nil {
42+
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
43+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
44+
return
45+
}
46+
fn(ctx, w, r)
47+
})
48+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package plg_handler_site
2+
3+
import "html/template"
4+
5+
var TmplAutoindex = template.Must(template.New("autoindex").Parse(`
6+
<html>
7+
<head>
8+
<title>Index of {{ .Base }}</title>
9+
</head>
10+
<body>
11+
<h1>Index of {{ .Base }}</h1><pre><a href="../">../</a><br>
12+
{{- range .Files -}}
13+
<a href="{{if .IsDir}}{{printf "./%s/" .Name}}{{else}}{{printf "./%s" .Name}}{{end}}">
14+
{{- if .IsDir -}}
15+
{{ printf "%-40.40s" (printf "%s/" .Name) }}
16+
{{- else -}}
17+
{{ printf "%-40.40s" .Name }}
18+
{{- end -}}
19+
</a> {{ (.ModTime).Format "2006-01-02 15:04:05" }} {{ printf "%8d" .Size }}<br>
20+
{{- end }}
21+
<hr>
22+
</pre>
23+
</body>
24+
</html>`))

0 commit comments

Comments
 (0)