Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop Gin as web framework for TS2019 API #656

Merged
merged 33 commits into from
Jun 26, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
367da0f
Remove Gin from simple endpoints for TS2019
juanfont Jun 17, 2022
d5e331a
Remove Gin from OIDC callback
juanfont Jun 17, 2022
d89fb68
Switch to use gorilla's mux as muxer
juanfont Jun 18, 2022
6c9c9a4
Remove gin from DERP server
juanfont Jun 18, 2022
e611063
Migrate platform config out of Gin
juanfont Jun 20, 2022
dedeb4c
Remove Gin from the Registration handler
juanfont Jun 20, 2022
53e5c05
Remove gin from the poll handlers
juanfont Jun 20, 2022
396c3ec
Remove Gin from the OIDC handlers
juanfont Jun 20, 2022
b0b919e
Added more logging to derp server
juanfont Jun 20, 2022
dec5134
Minor status change
juanfont Jun 20, 2022
73c16ff
Fixed issue with the method used to send data
juanfont Jun 20, 2022
082fbea
Added mux dependency
juanfont Jun 20, 2022
51b8c65
Updated changelog
juanfont Jun 20, 2022
294975b
Merge branch 'main' into abandon-gin
juanfont Jun 20, 2022
116bef2
Fixed wrong copy paste in Header
juanfont Jun 20, 2022
8e63b53
Merge branch 'abandon-gin' of https://github.com/juanfont/headscale i…
juanfont Jun 20, 2022
5e9004c
Fix issues in the poll loop
juanfont Jun 20, 2022
d404ba1
Use request context to close when client disconnects
juanfont Jun 20, 2022
39b58f7
Use a signal to close the longpolls on shutdown
juanfont Jun 23, 2022
657fb20
Flush buffered data on polling
juanfont Jun 25, 2022
bb4a958
Merge branch 'main' into abandon-gin
juanfont Jun 26, 2022
58c336e
updated nix flake go.sum
juanfont Jun 26, 2022
10cd87e
Lint fixes 1/n
juanfont Jun 26, 2022
a913d1b
Lint fixes 2/n
juanfont Jun 26, 2022
c859bea
Lint fixes 3/n
juanfont Jun 26, 2022
03ced0e
Lint fixes 4/n
juanfont Jun 26, 2022
c810b24
Lint fixes 5/n
juanfont Jun 26, 2022
fa91ece
Lint fixes 6/n
juanfont Jun 26, 2022
ffcc728
Lint fixes 7/n
juanfont Jun 26, 2022
00885df
Fix implicit memory aliasing in for loop (lint 8/n)
juanfont Jun 26, 2022
050782a
Merge branch 'main' into abandon-gin
juanfont Jun 26, 2022
8551b0d
Fixed issue when in linting rampage
juanfont Jun 26, 2022
625e45b
Merge branch 'abandon-gin' of https://github.com/juanfont/headscale i…
juanfont Jun 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ const (

// KeyHandler provides the Headscale pub key
// Listens in /key.
func (h *Headscale) KeyHandler(ctx *gin.Context) {
ctx.Data(
http.StatusOK,
"text/plain; charset=utf-8",
[]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())),
)
func (h *Headscale) KeyHandler(
w http.ResponseWriter,
r *http.Request,
) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())))
}

type registerWebAPITemplateConfig struct {
Expand All @@ -63,10 +64,15 @@ var registerWebAPITemplate = template.Must(

// RegisterWebAPI shows a simple message in the browser to point to the CLI
// Listens in /register.
func (h *Headscale) RegisterWebAPI(ctx *gin.Context) {
machineKeyStr := ctx.Query("key")
func (h *Headscale) RegisterWebAPI(
w http.ResponseWriter,
r *http.Request,
) {
machineKeyStr := r.URL.Query().Get("key")
if machineKeyStr == "" {
ctx.String(http.StatusBadRequest, "Wrong params")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Wrong params"))

return
}
Expand All @@ -79,14 +85,14 @@ func (h *Headscale) RegisterWebAPI(ctx *gin.Context) {
Str("func", "RegisterWebAPI").
Err(err).
Msg("Could not render register web API template")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render register web API template"),
)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Could not render register web API template"))
}

ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes())
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(content.Bytes())
}

// RegistrationHandler handles the actual registration process of a machine
Expand Down
135 changes: 75 additions & 60 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"github.com/gorilla/mux"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
Expand Down Expand Up @@ -326,48 +327,56 @@ func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
return handler(ctx, req)
}

