mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	* Changed all the HTML into go using go-elem
            Created templates package in ./hscontrol/templates.
            Moved the registerWebAPITemplate into the templates package as a function to be called.
            Replaced the apple and windows html files with go-elem.
* update flake
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
---------
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
Co-authored-by: Kristoffer Dalby <kristoffer@tailscale.com>
		
	
			
		
			
				
	
	
		
			235 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			235 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package hscontrol
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"net/http"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/chasefleming/elem-go"
 | 
						|
	"github.com/chasefleming/elem-go/attrs"
 | 
						|
	"github.com/chasefleming/elem-go/styles"
 | 
						|
	"github.com/gorilla/mux"
 | 
						|
	"github.com/juanfont/headscale/hscontrol/templates"
 | 
						|
	"github.com/rs/zerolog/log"
 | 
						|
	"tailscale.com/tailcfg"
 | 
						|
	"tailscale.com/types/key"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	// The CapabilityVersion is used by Tailscale clients to indicate
 | 
						|
	// their codebase version. Tailscale clients can communicate over TS2021
 | 
						|
	// from CapabilityVersion 28, but we only have good support for it
 | 
						|
	// since https://github.com/tailscale/tailscale/pull/4323 (Noise in any HTTPS port).
 | 
						|
	//
 | 
						|
	// Related to this change, there is https://github.com/tailscale/tailscale/pull/5379,
 | 
						|
	// where CapabilityVersion 39 is introduced to indicate #4323 was merged.
 | 
						|
	//
 | 
						|
	// See also https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go
 | 
						|
	NoiseCapabilityVersion = 39
 | 
						|
 | 
						|
	// TODO(juan): remove this once https://github.com/juanfont/headscale/issues/727 is fixed.
 | 
						|
	registrationHoldoff        = time.Second * 5
 | 
						|
	reservedResponseHeaderSize = 4
 | 
						|
)
 | 
						|
 | 
						|
var ErrRegisterMethodCLIDoesNotSupportExpire = errors.New(
 | 
						|
	"machines registered with CLI does not support expire",
 | 
						|
)
 | 
						|
var ErrNoCapabilityVersion = errors.New("no capability version set")
 | 
						|
 | 
						|
