Files
kubeviz/internal/httpserver/server.go
Clemens Hering 58a10625c3
All checks were successful
Deploy KubeViz / deploy (push) Successful in 11s
make local
2026-03-01 11:37:20 +01:00

144 lines
3.8 KiB
Go

package httpserver
import (
"context"
"embed"
"encoding/json"
"errors"
"html/template"
"io/fs"
"log"
"net/http"
"strings"
"time"
"kubeviz/internal/config"
"kubeviz/internal/session"
)
const sessionCookieName = "kubeviz_session"
type Server struct {
cfg config.Config
store *session.Store
templates *template.Template
mux *http.ServeMux
}
func New(cfg config.Config) (*Server, error) {
tpls, err := template.ParseFS(templateFS, "ui/templates/*.html")
if err != nil {
return nil, err
}
s := &Server{
cfg: cfg,
store: session.NewStore(cfg.SessionTTL),
templates: tpls,
mux: http.NewServeMux(),
}
s.routes()
return s, nil
}
func (s *Server) routes() {
s.mux.Handle("/", s.middleware(http.HandlerFunc(s.handleIndex)))
s.mux.Handle("/api/manifests/parse", s.middleware(http.HandlerFunc(s.handleParseManifests)))
s.mux.Handle("/api/helm/render", s.middleware(http.HandlerFunc(s.handleHelmRender)))
s.mux.Handle("/api/git/import", s.middleware(http.HandlerFunc(s.handleGitImport)))
s.mux.Handle("/api/graph", s.middleware(http.HandlerFunc(s.handleGraph)))
s.mux.Handle("/api/diff", s.middleware(http.HandlerFunc(s.handleDiff)))
s.mux.Handle("/api/resources/", s.middleware(http.HandlerFunc(s.handleResource)))
s.mux.Handle("/api/export/svg", s.middleware(http.HandlerFunc(s.handleExportSVG)))
s.mux.Handle("/api/export/png", s.middleware(http.HandlerFunc(s.handleExportPNG)))
s.mux.Handle("/api/session/clear", s.middleware(http.HandlerFunc(s.handleClearSession)))
staticSub, _ := fs.Sub(staticFS, "ui/static")
s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))))
}
func (s *Server) Handler() http.Handler {
return s.mux
}
func (s *Server) Shutdown(ctx context.Context) {
s.store.Stop()
_ = ctx
}
func (s *Server) middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
r = r.WithContext(ctx)
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "same-origin")
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self';")
if r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") {
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
next.ServeHTTP(w, r)
})
}
func (s *Server) sessionID(w http.ResponseWriter, r *http.Request) (string, error) {
cookie, err := r.Cookie(sessionCookieName)
if err == nil && cookie.Value != "" {
return cookie.Value, nil
}
if !errors.Is(err, http.ErrNoCookie) && err != nil {
return "", err
}
sid, err := s.store.NewSessionID()
if err != nil {
return "", err
}
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: sid,
Path: "/",
HttpOnly: true,
Secure: s.cfg.CookieSecure,
SameSite: http.SameSiteLaxMode,
MaxAge: int(s.cfg.SessionTTL.Seconds()),
})
return sid, nil
}
func parseRelations(raw string) map[string]bool {
return parseCSVSet(raw)
}
func parseCSVSet(raw string) map[string]bool {
if raw == "" {
return nil
}
out := map[string]bool{}
for _, item := range strings.Split(raw, ",") {
trimmed := strings.TrimSpace(item)
if trimmed == "" {
continue
}
out[trimmed] = true
}
return out
}
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(payload); err != nil {
log.Printf("failed writing JSON: %v", err)
}
}
func subFS(fsys embed.FS, dir string) fs.FS {
sub, err := fs.Sub(fsys, dir)
if err != nil {
panic(err)
}
return sub
}