2025-01-05 05:56:57 +01:00
|
|
|
// Copyright 2025, Command Line Inc.
|
2024-10-24 07:43:17 +02:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2024-08-16 06:32:08 +02:00
|
|
|
package remote
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2025-01-02 23:15:32 +01:00
|
|
|
"context"
|
2024-08-16 06:32:08 +02:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"os"
|
2024-09-06 02:02:44 +02:00
|
|
|
"os/user"
|
2024-08-16 06:32:08 +02:00
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
2025-01-02 23:15:32 +01:00
|
|
|
"text/template"
|
2025-01-20 23:06:37 +01:00
|
|
|
"time"
|
2024-08-16 06:32:08 +02:00
|
|
|
|
2025-01-14 23:09:26 +01:00
|
|
|
"github.com/wavetermdev/waveterm/pkg/blocklogger"
|
2025-01-02 23:15:32 +01:00
|
|
|
"github.com/wavetermdev/waveterm/pkg/genconn"
|
2025-01-22 23:50:09 +01:00
|
|
|
"github.com/wavetermdev/waveterm/pkg/remote/awsconn"
|
|
|
|
"github.com/wavetermdev/waveterm/pkg/util/iterfn"
|
2025-01-02 23:15:32 +01:00
|
|
|
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
|
2024-12-31 02:11:50 +01:00
|
|
|
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
2025-01-22 23:50:09 +01:00
|
|
|
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
2024-08-16 06:32:08 +02:00
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
)
|
|
|
|
|
2024-10-24 19:58:31 +02:00
|
|
|
var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-zA-Z0-9][a-zA-Z0-9.-]*)(?::([0-9]+))?$`)
|
2024-08-16 06:32:08 +02:00
|
|
|
|
|
|
|
func ParseOpts(input string) (*SSHOpts, error) {
|
|
|
|
m := userHostRe.FindStringSubmatch(input)
|
|
|
|
if m == nil {
|
|
|
|
return nil, fmt.Errorf("invalid format of user@host argument")
|
|
|
|
}
|
2024-12-24 00:12:14 +01:00
|
|
|
remoteUser, remoteHost, remotePort := m[1], m[2], m[3]
|
2024-08-16 06:32:08 +02:00
|
|
|
remoteUser = strings.Trim(remoteUser, "@")
|
|
|
|
|
|
|
|
return &SSHOpts{SSHHost: remoteHost, SSHUser: remoteUser, SSHPort: remotePort}, nil
|
|
|
|
}
|
|
|
|
|
2025-01-02 23:15:32 +01:00
|
|
|
func normalizeOs(os string) string {
|
|
|
|
os = strings.ToLower(strings.TrimSpace(os))
|
|
|
|
return os
|
|
|
|
}
|
2024-08-16 06:32:08 +02:00
|
|
|
|
2025-01-02 23:15:32 +01:00
|
|
|
func normalizeArch(arch string) string {
|
|
|
|
arch = strings.ToLower(strings.TrimSpace(arch))
|
|
|
|
switch arch {
|
|
|
|
case "x86_64", "amd64":
|
|
|
|
arch = "x64"
|
|
|
|
case "arm64", "aarch64":
|
|
|
|
arch = "arm64"
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-02 23:15:32 +01:00
|
|
|
return arch
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
|
|
|
|
2025-01-02 23:15:32 +01:00
|
|
|
// returns (os, arch, error)
|
|
|
|
// guaranteed to return a supported platform
|
|
|
|
func GetClientPlatform(ctx context.Context, shell genconn.ShellClient) (string, string, error) {
|
2025-01-14 23:09:26 +01:00
|
|
|
blocklogger.Infof(ctx, "[conndebug] running `uname -sm` to detect client platform\n")
|
2025-01-02 23:15:32 +01:00
|
|
|
stdout, stderr, err := genconn.RunSimpleCommand(ctx, shell, genconn.CommandSpec{
|
|
|
|
Cmd: "uname -sm",
|
|
|
|
})
|
2024-08-16 06:32:08 +02:00
|
|
|
if err != nil {
|
2025-01-02 23:15:32 +01:00
|
|
|
return "", "", fmt.Errorf("error running uname -sm: %w, stderr: %s", err, stderr)
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-02 23:15:32 +01:00
|
|
|
// Parse and normalize output
|
|
|
|
parts := strings.Fields(strings.ToLower(strings.TrimSpace(stdout)))
|
|
|
|
if len(parts) != 2 {
|
|
|
|
return "", "", fmt.Errorf("unexpected output from uname: %s", stdout)
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-02 23:15:32 +01:00
|
|
|
os, arch := normalizeOs(parts[0]), normalizeArch(parts[1])
|
|
|
|
if err := wavebase.ValidateWshSupportedArch(os, arch); err != nil {
|
|
|
|
return "", "", err
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-02 23:15:32 +01:00
|
|
|
return os, arch, nil
|
|
|
|
}
|
2024-08-16 06:32:08 +02:00
|
|
|
|
2025-01-14 23:09:26 +01:00
|
|
|
func GetClientPlatformFromOsArchStr(ctx context.Context, osArchStr string) (string, string, error) {
|
|
|
|
parts := strings.Fields(strings.TrimSpace(osArchStr))
|
|
|
|
if len(parts) != 2 {
|
|
|
|
return "", "", fmt.Errorf("unexpected output from uname: %s", osArchStr)
|
|
|
|
}
|
|
|
|
os, arch := normalizeOs(parts[0]), normalizeArch(parts[1])
|
|
|
|
if err := wavebase.ValidateWshSupportedArch(os, arch); err != nil {
|
|
|
|
return "", "", err
|
|
|
|
}
|
|
|
|
return os, arch, nil
|
|
|
|
}
|
|
|
|
|
2025-01-02 23:15:32 +01:00
|
|
|
var installTemplateRawDefault = strings.TrimSpace(`
|
2025-01-14 23:09:26 +01:00
|
|
|
mkdir -p {{.installDir}} || exit 1;
|
|
|
|
cat > {{.tempPath}} || exit 1;
|
|
|
|
mv {{.tempPath}} {{.installPath}} || exit 1;
|
|
|
|
chmod a+x {{.installPath}} || exit 1;
|
2025-01-02 23:15:32 +01:00
|
|
|
`)
|
|
|
|
var installTemplate = template.Must(template.New("wsh-install-template").Parse(installTemplateRawDefault))
|
2024-08-16 06:32:08 +02:00
|
|
|
|
2025-01-02 23:15:32 +01:00
|
|
|
func CpWshToRemote(ctx context.Context, client *ssh.Client, clientOs string, clientArch string) error {
|
2025-01-20 23:06:37 +01:00
|
|
|
deadline, ok := ctx.Deadline()
|
|
|
|
if ok {
|
|
|
|
blocklogger.Debugf(ctx, "[conndebug] CpWshToRemote, timeout: %v\n", time.Until(deadline))
|
|
|
|
}
|
2025-01-14 23:09:26 +01:00
|
|
|
wshLocalPath, err := shellutil.GetLocalWshBinaryPath(wavebase.WaveVersion, clientOs, clientArch)
|
2024-08-16 06:32:08 +02:00
|
|
|
if err != nil {
|
2025-01-02 23:15:32 +01:00
|
|
|
return err
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-02 23:15:32 +01:00
|
|
|
input, err := os.Open(wshLocalPath)
|
2024-08-16 06:32:08 +02:00
|
|
|
if err != nil {
|
2025-01-02 23:15:32 +01:00
|
|
|
return fmt.Errorf("cannot open local file %s: %w", wshLocalPath, err)
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-02 23:15:32 +01:00
|
|
|
defer input.Close()
|
|
|
|
installWords := map[string]string{
|
|
|
|
"installDir": filepath.ToSlash(filepath.Dir(wavebase.RemoteFullWshBinPath)),
|
2025-01-14 23:09:26 +01:00
|
|
|
"tempPath": wavebase.RemoteFullWshBinPath + ".temp",
|
|
|
|
"installPath": wavebase.RemoteFullWshBinPath,
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-02 23:15:32 +01:00
|
|
|
var installCmd bytes.Buffer
|
|
|
|
if err := installTemplate.Execute(&installCmd, installWords); err != nil {
|
|
|
|
return fmt.Errorf("failed to prepare install command: %w", err)
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-14 23:09:26 +01:00
|
|
|
blocklogger.Infof(ctx, "[conndebug] copying %q to remote server %q\n", wshLocalPath, wavebase.RemoteFullWshBinPath)
|
2025-01-02 23:15:32 +01:00
|
|
|
genCmd, err := genconn.MakeSSHCmdClient(client, genconn.CommandSpec{
|
|
|
|
Cmd: installCmd.String(),
|
|
|
|
})
|
2024-08-16 06:32:08 +02:00
|
|
|
if err != nil {
|
2025-01-02 23:15:32 +01:00
|
|
|
return fmt.Errorf("failed to create remote command: %w", err)
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-02 23:15:32 +01:00
|
|
|
stdin, err := genCmd.StdinPipe()
|
2024-08-16 06:32:08 +02:00
|
|
|
if err != nil {
|
2025-01-02 23:15:32 +01:00
|
|
|
return fmt.Errorf("failed to get stdin pipe: %w", err)
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-02 23:15:32 +01:00
|
|
|
defer stdin.Close()
|
|
|
|
stderrBuf, err := genconn.MakeStderrSyncBuffer(genCmd)
|
2024-08-16 06:32:08 +02:00
|
|
|
if err != nil {
|
2025-01-02 23:15:32 +01:00
|
|
|
return fmt.Errorf("failed to get stderr pipe: %w", err)
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-02 23:15:32 +01:00
|
|
|
if err := genCmd.Start(); err != nil {
|
|
|
|
return fmt.Errorf("failed to start remote command: %w", err)
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2025-01-02 23:15:32 +01:00
|
|
|
copyDone := make(chan error, 1)
|
2024-08-16 06:32:08 +02:00
|
|
|
go func() {
|
2025-01-02 23:15:32 +01:00
|
|
|
defer close(copyDone)
|
|
|
|
defer stdin.Close()
|
|
|
|
if _, err := io.Copy(stdin, input); err != nil && err != io.EOF {
|
|
|
|
copyDone <- fmt.Errorf("failed to copy data: %w", err)
|
|
|
|
} else {
|
|
|
|
copyDone <- nil
|
|
|
|
}
|
2024-08-16 06:32:08 +02:00
|
|
|
}()
|
2025-01-02 23:15:32 +01:00
|
|
|
procErr := genconn.ProcessContextWait(ctx, genCmd)
|
|
|
|
if procErr != nil {
|
|
|
|
return fmt.Errorf("remote command failed: %w (stderr: %s)", procErr, stderrBuf.String())
|
|
|
|
}
|
|
|
|
copyErr := <-copyDone
|
|
|
|
if copyErr != nil {
|
|
|
|
return fmt.Errorf("failed to copy data: %w (stderr: %s)", copyErr, stderrBuf.String())
|
|
|
|
}
|
|
|
|
return nil
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
|
|
|
|
2024-09-04 11:13:00 +02:00
|
|
|
func IsPowershell(shellPath string) bool {
|
|
|
|
// get the base path, and then check contains
|
|
|
|
shellBase := filepath.Base(shellPath)
|
|
|
|
return strings.Contains(shellBase, "powershell") || strings.Contains(shellBase, "pwsh")
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
2024-09-06 02:02:44 +02:00
|
|
|
|
|
|
|
func NormalizeConfigPattern(pattern string) string {
|
2024-10-28 04:35:19 +01:00
|
|
|
userName, err := WaveSshConfigUserSettings().GetStrict(pattern, "User")
|
2024-12-24 00:12:14 +01:00
|
|
|
if err != nil || userName == "" {
|
2024-11-21 03:11:47 +01:00
|
|
|
log.Printf("warning: error parsing username of %s for conn dropdown: %v", pattern, err)
|
2024-09-06 02:02:44 +02:00
|
|
|
localUser, err := user.Current()
|
|
|
|
if err == nil {
|
|
|
|
userName = localUser.Username
|
|
|
|
}
|
|
|
|
}
|
2024-10-28 04:35:19 +01:00
|
|
|
port, err := WaveSshConfigUserSettings().GetStrict(pattern, "Port")
|
2024-09-06 02:02:44 +02:00
|
|
|
if err != nil {
|
|
|
|
port = "22"
|
|
|
|
}
|
|
|
|
if userName != "" {
|
|
|
|
userName += "@"
|
|
|
|
}
|
|
|
|
if port == "22" {
|
|
|
|
port = ""
|
|
|
|
} else {
|
|
|
|
port = ":" + port
|
|
|
|
}
|
2024-10-09 20:08:07 +02:00
|
|
|
return fmt.Sprintf("%s%s%s", userName, pattern, port)
|
2024-09-06 02:02:44 +02:00
|
|
|
}
|
2025-01-22 23:50:09 +01:00
|
|
|
|
|
|
|
func ParseProfiles() []string {
|
|
|
|
connfile, cerrs := wconfig.ReadWaveHomeConfigFile(wconfig.ProfilesFile)
|
|
|
|
if len(cerrs) > 0 {
|
|
|
|
log.Printf("error reading config file: %v", cerrs[0])
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
awsProfiles := awsconn.ParseProfiles()
|
|
|
|
for profile := range awsProfiles {
|
|
|
|
connfile[profile] = struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
return iterfn.MapKeysToSorted(connfile)
|
|
|
|
}
|