diff --git a/flake.nix b/flake.nix index 858dabff..df1b7e12 100644 --- a/flake.nix +++ b/flake.nix @@ -32,7 +32,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorHash = "sha256-SDJSFji6498WI9bJLmY62VGt21TtD2GxrxRAWyYyr0c="; + vendorHash = "sha256-CMkYTRjmhvTTrB7JbLj0cj9VEyzpG0iUWXkaOagwYTk="; subPackages = ["cmd/headscale"]; diff --git a/go.mod b/go.mod index 2bd17cfd..7eac4652 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.1 require ( github.com/AlecAivazis/survey/v2 v2.3.7 + github.com/chasefleming/elem-go v0.29.0 github.com/coder/websocket v1.8.12 github.com/coreos/go-oidc/v3 v3.11.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc diff --git a/go.sum b/go.sum index e2489aa2..cc15ef6c 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,8 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chasefleming/elem-go v0.29.0 h1:WwrjQcVn6xldhexluvl2Z3sgKi9HTMuzWeEXO4PHsmg= +github.com/chasefleming/elem-go v0.29.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= diff --git a/hscontrol/handlers.go b/hscontrol/handlers.go index 9287eeff..72ec4e42 100644 --- a/hscontrol/handlers.go +++ b/hscontrol/handlers.go @@ -1,17 +1,19 @@ package hscontrol import ( - "bytes" "encoding/json" "errors" "fmt" - "html/template" "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" @@ -135,38 +137,37 @@ func (h *Headscale) HealthHandler( respond(nil) } -type registerWebAPITemplateConfig struct { - Key string +var codeStyleRegisterWebAPI = styles.Props{ + styles.Display: "block", + styles.Padding: "20px", + styles.Border: "1px solid #bbb", + styles.BackgroundColor: "#eee", } -var registerWebAPITemplate = template.Must( - template.New("registerweb").Parse(` - -
-- Run the command below in the headscale server to add this machine to your network: -
-headscale nodes register --user USERNAME --key {{.Key}}
-	
-
-`))
+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
@@ -220,34 +221,14 @@ func (a *AuthProviderWeb) RegisterHandler(
 		return
 	}
 
-	var content bytes.Buffer
-	if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{
-		Key: machineKey.String(),
-	}); err != nil {
-		log.Error().
-			Str("func", "RegisterWebAPI").
-			Err(err).
-			Msg("Could not render register web API template")
-		writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
-		writer.WriteHeader(http.StatusInternalServerError)
-		_, err = writer.Write([]byte("Could not render register web API template"))
-		if err != nil {
+	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")
 		}
-
-		return
-	}
-
-	writer.Header().Set("Content-Type", "text/html; charset=utf-8")
-	writer.WriteHeader(http.StatusOK)
-	_, err = writer.Write(content.Bytes())
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to write response")
 	}
 }
diff --git a/hscontrol/platform_config.go b/hscontrol/platform_config.go
index 9844a606..dc6174a9 100644
--- a/hscontrol/platform_config.go
+++ b/hscontrol/platform_config.go
@@ -9,49 +9,19 @@ import (
 
 	"github.com/gofrs/uuid/v5"
 	"github.com/gorilla/mux"
+	"github.com/juanfont/headscale/hscontrol/templates"
 	"github.com/rs/zerolog/log"
 )
 
