mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-13 03:19:08 +09:00
hscontrol: consolidate debug-endpoint and template helpers
This commit is contained in:
+38
-195
@@ -48,51 +48,45 @@ func protectedDebugHandler(h http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// writeJSON marshals v with indentation and writes it as a 200 JSON response.
|
||||
func writeJSON(w http.ResponseWriter, v any) {
|
||||
b, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
|
||||
// writeDebug renders a debug endpoint as JSON or text/plain depending on the
|
||||
// request's Accept header. JSON is produced only when explicitly requested;
|
||||
// text/plain is the default for backward compatibility.
|
||||
func writeDebug(w http.ResponseWriter, r *http.Request, jsonVal func() any, textVal func() string) {
|
||||
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||
writeJSON(w, jsonVal())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(textVal()))
|
||||
}
|
||||
|
||||
func (h *Headscale) debugHTTPServer() *http.Server {
|
||||
debugMux := http.NewServeMux()
|
||||
debug := tsweb.Debugger(debugMux)
|
||||
|
||||
// State overview endpoint
|
||||
debug.Handle("overview", "State overview", 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 {
|
||||
overview := h.state.DebugOverviewJSON()
|
||||
|
||||
overviewJSON, err := json.MarshalIndent(overview, "", " ")
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(overviewJSON)
|
||||
} else {
|
||||
// Default to text/plain for backward compatibility
|
||||
overview := h.state.DebugOverview()
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(overview))
|
||||
}
|
||||
writeDebug(w, r, func() any { return h.state.DebugOverviewJSON() }, h.state.DebugOverview)
|
||||
}))
|
||||
|
||||
// Configuration endpoint
|
||||
debug.Handle("config", "Current configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
config := h.state.DebugConfig()
|
||||
|
||||
configJSON, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(configJSON)
|
||||
writeJSON(w, h.state.DebugConfig())
|
||||
}))
|
||||
|
||||
// Policy endpoint
|
||||
@@ -123,157 +117,37 @@ func (h *Headscale) debugHTTPServer() *http.Server {
|
||||
return
|
||||
}
|
||||
|
||||
filterJSON, err := json.MarshalIndent(filter, "", " ")
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(filterJSON)
|
||||
writeJSON(w, filter)
|
||||
}))
|
||||
|
||||
// SSH policies endpoint
|
||||
debug.Handle("ssh", "SSH policies per node", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sshPolicies := h.state.DebugSSHPolicies()
|
||||
|
||||
sshJSON, err := json.MarshalIndent(sshPolicies, "", " ")
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(sshJSON)
|
||||
writeJSON(w, h.state.DebugSSHPolicies())
|
||||
}))
|
||||
|
||||
// DERP map endpoint
|
||||
debug.Handle("derp", "DERP map configuration", 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 {
|
||||
derpInfo := h.state.DebugDERPJSON()
|
||||
|
||||
derpJSON, err := json.MarshalIndent(derpInfo, "", " ")
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(derpJSON)
|
||||
} else {
|
||||
// Default to text/plain for backward compatibility
|
||||
derpInfo := h.state.DebugDERPMap()
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(derpInfo))
|
||||
}
|
||||
writeDebug(w, r, func() any { return h.state.DebugDERPJSON() }, h.state.DebugDERPMap)
|
||||
}))
|
||||
|
||||
// [state.NodeStore] endpoint
|
||||
debug.Handle("nodestore", "NodeStore information", 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 {
|
||||
nodeStoreNodes := h.state.DebugNodeStoreJSON()
|
||||
|
||||
nodeStoreJSON, err := json.MarshalIndent(nodeStoreNodes, "", " ")
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(nodeStoreJSON)
|
||||
} else {
|
||||
// Default to text/plain for backward compatibility
|
||||
nodeStoreInfo := h.state.DebugNodeStore()
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(nodeStoreInfo))
|
||||
}
|
||||
writeDebug(w, r, func() any { return h.state.DebugNodeStoreJSON() }, h.state.DebugNodeStore)
|
||||
}))
|
||||
|
||||
// Registration cache endpoint
|
||||
debug.Handle("registration-cache", "Registration cache information", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cacheInfo := h.state.DebugRegistrationCache()
|
||||
|
||||
cacheJSON, err := json.MarshalIndent(cacheInfo, "", " ")
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(cacheJSON)
|
||||
writeJSON(w, h.state.DebugRegistrationCache())
|
||||
}))
|
||||
|
||||
// Routes endpoint
|
||||
debug.Handle("routes", "Primary 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 {
|
||||
routes := h.state.DebugRoutes()
|
||||
|
||||
routesJSON, err := json.MarshalIndent(routes, "", " ")
|
||||
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
|
||||
routes := h.state.DebugRoutesString()
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(routes))
|
||||
}
|
||||
writeDebug(w, r, func() any { return h.state.DebugRoutes() }, h.state.DebugRoutesString)
|
||||
}))
|
||||
|
||||
// Policy manager endpoint
|
||||
debug.Handle("policy-manager", "Policy manager state", 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 {
|
||||
policyManagerInfo := h.state.DebugPolicyManagerJSON()
|
||||
|
||||
policyManagerJSON, err := json.MarshalIndent(policyManagerInfo, "", " ")
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(policyManagerJSON)
|
||||
} else {
|
||||
// Default to text/plain for backward compatibility
|
||||
policyManagerInfo := h.state.DebugPolicyManager()
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(policyManagerInfo))
|
||||
}
|
||||
writeDebug(w, r, func() any { return h.state.DebugPolicyManagerJSON() }, h.state.DebugPolicyManager)
|
||||
}))
|
||||
|
||||
debug.Handle("mapresponses", "Map responses for all nodes", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -290,43 +164,12 @@ func (h *Headscale) debugHTTPServer() *http.Server {
|
||||
return
|
||||
}
|
||||
|
||||
resJSON, err := json.MarshalIndent(res, "", " ")
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(resJSON)
|
||||
writeJSON(w, res)
|
||||
}))
|
||||
|
||||
// [mapper.Batcher] endpoint
|
||||
debug.Handle("batcher", "Batcher connected nodes", 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 {
|
||||
batcherInfo := h.debugBatcherJSON()
|
||||
|
||||
batcherJSON, err := json.MarshalIndent(batcherInfo, "", " ")
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(batcherJSON)
|
||||
} else {
|
||||
// Default to text/plain for backward compatibility
|
||||
batcherInfo := h.debugBatcher()
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(batcherInfo))
|
||||
}
|
||||
writeDebug(w, r, func() any { return h.debugBatcherJSON() }, h.debugBatcher)
|
||||
}))
|
||||
|
||||
// Ping endpoint: sends a [tailcfg.PingRequest] to a node and waits for it to respond.
|
||||
|
||||
+13
-8
@@ -300,18 +300,23 @@ func NewAuthProviderWeb(serverURL string) *AuthProviderWeb {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthProviderWeb) RegisterURL(authID types.AuthID) string {
|
||||
// authPathURL builds an auth-flow URL of the form
|
||||
// "<serverURL>/<kind>/<id>", trimming a trailing slash from serverURL.
|
||||
func authPathURL(serverURL, kind string, authID types.AuthID) string {
|
||||
return fmt.Sprintf(
|
||||
"%s/register/%s",
|
||||
strings.TrimSuffix(a.serverURL, "/"),
|
||||
authID.String())
|
||||
"%s/%s/%s",
|
||||
strings.TrimSuffix(serverURL, "/"),
|
||||
kind,
|
||||
authID.String(),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *AuthProviderWeb) RegisterURL(authID types.AuthID) string {
|
||||
return authPathURL(a.serverURL, "register", authID)
|
||||
}
|
||||
|
||||
func (a *AuthProviderWeb) AuthURL(authID types.AuthID) string {
|
||||
return fmt.Sprintf(
|
||||
"%s/auth/%s",
|
||||
strings.TrimSuffix(a.serverURL, "/"),
|
||||
authID.String())
|
||||
return authPathURL(a.serverURL, "auth", authID)
|
||||
}
|
||||
|
||||
func (a *AuthProviderWeb) AuthHandler(
|
||||
|
||||
+2
-8
@@ -116,10 +116,7 @@ func NewAuthProviderOIDC(
|
||||
}
|
||||
|
||||
func (a *AuthProviderOIDC) AuthURL(authID types.AuthID) string {
|
||||
return fmt.Sprintf(
|
||||
"%s/auth/%s",
|
||||
strings.TrimSuffix(a.serverURL, "/"),
|
||||
authID.String())
|
||||
return authPathURL(a.serverURL, "auth", authID)
|
||||
}
|
||||
|
||||
func (a *AuthProviderOIDC) AuthHandler(
|
||||
@@ -130,10 +127,7 @@ func (a *AuthProviderOIDC) AuthHandler(
|
||||
}
|
||||
|
||||
func (a *AuthProviderOIDC) RegisterURL(authID types.AuthID) string {
|
||||
return fmt.Sprintf(
|
||||
"%s/register/%s",
|
||||
strings.TrimSuffix(a.serverURL, "/"),
|
||||
authID.String())
|
||||
return authPathURL(a.serverURL, "register", authID)
|
||||
}
|
||||
|
||||
// RegisterHandler registers the OIDC callback handler with the given router.
|
||||
|
||||
@@ -3,7 +3,6 @@ package hscontrol
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"html/template"
|
||||
"net/http"
|
||||
textTemplate "text/template"
|
||||
|
||||
@@ -59,32 +58,20 @@ func (h *Headscale) ApplePlatformConfig(
|
||||
URL: h.cfg.ServerURL,
|
||||
}
|
||||
|
||||
var payload bytes.Buffer
|
||||
|
||||
switch platform {
|
||||
case "macos-standalone":
|
||||
err := macosStandaloneTemplate.Execute(&payload, platformConfig)
|
||||
if err != nil {
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
case "macos-app-store":
|
||||
err := macosAppStoreTemplate.Execute(&payload, platformConfig)
|
||||
if err != nil {
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
case "ios":
|
||||
err := iosTemplate.Execute(&payload, platformConfig)
|
||||
if err != nil {
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
payloadType, ok := applePayloadType[platform]
|
||||
if !ok {
|
||||
httpError(writer, NewHTTPError(http.StatusBadRequest, "platform must be ios, macos-app-store or macos-standalone", nil))
|
||||
return
|
||||
}
|
||||
|
||||
platformConfig.PayloadType = payloadType
|
||||
|
||||
var payload bytes.Buffer
|
||||
if err := payloadTemplate.Execute(&payload, platformConfig); err != nil { //nolint:noinlineerr
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
config := AppleMobileConfig{
|
||||
UUID: id,
|
||||
URL: h.cfg.ServerURL,
|
||||
@@ -110,8 +97,17 @@ type AppleMobileConfig struct {
|
||||
}
|
||||
|
||||
type AppleMobilePlatformConfig struct {
|
||||
UUID uuid.UUID
|
||||
URL string
|
||||
UUID uuid.UUID
|
||||
URL string
|
||||
PayloadType string
|
||||
}
|
||||
|
||||
// applePayloadType maps a platform request path to the Tailscale IPN
|
||||
// PayloadType emitted in the rendered Apple profile.
|
||||
var applePayloadType = map[string]string{
|
||||
"ios": "io.tailscale.ipn.ios",
|
||||
"macos-app-store": "io.tailscale.ipn.macos",
|
||||
"macos-standalone": "io.tailscale.ipn.macsys",
|
||||
}
|
||||
|
||||
var commonTemplate = textTemplate.Must(
|
||||
@@ -141,10 +137,10 @@ var commonTemplate = textTemplate.Must(
|
||||
</plist>`),
|
||||
)
|
||||
|
||||
var iosTemplate = textTemplate.Must(textTemplate.New("iosTemplate").Parse(`
|
||||
var payloadTemplate = textTemplate.Must(textTemplate.New("payloadTemplate").Parse(`
|
||||
<dict>
|
||||
<key>PayloadType</key>
|
||||
<string>io.tailscale.ipn.ios</string>
|
||||
<string>{{.PayloadType}}</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>{{.UUID}}</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
@@ -158,37 +154,3 @@ var iosTemplate = textTemplate.Must(textTemplate.New("iosTemplate").Parse(`
|
||||
<string>{{.URL}}</string>
|
||||
</dict>
|
||||
`))
|
||||
|
||||
var macosAppStoreTemplate = template.Must(template.New("macosTemplate").Parse(`
|
||||
<dict>
|
||||
<key>PayloadType</key>
|
||||
<string>io.tailscale.ipn.macos</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>{{.UUID}}</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.github.juanfont.headscale</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadEnabled</key>
|
||||
<true/>
|
||||
<key>ControlURL</key>
|
||||
<string>{{.URL}}</string>
|
||||
</dict>
|
||||
`))
|
||||
|
||||
var macosStandaloneTemplate = template.Must(template.New("macosStandaloneTemplate").Parse(`
|
||||
<dict>
|
||||
<key>PayloadType</key>
|
||||
<string>io.tailscale.ipn.macsys</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>{{.UUID}}</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.github.juanfont.headscale</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadEnabled</key>
|
||||
<true/>
|
||||
<key>ControlURL</key>
|
||||
<string>{{.URL}}</string>
|
||||
</dict>
|
||||
`))
|
||||
|
||||
Reference in New Issue
Block a user