func (h *Headscale) httpAuthenticationMiddleware(ctx *gin.Context) {
log.Trace().
Caller().
Str("client_address", ctx.ClientIP()).
Msg("HTTP authentication invoked")
func (h *Headscale) httpAuthenticationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(
w http.ResponseWriter,
r *http.Request,
) {
log.Trace().
Caller().
Str("client_address", r.RemoteAddr).
Msg("HTTP authentication invoked")

authHeader := ctx.GetHeader("authorization")
authHeader := r.Header.Get("X-Session-Token")

if !strings.HasPrefix(authHeader, AuthPrefix) {
log.Error().
Caller().
Str("client_address", ctx.ClientIP()).
Msg(`missing "Bearer " prefix in "Authorization" header`)
ctx.AbortWithStatus(http.StatusUnauthorized)
if !strings.HasPrefix(authHeader, AuthPrefix) {
log.Error().
Caller().
Str("client_address", r.RemoteAddr).
Msg(`missing "Bearer " prefix in "Authorization" header`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))

return
}
return
}

valid, err := h.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
if err != nil {
log.Error().
Caller().
Err(err).
Str("client_address", ctx.ClientIP()).
Msg("failed to validate token")
valid, err := h.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
if err != nil {
log.Error().
Caller().
Err(err).
Str("client_address", r.RemoteAddr).
Msg("failed to validate token")

ctx.AbortWithStatus(http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Unauthorized"))

return
}
return
}

if !valid {
log.Info().
Str("client_address", ctx.ClientIP()).
Msg("invalid token")
if !valid {
log.Info().
Str("client_address", r.RemoteAddr).
Msg("invalid token")

ctx.AbortWithStatus(http.StatusUnauthorized)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))

return
}
return
}

ctx.Next()
next.ServeHTTP(w, r)
})
}

// ensureUnixSocketIsAbsent will check if the given path for headscales unix socket is clear
Expand All @@ -390,39 +399,42 @@ func (h *Headscale) createPrometheusRouter() *gin.Engine {
return promRouter
}

func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
router := gin.Default()
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
router := mux.NewRouter()

router.GET(
router.HandleFunc(
"/health",
func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": "ok"}) },
)
router.GET("/key", h.KeyHandler)
router.GET("/register", h.RegisterWebAPI)
router.POST("/machine/:id/map", h.PollNetMapHandler)
router.POST("/machine/:id", h.RegistrationHandler)
router.GET("/oidc/register/:mkey", h.RegisterOIDC)
router.GET("/oidc/callback", h.OIDCCallback)
router.GET("/apple", h.AppleConfigMessage)
router.GET("/apple/:platform", h.ApplePlatformConfig)
router.GET("/windows", h.WindowsConfigMessage)
router.GET("/windows/tailscale.reg", h.WindowsRegConfig)
router.GET("/swagger", SwaggerUI)
router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("{\"healthy\": \"ok\"}"))
}).Methods(http.MethodGet)

router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
router.HandleFunc("/register", h.RegisterWebAPI).Methods(http.MethodGet)
router.HandleFunc("/machine/:id/map", h.PollNetMapHandler).Methods(http.MethodPost)
router.HandleFunc("/machine/:id", h.RegistrationHandler).Methods(http.MethodPost)
router.HandleFunc("/oidc/register/:mkey", h.RegisterOIDC).Methods(http.MethodGet)
router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet)
router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/apple/:platform", h.ApplePlatformConfig).Methods(http.MethodGet)
router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig).Methods(http.MethodGet)
router.HandleFunc("/swagger", SwaggerUI).Methods(http.MethodGet)
router.HandleFunc("/swagger/v1/openapiv2.json", SwaggerAPIv1).Methods(http.MethodGet)

