package server import ( "crypto/subtle" "io/fs" "net/http" "runtime" "sort" "strings" "time" "gitea.tzgyn.dev/TZGyn/tunnel/internal/dashboard" ) // ---- admin API: GET /api/admin/overview, bearer ADMIN_TOKEN. type adminServer struct { Domain string `json:"domain"` Version string `json:"version"` Go string `json:"go"` UptimeSeconds int64 `json:"uptime_seconds"` } type adminCA struct { Expires string `json:"expires"` } type adminTotals struct { Tunnels int `json:"tunnels"` Online int `json:"online"` Bridges int `json:"bridges"` Visitors int `json:"visitors"` } type adminTunnel struct { ID string `json:"id"` Hostname string `json:"hostname"` State string `json:"state"` Routes []string `json:"routes"` Visitors int `json:"visitors"` CertificateExpiry string `json:"certificate_expiry,omitempty"` CreatedAt string `json:"created_at,omitempty"` } type adminBridge struct { Session string `json:"session"` Tunnel string `json:"tunnel"` ClientVersion string `json:"client_version,omitempty"` Routes []string `json:"routes"` Visitors int `json:"visitors"` MaxConns int `json:"max_conns"` ConnectedAt string `json:"connected_at"` } type adminOverview struct { Server adminServer `json:"server"` CA adminCA `json:"ca"` Totals adminTotals `json:"totals"` Tunnels []adminTunnel `json:"tunnels"` Bridges []adminBridge `json:"bridges"` } // adminAuthorized enforces the bearer ADMIN_TOKEN; 503 when unset. func (a *API) adminAuthorized(w http.ResponseWriter, r *http.Request) bool { if a.AdminToken == "" { writeJSON(w, http.StatusServiceUnavailable, errInvalidRequest{ Tag: "ServiceUnavailableError", Message: "admin token not configured (set ADMIN_TOKEN)", }) return false } scheme, token, _ := strings.Cut(r.Header.Get("Authorization"), " ") if strings.EqualFold(scheme, "bearer") && token != "" && subtle.ConstantTimeCompare([]byte(token), []byte(a.AdminToken)) == 1 { return true } w.Header().Set("WWW-Authenticate", "Bearer") writeJSON(w, http.StatusUnauthorized, errUnauthorized{ Tag: "UnauthorizedError", Message: "Invalid admin token", }) return false } func (a *API) adminOverviewHandler(w http.ResponseWriter, r *http.Request) { if !a.adminAuthorized(w, r) { return } bridgeInfos := a.Hub.BridgeInfos() routes := map[string][]string{} visitors := map[string]int{} for _, b := range bridgeInfos { routes[b.TunnelID] = b.Routes visitors[b.TunnelID] += b.Visitors } overview := adminOverview{ Server: adminServer{ Domain: a.Domain, Version: a.Version, Go: runtime.Version(), UptimeSeconds: int64(time.Since(a.StartedAt).Seconds()), }, CA: adminCA{Expires: a.caExpiry()}, Totals: adminTotals{Bridges: len(bridgeInfos)}, } for _, b := range bridgeInfos { overview.Totals.Visitors += b.Visitors overview.Bridges = append(overview.Bridges, adminBridge{ Session: b.Session, Tunnel: b.TunnelID, ClientVersion: b.ClientVersion, Routes: b.Routes, Visitors: b.Visitors, MaxConns: b.MaxConns, ConnectedAt: b.ConnectedAt.UTC().Format(time.RFC3339), }) } sort.Slice(overview.Bridges, func(i, j int) bool { return overview.Bridges[i].Session < overview.Bridges[j].Session }) tunnels := a.Store.Tunnels() overview.Totals.Tunnels = len(tunnels) for _, t := range tunnels { state := "offline" if visitors[t.ID] > 0 || routes[t.ID] != nil { state = "online" overview.Totals.Online++ } entry := adminTunnel{ ID: t.ID, Hostname: t.Hostname, State: state, Routes: routes[t.ID], Visitors: visitors[t.ID], CreatedAt: t.CreatedAt, } if t.Certificate != nil { entry.CertificateExpiry = t.Certificate.Expiry } overview.Tunnels = append(overview.Tunnels, entry) } writeJSON(w, http.StatusOK, overview) } func (a *API) caExpiry() string { ca, err := a.Store.ca() if err != nil { return "" } return expiryString(ca.cert.NotAfter) } // dashboard serves the built Svelte admin page and its assets; data comes // from /api/admin/overview. Without a built frontend (only the .gitkeep // placeholder), it answers 503 with the build command. func (a *API) dashboard(w http.ResponseWriter, r *http.Request) { dist := dashboard.FS() if r.URL.Path == "/" || r.URL.Path == "/admin" { page, err := fs.ReadFile(dist, "index.html") if err != nil { http.Error(w, "dashboard not built: run `bun install && bun run build` in web/", http.StatusServiceUnavailable) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("X-Robots-Tag", "noindex") w.Write(page) return } http.FileServer(http.FS(dist)).ServeHTTP(w, r) }