This commit is contained in:
143
internal/httpserver/server.go
Normal file
143
internal/httpserver/server.go
Normal file
@@ -0,0 +1,143 @@
|
||||
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' https://unpkg.com https://cdn.jsdelivr.net; 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
|
||||
}
|
||||
Reference in New Issue
Block a user