package api import ( "net/http" "os" "path/filepath" "strings" "time" ) // ListenFrontend serves the static client from s.StaticDir on a separate port (e.g. :9082). func (s *Server) ListenFrontend(addr string) error { root := s.StaticDir if root == "" { root = "./client" } // Basic security/CSP headers for static content. addCommonHeaders := func(w http.ResponseWriter) { // CORS: static site can be embedded by any origin if you want, keep strict by default w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") w.Header().Set("Cross-Origin-Resource-Policy", "same-site") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") // Cache: avoid caching during test w.Header().Set("Cache-Control", "no-store") // CSP: no inline scripts/styles; allow XHR/SSE/Ws to any (tunnel/api) host w.Header().Set("Content-Security-Policy", strings.Join([]string{ "default-src 'self'", "script-src 'self'", "style-src 'self'", "img-src 'self' data:", "font-src 'self'", "connect-src *", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", }, "; "), ) } // File handler with index.html fallback for “/”. fileServer := http.FileServer(http.Dir(root)) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { addCommonHeaders(w) // Serve index.html at root or when requesting a directory. p := r.URL.Path if p == "/" || p == "" { http.ServeFile(w, r, filepath.Join(root, "index.html")) return } // If path maps to a directory, try its index.html. full := filepath.Join(root, filepath.Clean(strings.TrimPrefix(p, "/"))) if st, err := os.Stat(full); err == nil && st.IsDir() { indexFile := filepath.Join(full, "index.html") if _, err := os.Stat(indexFile); err == nil { http.ServeFile(w, r, indexFile) return } } // Normal static file. fileServer.ServeHTTP(w, r) }) srv := &http.Server{ Addr: addr, Handler: handler, ReadHeaderTimeout: 5 * time.Second, } return srv.ListenAndServe() }