mirror of
https://github.com/juanfont/headscale.git
synced 2025-11-10 01:20:58 +01:00
debug: add /debug/http-routes endpoint
Add new debug endpoint to list all registered HTTP routes with their
methods and optional names. This helps with debugging and understanding
the API surface area.
The endpoint supports both text and JSON formats:
Text format (default):
```
=== Registered HTTP Routes ===
/api/v1/ [ALL]
/apple [GET]
/health [GET]
/register/{registration_id} [GET]
/ts2021 [POST, GET]
Total routes: 14
```
JSON format:
```json
{
"routes": [
{
"path": "/health",
"methods": ["GET"]
},
{
"path": "/ts2021",
"methods": ["POST", "GET"]
}
],
"total_count": 14
}
```
Usage:
- Text: curl http://localhost:9090/debug/http-routes
- JSON: curl -H "Accept: application/json" http://localhost:9090/debug/http-routes
Changes:
- Modified debugHTTPServer() to accept router parameter
- Added collectRoutes() to walk router and collect route information
- Added debugHTTPRoutes() for text output
- Added debugHTTPRoutesJSON() for structured output
- Added HTTPRouteInfo and DebugHTTPRoutesInfo types
- Added unit tests for route listing functionality
This commit is contained in:
parent
e68e2288f7
commit
733fc40c3d
@ -733,7 +733,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