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)
|
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) })
|
errorGroup.Go(func() error { return debugHTTPServer.Serve(debugHTTPListener) })
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
|
|||||||
@ -4,16 +4,18 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/arl/statsviz"
|
"github.com/arl/statsviz"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
"github.com/juanfont/headscale/hscontrol/mapper"
|
"github.com/juanfont/headscale/hscontrol/mapper"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"tailscale.com/tsweb"
|
"tailscale.com/tsweb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *Headscale) debugHTTPServer() *http.Server {
|
func (h *Headscale) debugHTTPServer(mainRouter *mux.Router) *http.Server {
|
||||||
debugMux := http.NewServeMux()
|
debugMux := http.NewServeMux()
|
||||||
debug := tsweb.Debugger(debugMux)
|
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)
|
err := statsviz.Register(debugMux)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
debug.URL("/debug/statsviz", "Statsviz (visualise go metrics)")
|
debug.URL("/debug/statsviz", "Statsviz (visualise go metrics)")
|
||||||
@ -406,3 +436,96 @@ func (h *Headscale) debugBatcherJSON() DebugBatcherInfo {
|
|||||||
|
|
||||||
return info
|
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