From 7cfeb9906c8b832f3757d6b2fe52696dfae7de3a Mon Sep 17 00:00:00 2001 From: shrijayan <81805145+shrijayan@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:56:31 +0530 Subject: [PATCH 1/2] all: add Windows support for install, syslog, and login link Add Windows native messaging host registration via the registry, split syslog setup into platform-specific files since log/syslog is unavailable on Windows, handle .exe binary suffix, and fix the popup login link to actually open the auth URL. Signed-off-by: shrijayan <81805145+shrijayan@users.noreply.github.com> --- popup.js | 12 +++++++++--- registry_other.go | 6 ++++++ registry_windows.go | 27 +++++++++++++++++++++++++++ syslog_other.go | 24 ++++++++++++++++++++++++ syslog_windows.go | 7 +++++++ ts-browser-ext.go | 42 +++++++++++++++++++++++++++--------------- 6 files changed, 100 insertions(+), 18 deletions(-) create mode 100644 registry_other.go create mode 100644 registry_windows.go create mode 100644 syslog_other.go create mode 100644 syslog_windows.go diff --git a/popup.js b/popup.js index 9d8b28b..a583209 100644 --- a/popup.js +++ b/popup.js @@ -45,9 +45,15 @@ document.addEventListener("DOMContentLoaded", () => { return; } if (status.needsLogin) { - stateDisplay.innerHTML = status.browseToURL - ? `Log in` - : "Login required; no URL"; + if (status.browseToURL) { + stateDisplay.innerHTML = `Log in`; + stateDisplay.querySelector("a").addEventListener("click", (e) => { + e.preventDefault(); + chrome.tabs.create({ url: status.browseToURL }); + }); + } else { + stateDisplay.innerHTML = "Login required; no URL"; + } return; } if (typeof status === "string" && status === "Disconnected") { diff --git a/registry_other.go b/registry_other.go new file mode 100644 index 0000000..f633555 --- /dev/null +++ b/registry_other.go @@ -0,0 +1,6 @@ +//go:build !windows + +package main + +func installRegistry(browserByte, jsonPath string) error { return nil } +func uninstallRegistry(browserByte string) error { return nil } diff --git a/registry_windows.go b/registry_windows.go new file mode 100644 index 0000000..b1d974d --- /dev/null +++ b/registry_windows.go @@ -0,0 +1,27 @@ +package main + +import "golang.org/x/sys/windows/registry" + +func registryKeyPath(browserByte string) string { + if browserByte == "F" { + return `Software\Mozilla\NativeMessagingHosts\com.tailscale.browserext.firefox` + } + return `Software\Google\Chrome\NativeMessagingHosts\com.tailscale.browserext.chrome` +} + +func installRegistry(browserByte, jsonPath string) error { + key, _, err := registry.CreateKey(registry.CURRENT_USER, registryKeyPath(browserByte), registry.SET_VALUE) + if err != nil { + return err + } + defer key.Close() + return key.SetStringValue("", jsonPath) +} + +func uninstallRegistry(browserByte string) error { + err := registry.DeleteKey(registry.CURRENT_USER, registryKeyPath(browserByte)) + if err == registry.ErrNotExist { + return nil + } + return err +} diff --git a/syslog_other.go b/syslog_other.go new file mode 100644 index 0000000..dc098ef --- /dev/null +++ b/syslog_other.go @@ -0,0 +1,24 @@ +//go:build !windows + +package main + +import ( + "fmt" + "log" + "log/syslog" + + "tailscale.com/types/logger" +) + +func trySetSyslog(logf *logger.Logf) { + w, err := syslog.Dial("tcp", "localhost:5555", syslog.LOG_INFO, "browser") + if err != nil { + log.Printf("syslog: %v", err) + return + } + log.Printf("syslog dialed") + *logf = func(f string, a ...any) { + fmt.Fprintf(w, f, a...) + } + log.SetOutput(w) +} diff --git a/syslog_windows.go b/syslog_windows.go new file mode 100644 index 0000000..075bd6c --- /dev/null +++ b/syslog_windows.go @@ -0,0 +1,7 @@ +package main + +import "tailscale.com/types/logger" + +func trySetSyslog(logf *logger.Logf) { + // syslog is not available on Windows; use default logging. +} diff --git a/ts-browser-ext.go b/ts-browser-ext.go index fb8cc54..26486cd 100644 --- a/ts-browser-ext.go +++ b/ts-browser-ext.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "log" - "log/syslog" "net" "net/http" "net/http/httputil" @@ -70,15 +69,7 @@ To register it once, run: h := newHost(os.Stdin, os.Stdout) - if w, err := syslog.Dial("tcp", "localhost:5555", syslog.LOG_INFO, "browser"); err == nil { - log.Printf("syslog dialed") - h.logf = func(f string, a ...any) { - fmt.Fprintf(w, f, a...) - } - log.SetOutput(w) - } else { - log.Printf("syslog: %v", err) - } + trySetSyslog(&h.logf) ln := h.getProxyListener() port := ln.Addr().(*net.TCPAddr).Port @@ -112,8 +103,10 @@ func getTargetDir(browserByte string) (string, error) { } else if browserByte == "F" { dir = filepath.Join(home, "Library", "Application Support", "Mozilla", "NativeMessagingHosts") } + case "windows": + dir = filepath.Join(home, "AppData", "Local", "TailscaleBrowserExt") default: - return "", fmt.Errorf("TODO: implement support for installing on %q", runtime.GOOS) + return "", fmt.Errorf("unsupported OS %q", runtime.GOOS) } if err := os.MkdirAll(dir, 0755); err != nil { return "", err @@ -127,7 +120,11 @@ func uninstall() error { if err != nil { return err } - targetBin := filepath.Join(targetDir, "ts-browser-ext") + binName := "ts-browser-ext" + if runtime.GOOS == "windows" { + binName += ".exe" + } + targetBin := filepath.Join(targetDir, binName) targetJSON := filepath.Join(targetDir, "com.tailscale.browserext.chrome.json") if browserByte == "F" { targetJSON = filepath.Join(targetDir, "com.tailscale.browserext.firefox.json") @@ -138,6 +135,9 @@ func uninstall() error { if err := os.Remove(targetJSON); err != nil && !os.IsNotExist(err) { return err } + if err := uninstallRegistry(browserByte); err != nil { + return fmt.Errorf("removing registry key: %w", err) + } } return nil } @@ -167,13 +167,21 @@ func install(installArg string) error { if err != nil { return err } - targetBin := filepath.Join(targetDir, "ts-browser-ext") + binName := "ts-browser-ext" + if runtime.GOOS == "windows" { + binName += ".exe" + } + targetBin := filepath.Join(targetDir, binName) if err := os.WriteFile(targetBin, binary, 0755); err != nil { return err } log.SetFlags(0) log.Printf("copied binary to %v", targetBin) + // Use forward slashes in the JSON path so it works on all platforms + // (Chrome/Firefox on Windows accept forward slashes). + jsonBinPath := filepath.ToSlash(targetBin) + var targetJSON string var jsonConf []byte @@ -188,7 +196,7 @@ func install(installArg string) error { "allowed_origins": [ "chrome-extension://%s/" ] - }`, targetBin, extension) + }`, jsonBinPath, extension) case "F": targetJSON = filepath.Join(targetDir, "com.tailscale.browserext.firefox.json") jsonConf = fmt.Appendf(nil, `{ @@ -199,7 +207,7 @@ func install(installArg string) error { "allowed_extensions": [ "browser-ext@tailscale.com" ] - }`, targetBin) + }`, jsonBinPath) default: return fmt.Errorf("unknown browser prefix byte %q", browserByte) } @@ -207,6 +215,10 @@ func install(installArg string) error { return err } log.Printf("wrote registration to %v", targetJSON) + + if err := installRegistry(browserByte, targetJSON); err != nil { + return fmt.Errorf("writing registry: %w", err) + } return nil } From f3fb3fec27d977ef81620668de522f4793cbfc8f Mon Sep 17 00:00:00 2001 From: shrijayan <81805145+shrijayan@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:56:41 +0530 Subject: [PATCH 2/2] all: clean up dead code, add build tag, extract binaryName helper Remove unused browseToURL function and lastStatus variable from popup.js, add explicit //go:build windows tag to registry_windows.go, and extract binaryName() to deduplicate the platform-conditional binary naming in install/uninstall. Signed-off-by: shrijayan <81805145+shrijayan@users.noreply.github.com> --- popup.js | 8 -------- registry_windows.go | 2 ++ ts-browser-ext.go | 19 +++++++++---------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/popup.js b/popup.js index a583209..9f0bd95 100644 --- a/popup.js +++ b/popup.js @@ -1,11 +1,3 @@ -var lastStatus; - -function browseToURL() { - if (lastStatus && lastStatus.browseToURL) { - chrome.tabs.create({ url: lastStatus.browseToURL }); - } -} - document.addEventListener("DOMContentLoaded", () => { const toggleSlider = document.getElementById("toggleSlider"); const slider = document.querySelector(".slider"); diff --git a/registry_windows.go b/registry_windows.go index b1d974d..3ab1d7d 100644 --- a/registry_windows.go +++ b/registry_windows.go @@ -1,3 +1,5 @@ +//go:build windows + package main import "golang.org/x/sys/windows/registry" diff --git a/ts-browser-ext.go b/ts-browser-ext.go index 26486cd..4b790ea 100644 --- a/ts-browser-ext.go +++ b/ts-browser-ext.go @@ -114,17 +114,20 @@ func getTargetDir(browserByte string) (string, error) { return dir, nil } +func binaryName() string { + if runtime.GOOS == "windows" { + return "ts-browser-ext.exe" + } + return "ts-browser-ext" +} + func uninstall() error { for _, browserByte := range []string{"C", "F"} { targetDir, err := getTargetDir(browserByte) if err != nil { return err } - binName := "ts-browser-ext" - if runtime.GOOS == "windows" { - binName += ".exe" - } - targetBin := filepath.Join(targetDir, binName) + targetBin := filepath.Join(targetDir, binaryName()) targetJSON := filepath.Join(targetDir, "com.tailscale.browserext.chrome.json") if browserByte == "F" { targetJSON = filepath.Join(targetDir, "com.tailscale.browserext.firefox.json") @@ -167,11 +170,7 @@ func install(installArg string) error { if err != nil { return err } - binName := "ts-browser-ext" - if runtime.GOOS == "windows" { - binName += ".exe" - } - targetBin := filepath.Join(targetDir, binName) + targetBin := filepath.Join(targetDir, binaryName()) if err := os.WriteFile(targetBin, binary, 0755); err != nil { return err }