mirror of
https://github.com/juanfont/headscale.git
synced 2025-11-10 01:20:58 +01:00
Merge 733fc40c3d into 2024219bd1
This commit is contained in:
commit
411f492b35
@ -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().
|
||||
|
||||
@ -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
62
hscontrol/debug_test.go
Normal 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)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user