Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 76 additions & 7 deletions artifactory/utils/weblogin.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package utils

import (
"errors"
"os"
"os/exec"
"strings"
"time"

"github.com/google/uuid"
Expand Down Expand Up @@ -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 == "" {
Expand All @@ -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 {
Expand Down
130 changes: 130 additions & 0 deletions artifactory/utils/weblogin_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 2 additions & 0 deletions utils/coreutils/coreconsts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading