Skip to content
Open
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
32 changes: 19 additions & 13 deletions pkg/remote/connparse/connparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,26 +92,31 @@ func GetConnNameFromContext(ctx context.Context) (string, error) {
return handler.GetRpcContext().Conn, nil
}

// ParseURI parses a connection URI and returns the connection type, host/path, and parameters.
// It recognizes explicit schemes (scheme://...), shorthand forms starting with "//host/path" and WSL-style URIs (wsl://distro/path). When no scheme is provided the scheme defaults to "wsh" and the host may be set to the current connection marker or to the local connection name for local shorthand. For the "wsh" scheme: missing host defaults to the local connection name; paths beginning with "/~" are normalized by removing the leading slash; other paths may receive a prepended "/" except when they look like Windows drive paths, start with ".", "~", or already start with a slash. Trailing slashes in the original URI are preserved in the parsed Path.
func ParseURI(uri string) (*Connection, error) {
split := strings.SplitN(uri, "://", 2)
var scheme string
var rest string
if len(split) > 1 {
scheme = split[0]
rest = strings.TrimPrefix(split[1], "//")

if strings.HasPrefix(uri, "//") {
rest = strings.TrimPrefix(uri, "//")
} else {
rest = split[0]
split := strings.SplitN(uri, "://", 2)
if len(split) > 1 {
scheme = split[0]
rest = strings.TrimPrefix(split[1], "//")
} else {
rest = split[0]
}
}

var host string
var remotePath string

parseGenericPath := func() {
split = strings.SplitN(rest, "/", 2)
host = split[0]
if len(split) > 1 && split[1] != "" {
remotePath = split[1]
parts := strings.SplitN(rest, "/", 2)
host = parts[0]
if len(parts) > 1 && parts[1] != "" {
remotePath = parts[1]
} else if strings.HasSuffix(rest, "/") {
// preserve trailing slash
remotePath = "/"
Expand All @@ -133,8 +138,9 @@ func ParseURI(uri string) (*Connection, error) {
if scheme == "" {
scheme = ConnectionTypeWsh
addPrecedingSlash = false
if len(rest) != len(uri) {
// This accounts for when the uri starts with "//", which would get trimmed in the first split.
if strings.HasPrefix(uri, "//") {
rest = strings.TrimPrefix(uri, "//")
// Handles remote shorthand like //host/path and WSL URIs //wsl://distro/path
parseWshPath()
} else if strings.HasPrefix(rest, "/~") {
host = wshrpc.LocalConnName
Expand Down Expand Up @@ -166,4 +172,4 @@ func ParseURI(uri string) (*Connection, error) {
Path: remotePath,
}
return conn, nil
}
}
16 changes: 16 additions & 0 deletions pkg/remote/sshagent_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build windows

package remote

import (
"net"
"time"

"github.com/Microsoft/go-winio"
)

// dialIdentityAgent connects to the Windows OpenSSH agent named pipe at the given path and returns the established connection or an error.
func dialIdentityAgent(agentPath string) (net.Conn, error) {
timeout := 2 * time.Second
return winio.DialPipe(agentPath, &timeout)
}
74 changes: 61 additions & 13 deletions pkg/remote/sshclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -224,6 +225,14 @@
}
}

// createPasswordCallbackPrompt returns a function that obtains a password for SSH authentication.
//
// The returned callback returns a password string or an error when password acquisition fails.
// If the optional `password` pointer is non-nil, its value is returned directly without prompting.
// Otherwise the callback prompts the user (with a 60 second timeout) for a password using the
// provided connection context and includes `remoteDisplayName` in the prompt. On prompt or input
// errors the callback returns a ConnectionError that wraps the underlying error and includes
// `debugInfo` for diagnostics. The callback also converts panics into errors.
func createPasswordCallbackPrompt(connCtx context.Context, remoteDisplayName string, password *string, debugInfo *ConnectionDebugInfo) func() (secret string, err error) {
return func() (secret string, outErr error) {
defer func() {
Expand All @@ -233,12 +242,12 @@
}
}()
blocklogger.Infof(connCtx, "[conndebug] Password Authentication requested from connection %s...\n", remoteDisplayName)

if password != nil {
blocklogger.Infof(connCtx, "[conndebug] using password from secret store, sending to ssh\n")
return *password, nil
}

ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second)
defer cancelFn()
queryText := fmt.Sprintf(
Expand Down Expand Up @@ -598,6 +607,22 @@
return waveHostKeyCallback, hostKeyAlgorithms, nil
}

// createClientConfig builds an ssh.ClientConfig configured for the target described
// by sshKeywords and using connCtx for context-aware operations.
//
// The returned ClientConfig is populated with the selected user, authentication
// methods (publickey, keyboard-interactive, password) ordered by the host's
// PreferredAuthentications, a host key verification callback, and the host key
// algorithms appropriate for the destination. Batch mode, IdentitiesOnly, and
// preferred-authentication flags from sshKeywords are honored.
//
// This function may:
// - open and query an SSH identity agent socket if configured and allowed;
// - retrieve a password from the configured secret store when SshPasswordSecretName
// is set.
//
// It returns a non-nil error when required setup steps fail (for example, secret
// retrieval or host key callback construction).
func createClientConfig(connCtx context.Context, sshKeywords *wconfig.ConnKeywords, debugInfo *ConnectionDebugInfo) (*ssh.ClientConfig, error) {
chosenUser := utilfn.SafeDeref(sshKeywords.SshUser)
chosenHostName := utilfn.SafeDeref(sshKeywords.SshHostName)
Expand All @@ -612,10 +637,11 @@

// IdentitiesOnly indicates that only the keys listed in the identity and certificate files or passed as arguments should be used, even if there are matches in the SSH Agent, PKCS11Provider, or SecurityKeyProvider. See https://man.openbsd.org/ssh_config#IdentitiesOnly
// TODO: Update if we decide to support PKCS11Provider and SecurityKeyProvider
if !utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly) {
conn, err := net.Dial("unix", utilfn.SafeDeref(sshKeywords.SshIdentityAgent))
agentPath := strings.TrimSpace(utilfn.SafeDeref(sshKeywords.SshIdentityAgent))
if !utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly) && agentPath != "" {
conn, err := dialIdentityAgent(agentPath)

Check failure on line 642 in pkg/remote/sshclient.go

View workflow job for this annotation

GitHub Actions / Analyze (go)

undefined: dialIdentityAgent

Check failure on line 642 in pkg/remote/sshclient.go

View workflow job for this annotation

GitHub Actions / Analyze (javascript-typescript)

undefined: dialIdentityAgent

Check failure on line 642 in pkg/remote/sshclient.go

View workflow job for this annotation

GitHub Actions / Build for TestDriver.ai

undefined: dialIdentityAgent
if err != nil {
log.Printf("Failed to open Identity Agent Socket: %v", err)
log.Printf("Failed to open Identity Agent Socket %q: %v", agentPath, err)
} else {
agentClient = agent.NewClient(conn)
authSockSigners, _ = agentClient.Signers()
Expand Down Expand Up @@ -801,7 +827,19 @@

// note that a `var == "yes"` will default to false
// but `var != "no"` will default to true
// when given unexpected strings
// findSshConfigKeywords reads SSH configuration for the provided hostPattern and returns a populated
// wconfig.ConnKeywords describing the resolved connection parameters.
//
// The returned ConnKeywords includes resolved values for user, hostname, port, identity files,
// batch mode, publickey/password/keyboard-interactive authentication flags, preferred
// authentications, AddKeysToAgent, IdentitiesOnly, IdentityAgent (with home‑dir expansion and
// platform-aware fallbacks), ProxyJump entries, and user/global known_hosts files. Identity file
// paths are trimmed of surrounding quotes; boolean-style options are normalized from common SSH
// values (e.g., "yes"/"no"). ProxyJump entries are split on commas and empty/"none" values are
// ignored. Known-hosts file fields are split on whitespace.
//
// An error is returned if reading or expanding SSH configuration values fails. Panics are
// converted into errors and returned.
func findSshConfigKeywords(hostPattern string) (connKeywords *wconfig.ConnKeywords, outErr error) {
defer func() {
panicErr := panichandler.PanicHandler("sshclient:find-ssh-config-keywords", recover())
Expand Down Expand Up @@ -900,17 +938,27 @@
return nil, err
}
if identityAgentRaw == "" {
shellPath := shellutil.DetectLocalShellPath()
authSockCommand := exec.Command(shellPath, "-c", "echo ${SSH_AUTH_SOCK}")
sshAuthSock, err := authSockCommand.Output()
if err == nil {
agentPath, err := wavebase.ExpandHomeDir(trimquotes.TryTrimQuotes(strings.TrimSpace(string(sshAuthSock))))
if envSock := os.Getenv("SSH_AUTH_SOCK"); envSock != "" {
agentPath, err := wavebase.ExpandHomeDir(trimquotes.TryTrimQuotes(envSock))
if err != nil {
return nil, err
}
sshKeywords.SshIdentityAgent = utilfn.Ptr(agentPath)
} else if runtime.GOOS == "windows" {
sshKeywords.SshIdentityAgent = utilfn.Ptr(`\\.\\pipe\\openssh-ssh-agent`)
} else {
log.Printf("unable to find SSH_AUTH_SOCK: %v\n", err)
shellPath := shellutil.DetectLocalShellPath()
authSockCommand := exec.Command(shellPath, "-c", "echo ${SSH_AUTH_SOCK}")
sshAuthSock, err := authSockCommand.Output()
if err == nil {
agentPath, err := wavebase.ExpandHomeDir(trimquotes.TryTrimQuotes(strings.TrimSpace(string(sshAuthSock))))
if err != nil {
return nil, err
}
sshKeywords.SshIdentityAgent = utilfn.Ptr(agentPath)
} else {
log.Printf("unable to find SSH_AUTH_SOCK: %v\n", err)
}
}
} else {
agentPath, err := wavebase.ExpandHomeDir(trimquotes.TryTrimQuotes(identityAgentRaw))
Expand Down Expand Up @@ -1040,4 +1088,4 @@
}

return &outKeywords
}
}
Loading