Entirely Internal Connection (#1610)

This change allows the user to specify all connection details in an
internal connection rather than forcing them to rely on the ssh config
for certain keywords.
This commit is contained in:
Sylvie Crowe 2024-12-23 15:12:14 -08:00 committed by GitHub
parent dbacae8a99
commit b778417b9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 155 additions and 107 deletions

View File

@ -290,6 +290,7 @@ declare global {
type ConnKeywords = {
"conn:wshenabled"?: boolean;
"conn:askbeforewshinstall"?: boolean;
"conn:overrideconfig"?: boolean;
"display:hidden"?: boolean;
"display:order"?: number;
"term:*"?: boolean;

View File

@ -13,7 +13,6 @@ import (
"os/user"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/wavetermdev/waveterm/pkg/panichandler"
@ -27,16 +26,8 @@ func ParseOpts(input string) (*SSHOpts, error) {
if m == nil {
return nil, fmt.Errorf("invalid format of user@host argument")
}
remoteUser, remoteHost, remotePortStr := m[1], m[2], m[3]
remoteUser, remoteHost, remotePort := m[1], m[2], m[3]
remoteUser = strings.Trim(remoteUser, "@")
var remotePort int
if remotePortStr != "" {
var err error
remotePort, err = strconv.Atoi(remotePortStr)
if err != nil {
return nil, fmt.Errorf("invalid port specified on user@host argument")
}
}
return &SSHOpts{SSHHost: remoteHost, SSHUser: remoteUser, SSHPort: remotePort}, nil
}
@ -341,7 +332,7 @@ func IsPowershell(shellPath string) bool {
func NormalizeConfigPattern(pattern string) string {
userName, err := WaveSshConfigUserSettings().GetStrict(pattern, "User")
if err != nil {
if err != nil || userName == "" {
log.Printf("warning: error parsing username of %s for conn dropdown: %v", pattern, err)
localUser, err := user.Current()
if err == nil {

View File

@ -27,6 +27,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/trimquotes"
"github.com/wavetermdev/waveterm/pkg/userinput"
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
@ -153,7 +154,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *wshrpc.ConnKe
if err == nil {
signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey)
if err == nil {
if sshKeywords.SshAddKeysToAgent && agentClient != nil {
if utilfn.SafeDeref(sshKeywords.SshAddKeysToAgent) && agentClient != nil {
agentClient.Add(agent.AddedKey{
PrivateKey: unencryptedPrivateKey,
})
@ -167,7 +168,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *wshrpc.ConnKe
}
// batch mode deactivates user input
if sshKeywords.SshBatchMode {
if utilfn.SafeDeref(sshKeywords.SshBatchMode) {
// skip this key and try with the next
return createDummySigner()
}
@ -196,7 +197,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *wshrpc.ConnKe
// skip this key and try with the next
return createDummySigner()
}
if sshKeywords.SshAddKeysToAgent && agentClient != nil {
if utilfn.SafeDeref(sshKeywords.SshAddKeysToAgent) && agentClient != nil {
agentClient.Add(agent.AddedKey{
PrivateKey: unencryptedPrivateKey,
})
@ -553,11 +554,14 @@ func createHostKeyCallback(sshKeywords *wshrpc.ConnKeywords) (ssh.HostKeyCallbac
}
func createClientConfig(connCtx context.Context, sshKeywords *wshrpc.ConnKeywords, debugInfo *ConnectionDebugInfo) (*ssh.ClientConfig, error) {
remoteName := sshKeywords.SshUser + "@" + xknownhosts.Normalize(sshKeywords.SshHostName+":"+sshKeywords.SshPort)
chosenUser := utilfn.SafeDeref(sshKeywords.SshUser)
chosenHostName := utilfn.SafeDeref(sshKeywords.SshHostName)
chosenPort := utilfn.SafeDeref(sshKeywords.SshPort)
remoteName := chosenUser + xknownhosts.Normalize(chosenHostName+":"+chosenPort)
var authSockSigners []ssh.Signer
var agentClient agent.ExtendedAgent
conn, err := net.Dial("unix", sshKeywords.SshIdentityAgent)
conn, err := net.Dial("unix", utilfn.SafeDeref(sshKeywords.SshIdentityAgent))
if err != nil {
log.Printf("Failed to open Identity Agent Socket: %v", err)
} else {
@ -578,9 +582,9 @@ func createClientConfig(connCtx context.Context, sshKeywords *wshrpc.ConnKeyword
// note: batch mode turns off interactive input
authMethodActiveMap := map[string]bool{
"publickey": sshKeywords.SshPubkeyAuthentication,
"keyboard-interactive": sshKeywords.SshKbdInteractiveAuthentication && !sshKeywords.SshBatchMode,
"password": sshKeywords.SshPasswordAuthentication && !sshKeywords.SshBatchMode,
"publickey": utilfn.SafeDeref(sshKeywords.SshPubkeyAuthentication),
"keyboard-interactive": utilfn.SafeDeref(sshKeywords.SshKbdInteractiveAuthentication) && !utilfn.SafeDeref(sshKeywords.SshBatchMode),
"password": utilfn.SafeDeref(sshKeywords.SshPasswordAuthentication) && !utilfn.SafeDeref(sshKeywords.SshBatchMode),
}
var authMethods []ssh.AuthMethod
@ -601,9 +605,9 @@ func createClientConfig(connCtx context.Context, sshKeywords *wshrpc.ConnKeyword
return nil, err
}
networkAddr := sshKeywords.SshHostName + ":" + sshKeywords.SshPort
networkAddr := chosenHostName + ":" + chosenPort
return &ssh.ClientConfig{
User: sshKeywords.SshUser,
User: chosenUser,
Auth: authMethods,
HostKeyCallback: hostKeyCallback,
HostKeyAlgorithms: hostKeyAlgorithms(networkAddr),
@ -647,20 +651,38 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
}
connFlags.SshUser = opts.SSHUser
connFlags.SshHostName = opts.SSHHost
connFlags.SshPort = fmt.Sprintf("%d", opts.SSHPort)
parsedKeywords := &wshrpc.ConnKeywords{}
if opts.SSHUser != "" {
parsedKeywords.SshUser = &opts.SSHUser
}
if opts.SSHPort != "" {
parsedKeywords.SshPort = &opts.SSHPort
}
rawName := opts.String()
savedKeywords, ok := wconfig.ReadFullConfig().Connections[rawName]
fullConfig := wconfig.ReadFullConfig()
internalSshConfigKeywords, ok := fullConfig.Connections[rawName]
if !ok {
savedKeywords = wshrpc.ConnKeywords{}
internalSshConfigKeywords = wshrpc.ConnKeywords{}
}
sshKeywords, err := combineSshKeywords(connFlags, sshConfigKeywords, &savedKeywords)
if err != nil {
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
// cascade order:
// ssh config -> (optional) internal config -> specified flag keywords -> parsed keywords
partialMerged := sshConfigKeywords
if internalSshConfigKeywords.ConnOverrideConfig {
partialMerged = mergeKeywords(partialMerged, &internalSshConfigKeywords)
}
partialMerged = mergeKeywords(partialMerged, connFlags)
sshKeywords := mergeKeywords(partialMerged, parsedKeywords)
// handle these separately since
// - they append
// - since they append, the order is reversed
// - there is no reason to not include the internal config
// - they are never part of the parsedKeywords
sshKeywords.SshIdentityFile = append(sshKeywords.SshIdentityFile, connFlags.SshIdentityFile...)
sshKeywords.SshIdentityFile = append(sshKeywords.SshIdentityFile, internalSshConfigKeywords.SshIdentityFile...)
sshKeywords.SshIdentityFile = append(sshKeywords.SshIdentityFile, sshConfigKeywords.SshIdentityFile...)
for _, proxyName := range sshKeywords.SshProxyJump {
proxyOpts, err := ParseOpts(proxyName)
@ -685,7 +707,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
if err != nil {
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
}
networkAddr := sshKeywords.SshHostName + ":" + sshKeywords.SshPort
networkAddr := utilfn.SafeDeref(sshKeywords.SshHostName) + ":" + utilfn.SafeDeref(sshKeywords.SshPort)
client, err := connectInternal(connCtx, networkAddr, clientConfig, debugInfo.CurrentClient)
if err != nil {
return client, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
@ -693,61 +715,6 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
return client, debugInfo.JumpNum, nil
}
func combineSshKeywords(userProvidedOpts *wshrpc.ConnKeywords, configKeywords *wshrpc.ConnKeywords, savedKeywords *wshrpc.ConnKeywords) (*wshrpc.ConnKeywords, error) {
sshKeywords := &wshrpc.ConnKeywords{}
if userProvidedOpts.SshUser != "" {
sshKeywords.SshUser = userProvidedOpts.SshUser
} else if configKeywords.SshUser != "" {
sshKeywords.SshUser = configKeywords.SshUser
} else {
user, err := user.Current()
if err != nil {
return nil, fmt.Errorf("failed to get user for ssh: %+v", err)
}
sshKeywords.SshUser = user.Username
}
// we have to check the host value because of the weird way
// we store the pattern as the hostname for imported remotes
if configKeywords.SshHostName != "" {
sshKeywords.SshHostName = configKeywords.SshHostName
} else {
sshKeywords.SshHostName = userProvidedOpts.SshHostName
}
if userProvidedOpts.SshPort != "0" && userProvidedOpts.SshPort != "22" {
sshKeywords.SshPort = userProvidedOpts.SshPort
} else if configKeywords.SshPort != "" && configKeywords.SshPort != "22" {
sshKeywords.SshPort = configKeywords.SshPort
} else {
sshKeywords.SshPort = "22"
}
// use internal config ones
if savedKeywords != nil {
sshKeywords.SshIdentityFile = append(sshKeywords.SshIdentityFile, savedKeywords.SshIdentityFile...)
}
sshKeywords.SshIdentityFile = append(sshKeywords.SshIdentityFile, userProvidedOpts.SshIdentityFile...)
sshKeywords.SshIdentityFile = append(sshKeywords.SshIdentityFile, configKeywords.SshIdentityFile...)
// these are not officially supported in the waveterm frontend but can be configured
// in ssh config files
sshKeywords.SshBatchMode = configKeywords.SshBatchMode
sshKeywords.SshPubkeyAuthentication = configKeywords.SshPubkeyAuthentication
sshKeywords.SshPasswordAuthentication = configKeywords.SshPasswordAuthentication
sshKeywords.SshKbdInteractiveAuthentication = configKeywords.SshKbdInteractiveAuthentication
sshKeywords.SshPreferredAuthentications = configKeywords.SshPreferredAuthentications
sshKeywords.SshAddKeysToAgent = configKeywords.SshAddKeysToAgent
sshKeywords.SshIdentityAgent = configKeywords.SshIdentityAgent
sshKeywords.SshProxyJump = configKeywords.SshProxyJump
sshKeywords.SshUserKnownHostsFile = configKeywords.SshUserKnownHostsFile
sshKeywords.SshGlobalKnownHostsFile = configKeywords.SshGlobalKnownHostsFile
return sshKeywords, nil
}
// note that a `var == "yes"` will default to false
// but `var != "no"` will default to true
// when given unexpected strings
@ -761,25 +728,38 @@ func findSshConfigKeywords(hostPattern string) (connKeywords *wshrpc.ConnKeyword
WaveSshConfigUserSettings().ReloadConfigs()
sshKeywords := &wshrpc.ConnKeywords{}
var err error
//config := wconfig.ReadFullConfig()
userRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "User")
if err != nil {
return nil, err
}
sshKeywords.SshUser = trimquotes.TryTrimQuotes(userRaw)
userClean := trimquotes.TryTrimQuotes(userRaw)
if userClean == "" {
userDetails, err := user.Current()
if err != nil {
return nil, err
}
userClean = userDetails.Username
}
sshKeywords.SshUser = &userClean
hostNameRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "HostName")
if err != nil {
return nil, err
}
sshKeywords.SshHostName = trimquotes.TryTrimQuotes(hostNameRaw)
// manually implementing default HostName here as it is not handled by ssh_config library
hostNameProcessed := trimquotes.TryTrimQuotes(hostNameRaw)
if hostNameProcessed == "" {
sshKeywords.SshHostName = &hostPattern
} else {
sshKeywords.SshHostName = &hostNameRaw
}
portRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "Port")
if err != nil {
return nil, err
}
sshKeywords.SshPort = trimquotes.TryTrimQuotes(portRaw)
sshKeywords.SshPort = utilfn.Ptr(trimquotes.TryTrimQuotes(portRaw))
identityFileRaw := WaveSshConfigUserSettings().GetAll(hostPattern, "IdentityFile")
for i := 0; i < len(identityFileRaw); i++ {
@ -791,26 +771,26 @@ func findSshConfigKeywords(hostPattern string) (connKeywords *wshrpc.ConnKeyword
if err != nil {
return nil, err
}
sshKeywords.SshBatchMode = (strings.ToLower(trimquotes.TryTrimQuotes(batchModeRaw)) == "yes")
sshKeywords.SshBatchMode = utilfn.Ptr(strings.ToLower(trimquotes.TryTrimQuotes(batchModeRaw)) == "yes")
// we currently do not support host-bound or unbound but will use yes when they are selected
pubkeyAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "PubkeyAuthentication")
if err != nil {
return nil, err
}
sshKeywords.SshPubkeyAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(pubkeyAuthenticationRaw)) != "no")
sshKeywords.SshPubkeyAuthentication = utilfn.Ptr(strings.ToLower(trimquotes.TryTrimQuotes(pubkeyAuthenticationRaw)) != "no")
passwordAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "PasswordAuthentication")
if err != nil {
return nil, err
}
sshKeywords.SshPasswordAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(passwordAuthenticationRaw)) != "no")
sshKeywords.SshPasswordAuthentication = utilfn.Ptr(strings.ToLower(trimquotes.TryTrimQuotes(passwordAuthenticationRaw)) != "no")
kbdInteractiveAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "KbdInteractiveAuthentication")
if err != nil {
return nil, err
}
sshKeywords.SshKbdInteractiveAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(kbdInteractiveAuthenticationRaw)) != "no")
sshKeywords.SshKbdInteractiveAuthentication = utilfn.Ptr(strings.ToLower(trimquotes.TryTrimQuotes(kbdInteractiveAuthenticationRaw)) != "no")
// these are parsed as a single string and must be separated
// these are case sensitive in openssh so they are here too
@ -823,7 +803,7 @@ func findSshConfigKeywords(hostPattern string) (connKeywords *wshrpc.ConnKeyword
if err != nil {
return nil, err
}
sshKeywords.SshAddKeysToAgent = (strings.ToLower(trimquotes.TryTrimQuotes(addKeysToAgentRaw)) == "yes")
sshKeywords.SshAddKeysToAgent = utilfn.Ptr(strings.ToLower(trimquotes.TryTrimQuotes(addKeysToAgentRaw)) == "yes")
identityAgentRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "IdentityAgent")
if err != nil {
@ -838,7 +818,7 @@ func findSshConfigKeywords(hostPattern string) (connKeywords *wshrpc.ConnKeyword
if err != nil {
return nil, err
}
sshKeywords.SshIdentityAgent = agentPath
sshKeywords.SshIdentityAgent = utilfn.Ptr(agentPath)
} else {
log.Printf("unable to find SSH_AUTH_SOCK: %v\n", err)
}
@ -847,7 +827,7 @@ func findSshConfigKeywords(hostPattern string) (connKeywords *wshrpc.ConnKeyword
if err != nil {
return nil, err
}
sshKeywords.SshIdentityAgent = agentPath
sshKeywords.SshIdentityAgent = utilfn.Ptr(agentPath)
}
proxyJumpRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "ProxyJump")
@ -873,7 +853,7 @@ func findSshConfigKeywords(hostPattern string) (connKeywords *wshrpc.ConnKeyword
type SSHOpts struct {
SSHHost string `json:"sshhost"`
SSHUser string `json:"sshuser"`
SSHPort int `json:"sshport,omitempty"`
SSHPort string `json:"sshport,omitempty"`
}
func (opts SSHOpts) String() string {
@ -882,8 +862,61 @@ func (opts SSHOpts) String() string {
stringRepr = opts.SSHUser + "@"
}
stringRepr = stringRepr + opts.SSHHost
if opts.SSHPort != 0 {
if opts.SSHPort != "22" && opts.SSHPort != "" {
stringRepr = stringRepr + ":" + fmt.Sprint(opts.SSHPort)
}
return stringRepr
}
func mergeKeywords(oldKeywords *wshrpc.ConnKeywords, newKeywords *wshrpc.ConnKeywords) *wshrpc.ConnKeywords {
if oldKeywords == nil {
oldKeywords = &wshrpc.ConnKeywords{}
}
if newKeywords == nil {
return oldKeywords
}
outKeywords := *oldKeywords
if newKeywords.SshHostName != nil {
outKeywords.SshHostName = newKeywords.SshHostName
}
if newKeywords.SshUser != nil {
outKeywords.SshUser = newKeywords.SshUser
}
if newKeywords.SshPort != nil {
outKeywords.SshPort = newKeywords.SshPort
}
// skip identityfile (handled separately due to different behavior)
if newKeywords.SshBatchMode != nil {
outKeywords.SshBatchMode = newKeywords.SshBatchMode
}
if newKeywords.SshPubkeyAuthentication != nil {
outKeywords.SshPubkeyAuthentication = newKeywords.SshPubkeyAuthentication
}
if newKeywords.SshPasswordAuthentication != nil {
outKeywords.SshPasswordAuthentication = newKeywords.SshPasswordAuthentication
}
if newKeywords.SshKbdInteractiveAuthentication != nil {
outKeywords.SshKbdInteractiveAuthentication = newKeywords.SshKbdInteractiveAuthentication
}
if newKeywords.SshPreferredAuthentications != nil {
outKeywords.SshPreferredAuthentications = newKeywords.SshPreferredAuthentications
}
if newKeywords.SshAddKeysToAgent != nil {
outKeywords.SshAddKeysToAgent = newKeywords.SshAddKeysToAgent
}
if newKeywords.SshIdentityAgent != nil {
outKeywords.SshIdentityAgent = newKeywords.SshIdentityAgent
}
if newKeywords.SshProxyJump != nil {
outKeywords.SshProxyJump = newKeywords.SshProxyJump
}
if newKeywords.SshUserKnownHostsFile != nil {
outKeywords.SshUserKnownHostsFile = newKeywords.SshUserKnownHostsFile
}
if newKeywords.SshGlobalKnownHostsFile != nil {
outKeywords.SshGlobalKnownHostsFile = newKeywords.SshGlobalKnownHostsFile
}
return &outKeywords
}

View File

@ -942,3 +942,25 @@ func FormatLsTime(t time.Time) string {
return t.Format("Jan _2 2006")
}
}
/**
* Helper function that will deref a pointer if not null
* but returns a default value if it is null.
*/
func SafeDeref[T any](x *T) T {
if x == nil {
var safeOut T
return safeOut
}
return *x
}
/**
* Utility function for referencing a type with a pointer.
* This is the same as dereferencing with &, but unlike &
* you can directly use it on the ouput of a function
* without needing to create an intermediate variable
*/
func Ptr[T any](x T) *T {
return &x
}

View File

@ -461,6 +461,7 @@ type CommandRemoteWriteFileData struct {
type ConnKeywords struct {
ConnWshEnabled *bool `json:"conn:wshenabled,omitempty"`
ConnAskBeforeWshInstall *bool `json:"conn:askbeforewshinstall,omitempty"`
ConnOverrideConfig bool `json:"conn:overrideconfig,omitempty"`
DisplayHidden *bool `json:"display:hidden,omitempty"`
DisplayOrder float32 `json:"display:order,omitempty"`
@ -470,17 +471,17 @@ type ConnKeywords struct {
TermFontFamily string `json:"term:fontfamily,omitempty"`
TermTheme string `json:"term:theme,omitempty"`
SshUser string `json:"ssh:user,omitempty"`
SshHostName string `json:"ssh:hostname,omitempty"`
SshPort string `json:"ssh:port,omitempty"`
SshUser *string `json:"ssh:user,omitempty"`
SshHostName *string `json:"ssh:hostname,omitempty"`
SshPort *string `json:"ssh:port,omitempty"`
SshIdentityFile []string `json:"ssh:identityfile,omitempty"`
SshBatchMode bool `json:"ssh:batchmode,omitempty"`
SshPubkeyAuthentication bool `json:"ssh:pubkeyauthentication,omitempty"`
SshPasswordAuthentication bool `json:"ssh:passwordauthentication,omitempty"`
SshKbdInteractiveAuthentication bool `json:"ssh:kbdinteractiveauthentication,omitempty"`
SshBatchMode *bool `json:"ssh:batchmode,omitempty"`
SshPubkeyAuthentication *bool `json:"ssh:pubkeyauthentication,omitempty"`
SshPasswordAuthentication *bool `json:"ssh:passwordauthentication,omitempty"`
SshKbdInteractiveAuthentication *bool `json:"ssh:kbdinteractiveauthentication,omitempty"`
SshPreferredAuthentications []string `json:"ssh:preferredauthentications,omitempty"`
SshAddKeysToAgent bool `json:"ssh:addkeystoagent,omitempty"`
SshIdentityAgent string `json:"ssh:identityagent,omitempty"`
SshAddKeysToAgent *bool `json:"ssh:addkeystoagent,omitempty"`
SshIdentityAgent *string `json:"ssh:identityagent,omitempty"`
SshProxyJump []string `json:"ssh:proxyjump,omitempty"`
SshUserKnownHostsFile []string `json:"ssh:userknownhostsfile,omitempty"`
SshGlobalKnownHostsFile []string `json:"ssh:globalknownhostsfile,omitempty"`