-//go:embed templates/apple.html
-var appleTemplate string
-
-//go:embed templates/windows.html
-var windowsTemplate string
-
 // WindowsConfigMessage shows a simple message in the browser for how to configure the Windows Tailscale client.
 func (h *Headscale) WindowsConfigMessage(
 	writer http.ResponseWriter,
 	req *http.Request,
 ) {
-	winTemplate := template.Must(template.New("windows").Parse(windowsTemplate))
-	config := map[string]interface{}{
-		"URL": h.cfg.ServerURL,
-	}
-
-	var payload bytes.Buffer
-	if err := winTemplate.Execute(&payload, config); err != nil {
-		log.Error().
-			Str("handler", "WindowsRegConfig").
-			Err(err).
-			Msg("Could not render Windows index template")
-
-		writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
-		writer.WriteHeader(http.StatusInternalServerError)
-		_, err := writer.Write([]byte("Could not render Windows index template"))
-		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)
-	_, err := writer.Write(payload.Bytes())
-	if err != nil {
+
+	if _, err := writer.Write([]byte(templates.Windows(h.cfg.ServerURL).Render())); err != nil {
 		log.Error().
 			Caller().
 			Err(err).
@@ -64,36 +34,10 @@ func (h *Headscale) AppleConfigMessage(
 	writer http.ResponseWriter,
 	req *http.Request,
 ) {
-	appleTemplate := template.Must(template.New("apple").Parse(appleTemplate))
-
-	config := map[string]interface{}{
-		"URL": h.cfg.ServerURL,
-	}
-
-	var payload bytes.Buffer
-	if err := appleTemplate.Execute(&payload, config); err != nil {
-		log.Error().
-			Str("handler", "AppleMobileConfig").
-			Err(err).
-			Msg("Could not render Apple index template")
-
-		writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
-		writer.WriteHeader(http.StatusInternalServerError)
-		_, err := writer.Write([]byte("Could not render Apple index template"))
-		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)
-	_, err := writer.Write(payload.Bytes())
-	if err != nil {
+
+	if _, err := writer.Write([]byte(templates.Apple(h.cfg.ServerURL).Render())); err != nil {
 		log.Error().
 			Caller().
 			Err(err).
diff --git a/hscontrol/templates/apple.go b/hscontrol/templates/apple.go
new file mode 100644
index 00000000..93f0034d
--- /dev/null
+++ b/hscontrol/templates/apple.go
@@ -0,0 +1,149 @@
+package templates
+
+import (
+	"fmt"
+
+	"github.com/chasefleming/elem-go"
+	"github.com/chasefleming/elem-go/attrs"
+)
+
+func Apple(url string) *elem.Element {
+	return HtmlStructure(
+		elem.Title(nil,
+			elem.Text("headscale - Apple")),
+		elem.Body(attrs.Props{
+			attrs.Style: bodyStyle.ToInline(),
+		},
+			headerOne("headscale: iOS configuration"),
+			headerTwo("GUI"),
+			elem.Ol(nil,
+				elem.Li(nil,
+					elem.Text("Install the official Tailscale iOS client from the "),
+					elem.A(attrs.Props{attrs.Href: "https://apps.apple.com/app/tailscale/id1470499037"},
+						elem.Text("App store"),
+					),
+				),
+				elem.Li(nil,
+					elem.Text("Open Tailscale and make sure you are "),
+					elem.I(nil, elem.Text("not ")),
+					elem.Text("logged in to any account"),
+				),
+				elem.Li(nil,
+					elem.Text("Open Settings on the iOS device"),
+				),
+				elem.Li(nil,
+					elem.Text(`Scroll down to the "third party apps" section, under "Game Center" or "TV Provider"`),
+				),
+				elem.Li(nil,
+					elem.Text("Find Tailscale and select it"),
+					elem.Ul(nil,
+						elem.Li(nil,
+							elem.Text(`If the iOS device was previously logged into Tailscale, switch the "Reset Keychain" toggle to "on"`),
+						),
+					),
+				),
+				elem.Li(nil,
+					elem.Text(fmt.Sprintf(`Enter "%s" under "Alternate Coordination Server URL"`,url)),
+				),
+				elem.Li(nil,
+					elem.Text("Restart the app by closing it from the iOS app switcher, open the app and select the regular sign in option "),
+					elem.I(nil, elem.Text("(non-SSO)")),
+					elem.Text(". It should open up to the headscale authentication page."),
+				),
+				elem.Li(nil,
+					elem.Text("Enter your credentials and log in. Headscale should now be working on your iOS device"),
+				),
+			),
+			headerOne("headscale: macOS configuration"),
+			headerTwo("Command line"),
+			elem.P(nil,
+				elem.Text("Use Tailscale's login command to add your profile:"),
+			),
+			elem.Pre(nil,
+				elem.Code(nil,
+					elem.Text(fmt.Sprintf("tailscale login --login-server %s",url)),
+				),
+			),
+			headerTwo("GUI"),
+			elem.Ol(nil,
+				elem.Li(nil,
+					elem.Text("ALT + Click the Tailscale icon in the menu and hover over the Debug menu"),
+				),
+				elem.Li(nil,
+					elem.Text(`Under "Custom Login Server", select "Add Account..."`),
+				),
+				elem.Li(nil,
+					elem.Text(fmt.Sprintf(`Enter "%s" of the headscale instance and press "Add Account"`,url)),
+				),
+				elem.Li(nil,
+					elem.Text(`Follow the login procedure in the browser`),
+				),
+			),
+			headerTwo("Profiles"),
+			elem.P(nil,
+				elem.Text("Headscale can be set to the default server by installing a Headscale configuration profile:"),
+			),
+			elem.P(nil,
+				elem.A(attrs.Props{attrs.Href: "/apple/macos-app-store", attrs.Download: "headscale_macos.mobileconfig"},
+					elem.Text("macOS AppStore profile "),
+				),
+				elem.A(attrs.Props{attrs.Href: "/apple/macos-standalone", attrs.Download: "headscale_macos.mobileconfig"},
+					elem.Text("macOS Standalone profile"),
+				),
+			),
+			elem.Ol(nil,
+				elem.Li(nil,
+					elem.Text("Download the profile, then open it. When it has been opened, there should be a notification that a profile can be installed"),
+				),
+				elem.Li(nil,
+					elem.Text(`Open System Preferences and go to "Profiles"`),
+				),
+				elem.Li(nil,
+					elem.Text(`Find and install the Headscale profile`),
+				),
+				elem.Li(nil,
+					elem.Text(`Restart Tailscale.app and log in`),
+				),
+			),
+			elem.P(nil, elem.Text("Or")),
+			elem.P(nil,
+				elem.Text("Use your terminal to configure the default setting for Tailscale by issuing:"),
+			),
+			elem.Ul(nil,
+				elem.Li(nil,
+					elem.Text(`for app store client:`),
+					elem.Code(nil,
+						elem.Text(fmt.Sprintf(`defaults write io.tailscale.ipn.macos ControlURL %s`,url)),
+					),
+				),
+				elem.Li(nil,
+					elem.Text(`for standalone client:`),
+					elem.Code(nil,
+						elem.Text(fmt.Sprintf(`defaults write io.tailscale.ipn.macsys ControlURL %s`,url)),
+					),
+				),
+			),
+			elem.P(nil,
+				elem.Text("Restart Tailscale.app and log in."),
+			),
+			headerThree("Caution"),
+			elem.P(nil,
+				elem.Text("You should always download and inspect the profile before installing it:"),
+			),
+			elem.Ul(nil,
+				elem.Li(nil,
+					elem.Text(`for app store client: `),
+					elem.Code(nil,
+						elem.Text(fmt.Sprintf(`curl %s/apple/macos-app-store`,url)),
+					),
+				),
+				elem.Li(nil,
+					elem.Text(`for standalone client: `),
+					elem.Code(nil,
+						elem.Text(fmt.Sprintf(`curl %s/apple/macos-standalone`,url)),
+					),
+				),
+			),
+		),
+	)
+}
diff --git a/hscontrol/templates/apple.html b/hscontrol/templates/apple.html
deleted file mode 100644
index 9582594a..00000000
--- a/hscontrol/templates/apple.html
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
-  
-    
-    
-    
-    Use Tailscale's login command to add your profile:
-tailscale login --login-server {{.URL}}- Headscale can be set to the default server by installing a Headscale - configuration profile: -
-- macOS AppStore profile - macOS Standalone profile -
-Or
-- Use your terminal to configure the default setting for Tailscale by - issuing: -
-defaults write io.tailscale.ipn.macos ControlURL {{.URL}}
-      defaults write io.tailscale.ipn.macsys ControlURL {{.URL}}
-      Restart Tailscale.app and log in.
-- You should always download and inspect the profile before installing it: -
-curl {{.URL}}/apple/macos-app-store
-      curl {{.URL}}/apple/macos-standalone
-      - Download - Tailscale for Windows - and install it. -
- -- Open a Command Prompt or Powershell and use Tailscale's login command to - connect with headscale: -
-tailscale login --login-server {{.URL}}