From a83b199a503d54b1260866392ba5eae110fd8f8f Mon Sep 17 00:00:00 2001 From: Michael Freeman Date: Wed, 26 Feb 2025 09:56:08 -0600 Subject: [PATCH] saving and abandoning in favour of ssr approach --- pkg/cloud/api/server.go | 64 +++++++++++++++++++++++++++++-- pkg/cloud/api/web/dist/index.html | 2 +- web/dist/index.html | 2 +- web/src/services/api.js | 18 ++++++++- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/pkg/cloud/api/server.go b/pkg/cloud/api/server.go index 90b4b27..b587474 100644 --- a/pkg/cloud/api/server.go +++ b/pkg/cloud/api/server.go @@ -3,7 +3,9 @@ package api import ( + "crypto/rand" "embed" + "encoding/base64" "encoding/json" "fmt" "io" @@ -174,14 +176,27 @@ func (*APIServer) writeError(w http.ResponseWriter, msg string, status int) { log.Printf("%s: %d", msg, status) } +// generateCSRFToken creates a random token. +func generateCSRFToken() (string, error) { + b := make([]byte, 32) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(b), nil +} + func (s *APIServer) setupRoutes() { log.Println("Setting up routes...") - // 1. Proxy routes first, no middleware + // Apply CSRF middleware to all routes + s.router.Use(s.csrfMiddleware) + + // Proxy routes s.setupWebProxyRoutes("localhost:8090") log.Println("Web proxy routes registered for /web-api/") - // 2. API routes with middleware on a subrouter + // API routes with additional middleware apiRouter := s.router.PathPrefix("/api").Subrouter() middlewareChain := func(next http.Handler) http.Handler { return srHttp.CommonMiddleware(srHttp.APIKeyMiddleware(next)) @@ -197,7 +212,7 @@ func (s *APIServer) setupRoutes() { apiRouter.HandleFunc("/nodes/{id}/snmp", s.getSNMPData).Methods("GET") log.Println("API routes registered with middleware under /api/") - // 3. Static file serving as fallback + // Static file serving s.configureStaticServing() log.Println("Static file serving registered") } @@ -209,10 +224,51 @@ func (s *APIServer) setupWebProxyRoutes(listenAddr string) { log.Printf("Proxy handler hit for %s", r.URL.Path) s.proxyAPIRequest(w, r, listenAddr) } - webProxyRouter.PathPrefix("/").HandlerFunc(proxyHandler) // Match all under /web-api/ + webProxyRouter.PathPrefix("/").HandlerFunc(proxyHandler) s.router.PathPrefix("/web-api/").Handler(webProxyRouter) } +// csrfMiddleware ensures CSRF token is set and validated +func (s *APIServer) csrfMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set CSRF token on any GET request not under /web-api/ or /api/ + if r.Method == http.MethodGet && !strings.HasPrefix(r.URL.Path, "/web-api/") && !strings.HasPrefix(r.URL.Path, "/api/") { + token, err := generateCSRFToken() + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + http.SetCookie(w, &http.Cookie{ + Name: "csrf_token", + Value: token, + Path: "/", + HttpOnly: false, + SameSite: http.SameSiteLaxMode, + }) + log.Printf("Set CSRF token: %s for %s", token, r.URL.Path) + } + + // Validate CSRF token for /web-api/ requests + if strings.HasPrefix(r.URL.Path, "/web-api/") { + cookie, err := r.Cookie("csrf_token") + if err != nil || cookie == nil || cookie.Value == "" { + http.Error(w, "CSRF token missing", http.StatusForbidden) + log.Printf("CSRF token missing for %s", r.URL.Path) + return + } + headerToken := r.Header.Get("X-CSRF-Token") + if headerToken == "" || headerToken != cookie.Value { + http.Error(w, "Invalid CSRF token", http.StatusForbidden) + log.Printf("Invalid CSRF token for %s", r.URL.Path) + return + } + log.Printf("CSRF token validated for %s", r.URL.Path) + } + + next.ServeHTTP(w, r) + }) +} + // proxyAPIRequest forwards an incoming request to an internal API server. func (s *APIServer) proxyAPIRequest(w http.ResponseWriter, r *http.Request, serverAddr string) { apiPath := strings.TrimPrefix(r.URL.Path, "/web-api/") diff --git a/pkg/cloud/api/web/dist/index.html b/pkg/cloud/api/web/dist/index.html index 9862a83..316a689 100644 --- a/pkg/cloud/api/web/dist/index.html +++ b/pkg/cloud/api/web/dist/index.html @@ -12,7 +12,7 @@ - + diff --git a/web/dist/index.html b/web/dist/index.html index 9862a83..316a689 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -12,7 +12,7 @@ - + diff --git a/web/src/services/api.js b/web/src/services/api.js index 078030a..b7b6721 100644 --- a/web/src/services/api.js +++ b/web/src/services/api.js @@ -11,9 +11,23 @@ */ export const apiRequest = async (url, options = {}) => { - console.log('API request to (originalUrl): ', url); const headers = { ...options.headers || {} }; - const proxyUrl = url.startsWith('/api/') ? `/web-api${url}` : url; // Makes /api/status -> /web-api/api/status + const proxyUrl = url.startsWith('/api/') ? `/web-api${url}` : url; + + // Fetch CSRF token from cookie + const getCookie = (name) => { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + }; + const csrfToken = getCookie('csrf_token'); + if (csrfToken) { + headers['X-CSRF-Token'] = csrfToken; + console.log(`Sending CSRF token: ${csrfToken}`); + } else { + console.warn('CSRF token not found in cookies'); + } + console.log(`API request to: ${proxyUrl}`); return fetch(proxyUrl, { ...options, headers }); };