if h.cfg.DERP.ServerEnabled {
router.Any("/derp", h.DERPHandler)
router.Any("/derp/probe", h.DERPProbeHandler)
router.Any("/bootstrap-dns", h.DERPBootstrapDNSHandler)
router.HandleFunc("/derp", h.DERPHandler)
router.HandleFunc("/derp/probe", h.DERPProbeHandler)
router.HandleFunc("/bootstrap-dns", h.DERPBootstrapDNSHandler)
}

api := router.Group("/api")
api := router.PathPrefix("/api").Subrouter()
api.Use(h.httpAuthenticationMiddleware)
{
api.Any("/v1/*any", gin.WrapF(grpcMux.ServeHTTP))
api.HandleFunc("/v1/*any", grpcMux.ServeHTTP)
}

router.NoRoute(stdoutHandler)
router.PathPrefix("/").HandlerFunc(stdoutHandler)

return router
}
Expand Down Expand Up @@ -811,13 +823,16 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
}
}

func stdoutHandler(ctx *gin.Context) {
body, _ := io.ReadAll(ctx.Request.Body)
func stdoutHandler(
w http.ResponseWriter,
r *http.Request,
) {
body, _ := io.ReadAll(r.Body)

log.Trace().
Interface("header", ctx.Request.Header).
Interface("proto", ctx.Request.Proto).
Interface("url", ctx.Request.URL).
Interface("header", r.Header).
Interface("proto", r.Proto).
Interface("url", r.URL).
Bytes("body", body).
Msg("Request did not match")
}
Expand Down
20 changes: 14 additions & 6 deletions derp_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"strings"
"time"

"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"tailscale.com/derp"
"tailscale.com/net/stun"
Expand Down Expand Up @@ -90,7 +89,10 @@ func (h *Headscale) generateRegionLocalDERP() (tailcfg.DERPRegion, error) {
return localDERPregion, nil
}

func (h *Headscale) DERPHandler(ctx *gin.Context) {
func (h *Headscale) DERPHandler(
w http.ResponseWriter,
r *http.Request,
) {
log.Trace().Caller().Msgf("/derp request from %v", ctx.ClientIP())
up := strings.ToLower(ctx.Request.Header.Get("Upgrade"))
if up != "websocket" && up != "derp" {
Expand Down Expand Up @@ -143,7 +145,10 @@ func (h *Headscale) DERPHandler(ctx *gin.Context) {

// DERPProbeHandler is the endpoint that js/wasm clients hit to measure
// DERP latency, since they can't do UDP STUN queries.
func (h *Headscale) DERPProbeHandler(ctx *gin.Context) {
func (h *Headscale) DERPProbeHandler(
w http.ResponseWriter,
r *http.Request,
) {
switch ctx.Request.Method {
case "HEAD", "GET":
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
Expand All @@ -159,15 +164,18 @@ func (h *Headscale) DERPProbeHandler(ctx *gin.Context) {
// The initial implementation is here https://github.com/tailscale/tailscale/pull/1406
// They have a cache, but not clear if that is really necessary at Headscale, uh, scale.
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
func (h *Headscale) DERPBootstrapDNSHandler(
w http.ResponseWriter,
r *http.Request,
) {
dnsEntries := make(map[string][]net.IP)

resolvCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var r net.Resolver
var resolver net.Resolver
for _, region := range h.DERPMap.Regions {
for _, node := range region.Nodes { // we don't care if we override some nodes
addrs, err := r.LookupIP(resolvCtx, "ip", node.HostName)
addrs, err := resolver.LookupIP(resolvCtx, "ip", node.HostName)
if err != nil {
log.Trace().
Caller().
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gookit/color v1.5.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/go-version v1.4.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254/go.mod h1:M9mZEtGIsR1oDaZagNPNG9iq9n2HrhZ17dsXk73V3Lw=
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
Expand Down
Loading