diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 8b8c67f7a1..9a5c311c26 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -241,6 +241,18 @@ func dialHeadscaleSocket(ctx context.Context, socketPath string) (net.Conn, erro }, backoff.WithBackOff(b)) } +// clientBaseURL turns a configured CLI address into a client base URL. A bare +// host[:port] (the historical form) defaults to https; an address that already +// carries a scheme is used as-is, so an explicit http:// or https:// is honoured +// rather than doubled into https://https://... +func clientBaseURL(address string) string { + if strings.Contains(address, "://") { + return address + } + + return "https://" + address +} + // newRemoteClient builds an API client for a remote Headscale over HTTPS, // honouring insecure (skip TLS verification) and injecting the API key as a // bearer token on every request. @@ -257,7 +269,7 @@ func newRemoteClient(address, apiKey string, insecure bool) (*clientv1.ClientWit httpClient := &http.Client{Transport: transport} return clientv1.NewClientWithResponses( - "https://"+address, + clientBaseURL(address), clientv1.WithHTTPClient(httpClient), clientv1.WithRequestEditorFn(func(_ context.Context, req *http.Request) error { req.Header.Set("Authorization", "Bearer "+apiKey) diff --git a/cmd/headscale/cli/utils_test.go b/cmd/headscale/cli/utils_test.go new file mode 100644 index 0000000000..845d676c51 --- /dev/null +++ b/cmd/headscale/cli/utils_test.go @@ -0,0 +1,35 @@ +package cli + +import "testing" + +func TestClientBaseURL(t *testing.T) { + tests := []struct { + name string + address string + want string + }{ + { + name: "bare host defaults to https", + address: "headscale.example.com:50443", + want: "https://headscale.example.com:50443", + }, + { + name: "explicit https scheme is kept", + address: "https://headscale.example.com", + want: "https://headscale.example.com", + }, + { + name: "explicit http scheme is kept", + address: "http://localhost:8080", + want: "http://localhost:8080", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := clientBaseURL(tt.address); got != tt.want { + t.Errorf("clientBaseURL(%q) = %q, want %q", tt.address, got, tt.want) + } + }) + } +} diff --git a/docs/ref/api.md b/docs/ref/api.md index cd5421d24d..8f492dfd37 100644 --- a/docs/ref/api.md +++ b/docs/ref/api.md @@ -54,7 +54,7 @@ Headscale server at `/api/v1/docs` for details. ```console curl -H "Authorization: Bearer " \ - --json '{"user": "", "authId": "AUTH_ID>"}' \ + --json '{"user": "", "authId": ""}' \ https://headscale.example.com/api/v1/auth/register ``` diff --git a/docs/ref/integration/web-ui.md b/docs/ref/integration/web-ui.md index 8f00695a83..12aa6d373e 100644 --- a/docs/ref/integration/web-ui.md +++ b/docs/ref/integration/web-ui.md @@ -23,5 +23,7 @@ Headscale doesn't provide a built-in web interface but users may pick one from t - [HeadControl](https://github.com/ahmadzip/HeadControl) - Minimal Headscale admin dashboard, built with Go and HTMX - [Headscale Manager](https://github.com/hkdone/headscalemanager) - Headscale UI for Android - [Headscale UI](https://github.com/MunMunMiao/headscale-ui) - Headscale UI online and Self-hosting +- [Headscale Panel](https://github.com/headscale-panel/panel) - A modern Headscale management panel with a clean, + network-operations-focused UI You can ask for support on our [Discord server](https://discord.gg/c84AZQhmpx) in the "web-interfaces" channel.