Compare commits

..

6 Commits

Author SHA1 Message Date
Sylvia Crowe 00c2ab9fc4 merge branch 'main' into ssh-extra-fixes 2024-03-14 16:20:55 -07:00
Sylvia Crowe 81294820c8 feat: add connection segfault protection
A user recently reported a crash on a failed connection. This handles
these errors more gracefully than the previous system and prints the
errors to the connection prompt.
2024-03-14 16:19:32 -07:00
Sylvia Crowe bb8f5dc660 chore: remove legacy remote launcher
The Legacy Launch function was kept as a way to maintain the old ssh
system while adding the new one. Now that the new system has been
released with minimal issues, it is safe to fully remove the legacy
launcher.
2024-03-14 01:07:54 -07:00
Sylvia Crowe c3917444a2 merge branch 'main' into ssh-extra-fixes 2024-03-13 18:08:29 -07:00
Sylvia Crowe 2cc86f39d6 feat: relax user requirement for canonical name
Previously, the canonical name required a user as a part of it. This
change makes it possible to omit the username if desired. In this case,
the connection will default to your current user.
2024-03-08 00:32:46 -08:00
Sylvia Crowe af22fd38af feat: don't hide nonsecret kbd-interactive answers
Some Kbd-Interactive challenges are marked with echo true. For these, it
makes sense to use a TextField in place of a PasswordField. This change
differentiates between the two options.
2024-03-07 23:32:15 -08:00
7 changed files with 56 additions and 193 deletions
+2 -9
View File
@@ -48,15 +48,8 @@ class TosModal extends React.Component<{}, {}> {
<div className="item-inner">
<div className="item-title">Telemetry</div>
<div className="item-text">
We only collect minimal <i>anonymous</i>
<a
target="_blank"
href={util.makeExternLink("https://docs.waveterm.dev/reference/telemetry")}
rel={"noopener"}
>
&nbsp;telemetry data&nbsp;
</a>
to help us understand how many people are using Wave.
We only collect minimal <i>anonymous</i> telemetry data to help us understand
how many people are using Wave.
</div>
<div className="item-field" style={{ marginTop: 2 }}>
<Toggle
+14 -5
View File
@@ -1,7 +1,7 @@
import * as React from "react";
import { GlobalModel } from "@/models";
import { Choose, When, If } from "tsx-control-statements/components";
import { Modal, PasswordField, Markdown, Checkbox } from "@/elements";
import { Modal, PasswordField, TextField, Markdown, Checkbox } from "@/elements";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import "./userinput.less";
@@ -82,8 +82,17 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
</If>
<If condition={!userInputRequest.markdown}>{userInputRequest.querytext}</If>
</div>
<Choose>
<When condition={userInputRequest.responsetype == "text"}>
<If condition={userInputRequest.responsetype == "text"}>
<If condition={userInputRequest.publictext}>
<TextField
onChange={setResponseText}
value={responseText}
maxLength={400}
autoFocus={true}
onKeyDown={(e) => handleTextKeyDown(e)}
/>
</If>
<If condition={!userInputRequest.publictext}>
<PasswordField
onChange={setResponseText}
value={responseText}
@@ -91,8 +100,8 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
autoFocus={true}
onKeyDown={(e) => handleTextKeyDown(e)}
/>
</When>
</Choose>
</If>
</If>
</div>
<If condition={userInputRequest.checkboxmsg != ""}>
<Checkbox
+1
View File
@@ -667,6 +667,7 @@ declare global {
markdown: boolean;
timeoutms: number;
checkboxmsg: string;
publictext: boolean;
};
type UserInputResponsePacket = {
+17 -12
View File
@@ -126,7 +126,7 @@ var SetVarScopes = []SetVarScope{
{ScopeName: "remote", VarNames: []string{}},
}
var userHostRe = regexp.MustCompile(`^(sudo@)?([a-zA-Z0-9][a-zA-Z0-9._@\\-]*)@([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
var userHostRe = regexp.MustCompile(`^(sudo@)?([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
var remoteAliasRe = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$")
var rendererRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.:-]*$")
@@ -1796,6 +1796,7 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType, isLocal b
return nil, fmt.Errorf("invalid format of user@host argument")
}
sudoStr, remoteUser, remoteHost, remotePortStr := m[1], m[2], m[3], m[4]
remoteUser = strings.Trim(remoteUser, "@")
var uhPort int
if remotePortStr != "" {
var err error
@@ -1837,7 +1838,11 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType, isLocal b
return nil, fmt.Errorf("invalid port argument, \"%d\" is not in the range of 1 to 65535", portVal)
}
sshOpts.SSHPort = portVal
canonicalName = remoteUser + "@" + remoteHost
if remoteUser == "" {
canonicalName = remoteHost
} else {
canonicalName = remoteUser + "@" + remoteHost
}
if portVal != 0 && portVal != 22 {
canonicalName = canonicalName + ":" + strconv.Itoa(portVal)
}
@@ -2136,17 +2141,17 @@ func createSshImportSummary(changeList map[string][]string) string {
func NewHostInfo(hostName string) (*HostInfoType, error) {
userName, _ := ssh_config.GetStrict(hostName, "User")
if userName == "" {
// we cannot store a remote with a missing user
// in the current setup
return nil, fmt.Errorf("could not parse \"%s\" - no User in config\n", hostName)
var canonicalName string
if userName != "" {
canonicalName = userName + "@" + hostName
} else {
canonicalName = hostName
}
canonicalName := userName + "@" + hostName
// check if user and host are okay
// check if canonicalname is okay
m := userHostRe.FindStringSubmatch(canonicalName)
if m == nil || m[2] == "" || m[3] == "" {
return nil, fmt.Errorf("could not parse \"%s\" - %s did not fit user@host requirement\n", hostName, canonicalName)
if m == nil {
return nil, fmt.Errorf("could not parse \"%s\" - %s did not fit user@host requirement", hostName, canonicalName)
}
portStr, _ := ssh_config.GetStrict(hostName, "Port")
@@ -2157,10 +2162,10 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
portVal, err = strconv.Atoi(portStr)
if err != nil {
// do not make assumptions about port if incorrectly configured
return nil, fmt.Errorf("could not parse \"%s\" (%s) - %s could not be converted to a valid port\n", hostName, canonicalName, portStr)
return nil, fmt.Errorf("could not parse \"%s\" (%s) - %s could not be converted to a valid port", hostName, canonicalName, portStr)
}
if portVal <= 0 || portVal > 65535 {
return nil, fmt.Errorf("could not parse port \"%d\": number is not valid for a port\n", portVal)
return nil, fmt.Errorf("could not parse port \"%d\": number is not valid for a port", portVal)
}
}
identityFile, _ := ssh_config.GetStrict(hostName, "IdentityFile")
+20 -167
View File
@@ -15,6 +15,7 @@ import (
"os/exec"
"path"
"regexp"
"runtime/debug"
"strconv"
"strings"
"sync"
@@ -44,8 +45,6 @@ import (
"golang.org/x/mod/semver"
)
const UseSshLibrary = true
const RemoteTypeMShell = "mshell"
const DefaultTerm = "xterm-256color"
const DefaultMaxPtySize = 1024 * 1024
@@ -129,12 +128,6 @@ type pendingStateKey struct {
RemotePtr sstore.RemotePtrType
}
// for conditional launch method based on ssh library in use
// remove once ssh library is stabilized
type Launcher interface {
Launch(*MShellProc, bool)
}
type MShellProc struct {
Lock *sync.Mutex
Remote *sstore.RemoteType
@@ -163,7 +156,6 @@ type MShellProc struct {
RunningCmds map[base.CommandKey]*RunCmdType
PendingStateCmds map[pendingStateKey]base.CommandKey // key=[remoteinstance name]
launcher Launcher // for conditional launch method based on ssh library in use. remove once ssh library is stabilized
Client *ssh.Client
}
@@ -188,12 +180,6 @@ func CanComplete(remoteType string) bool {
}
}
// for conditional launch method based on ssh library in use
// remove once ssh library is stabilized
func (msh *MShellProc) Launch(interactive bool) {
msh.launcher.Launch(msh, interactive)
}
func (msh *MShellProc) GetStatus() string {
msh.Lock.Lock()
defer msh.Lock.Unlock()
@@ -711,14 +697,8 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc {
RunningCmds: make(map[base.CommandKey]*RunCmdType),
PendingStateCmds: make(map[pendingStateKey]base.CommandKey),
StateMap: server.MakeShellStateMap(),
launcher: LegacyLauncher{}, // for conditional launch method based on ssh library in use. remove once ssh library is stabilized
DataPosMap: utilfn.MakeSyncMap[base.CommandKey, int64](),
}
// for conditional launch method based on ssh library in use
// remove once ssh library is stabilized
if UseSshLibrary {
rtn.launcher = NewLauncher{}
}
rtn.WriteToPtyBuffer("console for connection [%s]\n", r.GetName())
return rtn
@@ -1201,6 +1181,15 @@ func (msh *MShellProc) WaitAndSendPassword(pw string) {
}
func (msh *MShellProc) RunInstall(autoInstall bool) {
defer func() {
if r := recover(); r != nil {
errMsg := fmt.Errorf("this should not happen. if it does, please reach out to us in our discord or open an issue on our github\n\n"+
"error:\n%v\n\nstack trace:\n%s", r, string(debug.Stack()))
log.Printf("fatal error, %s\n", errMsg)
msh.WriteToPtyBuffer("*fatal error, %s\n", errMsg)
msh.setErrorStatus(errMsg)
}
}()
remoteCopy := msh.GetRemoteCopy()
if remoteCopy.Archived {
msh.WriteToPtyBuffer("*error: cannot install on archived remote\n")
@@ -1574,12 +1563,16 @@ func (msh *MShellProc) createWaveshellSession(clientCtx context.Context, remoteC
return wsSession, nil
}
// for conditional launch method based on ssh library in use
// remove once ssh library is stabilized
type NewLauncher struct{}
// func (msh *MShellProc) LaunchNew(interactive bool) {
func (NewLauncher) Launch(msh *MShellProc, interactive bool) {
func (msh *MShellProc) Launch(interactive bool) {
defer func() {
if r := recover(); r != nil {
errMsg := fmt.Errorf("this should not happen. if it does, please reach out to us in our discord or open an issue on our github\n\n"+
"error:\n%v\n\nstack trace:\n%s", r, string(debug.Stack()))
log.Printf("fatal error, %s\n", errMsg)
msh.WriteToPtyBuffer("*fatal error, %s\n", errMsg)
msh.setErrorStatus(errMsg)
}
}()
remoteCopy := msh.GetRemoteCopy()
if remoteCopy.Archived {
msh.WriteToPtyBuffer("cannot launch archived remote\n")
@@ -1687,146 +1680,6 @@ func (NewLauncher) Launch(msh *MShellProc, interactive bool) {
go msh.NotifyRemoteUpdate()
}
// for conditional launch method based on ssh library in use
// remove once ssh library is stabilized
type LegacyLauncher struct{}
// func (msh *MShellProc) LaunchLegacy(interactive bool) {
func (LegacyLauncher) Launch(msh *MShellProc, interactive bool) {
remoteCopy := msh.GetRemoteCopy()
if remoteCopy.Archived {
msh.WriteToPtyBuffer("cannot launch archived remote\n")
return
}
curStatus := msh.GetStatus()
if curStatus == StatusConnected {
msh.WriteToPtyBuffer("remote is already connected (no action taken)\n")
return
}
if curStatus == StatusConnecting {
msh.WriteToPtyBuffer("remote is already connecting, disconnect before trying to connect again\n")
return
}
sapi, err := shellapi.MakeShellApi(msh.GetShellType())
if err != nil {
msh.WriteToPtyBuffer("*error, %v\n", err)
return
}
istatus := msh.GetInstallStatus()
if istatus == StatusConnecting {
msh.WriteToPtyBuffer("remote is trying to install, cancel install before trying to connect again\n")
return
}
if remoteCopy.SSHOpts.SSHPort != 0 && remoteCopy.SSHOpts.SSHPort != 22 {
msh.WriteToPtyBuffer("connecting to %s (port %d)...\n", remoteCopy.RemoteCanonicalName, remoteCopy.SSHOpts.SSHPort)
} else {
msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName)
}
sshOpts := convertSSHOpts(remoteCopy.SSHOpts)
sshOpts.SSHErrorsToTty = true
if remoteCopy.ConnectMode != sstore.ConnectModeManual && remoteCopy.SSHOpts.SSHPassword == "" && !interactive {
sshOpts.BatchMode = true
}
var cmdStr string
if sshOpts.SSHHost == "" && remoteCopy.Local {
var err error
cmdStr, err = MakeLocalMShellCommandStr(remoteCopy.IsSudo())
if err != nil {
msh.WriteToPtyBuffer("*error, cannot find local mshell binary: %v\n", err)
return
}
} else {
cmdStr = MakeServerCommandStr()
}
ecmd := sshOpts.MakeSSHExecCmd(cmdStr, sapi)
cmdPty, err := msh.addControllingTty(ecmd)
if err != nil {
statusErr := fmt.Errorf("cannot attach controlling tty to mshell command: %w", err)
msh.WriteToPtyBuffer("*error, %s\n", statusErr.Error())
msh.setErrorStatus(statusErr)
return
}
defer func() {
if len(ecmd.ExtraFiles) > 0 {
ecmd.ExtraFiles[len(ecmd.ExtraFiles)-1].Close()
}
}()
go msh.RunPtyReadLoop(cmdPty)
if remoteCopy.SSHOpts.SSHPassword != "" {
go msh.WaitAndSendPassword(remoteCopy.SSHOpts.SSHPassword)
}
var makeClientCtx context.Context
var makeClientCancelFn context.CancelFunc
msh.WithLock(func() {
deadlineTime := time.Now().Add(RemoteConnectTimeout)
makeClientCtx, makeClientCancelFn = context.WithDeadline(context.Background(), deadlineTime)
defer makeClientCancelFn()
msh.Err = nil
msh.ErrNoInitPk = false
msh.Status = StatusConnecting
msh.MakeClientCancelFn = makeClientCancelFn
msh.MakeClientDeadline = &deadlineTime
go msh.NotifyRemoteUpdate()
})
go msh.watchClientDeadlineTime()
cproc, err := shexec.MakeClientProc(makeClientCtx, shexec.CmdWrap{Cmd: ecmd})
msh.WithLock(func() {
msh.MakeClientCancelFn = nil
msh.MakeClientDeadline = nil
msh.StateMap.Clear()
// no notify here, because we'll call notify in either case below
})
if err == context.DeadlineExceeded {
msh.WriteToPtyBuffer("*connect timeout\n")
msh.setErrorStatus(errors.New("connect timeout"))
return
} else if err == context.Canceled {
msh.WriteToPtyBuffer("*forced disconnection\n")
msh.WithLock(func() {
msh.Status = StatusDisconnected
go msh.NotifyRemoteUpdate()
})
return
} else if serr, ok := err.(shexec.WaveshellLaunchError); ok {
msh.WithLock(func() {
msh.UName = serr.InitPk.UName
if semver.Compare(serr.InitPk.Version, scbase.MShellVersion) < 0 {
// only set NeedsMShellUpgrade if we got an InitPk
msh.NeedsMShellUpgrade = true
}
msh.InitPkShellType = serr.InitPk.Shell
})
msh.WriteToPtyBuffer("*error, %s\n", serr.Error())
msh.setErrorStatus(serr)
go msh.tryAutoInstall()
return
} else if err != nil {
msh.WriteToPtyBuffer("*error, %s\n", serr.Error())
msh.setErrorStatus(err)
return
}
msh.updateRemoteStateVars(context.Background(), msh.RemoteId, cproc.InitPk)
msh.WithLock(func() {
msh.ServerProc = cproc
msh.Status = StatusConnected
})
go func() {
exitErr := cproc.Cmd.Wait()
exitCode := shexec.GetExitCode(exitErr)
msh.WithLock(func() {
if msh.Status == StatusConnected || msh.Status == StatusConnecting {
msh.Status = StatusDisconnected
go msh.NotifyRemoteUpdate()
}
})
msh.WriteToPtyBuffer("*disconnected exitcode=%d\n", exitCode)
}()
go msh.ProcessPackets()
msh.initActiveShells()
go msh.NotifyRemoteUpdate()
}
func (msh *MShellProc) initActiveShells() {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
+1
View File
@@ -230,6 +230,7 @@ func promptChallengeQuestion(connCtx context.Context, question string, echo bool
QueryText: queryText,
Markdown: true,
Title: "Keyboard Interactive Authentication",
PublicText: echo,
}
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
if err != nil {
+1
View File
@@ -22,6 +22,7 @@ type UserInputRequestType struct {
Markdown bool `json:"markdown"`
TimeoutMs int `json:"timeoutms"`
CheckBoxMsg string `json:"checkboxmsg"`
PublicText bool `json:"publictext"`
}
func (*UserInputRequestType) GetType() string {