diff --git a/artifactory/utils/weblogin.go b/artifactory/utils/weblogin.go index 184e0fc2a..7066c2ea9 100644 --- a/artifactory/utils/weblogin.go +++ b/artifactory/utils/weblogin.go @@ -2,6 +2,9 @@ package utils import ( "errors" + "os" + "os/exec" + "strings" "time" "github.com/google/uuid" @@ -34,22 +37,49 @@ func DoWebLogin(serverDetails *config.ServerDetails) (token auth.CommonTokenPara "Don't worry! You can use the \"jf c add\" command to authenticate with the JFrog Platform using other methods")) return } - log.Info("After logging in via your web browser, please enter the code if prompted: " + coreutils.PrintBoldTitle(uuidStr[len(uuidStr)-4:])) - loginUrl := clientUtils.AddTrailingSlashIfNeeded(serverDetails.Url) + "ui/login?jfClientSession=" + uuidStr + "&jfClientName=JFrog-CLI&jfClientCode=1" + log.Info("Please open the following URL in your browser to authenticate:") - log.Info(loginUrl) + log.Info(coreutils.PrintBoldTitle(loginUrl)) + log.Info("") + log.Info("After logging in via your web browser, please enter the code if prompted: " + coreutils.PrintBoldTitle(uuidStr[len(uuidStr)-4:])) - // Attempt to open in browser if available - if err = browser.OpenURL(loginUrl); err != nil { - log.Warn("Failed to automatically open the browser. Please open the URL manually.") + // Attempt to open in browser with improved error handling + if err = openBrowserWithFallback(loginUrl); err != nil { + log.Warn("Failed to automatically open the browser: " + err.Error()) + log.Info("") + log.Info("Please manually copy and paste the URL above into your browser.") + log.Info("If you're using WSL2, you can set the JFROG_CLI_BROWSER_COMMAND environment variable") + log.Info("to specify a custom browser command, for example:") + log.Info(" export JFROG_CLI_BROWSER_COMMAND=\"/mnt/c/Program\\ Files/Google/Chrome/Application/chrome.exe\"") + log.Info(" or") + log.Info(" export BROWSER=\"wslview\" # if you have wslu installed") + log.Info("") // Do not return, continue the flow + } else { + log.Debug("Browser opened successfully") } - time.Sleep(1 * time.Second) + // Give a bit more time for browser to open and user to see the instructions + time.Sleep(2 * time.Second) + + log.Info("Waiting for authentication to complete...") + log.Info("Please complete the login process in your browser.") log.Debug("Attempting to get the authentication token...") + + // The GetLoginAuthenticationToken method handles its own polling and timeout logic token, err = accessManager.GetLoginAuthenticationToken(uuidStr) if err != nil { + // Provide helpful error message for common timeout scenarios + if strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "context deadline exceeded") { + log.Error("Authentication timed out. This may happen if:") + log.Error(" 1. The browser failed to open (check the URL above)") + log.Error(" 2. You didn't complete the login process in time") + log.Error(" 3. Network connectivity issues") + log.Error("") + log.Error("Please try again. If the issue persists in WSL2, consider setting:") + log.Error(" export JFROG_CLI_BROWSER_COMMAND=\"/mnt/c/Program\\ Files/Google/Chrome/Application/chrome.exe\"") + } return } if token.AccessToken == "" { @@ -59,6 +89,45 @@ func DoWebLogin(serverDetails *config.ServerDetails) (token auth.CommonTokenPara return } +// openBrowserWithFallback attempts to open a URL in a browser with improved error handling +// and support for custom browser commands via environment variables. +// This is particularly useful for WSL2 environments where standard browser opening may fail. +func openBrowserWithFallback(url string) error { + // Check if user has specified a custom browser command + if customBrowserCmd := os.Getenv(coreutils.BrowserCommand); customBrowserCmd != "" { + log.Debug("Using custom browser command from environment variable:", customBrowserCmd) + return runCustomBrowserCommand(customBrowserCmd, url) + } + + // Check if BROWSER environment variable is set (common convention) + if browserEnv := os.Getenv("BROWSER"); browserEnv != "" { + log.Debug("Using browser from BROWSER environment variable:", browserEnv) + if browserEnv == "none" || browserEnv == "" { + // User explicitly disabled browser opening + return errors.New("browser opening disabled via BROWSER environment variable") + } + return runCustomBrowserCommand(browserEnv, url) + } + + // Fall back to pkg/browser default behavior + return browser.OpenURL(url) +} + +// runCustomBrowserCommand executes a custom browser command with the given URL +func runCustomBrowserCommand(browserCmd, url string) error { + // Split the command to handle arguments + parts := strings.Fields(browserCmd) + if len(parts) == 0 { + return errors.New("empty browser command") + } + + cmd := exec.Command(parts[0], append(parts[1:], url)...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + func sendUnauthenticatedPing(serverDetails *config.ServerDetails) error { artifactoryManager, err := CreateServiceManager(serverDetails, 3, 0, false) if err != nil { diff --git a/artifactory/utils/weblogin_test.go b/artifactory/utils/weblogin_test.go new file mode 100644 index 000000000..da301dcdc --- /dev/null +++ b/artifactory/utils/weblogin_test.go @@ -0,0 +1,130 @@ +package utils + +import ( + "os" + "strings" + "testing" + + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/stretchr/testify/assert" +) + +func TestOpenBrowserWithFallback_CustomBrowserCommand(t *testing.T) { + // Test with JFROG_CLI_BROWSER_COMMAND environment variable + testUrl := "https://example.com" + customCmd := "echo" + + // Set environment variable + oldValue := os.Getenv(coreutils.BrowserCommand) + defer func() { + if oldValue == "" { + os.Unsetenv(coreutils.BrowserCommand) + } else { + os.Setenv(coreutils.BrowserCommand, oldValue) + } + }() + + os.Setenv(coreutils.BrowserCommand, customCmd) + + // This should succeed since 'echo' command exists and will just print the URL + err := openBrowserWithFallback(testUrl) + assert.NoError(t, err) +} + +func TestOpenBrowserWithFallback_BrowserEnvironmentVariable(t *testing.T) { + // Test with BROWSER environment variable + testUrl := "https://example.com" + + // Ensure JFROG_CLI_BROWSER_COMMAND is not set + oldJfrogValue := os.Getenv(coreutils.BrowserCommand) + defer func() { + if oldJfrogValue == "" { + os.Unsetenv(coreutils.BrowserCommand) + } else { + os.Setenv(coreutils.BrowserCommand, oldJfrogValue) + } + }() + os.Unsetenv(coreutils.BrowserCommand) + + // Test with BROWSER=none (should fail) + oldBrowserValue := os.Getenv("BROWSER") + defer func() { + if oldBrowserValue == "" { + os.Unsetenv("BROWSER") + } else { + os.Setenv("BROWSER", oldBrowserValue) + } + }() + + os.Setenv("BROWSER", "none") + err := openBrowserWithFallback(testUrl) + assert.Error(t, err) + assert.Contains(t, err.Error(), "browser opening disabled") + + // Test with BROWSER=echo (should succeed) + os.Setenv("BROWSER", "echo") + err = openBrowserWithFallback(testUrl) + assert.NoError(t, err) +} + +func TestRunCustomBrowserCommand_EmptyCommand(t *testing.T) { + err := runCustomBrowserCommand("", "https://example.com") + assert.Error(t, err) + assert.Contains(t, err.Error(), "empty browser command") +} + +func TestRunCustomBrowserCommand_ValidCommand(t *testing.T) { + // Use 'echo' command which should succeed + err := runCustomBrowserCommand("echo", "https://example.com") + assert.NoError(t, err) +} + +func TestRunCustomBrowserCommand_CommandWithArgs(t *testing.T) { + // Test command with arguments + err := runCustomBrowserCommand("echo test", "https://example.com") + assert.NoError(t, err) +} + +func TestRunCustomBrowserCommand_NonExistentCommand(t *testing.T) { + // Test with a command that doesn't exist + err := runCustomBrowserCommand("nonexistentcommand12345", "https://example.com") + assert.Error(t, err) + // The error should be related to command not found + assert.True(t, strings.Contains(err.Error(), "not found") || + strings.Contains(err.Error(), "no such file") || + strings.Contains(err.Error(), "executable file not found")) +} + +// TestOpenBrowserWithFallback_FallbackToPkgBrowser tests the fallback to pkg/browser +// when no environment variables are set. This test might fail in CI environments +// where no browser is available, so we just check that it attempts the fallback. +func TestOpenBrowserWithFallback_FallbackToPkgBrowser(t *testing.T) { + testUrl := "https://example.com" + + // Ensure both environment variables are not set + oldJfrogValue := os.Getenv(coreutils.BrowserCommand) + oldBrowserValue := os.Getenv("BROWSER") + + defer func() { + if oldJfrogValue == "" { + os.Unsetenv(coreutils.BrowserCommand) + } else { + os.Setenv(coreutils.BrowserCommand, oldJfrogValue) + } + if oldBrowserValue == "" { + os.Unsetenv("BROWSER") + } else { + os.Setenv("BROWSER", oldBrowserValue) + } + }() + + os.Unsetenv(coreutils.BrowserCommand) + os.Unsetenv("BROWSER") + + // This will likely fail in CI environments, but that's expected + // We're just testing that it attempts to use pkg/browser + err := openBrowserWithFallback(testUrl) + // We don't assert on the error since it depends on the environment + // The important thing is that the function doesn't panic + t.Logf("Fallback to pkg/browser result: %v", err) +} diff --git a/utils/coreutils/coreconsts.go b/utils/coreutils/coreconsts.go index e742b9a64..f0ae506ed 100644 --- a/utils/coreutils/coreconsts.go +++ b/utils/coreutils/coreconsts.go @@ -57,6 +57,8 @@ const ( // Set by the setup-jfrog-cli GitHub Action to identify specific command usage scenarios. // True if an automatic build publication was triggered. UsageAutoPublishedBuild = "JFROG_CLI_USAGE_AUTO_BUILD_PUBLISHED" + // Custom browser command for web login, useful for WSL2 environments + BrowserCommand = "JFROG_CLI_BROWSER_COMMAND" // Deprecated and replaced with TransitiveDownload TransitiveDownloadExperimental = "JFROG_CLI_TRANSITIVE_DOWNLOAD_EXPERIMENTAL"