zoraxy-dhcp/zoraxy_plugin/embed_webserver.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())
}