first commit
This commit is contained in:
136
transports/bifrost-http/handlers/ui.go
Normal file
136
transports/bifrost-http/handlers/ui.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"mime"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fasthttp/router"
|
||||
"github.com/maximhq/bifrost/core/schemas"
|
||||
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// UIHandler handles UI routes.
|
||||
type UIHandler struct {
|
||||
uiContent embed.FS
|
||||
}
|
||||
|
||||
// NewUIHandler creates a new UIHandler instance.
|
||||
func NewUIHandler(uiContent embed.FS) *UIHandler {
|
||||
return &UIHandler{
|
||||
uiContent: uiContent,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers the UI routes with the provided router.
|
||||
func (h *UIHandler) RegisterRoutes(router *router.Router, middlewares ...schemas.BifrostHTTPMiddleware) {
|
||||
router.GET("/", lib.ChainMiddlewares(h.serveDashboard, middlewares...))
|
||||
router.GET("/{filepath:*}", lib.ChainMiddlewares(h.serveDashboard, middlewares...))
|
||||
}
|
||||
|
||||
// ServeDashboard serves the dashboard UI.
|
||||
func (h *UIHandler) serveDashboard(ctx *fasthttp.RequestCtx) {
|
||||
// Get the request path
|
||||
requestPath := string(ctx.Path())
|
||||
|
||||
// Clean the path to prevent directory traversal
|
||||
cleanPath := path.Clean(requestPath)
|
||||
|
||||
// Handle .txt files - map from /{page}.txt to /{page}/index.txt
|
||||
if strings.HasSuffix(cleanPath, ".txt") {
|
||||
// Remove .txt extension and add /index.txt
|
||||
basePath := strings.TrimSuffix(cleanPath, ".txt")
|
||||
if basePath == "/" || basePath == "" {
|
||||
basePath = "/index"
|
||||
}
|
||||
cleanPath = basePath + "/index.txt"
|
||||
}
|
||||
|
||||
// Remove leading slash and add ui prefix
|
||||
if cleanPath == "/" {
|
||||
cleanPath = "ui/index.html"
|
||||
} else {
|
||||
cleanPath = "ui" + cleanPath
|
||||
}
|
||||
|
||||
// Block hidden directories and files (any path segment starting with .)
|
||||
segments := strings.Split(cleanPath, "/")
|
||||
for _, segment := range segments {
|
||||
if strings.HasPrefix(segment, ".") {
|
||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
||||
ctx.SetBodyString("404 - Not found")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Block sensitive files
|
||||
baseName := filepath.Base(cleanPath)
|
||||
sensitiveFiles := []string{"package.json", "package-lock.json"}
|
||||
for _, sensitive := range sensitiveFiles {
|
||||
if baseName == sensitive {
|
||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
||||
ctx.SetBodyString("404 - Not found")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a static asset request (has file extension)
|
||||
hasExtension := strings.Contains(filepath.Base(cleanPath), ".")
|
||||
|
||||
// Try to read the file from embedded filesystem
|
||||
data, err := h.uiContent.ReadFile(cleanPath)
|
||||
if err != nil {
|
||||
|
||||
// If it's a static asset (has extension) and not found, return 404
|
||||
if hasExtension {
|
||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
||||
ctx.SetBodyString("404 - Static asset not found: " + requestPath)
|
||||
return
|
||||
}
|
||||
|
||||
// For routes without extensions (SPA routing), try {path}/index.html first
|
||||
if !hasExtension {
|
||||
indexPath := cleanPath + "/index.html"
|
||||
data, err = h.uiContent.ReadFile(indexPath)
|
||||
if err == nil {
|
||||
cleanPath = indexPath
|
||||
} else {
|
||||
// If that fails, serve root index.html as fallback
|
||||
data, err = h.uiContent.ReadFile("ui/index.html")
|
||||
if err != nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
||||
ctx.SetBodyString("404 - File not found")
|
||||
return
|
||||
}
|
||||
cleanPath = "ui/index.html"
|
||||
}
|
||||
} else {
|
||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
||||
ctx.SetBodyString("404 - File not found")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set content type based on file extension
|
||||
ext := filepath.Ext(cleanPath)
|
||||
contentType := mime.TypeByExtension(ext)
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
ctx.SetContentType(contentType)
|
||||
|
||||
// Set cache headers for static assets
|
||||
if strings.HasPrefix(cleanPath, "ui/assets/") {
|
||||
ctx.Response.Header.Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
} else if ext == ".html" {
|
||||
ctx.Response.Header.Set("Cache-Control", "no-cache")
|
||||
} else {
|
||||
ctx.Response.Header.Set("Cache-Control", "public, max-age=3600")
|
||||
}
|
||||
|
||||
// Send the file content
|
||||
ctx.SetBody(data)
|
||||
}
|
||||
Reference in New Issue
Block a user