mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +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 {
|
function buildWaveSrv {
|
||||||
# adds -extldflags=-static, *only* on linux (macos does not support fully static binaries) to avoid a glibc dependency
|
# 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 amd64
|
||||||
buildWaveShell darwin arm64
|
buildWaveShell darwin arm64
|
||||||
@ -90,7 +90,7 @@ yarn run electron-builder -c electron-builder.config.js -l -p never
|
|||||||
# @scripthaus command build-wavesrv
|
# @scripthaus command build-wavesrv
|
||||||
WAVESRV_VERSION=$(node -e 'console.log(require("./version.js"))')
|
WAVESRV_VERSION=$(node -e 'console.log(require("./version.js"))')
|
||||||
cd wavesrv
|
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
|
```bash
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@ -673,3 +674,26 @@ func GetFirstLine(s string) string {
|
|||||||
}
|
}
|
||||||
return s[0:idx]
|
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")
|
wsh.WriteToPtyBuffer("*error: cannot install on a local remote\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = shellapi.MakeShellApi(packet.ShellType_bash)
|
sapi, err := shellapi.MakeShellApi(wsh.GetShellType())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsh.WriteToPtyBuffer("*error: %v\n", err)
|
wsh.WriteToPtyBuffer("*error: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if wsh.Client == nil {
|
if wsh.Client == nil {
|
||||||
remoteDisplayName := fmt.Sprintf("%s [%s]", remoteCopy.RemoteAlias, remoteCopy.RemoteCanonicalName)
|
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 {
|
if err != nil {
|
||||||
statusErr := fmt.Errorf("ssh cannot connect to client: %w", err)
|
statusErr := fmt.Errorf("ssh cannot connect to client: %w", err)
|
||||||
wsh.setInstallErrorStatus(statusErr)
|
wsh.setInstallErrorStatus(statusErr)
|
||||||
@ -1614,7 +1615,8 @@ func (wsh *WaveshellProc) createWaveshellSession(clientCtx context.Context, remo
|
|||||||
wsSession = shexec.CmdWrap{Cmd: ecmd}
|
wsSession = shexec.CmdWrap{Cmd: ecmd}
|
||||||
} else if wsh.Client == nil {
|
} else if wsh.Client == nil {
|
||||||
remoteDisplayName := fmt.Sprintf("%s [%s]", remoteCopy.RemoteAlias, remoteCopy.RemoteCanonicalName)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ssh cannot connect to client: %w", err)
|
return nil, fmt.Errorf("ssh cannot connect to client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,12 @@ import (
|
|||||||
"github.com/kevinburke/ssh_config"
|
"github.com/kevinburke/ssh_config"
|
||||||
"github.com/skeema/knownhosts"
|
"github.com/skeema/knownhosts"
|
||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
"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/scbus"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/userinput"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/userinput"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/crypto/ssh/agent"
|
||||||
xknownhosts "golang.org/x/crypto/ssh/knownhosts"
|
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
|
// 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
|
// 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.
|
// 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
|
var identityFiles []string
|
||||||
existingKeys := make(map[string][]byte)
|
existingKeys := make(map[string][]byte)
|
||||||
|
|
||||||
@ -86,7 +88,19 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
|||||||
// require pointer to modify list in closure
|
// require pointer to modify list in closure
|
||||||
identityFilesPtr := &identityFiles
|
identityFilesPtr := &identityFiles
|
||||||
|
|
||||||
|
var authSockSigners []ssh.Signer
|
||||||
|
authSockSigners = append(authSockSigners, authSockSignersExt...)
|
||||||
|
authSockSignersPtr := &authSockSigners
|
||||||
|
|
||||||
return func() ([]ssh.Signer, error) {
|
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 {
|
if len(*identityFilesPtr) == 0 {
|
||||||
return nil, fmt.Errorf("no identity files remaining")
|
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
|
// skip this key and try with the next
|
||||||
return createDummySigner()
|
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)
|
signer, err := ssh.ParsePrivateKey(privateKey)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return []ssh.Signer{signer}, err
|
return []ssh.Signer{signer}, err
|
||||||
@ -107,10 +139,18 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
|||||||
return createDummySigner()
|
return createDummySigner()
|
||||||
}
|
}
|
||||||
|
|
||||||
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(passphrase))
|
unencryptedPrivateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(privateKey, []byte(passphrase))
|
||||||
if err == nil {
|
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
|
return []ssh.Signer{signer}, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
|
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
|
||||||
// skip this key and try with the next
|
// skip this key and try with the next
|
||||||
return createDummySigner()
|
return createDummySigner()
|
||||||
@ -135,11 +175,22 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
|||||||
// trying keys
|
// trying keys
|
||||||
return nil, UserInputCancelError{Err: err}
|
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 {
|
if err != nil {
|
||||||
// skip this key and try with the next
|
// skip this key and try with the next
|
||||||
return createDummySigner()
|
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
|
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
|
return ssh.NewClient(c, chans, reqs), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectToClient(connCtx context.Context, opts *sstore.SSHOpts, remoteDisplayName string) (*ssh.Client, error) {
|
func ConnectToClient(connCtx context.Context, opts *sstore.SSHOpts, remoteDisplayName string, sshAuthSock string) (*ssh.Client, error) {
|
||||||
sshConfigKeywords, err := findSshConfigKeywords(opts.SSHHost)
|
sshConfigKeywords, err := findSshConfigKeywords(opts.SSHHost, sshAuthSock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -549,7 +600,17 @@ func ConnectToClient(connCtx context.Context, opts *sstore.SSHOpts, remoteDispla
|
|||||||
return nil, err
|
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))
|
keyboardInteractive := ssh.KeyboardInteractive(createCombinedKbdInteractiveChallenge(connCtx, opts.SSHPassword, remoteDisplayName))
|
||||||
passwordCallback := ssh.PasswordCallback(createCombinedPasswordCallbackPrompt(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
|
// exclude gssapi-with-mic and hostbased until implemented
|
||||||
authMethodMap := map[string]ssh.AuthMethod{
|
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),
|
"keyboard-interactive": ssh.RetryableAuthMethod(keyboardInteractive, attemptsAllowed),
|
||||||
"password": ssh.RetryableAuthMethod(passwordCallback, attemptsAllowed),
|
"password": ssh.RetryableAuthMethod(passwordCallback, attemptsAllowed),
|
||||||
}
|
}
|
||||||
@ -613,6 +674,8 @@ type SshKeywords struct {
|
|||||||
PasswordAuthentication bool
|
PasswordAuthentication bool
|
||||||
KbdInteractiveAuthentication bool
|
KbdInteractiveAuthentication bool
|
||||||
PreferredAuthentications []string
|
PreferredAuthentications []string
|
||||||
|
AddKeysToAgent bool
|
||||||
|
IdentityAgent string
|
||||||
}
|
}
|
||||||
|
|
||||||
func combineSshKeywords(opts *sstore.SSHOpts, configKeywords *SshKeywords) (*SshKeywords, error) {
|
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.PasswordAuthentication = configKeywords.PasswordAuthentication
|
||||||
sshKeywords.KbdInteractiveAuthentication = configKeywords.KbdInteractiveAuthentication
|
sshKeywords.KbdInteractiveAuthentication = configKeywords.KbdInteractiveAuthentication
|
||||||
sshKeywords.PreferredAuthentications = configKeywords.PreferredAuthentications
|
sshKeywords.PreferredAuthentications = configKeywords.PreferredAuthentications
|
||||||
|
sshKeywords.AddKeysToAgent = configKeywords.AddKeysToAgent
|
||||||
|
sshKeywords.IdentityAgent = configKeywords.IdentityAgent
|
||||||
|
|
||||||
return sshKeywords, nil
|
return sshKeywords, nil
|
||||||
}
|
}
|
||||||
@ -669,7 +734,7 @@ func combineSshKeywords(opts *sstore.SSHOpts, configKeywords *SshKeywords) (*Ssh
|
|||||||
// note that a `var == "yes"` will default to false
|
// note that a `var == "yes"` will default to false
|
||||||
// but `var != "no"` will default to true
|
// but `var != "no"` will default to true
|
||||||
// when given unexpected strings
|
// when given unexpected strings
|
||||||
func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
|
func findSshConfigKeywords(hostPattern string, sshAuthSock string) (*SshKeywords, error) {
|
||||||
ssh_config.ReloadConfigs()
|
ssh_config.ReloadConfigs()
|
||||||
sshKeywords := &SshKeywords{}
|
sshKeywords := &SshKeywords{}
|
||||||
var err error
|
var err error
|
||||||
@ -724,5 +789,21 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
|
|||||||
}
|
}
|
||||||
sshKeywords.PreferredAuthentications = strings.Split(preferredAuthenticationsRaw, ",")
|
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
|
return sshKeywords, nil
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user