mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
Add ssh-agent support to ssh client (#673)
This will attempt to use the ssh agent before trying other ssh keys in case other integrations are being used through it. --------- Co-authored-by: Evan Simkowitz <esimkowitz@users.noreply.github.com>
This commit is contained in:
parent
0505205df5
commit
8bb989fc6f
@ -76,7 +76,7 @@ function buildWaveShell {
|
||||
}
|
||||
function buildWaveSrv {
|
||||
# adds -extldflags=-static, *only* on linux (macos does not support fully static binaries) to avoid a glibc dependency
|
||||
(cd wavesrv; CGO_ENABLED=1 GOARCH=$1 go build -tags "osusergo,netgo,sqlite_omit_load_extension" -ldflags "-linkmode 'external' -extldflags=-static $GO_LDFLAGS -X main.WaveVersion=$WAVESRV_VERSION" -o ../bin/wavesrv.$1 ./cmd)
|
||||
(cd wavesrv; CGO_ENABLED=1 GOARCH=$1 go build -tags "osusergo,netcgo,sqlite_omit_load_extension" -ldflags "-linkmode 'external' -extldflags=-static $GO_LDFLAGS -X main.WaveVersion=$WAVESRV_VERSION" -o ../bin/wavesrv.$1 ./cmd)
|
||||
}
|
||||
buildWaveShell darwin amd64
|
||||
buildWaveShell darwin arm64
|
||||
@ -90,7 +90,7 @@ yarn run electron-builder -c electron-builder.config.js -l -p never
|
||||
# @scripthaus command build-wavesrv
|
||||
WAVESRV_VERSION=$(node -e 'console.log(require("./version.js"))')
|
||||
cd wavesrv
|
||||
CGO_ENABLED=1 go build -tags "osusergo,netgo,sqlite_omit_load_extension" -ldflags "-X main.BuildTime=$(date +'%Y%m%d%H%M') -X main.WaveVersion=$WAVESRV_VERSION" -o ../bin/wavesrv ./cmd
|
||||
CGO_ENABLED=1 go build -tags "osusergo,netcgo,sqlite_omit_load_extension" -ldflags "-X main.BuildTime=$(date +'%Y%m%d%H%M') -X main.WaveVersion=$WAVESRV_VERSION" -o ../bin/wavesrv ./cmd
|
||||
```
|
||||
|
||||
```bash
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode/utf8"
|
||||
@ -673,3 +674,26 @@ func GetFirstLine(s string) string {
|
||||
}
|
||||
return s[0:idx]
|
||||
}
|
||||
|
||||
func TrimQuotes(s string) (string, bool) {
|
||||
if len(s) > 2 && s[0] == '"' {
|
||||
trimmed, err := strconv.Unquote(s)
|
||||
if err != nil {
|
||||
return s, false
|
||||
}
|
||||
return trimmed, true
|
||||
}
|
||||
return s, false
|
||||
}
|
||||
|
||||
func TryTrimQuotes(s string) string {
|
||||
trimmed, _ := TrimQuotes(s)
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func ReplaceQuotes(s string, shouldReplace bool) string {
|
||||
if shouldReplace {
|
||||
return strconv.Quote(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
@ -1314,14 +1314,15 @@ func (wsh *WaveshellProc) RunInstall(autoInstall bool) {
|
||||
wsh.WriteToPtyBuffer("*error: cannot install on a local remote\n")
|
||||
return
|
||||
}
|
||||
_, err = shellapi.MakeShellApi(packet.ShellType_bash)
|
||||
sapi, err := shellapi.MakeShellApi(wsh.GetShellType())
|
||||
if err != nil {
|
||||
wsh.WriteToPtyBuffer("*error: %v\n", err)
|
||||
return
|
||||
}
|
||||
if wsh.Client == nil {
|
||||
remoteDisplayName := fmt.Sprintf("%s [%s]", remoteCopy.RemoteAlias, remoteCopy.RemoteCanonicalName)
|
||||
client, err := ConnectToClient(makeClientCtx, remoteCopy.SSHOpts, remoteDisplayName)
|
||||
sshAuthSock, _ := exec.CommandContext(makeClientCtx, sapi.GetLocalShellPath(), "-c", "echo \"${SSH_AUTH_SOCK}\"").CombinedOutput()
|
||||
client, err := ConnectToClient(makeClientCtx, remoteCopy.SSHOpts, remoteDisplayName, strings.TrimSpace(string(sshAuthSock)))
|
||||
if err != nil {
|
||||
statusErr := fmt.Errorf("ssh cannot connect to client: %w", err)
|
||||
wsh.setInstallErrorStatus(statusErr)
|
||||
@ -1614,7 +1615,8 @@ func (wsh *WaveshellProc) createWaveshellSession(clientCtx context.Context, remo
|
||||
wsSession = shexec.CmdWrap{Cmd: ecmd}
|
||||
} else if wsh.Client == nil {
|
||||
remoteDisplayName := fmt.Sprintf("%s [%s]", remoteCopy.RemoteAlias, remoteCopy.RemoteCanonicalName)
|
||||
client, err := ConnectToClient(clientCtx, remoteCopy.SSHOpts, remoteDisplayName)
|
||||
sshAuthSock, _ := exec.CommandContext(clientCtx, sapi.GetLocalShellPath(), "-c", "echo \"${SSH_AUTH_SOCK}\"").CombinedOutput()
|
||||
client, err := ConnectToClient(clientCtx, remoteCopy.SSHOpts, remoteDisplayName, strings.TrimSpace(string(sshAuthSock)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh cannot connect to client: %w", err)
|
||||
}
|
||||
|
@ -24,10 +24,12 @@ import (
|
||||
"github.com/kevinburke/ssh_config"
|
||||
"github.com/skeema/knownhosts"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/userinput"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
xknownhosts "golang.org/x/crypto/ssh/knownhosts"
|
||||
)
|
||||
|
||||
@ -68,7 +70,7 @@ func createDummySigner() ([]ssh.Signer, error) {
|
||||
// they were successes. An error in this function prevents any other
|
||||
// keys from being attempted. But if there's an error because of a dummy
|
||||
// file, the library can still try again with a new key.
|
||||
func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords, passphrase string) func() ([]ssh.Signer, error) {
|
||||
func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords, passphrase string, authSockSignersExt []ssh.Signer, agentClient agent.ExtendedAgent) func() ([]ssh.Signer, error) {
|
||||
var identityFiles []string
|
||||
existingKeys := make(map[string][]byte)
|
||||
|
||||
@ -86,7 +88,19 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
||||
// require pointer to modify list in closure
|
||||
identityFilesPtr := &identityFiles
|
||||
|
||||
var authSockSigners []ssh.Signer
|
||||
authSockSigners = append(authSockSigners, authSockSignersExt...)
|
||||
authSockSignersPtr := &authSockSigners
|
||||
|
||||
return func() ([]ssh.Signer, error) {
|
||||
// try auth sock
|
||||
if len(*authSockSignersPtr) != 0 {
|
||||
authSockSigner := (*authSockSignersPtr)[0]
|
||||
*authSockSignersPtr = (*authSockSignersPtr)[1:]
|
||||
return []ssh.Signer{authSockSigner}, nil
|
||||
}
|
||||
|
||||
// try manual identity files
|
||||
if len(*identityFilesPtr) == 0 {
|
||||
return nil, fmt.Errorf("no identity files remaining")
|
||||
}
|
||||
@ -98,6 +112,24 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
||||
// skip this key and try with the next
|
||||
return createDummySigner()
|
||||
}
|
||||
|
||||
unencryptedPrivateKey, err := ssh.ParseRawPrivateKey(privateKey)
|
||||
if err == nil {
|
||||
signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey)
|
||||
if err == nil {
|
||||
if sshKeywords.AddKeysToAgent && agentClient != nil {
|
||||
agentClient.Add(agent.AddedKey{
|
||||
PrivateKey: unencryptedPrivateKey,
|
||||
})
|
||||
}
|
||||
return []ssh.Signer{signer}, err
|
||||
}
|
||||
}
|
||||
if _, ok := err.(*ssh.PassphraseMissingError); !ok {
|
||||
// skip this key and try with the next
|
||||
return createDummySigner()
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey(privateKey)
|
||||
if err == nil {
|
||||
return []ssh.Signer{signer}, err
|
||||
@ -107,10 +139,18 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
||||
return createDummySigner()
|
||||
}
|
||||
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(passphrase))
|
||||
unencryptedPrivateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(privateKey, []byte(passphrase))
|
||||
if err == nil {
|
||||
signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey)
|
||||
if err == nil {
|
||||
if sshKeywords.AddKeysToAgent && agentClient != nil {
|
||||
agentClient.Add(agent.AddedKey{
|
||||
PrivateKey: unencryptedPrivateKey,
|
||||
})
|
||||
}
|
||||
return []ssh.Signer{signer}, err
|
||||
}
|
||||
}
|
||||
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
|
||||
// skip this key and try with the next
|
||||
return createDummySigner()
|
||||
@ -135,11 +175,22 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
||||
// trying keys
|
||||
return nil, UserInputCancelError{Err: err}
|
||||
}
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(response.Text))
|
||||
|
||||
unencryptedPrivateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(privateKey, []byte([]byte(response.Text)))
|
||||
if err != nil {
|
||||
// skip this key and try with the next
|
||||
return createDummySigner()
|
||||
}
|
||||
signer, err = ssh.NewSignerFromKey(unencryptedPrivateKey)
|
||||
if err != nil {
|
||||
// skip this key and try with the next
|
||||
return createDummySigner()
|
||||
}
|
||||
if sshKeywords.AddKeysToAgent && agentClient != nil {
|
||||
agentClient.Add(agent.AddedKey{
|
||||
PrivateKey: unencryptedPrivateKey,
|
||||
})
|
||||
}
|
||||
return []ssh.Signer{signer}, err
|
||||
}
|
||||
}
|
||||
@ -538,8 +589,8 @@ func DialContext(ctx context.Context, network string, addr string, config *ssh.C
|
||||
return ssh.NewClient(c, chans, reqs), nil
|
||||
}
|
||||
|
||||
func ConnectToClient(connCtx context.Context, opts *sstore.SSHOpts, remoteDisplayName string) (*ssh.Client, error) {
|
||||
sshConfigKeywords, err := findSshConfigKeywords(opts.SSHHost)
|
||||
func ConnectToClient(connCtx context.Context, opts *sstore.SSHOpts, remoteDisplayName string, sshAuthSock string) (*ssh.Client, error) {
|
||||
sshConfigKeywords, err := findSshConfigKeywords(opts.SSHHost, sshAuthSock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -549,7 +600,17 @@ func ConnectToClient(connCtx context.Context, opts *sstore.SSHOpts, remoteDispla
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicKeyCallback := ssh.PublicKeysCallback(createPublicKeyCallback(connCtx, sshKeywords, opts.SSHPassword))
|
||||
conn, err := net.Dial("unix", sshKeywords.IdentityAgent)
|
||||
var authSockSigners []ssh.Signer
|
||||
var agentClient agent.ExtendedAgent
|
||||
if err != nil {
|
||||
log.Printf("Failed to open Identity Agent Socket: %v", err)
|
||||
} else {
|
||||
agentClient = agent.NewClient(conn)
|
||||
authSockSigners, _ = agentClient.Signers()
|
||||
}
|
||||
|
||||
publicKeyCallback := ssh.PublicKeysCallback(createPublicKeyCallback(connCtx, sshKeywords, opts.SSHPassword, authSockSigners, agentClient))
|
||||
keyboardInteractive := ssh.KeyboardInteractive(createCombinedKbdInteractiveChallenge(connCtx, opts.SSHPassword, remoteDisplayName))
|
||||
passwordCallback := ssh.PasswordCallback(createCombinedPasswordCallbackPrompt(connCtx, opts.SSHPassword, remoteDisplayName))
|
||||
|
||||
@ -564,7 +625,7 @@ func ConnectToClient(connCtx context.Context, opts *sstore.SSHOpts, remoteDispla
|
||||
|
||||
// exclude gssapi-with-mic and hostbased until implemented
|
||||
authMethodMap := map[string]ssh.AuthMethod{
|
||||
"publickey": ssh.RetryableAuthMethod(publicKeyCallback, len(sshKeywords.IdentityFile)),
|
||||
"publickey": ssh.RetryableAuthMethod(publicKeyCallback, len(sshKeywords.IdentityFile)+len(authSockSigners)),
|
||||
"keyboard-interactive": ssh.RetryableAuthMethod(keyboardInteractive, attemptsAllowed),
|
||||
"password": ssh.RetryableAuthMethod(passwordCallback, attemptsAllowed),
|
||||
}
|
||||
@ -613,6 +674,8 @@ type SshKeywords struct {
|
||||
PasswordAuthentication bool
|
||||
KbdInteractiveAuthentication bool
|
||||
PreferredAuthentications []string
|
||||
AddKeysToAgent bool
|
||||
IdentityAgent string
|
||||
}
|
||||
|
||||
func combineSshKeywords(opts *sstore.SSHOpts, configKeywords *SshKeywords) (*SshKeywords, error) {
|
||||
@ -662,6 +725,8 @@ func combineSshKeywords(opts *sstore.SSHOpts, configKeywords *SshKeywords) (*Ssh
|
||||
sshKeywords.PasswordAuthentication = configKeywords.PasswordAuthentication
|
||||
sshKeywords.KbdInteractiveAuthentication = configKeywords.KbdInteractiveAuthentication
|
||||
sshKeywords.PreferredAuthentications = configKeywords.PreferredAuthentications
|
||||
sshKeywords.AddKeysToAgent = configKeywords.AddKeysToAgent
|
||||
sshKeywords.IdentityAgent = configKeywords.IdentityAgent
|
||||
|
||||
return sshKeywords, nil
|
||||
}
|
||||
@ -669,7 +734,7 @@ func combineSshKeywords(opts *sstore.SSHOpts, configKeywords *SshKeywords) (*Ssh
|
||||
// note that a `var == "yes"` will default to false
|
||||
// but `var != "no"` will default to true
|
||||
// when given unexpected strings
|
||||
func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
|
||||
func findSshConfigKeywords(hostPattern string, sshAuthSock string) (*SshKeywords, error) {
|
||||
ssh_config.ReloadConfigs()
|
||||
sshKeywords := &SshKeywords{}
|
||||
var err error
|
||||
@ -724,5 +789,21 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
|
||||
}
|
||||
sshKeywords.PreferredAuthentications = strings.Split(preferredAuthenticationsRaw, ",")
|
||||
|
||||
addKeysToAgentRaw, err := ssh_config.GetStrict(hostPattern, "AddKeysToAgent")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sshKeywords.AddKeysToAgent = (strings.ToLower(addKeysToAgentRaw) == "yes")
|
||||
|
||||
identityAgentRaw, err := ssh_config.GetStrict(hostPattern, "IdentityAgent")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if identityAgentRaw == "" {
|
||||
sshKeywords.IdentityAgent = base.ExpandHomeDir(utilfn.TryTrimQuotes(strings.TrimSpace(string(sshAuthSock))))
|
||||
} else {
|
||||
sshKeywords.IdentityAgent = base.ExpandHomeDir(utilfn.TryTrimQuotes(identityAgentRaw))
|
||||
}
|
||||
|
||||
return sshKeywords, nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user