Files
Agent-System/internal/engineeringdashboard/server.go
T
weichenandCursor aba38ef7a9 fix(engineering-dashboard): drop startup token for personal local use
Rely on loopback bind plus Host/Origin checks; open http://127.0.0.1:8787/
without a query token after SSH tunnel.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 09:46:26 +00:00

192 lines
5.3 KiB
Go

package engineeringdashboard
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"sync"
"time"
)
// Server is the local read-only HTTP surface.
// Access control is loopback bind + Host/Origin checks (personal local tool; no startup token).
type Server struct {
Collector *Collector
mu sync.Mutex
}
// NewServer creates a local read-only server.
func NewServer(c *Collector) (*Server, error) {
return &Server{Collector: c}, nil
}
// Handler returns the HTTP handler.
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", s.handleHealthz)
mux.HandleFunc("/api/v1/snapshot", s.handleSnapshot)
mux.HandleFunc("/api/v1/doc/", s.handleDoc)
mux.HandleFunc("/", s.handleStatic)
return s.secure(mux)
}
func (s *Server) secure(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "..") || strings.Contains(r.URL.RawPath, "..") || strings.Contains(r.URL.RawQuery, "..") {
http.Error(w, "path traversal denied", http.StatusForbidden)
return
}
switch r.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
host := r.Host
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
if host != "127.0.0.1" && host != "localhost" && host != "::1" {
http.Error(w, "host not allowed", http.StatusForbidden)
return
}
origin := r.Header.Get("Origin")
if origin != "" && !isLocalOrigin(origin) {
http.Error(w, "origin not allowed", http.StatusForbidden)
return
}
site := r.Header.Get("Sec-Fetch-Site")
if site == "cross-site" {
http.Error(w, "cross-site fetch denied", http.StatusForbidden)
return
}
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'; img-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("Cache-Control", "no-store")
next.ServeHTTP(w, r)
})
}
func isLocalOrigin(origin string) bool {
return strings.HasPrefix(origin, "http://127.0.0.1") ||
strings.HasPrefix(origin, "http://localhost") ||
strings.HasPrefix(origin, "http://[::1]")
}
func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"ok":true}`))
}
func (s *Server) handleSnapshot(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
snap := s.Collector.Current()
if snap == nil || r.URL.Query().Get("refresh") == "1" {
var err error
snap, err = s.Collector.Refresh(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
} else {
if time.Since(snap.GeneratedAt) > s.Collector.TTL {
go func() { _, _ = s.Collector.Refresh(ctx) }()
}
}
etag := `"` + snap.RevisionID + `"`
w.Header().Set("ETag", etag)
if match := r.Header.Get("If-None-Match"); match == etag {
w.WriteHeader(http.StatusNotModified)
return
}
raw, err := MarshalSnapshotJSON(*snap)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write(raw)
}
func (s *Server) handleDoc(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/v1/doc/")
id = strings.TrimPrefix(id, "/")
if id == "" {
http.Error(w, "missing doc id", http.StatusBadRequest)
return
}
if _, err := SafeDocPath(s.Collector.RepoRoot, id); err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
snap := s.Collector.Current()
if snap == nil {
var err error
snap, err = s.Collector.Refresh(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
}
for _, d := range snap.Documents {
if d.ID == id || d.Path == id {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(d)
return
}
}
http.Error(w, "not found", http.StatusNotFound)
}
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
switch path {
case "/", "/index.html":
b, err := readAsset("assets/index.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(b)
case "/assets/app.js":
b, err := readAsset("assets/app.js")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
_, _ = w.Write(b)
case "/assets/app.css":
b, err := readAsset("assets/app.css")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "text/css; charset=utf-8")
_, _ = w.Write(b)
default:
http.NotFound(w, r)
}
}
// ListenAndServe binds loopback-only.
func ListenAndServe(addr string, h http.Handler) error {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return err
}
ip := net.ParseIP(host)
if ip == nil || !ip.IsLoopback() {
return fmt.Errorf("refusing non-loopback bind address: %s", addr)
}
ln, err := net.Listen("tcp", net.JoinHostPort(host, port))
if err != nil {
return err
}
return http.Serve(ln, h)
}