|
|
|
|
|
|
|
|
package browser |
|
|
|
|
|
import ( |
|
|
"fmt" |
|
|
"os/exec" |
|
|
"runtime" |
|
|
"strings" |
|
|
"sync" |
|
|
|
|
|
pkgbrowser "github.com/pkg/browser" |
|
|
log "github.com/sirupsen/logrus" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
var incognitoMode bool |
|
|
|
|
|
|
|
|
var lastBrowserProcess *exec.Cmd |
|
|
var browserMutex sync.Mutex |
|
|
|
|
|
|
|
|
func SetIncognitoMode(enabled bool) { |
|
|
incognitoMode = enabled |
|
|
} |
|
|
|
|
|
|
|
|
func IsIncognitoMode() bool { |
|
|
return incognitoMode |
|
|
} |
|
|
|
|
|
|
|
|
func CloseBrowser() error { |
|
|
browserMutex.Lock() |
|
|
defer browserMutex.Unlock() |
|
|
|
|
|
if lastBrowserProcess == nil || lastBrowserProcess.Process == nil { |
|
|
return nil |
|
|
} |
|
|
|
|
|
err := lastBrowserProcess.Process.Kill() |
|
|
lastBrowserProcess = nil |
|
|
return err |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func OpenURL(url string) error { |
|
|
log.Debugf("Opening URL in browser: %s (incognito=%v)", url, incognitoMode) |
|
|
|
|
|
|
|
|
if incognitoMode { |
|
|
log.Debug("Using incognito mode") |
|
|
return openURLIncognito(url) |
|
|
} |
|
|
|
|
|
|
|
|
err := pkgbrowser.OpenURL(url) |
|
|
if err == nil { |
|
|
log.Debug("Successfully opened URL using pkg/browser library") |
|
|
return nil |
|
|
} |
|
|
|
|
|
log.Debugf("pkg/browser failed: %v, trying platform-specific commands", err) |
|
|
|
|
|
|
|
|
return openURLPlatformSpecific(url) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func openURLPlatformSpecific(url string) error { |
|
|
var cmd *exec.Cmd |
|
|
|
|
|
switch runtime.GOOS { |
|
|
case "darwin": |
|
|
cmd = exec.Command("open", url) |
|
|
case "windows": |
|
|
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) |
|
|
case "linux": |
|
|
|
|
|
browsers := []string{"xdg-open", "x-www-browser", "www-browser", "firefox", "chromium", "google-chrome"} |
|
|
for _, browser := range browsers { |
|
|
if _, err := exec.LookPath(browser); err == nil { |
|
|
cmd = exec.Command(browser, url) |
|
|
break |
|
|
} |
|
|
} |
|
|
if cmd == nil { |
|
|
return fmt.Errorf("no suitable browser found on Linux system") |
|
|
} |
|
|
default: |
|
|
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) |
|
|
} |
|
|
|
|
|
log.Debugf("Running command: %s %v", cmd.Path, cmd.Args[1:]) |
|
|
err := cmd.Start() |
|
|
if err != nil { |
|
|
return fmt.Errorf("failed to start browser command: %w", err) |
|
|
} |
|
|
|
|
|
log.Debug("Successfully opened URL using platform-specific command") |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func openURLIncognito(url string) error { |
|
|
|
|
|
if cmd := tryDefaultBrowserIncognito(url); cmd != nil { |
|
|
log.Debugf("Using detected default browser: %s %v", cmd.Path, cmd.Args[1:]) |
|
|
if err := cmd.Start(); err == nil { |
|
|
storeBrowserProcess(cmd) |
|
|
log.Debug("Successfully opened URL in default browser's incognito mode") |
|
|
return nil |
|
|
} |
|
|
log.Debugf("Failed to start default browser, trying fallback chain") |
|
|
} |
|
|
|
|
|
|
|
|
cmd := tryFallbackBrowsersIncognito(url) |
|
|
if cmd == nil { |
|
|
log.Warn("No browser with incognito support found, falling back to normal mode") |
|
|
return openURLPlatformSpecific(url) |
|
|
} |
|
|
|
|
|
log.Debugf("Running incognito command: %s %v", cmd.Path, cmd.Args[1:]) |
|
|
err := cmd.Start() |
|
|
if err != nil { |
|
|
log.Warnf("Failed to open incognito browser: %v, falling back to normal mode", err) |
|
|
return openURLPlatformSpecific(url) |
|
|
} |
|
|
|
|
|
storeBrowserProcess(cmd) |
|
|
log.Debug("Successfully opened URL in incognito/private mode") |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
func storeBrowserProcess(cmd *exec.Cmd) { |
|
|
browserMutex.Lock() |
|
|
lastBrowserProcess = cmd |
|
|
browserMutex.Unlock() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func tryDefaultBrowserIncognito(url string) *exec.Cmd { |
|
|
switch runtime.GOOS { |
|
|
case "darwin": |
|
|
return tryDefaultBrowserMacOS(url) |
|
|
case "windows": |
|
|
return tryDefaultBrowserWindows(url) |
|
|
case "linux": |
|
|
return tryDefaultBrowserLinux(url) |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
func tryDefaultBrowserMacOS(url string) *exec.Cmd { |
|
|
|
|
|
out, err := exec.Command("defaults", "read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers").Output() |
|
|
if err != nil { |
|
|
return nil |
|
|
} |
|
|
|
|
|
output := string(out) |
|
|
var browserName string |
|
|
|
|
|
|
|
|
if containsBrowserID(output, "com.google.chrome") { |
|
|
browserName = "chrome" |
|
|
} else if containsBrowserID(output, "org.mozilla.firefox") { |
|
|
browserName = "firefox" |
|
|
} else if containsBrowserID(output, "com.apple.safari") { |
|
|
browserName = "safari" |
|
|
} else if containsBrowserID(output, "com.brave.browser") { |
|
|
browserName = "brave" |
|
|
} else if containsBrowserID(output, "com.microsoft.edgemac") { |
|
|
browserName = "edge" |
|
|
} |
|
|
|
|
|
return createMacOSIncognitoCmd(browserName, url) |
|
|
} |
|
|
|
|
|
|
|
|
func containsBrowserID(output, bundleID string) bool { |
|
|
return strings.Contains(output, bundleID) |
|
|
} |
|
|
|
|
|
|
|
|
func createMacOSIncognitoCmd(browserName, url string) *exec.Cmd { |
|
|
switch browserName { |
|
|
case "chrome": |
|
|
|
|
|
chromePath := "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" |
|
|
if _, err := exec.LookPath(chromePath); err == nil { |
|
|
return exec.Command(chromePath, "--incognito", url) |
|
|
} |
|
|
return exec.Command("open", "-na", "Google Chrome", "--args", "--incognito", url) |
|
|
case "firefox": |
|
|
return exec.Command("open", "-na", "Firefox", "--args", "--private-window", url) |
|
|
case "safari": |
|
|
|
|
|
return tryAppleScriptSafariPrivate(url) |
|
|
case "brave": |
|
|
return exec.Command("open", "-na", "Brave Browser", "--args", "--incognito", url) |
|
|
case "edge": |
|
|
return exec.Command("open", "-na", "Microsoft Edge", "--args", "--inprivate", url) |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
func tryAppleScriptSafariPrivate(url string) *exec.Cmd { |
|
|
|
|
|
script := fmt.Sprintf(` |
|
|
tell application "Safari" |
|
|
activate |
|
|
tell application "System Events" |
|
|
keystroke "n" using {command down, shift down} |
|
|
delay 0.5 |
|
|
end tell |
|
|
set URL of document 1 to "%s" |
|
|
end tell |
|
|
`, url) |
|
|
|
|
|
cmd := exec.Command("osascript", "-e", script) |
|
|
|
|
|
if _, err := exec.LookPath("/Applications/Safari.app/Contents/MacOS/Safari"); err != nil { |
|
|
log.Debug("Safari not found, AppleScript private window not available") |
|
|
return nil |
|
|
} |
|
|
log.Debug("Attempting Safari private window via AppleScript") |
|
|
return cmd |
|
|
} |
|
|
|
|
|
|
|
|
func tryDefaultBrowserWindows(url string) *exec.Cmd { |
|
|
|
|
|
out, err := exec.Command("reg", "query", |
|
|
`HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice`, |
|
|
"/v", "ProgId").Output() |
|
|
if err != nil { |
|
|
return nil |
|
|
} |
|
|
|
|
|
output := string(out) |
|
|
var browserName string |
|
|
|
|
|
|
|
|
if strings.Contains(output, "ChromeHTML") { |
|
|
browserName = "chrome" |
|
|
} else if strings.Contains(output, "FirefoxURL") { |
|
|
browserName = "firefox" |
|
|
} else if strings.Contains(output, "MSEdgeHTM") { |
|
|
browserName = "edge" |
|
|
} else if strings.Contains(output, "BraveHTML") { |
|
|
browserName = "brave" |
|
|
} |
|
|
|
|
|
return createWindowsIncognitoCmd(browserName, url) |
|
|
} |
|
|
|
|
|
|
|
|
func createWindowsIncognitoCmd(browserName, url string) *exec.Cmd { |
|
|
switch browserName { |
|
|
case "chrome": |
|
|
paths := []string{ |
|
|
"chrome", |
|
|
`C:\Program Files\Google\Chrome\Application\chrome.exe`, |
|
|
`C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`, |
|
|
} |
|
|
for _, p := range paths { |
|
|
if _, err := exec.LookPath(p); err == nil { |
|
|
return exec.Command(p, "--incognito", url) |
|
|
} |
|
|
} |
|
|
case "firefox": |
|
|
if path, err := exec.LookPath("firefox"); err == nil { |
|
|
return exec.Command(path, "--private-window", url) |
|
|
} |
|
|
case "edge": |
|
|
paths := []string{ |
|
|
"msedge", |
|
|
`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`, |
|
|
`C:\Program Files\Microsoft\Edge\Application\msedge.exe`, |
|
|
} |
|
|
for _, p := range paths { |
|
|
if _, err := exec.LookPath(p); err == nil { |
|
|
return exec.Command(p, "--inprivate", url) |
|
|
} |
|
|
} |
|
|
case "brave": |
|
|
paths := []string{ |
|
|
`C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe`, |
|
|
`C:\Program Files (x86)\BraveSoftware\Brave-Browser\Application\brave.exe`, |
|
|
} |
|
|
for _, p := range paths { |
|
|
if _, err := exec.LookPath(p); err == nil { |
|
|
return exec.Command(p, "--incognito", url) |
|
|
} |
|
|
} |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
func tryDefaultBrowserLinux(url string) *exec.Cmd { |
|
|
out, err := exec.Command("xdg-settings", "get", "default-web-browser").Output() |
|
|
if err != nil { |
|
|
return nil |
|
|
} |
|
|
|
|
|
desktop := string(out) |
|
|
var browserName string |
|
|
|
|
|
|
|
|
if strings.Contains(desktop, "google-chrome") || strings.Contains(desktop, "chrome") { |
|
|
browserName = "chrome" |
|
|
} else if strings.Contains(desktop, "firefox") { |
|
|
browserName = "firefox" |
|
|
} else if strings.Contains(desktop, "chromium") { |
|
|
browserName = "chromium" |
|
|
} else if strings.Contains(desktop, "brave") { |
|
|
browserName = "brave" |
|
|
} else if strings.Contains(desktop, "microsoft-edge") || strings.Contains(desktop, "msedge") { |
|
|
browserName = "edge" |
|
|
} |
|
|
|
|
|
return createLinuxIncognitoCmd(browserName, url) |
|
|
} |
|
|
|
|
|
|
|
|
func createLinuxIncognitoCmd(browserName, url string) *exec.Cmd { |
|
|
switch browserName { |
|
|
case "chrome": |
|
|
paths := []string{"google-chrome", "google-chrome-stable"} |
|
|
for _, p := range paths { |
|
|
if path, err := exec.LookPath(p); err == nil { |
|
|
return exec.Command(path, "--incognito", url) |
|
|
} |
|
|
} |
|
|
case "firefox": |
|
|
paths := []string{"firefox", "firefox-esr"} |
|
|
for _, p := range paths { |
|
|
if path, err := exec.LookPath(p); err == nil { |
|
|
return exec.Command(path, "--private-window", url) |
|
|
} |
|
|
} |
|
|
case "chromium": |
|
|
paths := []string{"chromium", "chromium-browser"} |
|
|
for _, p := range paths { |
|
|
if path, err := exec.LookPath(p); err == nil { |
|
|
return exec.Command(path, "--incognito", url) |
|
|
} |
|
|
} |
|
|
case "brave": |
|
|
if path, err := exec.LookPath("brave-browser"); err == nil { |
|
|
return exec.Command(path, "--incognito", url) |
|
|
} |
|
|
case "edge": |
|
|
if path, err := exec.LookPath("microsoft-edge"); err == nil { |
|
|
return exec.Command(path, "--inprivate", url) |
|
|
} |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
func tryFallbackBrowsersIncognito(url string) *exec.Cmd { |
|
|
switch runtime.GOOS { |
|
|
case "darwin": |
|
|
return tryFallbackBrowsersMacOS(url) |
|
|
case "windows": |
|
|
return tryFallbackBrowsersWindows(url) |
|
|
case "linux": |
|
|
return tryFallbackBrowsersLinuxChain(url) |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
func tryFallbackBrowsersMacOS(url string) *exec.Cmd { |
|
|
|
|
|
chromePath := "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" |
|
|
if _, err := exec.LookPath(chromePath); err == nil { |
|
|
return exec.Command(chromePath, "--incognito", url) |
|
|
} |
|
|
|
|
|
if _, err := exec.LookPath("/Applications/Firefox.app/Contents/MacOS/firefox"); err == nil { |
|
|
return exec.Command("open", "-na", "Firefox", "--args", "--private-window", url) |
|
|
} |
|
|
|
|
|
if _, err := exec.LookPath("/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"); err == nil { |
|
|
return exec.Command("open", "-na", "Brave Browser", "--args", "--incognito", url) |
|
|
} |
|
|
|
|
|
if _, err := exec.LookPath("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"); err == nil { |
|
|
return exec.Command("open", "-na", "Microsoft Edge", "--args", "--inprivate", url) |
|
|
} |
|
|
|
|
|
if cmd := tryAppleScriptSafariPrivate(url); cmd != nil { |
|
|
log.Info("Using Safari with AppleScript for private browsing (may require accessibility permissions)") |
|
|
return cmd |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
func tryFallbackBrowsersWindows(url string) *exec.Cmd { |
|
|
|
|
|
chromePaths := []string{ |
|
|
"chrome", |
|
|
`C:\Program Files\Google\Chrome\Application\chrome.exe`, |
|
|
`C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`, |
|
|
} |
|
|
for _, p := range chromePaths { |
|
|
if _, err := exec.LookPath(p); err == nil { |
|
|
return exec.Command(p, "--incognito", url) |
|
|
} |
|
|
} |
|
|
|
|
|
if path, err := exec.LookPath("firefox"); err == nil { |
|
|
return exec.Command(path, "--private-window", url) |
|
|
} |
|
|
|
|
|
edgePaths := []string{ |
|
|
"msedge", |
|
|
`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`, |
|
|
`C:\Program Files\Microsoft\Edge\Application\msedge.exe`, |
|
|
} |
|
|
for _, p := range edgePaths { |
|
|
if _, err := exec.LookPath(p); err == nil { |
|
|
return exec.Command(p, "--inprivate", url) |
|
|
} |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
func tryFallbackBrowsersLinuxChain(url string) *exec.Cmd { |
|
|
type browserConfig struct { |
|
|
name string |
|
|
flag string |
|
|
} |
|
|
browsers := []browserConfig{ |
|
|
{"google-chrome", "--incognito"}, |
|
|
{"google-chrome-stable", "--incognito"}, |
|
|
{"chromium", "--incognito"}, |
|
|
{"chromium-browser", "--incognito"}, |
|
|
{"firefox", "--private-window"}, |
|
|
{"firefox-esr", "--private-window"}, |
|
|
{"brave-browser", "--incognito"}, |
|
|
{"microsoft-edge", "--inprivate"}, |
|
|
} |
|
|
for _, b := range browsers { |
|
|
if path, err := exec.LookPath(b.name); err == nil { |
|
|
return exec.Command(path, b.flag, url) |
|
|
} |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func IsAvailable() bool { |
|
|
|
|
|
switch runtime.GOOS { |
|
|
case "darwin": |
|
|
_, err := exec.LookPath("open") |
|
|
return err == nil |
|
|
case "windows": |
|
|
_, err := exec.LookPath("rundll32") |
|
|
return err == nil |
|
|
case "linux": |
|
|
browsers := []string{"xdg-open", "x-www-browser", "www-browser", "firefox", "chromium", "google-chrome"} |
|
|
for _, browser := range browsers { |
|
|
if _, err := exec.LookPath(browser); err == nil { |
|
|
return true |
|
|
} |
|
|
} |
|
|
return false |
|
|
default: |
|
|
return false |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func GetPlatformInfo() map[string]interface{} { |
|
|
info := map[string]interface{}{ |
|
|
"os": runtime.GOOS, |
|
|
"arch": runtime.GOARCH, |
|
|
"available": IsAvailable(), |
|
|
} |
|
|
|
|
|
switch runtime.GOOS { |
|
|
case "darwin": |
|
|
info["default_command"] = "open" |
|
|
case "windows": |
|
|
info["default_command"] = "rundll32" |
|
|
case "linux": |
|
|
browsers := []string{"xdg-open", "x-www-browser", "www-browser", "firefox", "chromium", "google-chrome"} |
|
|
var availableBrowsers []string |
|
|
for _, browser := range browsers { |
|
|
if _, err := exec.LookPath(browser); err == nil { |
|
|
availableBrowsers = append(availableBrowsers, browser) |
|
|
} |
|
|
} |
|
|
info["available_browsers"] = availableBrowsers |
|
|
if len(availableBrowsers) > 0 { |
|
|
info["default_command"] = availableBrowsers[0] |
|
|
} |
|
|
} |
|
|
|
|
|
return info |
|
|
} |
|
|
|