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(` - - - Registration - Headscale - - - - -

headscale

-

Machine registration

-

- 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 @@ - - - - - - - headscale - Apple - - - - -

headscale: iOS configuration

-

GUI

-
    -
  1. - Install the official Tailscale iOS client from the - App store -
  2. -
  3. - Open Tailscale and make sure you are not logged in to any account -
  4. -
  5. Open Settings on the iOS device
  6. -
  7. - Scroll down to the "third party apps" section, under "Game Center" or - "TV Provider" -
  8. -
  9. - Find Tailscale and select it - -
  10. -
  11. Enter "{{.URL}}" under "Alternate Coordination Server URL"
  12. -
  13. - Restart the app by closing it from the iOS app switcher, open the app - and select the regular sign in option (non-SSO). It should open - up to the headscale authentication page. -
  14. -
  15. - Enter your credentials and log in. Headscale should now be working on - your iOS device -
  16. -
-

headscale: macOS configuration

-

Command line

-

Use Tailscale's login command to add your profile:

-
tailscale login --login-server {{.URL}}
-

GUI

-
    -
  1. - ALT + Click the Tailscale icon in the menu and hover over the Debug menu -
  2. -
  3. Under "Custom Login Server", select "Add Account..."
  4. -
  5. - Enter "{{.URL}}" of the headscale instance and press "Add Account" -
  6. -
  7. Follow the login procedure in the browser
  8. -
-

Profiles

-

- Headscale can be set to the default server by installing a Headscale - configuration profile: -

-

- macOS AppStore profile - macOS Standalone profile -

-
    -
  1. - Download the profile, then open it. When it has been opened, there - should be a notification that a profile can be installed -
  2. -
  3. Open System Preferences and go to "Profiles"
  4. -
  5. Find and install the Headscale profile
  6. -
  7. Restart Tailscale.app and log in
  8. -
-

Or

-

- Use your terminal to configure the default setting for Tailscale by - issuing: -

- -

Restart Tailscale.app and log in.

-

Caution

-

- You should always download and inspect the profile before installing it: -

- - - diff --git a/hscontrol/templates/general.go b/hscontrol/templates/general.go new file mode 100644 index 00000000..3728b736 --- /dev/null +++ b/hscontrol/templates/general.go @@ -0,0 +1,56 @@ +package templates + +import ( + "github.com/chasefleming/elem-go" + "github.com/chasefleming/elem-go/attrs" + "github.com/chasefleming/elem-go/styles" +) + +var bodyStyle = styles.Props{ + styles.Margin: "40px auto", + styles.MaxWidth: "800px", + styles.LineHeight: "1.5", + styles.FontSize: "16px", + styles.Color: "#444", + styles.Padding: "0 10px", + styles.FontFamily: "Sans-serif", +} + +var headerStyle = styles.Props{ + styles.LineHeight: "1.2", +} + +func headerOne(text string) *elem.Element { + return elem.H1(attrs.Props{attrs.Style: headerStyle.ToInline()}, elem.Text(text)) +} + +func headerTwo(text string) *elem.Element { + return elem.H2(attrs.Props{attrs.Style: headerStyle.ToInline()}, elem.Text(text)) +} + +func headerThree(text string) *elem.Element { + return elem.H3(attrs.Props{attrs.Style: headerStyle.ToInline()}, elem.Text(text)) +} + +func HtmlStructure(head, body *elem.Element) *elem.Element { + return elem.Html(nil, + elem.Head( + attrs.Props{ + attrs.Lang: "en", + }, + elem.Meta(attrs.Props{ + attrs.Charset: "UTF-8", + }), + elem.Meta(attrs.Props{ + attrs.HTTPequiv: "X-UA-Compatible", + attrs.Content: "IE=edge", + }), + elem.Meta(attrs.Props{ + attrs.Name: "viewport", + attrs.Content: "width=device-width, initial-scale=1.0", + }), + head, + ), + body, + ) +} diff --git a/hscontrol/templates/register_web.go b/hscontrol/templates/register_web.go new file mode 100644 index 00000000..8361048a --- /dev/null +++ b/hscontrol/templates/register_web.go @@ -0,0 +1,34 @@ +package templates + +import ( + "fmt" + + "github.com/chasefleming/elem-go" + "github.com/chasefleming/elem-go/attrs" + "github.com/chasefleming/elem-go/styles" +) + +var codeStyleRegisterWebAPI = styles.Props{ + styles.Display: "block", + styles.Padding: "20px", + styles.Border: "1px solid #bbb", + styles.BackgroundColor: "#eee", +} + +func RegisterWeb(key string) *elem.Element { + return HtmlStructure( + elem.Title(nil, elem.Text("Registration - Headscale")), + 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)), + ), + ), + ) +} diff --git a/hscontrol/templates/windows.go b/hscontrol/templates/windows.go new file mode 100644 index 00000000..b233bac4 --- /dev/null +++ b/hscontrol/templates/windows.go @@ -0,0 +1,38 @@ +package templates + +import ( + "fmt" + + "github.com/chasefleming/elem-go" + "github.com/chasefleming/elem-go/attrs" +) + +func Windows(url string) *elem.Element { + return HtmlStructure( + elem.Title(nil, + elem.Text("headscale - Windows"), + ), + elem.Body(attrs.Props{ + attrs.Style : bodyStyle.ToInline(), + }, + headerOne("headscale: Windows configuration"), + elem.P(nil, + elem.Text("Download "), + elem.A(attrs.Props{ + attrs.Href: "https://tailscale.com/download/windows", + attrs.Rel: "noreferrer noopener", + attrs.Target: "_blank"}, + elem.Text("Tailscale for Windows ")), + elem.Text("and install it."), + ), + elem.P(nil, + elem.Text("Open a Command Prompt or Powershell and use Tailscale's login command to connect with headscale: "), + ), + elem.Pre(nil, + elem.Code(nil, + elem.Text(fmt.Sprintf(`tailscale login --login-server %s`, url)), + ), + ), + ), + ) +} diff --git a/hscontrol/templates/windows.html b/hscontrol/templates/windows.html deleted file mode 100644 index 34aaa0ae..00000000 --- a/hscontrol/templates/windows.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - headscale - Windows - - - - -

headscale: Windows configuration

-

- 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}}
- -