135 lines
4.1 KiB
Go
135 lines
4.1 KiB
Go
package zoraxy_plugin
|
|
|
|
import (
|
|
"embed"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// PluginUiRouter serves an embedded web UI and provides CSRF token injection.
|
|
type PluginUiRouter struct {
|
|
PluginID string
|
|
TargetFs *embed.FS
|
|
TargetFsPrefix string
|
|
HandlerPrefix string
|
|
EnableDebug bool
|
|
terminateHandler func()
|
|
}
|
|
|
|
// NewPluginEmbedUIRouter creates a router backed by an embed.FS.
|
|
// targetFsPrefix is the root folder within the embed.FS (e.g. "/web").
|
|
// handlerPrefix is the HTTP path prefix (e.g. "/ui").
|
|
func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter {
|
|
if !strings.HasPrefix(targetFsPrefix, "/") {
|
|
targetFsPrefix = "/" + targetFsPrefix
|
|
}
|
|
targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/")
|
|
|
|
if !strings.HasPrefix(handlerPrefix, "/") {
|
|
handlerPrefix = "/" + handlerPrefix
|
|
}
|
|
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
|
|
|
return &PluginUiRouter{
|
|
PluginID: pluginID,
|
|
TargetFs: targetFs,
|
|
TargetFsPrefix: targetFsPrefix,
|
|
HandlerPrefix: handlerPrefix,
|
|
}
|
|
}
|
|
|
|
// csrfMiddleware intercepts HTML responses and injects the CSRF token.
|
|
func (p *PluginUiRouter) csrfMiddleware(r *http.Request, fsHandler http.Handler) http.Handler {
|
|
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
|
if csrfToken == "" {
|
|
csrfToken = "missing-csrf-token"
|
|
}
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, ".html") {
|
|
targetPath := p.TargetFsPrefix + "/" + strings.TrimPrefix(r.URL.Path, "/")
|
|
targetPath = strings.TrimPrefix(targetPath, "/")
|
|
content, err := fs.ReadFile(*p.TargetFs, targetPath)
|
|
if err != nil {
|
|
http.Error(w, "File not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
body := strings.ReplaceAll(string(content), "{{.csrfToken}}", csrfToken)
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(body))
|
|
return
|
|
}
|
|
if strings.HasSuffix(r.URL.Path, "/") {
|
|
indexPath := p.TargetFsPrefix + "/" + strings.TrimPrefix(r.URL.Path, "/") + "index.html"
|
|
indexPath = strings.TrimPrefix(indexPath, "/")
|
|
content, err := fs.ReadFile(*p.TargetFs, indexPath)
|
|
if err == nil {
|
|
body := strings.ReplaceAll(string(content), "{{.csrfToken}}", csrfToken)
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(body))
|
|
return
|
|
}
|
|
}
|
|
fsHandler.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// Handler returns an http.Handler for the embedded UI.
|
|
func (p *PluginUiRouter) Handler() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
rewrittenURL := strings.TrimPrefix(r.RequestURI, p.HandlerPrefix)
|
|
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
|
r.URL, _ = url.Parse(rewrittenURL)
|
|
r.RequestURI = rewrittenURL
|
|
|
|
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
|
if err != nil {
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
p.csrfMiddleware(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// RegisterTerminateHandler registers a graceful shutdown endpoint at {prefix}/term.
|
|
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
|
p.terminateHandler = termFunc
|
|
if mux == nil {
|
|
mux = http.DefaultServeMux
|
|
}
|
|
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
|
p.terminateHandler()
|
|
w.WriteHeader(http.StatusOK)
|
|
go func() {
|
|
time.Sleep(100 * time.Millisecond)
|
|
os.Exit(0)
|
|
}()
|
|
})
|
|
}
|
|
|
|
// HandleFunc registers a handler under the UI path prefix.
|
|
func (p *PluginUiRouter) HandleFunc(pattern string, handler http.HandlerFunc, mux *http.ServeMux) {
|
|
if mux == nil {
|
|
mux = http.DefaultServeMux
|
|
}
|
|
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
|
|
pattern = p.HandlerPrefix + pattern
|
|
}
|
|
mux.HandleFunc(pattern, handler)
|
|
}
|
|
|
|
// AttachHandlerToMux attaches the UI file handler to the mux.
|
|
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
|
if mux == nil {
|
|
mux = http.DefaultServeMux
|
|
}
|
|
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
|
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
|
}
|