All checks were successful
Deploy KubeViz / deploy (push) Successful in 12s
146 lines
4.0 KiB
Go
146 lines
4.0 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")
|
|
if s.cfg.AppCSPEnabled {
|
|
w.Header().Set("Content-Security-Policy", "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; script-src 'self'; script-src-elem 'self'; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:;")
|
|
}
|
|
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
|
|
}
|