1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-11-10 01:20:58 +01:00
This commit is contained in:
Kristoffer Dalby 2025-11-02 13:20:26 +01:00 committed by GitHub
commit 411f492b35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 187 additions and 2 deletions

View File

@ -734,7 +734,7 @@ func (h *Headscale) Serve() error {
return fmt.Errorf("failed to bind to TCP address: %w", err)
}
debugHTTPServer := h.debugHTTPServer()
debugHTTPServer := h.debugHTTPServer(router)
errorGroup.Go(func() error { return debugHTTPServer.Serve(debugHTTPListener) })
log.Info().

View File

@ -4,16 +4,18 @@ import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
"github.com/arl/statsviz"
"github.com/gorilla/mux"
"github.com/juanfont/headscale/hscontrol/mapper"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/prometheus/client_golang/prometheus/promhttp"
"tailscale.com/tsweb"
)
func (h *Headscale) debugHTTPServer() *http.Server {
func (h *Headscale) debugHTTPServer(mainRouter *mux.Router) *http.Server {
debugMux := http.NewServeMux()
debug := tsweb.Debugger(debugMux)
@ -268,6 +270,34 @@ func (h *Headscale) debugHTTPServer() *http.Server {
}
}))
// HTTP routes endpoint
debug.Handle("http-routes", "Registered HTTP routes", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check Accept header to determine response format
acceptHeader := r.Header.Get("Accept")
wantsJSON := strings.Contains(acceptHeader, "application/json")
if wantsJSON {
routesInfo := h.debugHTTPRoutesJSON(mainRouter)
routesJSON, err := json.MarshalIndent(routesInfo, "", " ")
if err != nil {
httpError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(routesJSON)
} else {
// Default to text/plain for backward compatibility
routesInfo := h.debugHTTPRoutes(mainRouter)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(routesInfo))
}
}))
err := statsviz.Register(debugMux)
if err == nil {
debug.URL("/debug/statsviz", "Statsviz (visualise go metrics)")
@ -406,3 +436,96 @@ func (h *Headscale) debugBatcherJSON() DebugBatcherInfo {
return info
}
// HTTPRouteInfo represents information about a registered HTTP route.
type HTTPRouteInfo struct {
Path string `json:"path"`
Methods []string `json:"methods"`
Name string `json:"name,omitempty"`
}
// DebugHTTPRoutesInfo represents all HTTP routes in a structured format.
type DebugHTTPRoutesInfo struct {
Routes []HTTPRouteInfo `json:"routes"`
TotalCount int `json:"total_count"`
}
// debugHTTPRoutes returns a text representation of all registered HTTP routes.
func (h *Headscale) debugHTTPRoutes(router *mux.Router) string {
var sb strings.Builder
sb.WriteString("=== Registered HTTP Routes ===\n\n")
routes := collectRoutes(router)
for _, route := range routes {
methods := strings.Join(route.Methods, ", ")
if methods == "" {
methods = "ALL"
}
if route.Name != "" {
sb.WriteString(fmt.Sprintf("%-50s [%-20s] %s\n", route.Path, methods, route.Name))
} else {
sb.WriteString(fmt.Sprintf("%-50s [%-20s]\n", route.Path, methods))
}
}
sb.WriteString(fmt.Sprintf("\nTotal routes: %d\n", len(routes)))
return sb.String()
}
// debugHTTPRoutesJSON returns a structured representation of all registered HTTP routes.
func (h *Headscale) debugHTTPRoutesJSON(router *mux.Router) DebugHTTPRoutesInfo {
routes := collectRoutes(router)
return DebugHTTPRoutesInfo{
Routes: routes,
TotalCount: len(routes),
}
}
// collectRoutes walks the router and collects all registered routes.
// Routes are returned sorted by path for consistent output.
func collectRoutes(router *mux.Router) []HTTPRouteInfo {
var routes []HTTPRouteInfo
_ = router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
pathTemplate, err := route.GetPathTemplate()
if err != nil {
// If we can't get a path template, try GetPathRegexp
var pathRegexp string
pathRegexp, err = route.GetPathRegexp()
if err != nil {
// Skip routes without a path (both template and regexp failed)
return nil //nolint:nilerr // intentionally skip routes without paths
}
pathTemplate = pathRegexp
}
methods, err := route.GetMethods()
if err != nil {
// No methods means it accepts all methods
methods = []string{}
}
name := route.GetName()
routes = append(routes, HTTPRouteInfo{
Path: pathTemplate,
Methods: methods,
Name: name,
})
return nil
})
// Sort routes by path for consistent output
sort.Slice(routes, func(i, j int) bool {
return routes[i].Path < routes[j].Path
})
return routes
}

62
hscontrol/debug_test.go Normal file
View File

@ -0,0 +1,62 @@
package hscontrol
import (
"net/http"
"strings"
"testing"
"github.com/gorilla/mux"
)
func TestDebugHTTPRoutes(t *testing.T) {
// Create a test router with some sample routes
router := mux.NewRouter()
router.HandleFunc("/test1", testHandler1).Methods(http.MethodGet).Name("test1-route")
router.HandleFunc("/test2", testHandler2).Methods(http.MethodPost, http.MethodPut)
router.HandleFunc("/test3/{id}", testHandler3)
// Create a test Headscale instance
h := &Headscale{}
// Test text format
textOutput := h.debugHTTPRoutes(router)
if !strings.Contains(textOutput, "/test1") {
t.Errorf("Expected output to contain /test1, got: %s", textOutput)
}
if !strings.Contains(textOutput, "Total routes:") {
t.Errorf("Expected output to contain total routes count, got: %s", textOutput)
}
// Test JSON format
jsonOutput := h.debugHTTPRoutesJSON(router)
if jsonOutput.TotalCount != 3 {
t.Errorf("Expected 3 routes, got: %d", jsonOutput.TotalCount)
}
// Verify first route has the name we set
foundNamedRoute := false
for _, route := range jsonOutput.Routes {
if route.Name == "test1-route" {
foundNamedRoute = true
break
}
}
if !foundNamedRoute {
t.Error("Expected to find route with name 'test1-route'")
}
}
func testHandler1(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func testHandler2(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func testHandler3(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}