func parseCabailityVersion(req *http.Request) (tailcfg.CapabilityVersion, error) {
 | 
						|
	clientCapabilityStr := req.URL.Query().Get("v")
 | 
						|
 | 
						|
	if clientCapabilityStr == "" {
 | 
						|
		return 0, ErrNoCapabilityVersion
 | 
						|
	}
 | 
						|
 | 
						|
	clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
 | 
						|
	if err != nil {
 | 
						|
		return 0, fmt.Errorf("failed to parse capability version: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
 | 
						|
}
 | 
						|
 | 
						|
// KeyHandler provides the Headscale pub key
 | 
						|
// Listens in /key.
 | 
						|
func (h *Headscale) KeyHandler(
 | 
						|
	writer http.ResponseWriter,
 | 
						|
	req *http.Request,
 | 
						|
) {
 | 
						|
	// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
 | 
						|
	capVer, err := parseCabailityVersion(req)
 | 
						|
	if err != nil {
 | 
						|
		log.Error().
 | 
						|
			Caller().
 | 
						|
			Err(err).
 | 
						|
			Msg("could not get capability version")
 | 
						|
		writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
						|
		writer.WriteHeader(http.StatusInternalServerError)
 | 
						|
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debug().
 | 
						|
		Str("handler", "/key").
 | 
						|
		Int("cap_ver", int(capVer)).
 | 
						|
		Msg("New noise client")
 | 
						|
 | 
						|
	// TS2021 (Tailscale v2 protocol) requires to have a different key
 | 
						|
	if capVer >= NoiseCapabilityVersion {
 | 
						|
		resp := tailcfg.OverTLSPublicKeyResponse{
 | 
						|
			PublicKey: h.noisePrivateKey.Public(),
 | 
						|
		}
 | 
						|
		writer.Header().Set("Content-Type", "application/json")
 | 
						|
		writer.WriteHeader(http.StatusOK)
 | 
						|
		err = json.NewEncoder(writer).Encode(resp)
 | 
						|
		if err != nil {
 | 
						|
			log.Error().
 | 
						|
				Caller().
 | 
						|
				Err(err).
 | 
						|
				Msg("Failed to write response")
 | 
						|
		}
 | 
						|
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (h *Headscale) HealthHandler(
 | 
						|
	writer http.ResponseWriter,
 | 
						|
	req *http.Request,
 | 
						|
) {
 | 
						|
	respond := func(err error) {
 | 
						|
		writer.Header().Set("Content-Type", "application/health+json; charset=utf-8")
 | 
						|
 | 
						|
		res := struct {
 | 
						|
			Status string `json:"status"`
 | 
						|
		}{
 | 
						|
			Status: "pass",
 | 
						|
		}
 | 
						|
 | 
						|
		if err != nil {
 | 
						|
			writer.WriteHeader(http.StatusInternalServerError)
 | 
						|
			log.Error().Caller().Err(err).Msg("health check failed")
 | 
						|
			res.Status = "fail"
 | 
						|
		}
 | 
						|
 | 
						|
		buf, err := json.Marshal(res)
 | 
						|
		if err != nil {
 | 
						|
			log.Error().Caller().Err(err).Msg("marshal failed")
 | 
						|
		}
 | 
						|
		_, err = writer.Write(buf)
 | 
						|
		if err != nil {
 | 
						|
			log.Error().Caller().Err(err).Msg("write failed")
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if err := h.db.PingDB(req.Context()); err != nil {
 | 
						|
		respond(err)
 | 
						|
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	respond(nil)
 | 
						|
}
 | 
						|
 | 
						|
var codeStyleRegisterWebAPI = styles.Props{
 | 
						|
	styles.Display:         "block",
 | 
						|
	styles.Padding:         "20px",
 | 
						|
	styles.Border:          "1px solid #bbb",
 | 
						|
	styles.BackgroundColor: "#eee",
 | 
						|
}
 | 
						|
 | 
						|
func registerWebHTML(key string) *elem.Element {
 | 
						|
	return elem.Html(nil,
 | 
						|
		elem.Head(
 | 
						|
			nil,
 | 
						|
			elem.Title(nil, elem.Text("Registration - Headscale")),
 | 
						|
			elem.Meta(attrs.Props{
 | 
						|
				attrs.Name:    "viewport",
 | 
						|
				attrs.Content: "width=device-width, initial-scale=1",
 | 
						|
			}),
 | 
						|
		),
 | 
						|
		elem.Body(attrs.Props{
 | 
						|
			attrs.Style: styles.Props{
 | 
						|
				styles.FontFamily: "sans",
 | 
						|
			}.ToInline(),
 | 
						|
		},
 | 
						|
			elem.H1(nil, elem.Text("headscale")),
 | 
						|
			elem.H2(nil, elem.Text("Machine registration")),
 | 
						|
			elem.P(nil, elem.Text("Run the command below in the headscale server to add this machine to your network:")),
 | 
						|
			elem.Code(attrs.Props{attrs.Style: codeStyleRegisterWebAPI.ToInline()},
 | 
						|
				elem.Text(fmt.Sprintf("headscale nodes register --user USERNAME --key %s", key)),
 | 
						|
			),
 | 
						|
		),
 | 
						|
	)
 | 
						|
}
 | 
						|
 | 
						|
type AuthProviderWeb struct {
 | 
						|
	serverURL string
 | 
						|
}
 | 
						|
 | 
						|
func NewAuthProviderWeb(serverURL string) *AuthProviderWeb {
 | 
						|
	return &AuthProviderWeb{
 | 
						|
		serverURL: serverURL,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (a *AuthProviderWeb) AuthURL(mKey key.MachinePublic) string {
 | 
						|
	return fmt.Sprintf(
 | 
						|
		"%s/register/%s",
 | 
						|
		strings.TrimSuffix(a.serverURL, "/"),
 | 
						|
		mKey.String())
 | 
						|
}
 | 
						|
 | 
						|
// RegisterWebAPI shows a simple message in the browser to point to the CLI
 | 
						|
// Listens in /register/:nkey.
 | 
						|
//
 | 
						|
// This is not part of the Tailscale control API, as we could send whatever URL
 | 
						|
// in the RegisterResponse.AuthURL field.
 | 
						|
func (a *AuthProviderWeb) RegisterHandler(
 | 
						|
	writer http.ResponseWriter,
 | 
						|
	req *http.Request,
 | 
						|
) {
 | 
						|
	vars := mux.Vars(req)
 | 
						|
	machineKeyStr := vars["mkey"]
 | 
						|
 | 
						|
	// We need to make sure we dont open for XSS style injections, if the parameter that
 | 
						|
	// is passed as a key is not parsable/validated as a NodePublic key, then fail to render
 | 
						|
	// the template and log an error.
 | 
						|
	var machineKey key.MachinePublic
 | 
						|
	err := machineKey.UnmarshalText(
 | 
						|
		[]byte(machineKeyStr),
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		log.Warn().Err(err).Msg("Failed to parse incoming machinekey")
 | 
						|
 | 
						|
		writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
						|
		writer.WriteHeader(http.StatusBadRequest)
 | 
						|
		_, err := writer.Write([]byte("Wrong params"))
 | 
						|
		if err != nil {
 | 
						|
			log.Error().
 | 
						|
				Caller().
 | 
						|
				Err(err).
 | 
						|
				Msg("Failed to write response")
 | 
						|
		}
 | 
						|
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	writer.Header().Set("Content-Type", "text/html; charset=utf-8")
 | 
						|
	writer.WriteHeader(http.StatusOK)
 | 
						|
	if _, err := writer.Write([]byte(registerWebHTML(machineKey.String()).Render())); err != nil {
 | 
						|
		if _, err := writer.Write([]byte(templates.RegisterWeb(machineKey.String()).Render())); err != nil {
 | 
						|
			log.Error().
 | 
						|
				Caller().
 | 
						|
				Err(err).
 | 
						|
				Msg("Failed to write response")
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |