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
6 changed files with 54 additions and 184 deletions
+14 -5
View File
@@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { GlobalModel } from "@/models"; import { GlobalModel } from "@/models";
import { Choose, When, If } from "tsx-control-statements/components"; 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 { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import "./userinput.less"; import "./userinput.less";
@@ -82,8 +82,17 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
</If> </If>
<If condition={!userInputRequest.markdown}>{userInputRequest.querytext}</If> <If condition={!userInputRequest.markdown}>{userInputRequest.querytext}</If>
</div> </div>
<Choose> <If condition={userInputRequest.responsetype == "text"}>
<When 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 <PasswordField
onChange={setResponseText} onChange={setResponseText}
value={responseText} value={responseText}
@@ -91,8 +100,8 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
autoFocus={true} autoFocus={true}
onKeyDown={(e) => handleTextKeyDown(e)} onKeyDown={(e) => handleTextKeyDown(e)}
/> />
</When> </If>
</Choose> </If>
</div> </div>
<If condition={userInputRequest.checkboxmsg != ""}> <If condition={userInputRequest.checkboxmsg != ""}>
<Checkbox <Checkbox
+1
View File
@@ -667,6 +667,7 @@ declare global {
markdown: boolean; markdown: boolean;
timeoutms: number; timeoutms: number;
checkboxmsg: string; checkboxmsg: string;
publictext: boolean;
}; };
type UserInputResponsePacket = { type UserInputResponsePacket = {
+17 -12
View File
@@ -126,7 +126,7 @@ var SetVarScopes = []SetVarScope{
{ScopeName: "remote", VarNames: []string{}}, {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 remoteAliasRe = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
var genericNameRe = regexp.MustCompile("^[a-zA-Z][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_.:-]*$") 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") return nil, fmt.Errorf("invalid format of user@host argument")
} }
sudoStr, remoteUser, remoteHost, remotePortStr := m[1], m[2], m[3], m[4] sudoStr, remoteUser, remoteHost, remotePortStr := m[1], m[2], m[3], m[4]
remoteUser = strings.Trim(remoteUser, "@")
var uhPort int var uhPort int
if remotePortStr != "" { if remotePortStr != "" {
var err error 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) return nil, fmt.Errorf("invalid port argument, \"%d\" is not in the range of 1 to 65535", portVal)
} }
sshOpts.SSHPort = portVal sshOpts.SSHPort = portVal
canonicalName = remoteUser + "@" + remoteHost if remoteUser == "" {
canonicalName = remoteHost
} else {
canonicalName = remoteUser + "@" + remoteHost
}
if portVal != 0 && portVal != 22 { if portVal != 0 && portVal != 22 {
canonicalName = canonicalName + ":" + strconv.Itoa(portVal) canonicalName = canonicalName + ":" + strconv.Itoa(portVal)
} }
@@ -2136,17 +2141,17 @@ func createSshImportSummary(changeList map[string][]string) string {
func NewHostInfo(hostName string) (*HostInfoType, error) { func NewHostInfo(hostName string) (*HostInfoType, error) {
userName, _ := ssh_config.GetStrict(hostName, "User") userName, _ := ssh_config.GetStrict(hostName, "User")
if userName == "" { var canonicalName string
// we cannot store a remote with a missing user if userName != "" {
// in the current setup canonicalName = userName + "@" + hostName
return nil, fmt.Errorf("could not parse \"%s\" - no User in config\n", hostName) } else {
canonicalName = hostName
} }
canonicalName := userName + "@" + hostName
// check if user and host are okay // check if canonicalname is okay
m := userHostRe.FindStringSubmatch(canonicalName) m := userHostRe.FindStringSubmatch(canonicalName)
if m == nil || m[2] == "" || m[3] == "" { if m == nil {
return nil, fmt.Errorf("could not parse \"%s\" - %s did not fit user@host requirement\n", hostName, canonicalName) return nil, fmt.Errorf("could not parse \"%s\" - %s did not fit user@host requirement", hostName, canonicalName)
} }
portStr, _ := ssh_config.GetStrict(hostName, "Port") portStr, _ := ssh_config.GetStrict(hostName, "Port")
@@ -2157,10 +2162,10 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
portVal, err = strconv.Atoi(portStr) portVal, err = strconv.Atoi(portStr)
if err != nil { if err != nil {
// do not make assumptions about port if incorrectly configured // 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 { 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") identityFile, _ := ssh_config.GetStrict(hostName, "IdentityFile")
+20 -167
View File
@@ -15,6 +15,7 @@ import (
"os/exec" "os/exec"
"path" "path"
"regexp" "regexp"
"runtime/debug"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -44,8 +45,6 @@ import (
"golang.org/x/mod/semver" "golang.org/x/mod/semver"
) )
const UseSshLibrary = true
const RemoteTypeMShell = "mshell" const RemoteTypeMShell = "mshell"
const DefaultTerm = "xterm-256color" const DefaultTerm = "xterm-256color"
const DefaultMaxPtySize = 1024 * 1024 const DefaultMaxPtySize = 1024 * 1024
@@ -129,12 +128,6 @@ type pendingStateKey struct {
RemotePtr sstore.RemotePtrType 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 { type MShellProc struct {
Lock *sync.Mutex Lock *sync.Mutex
Remote *sstore.RemoteType Remote *sstore.RemoteType
@@ -163,7 +156,6 @@ type MShellProc struct {
RunningCmds map[base.CommandKey]*RunCmdType RunningCmds map[base.CommandKey]*RunCmdType
PendingStateCmds map[pendingStateKey]base.CommandKey // key=[remoteinstance name] 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 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 { func (msh *MShellProc) GetStatus() string {
msh.Lock.Lock() msh.Lock.Lock()
defer msh.Lock.Unlock() defer msh.Lock.Unlock()
@@ -711,14 +697,8 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc {
RunningCmds: make(map[base.CommandKey]*RunCmdType), RunningCmds: make(map[base.CommandKey]*RunCmdType),
PendingStateCmds: make(map[pendingStateKey]base.CommandKey), PendingStateCmds: make(map[pendingStateKey]base.CommandKey),
StateMap: server.MakeShellStateMap(), 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](), 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()) rtn.WriteToPtyBuffer("console for connection [%s]\n", r.GetName())
return rtn return rtn
@@ -1201,6 +1181,15 @@ func (msh *MShellProc) WaitAndSendPassword(pw string) {
} }
func (msh *MShellProc) RunInstall(autoInstall bool) { 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() remoteCopy := msh.GetRemoteCopy()
if remoteCopy.Archived { if remoteCopy.Archived {
msh.WriteToPtyBuffer("*error: cannot install on archived remote\n") msh.WriteToPtyBuffer("*error: cannot install on archived remote\n")
@@ -1574,12 +1563,16 @@ func (msh *MShellProc) createWaveshellSession(clientCtx context.Context, remoteC
return wsSession, nil return wsSession, nil
} }
// for conditional launch method based on ssh library in use func (msh *MShellProc) Launch(interactive bool) {
// remove once ssh library is stabilized defer func() {
type NewLauncher struct{} 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"+
// func (msh *MShellProc) LaunchNew(interactive bool) { "error:\n%v\n\nstack trace:\n%s", r, string(debug.Stack()))
func (NewLauncher) Launch(msh *MShellProc, interactive bool) { log.Printf("fatal error, %s\n", errMsg)
msh.WriteToPtyBuffer("*fatal error, %s\n", errMsg)
msh.setErrorStatus(errMsg)
}
}()
remoteCopy := msh.GetRemoteCopy() remoteCopy := msh.GetRemoteCopy()
if remoteCopy.Archived { if remoteCopy.Archived {
msh.WriteToPtyBuffer("cannot launch archived remote\n") msh.WriteToPtyBuffer("cannot launch archived remote\n")
@@ -1687,146 +1680,6 @@ func (NewLauncher) Launch(msh *MShellProc, interactive bool) {
go msh.NotifyRemoteUpdate() 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() { func (msh *MShellProc) initActiveShells() {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn() defer cancelFn()
+1
View File
@@ -230,6 +230,7 @@ func promptChallengeQuestion(connCtx context.Context, question string, echo bool
QueryText: queryText, QueryText: queryText,
Markdown: true, Markdown: true,
Title: "Keyboard Interactive Authentication", Title: "Keyboard Interactive Authentication",
PublicText: echo,
} }
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request) response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
if err != nil { if err != nil {
+1
View File
@@ -22,6 +22,7 @@ type UserInputRequestType struct {
Markdown bool `json:"markdown"` Markdown bool `json:"markdown"`
TimeoutMs int `json:"timeoutms"` TimeoutMs int `json:"timeoutms"`
CheckBoxMsg string `json:"checkboxmsg"` CheckBoxMsg string `json:"checkboxmsg"`
PublicText bool `json:"publictext"`
} }
func (*UserInputRequestType) GetType() string { func (*UserInputRequestType) GetType() string {