mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
SSH UI Quick Fixes (#408)
* fix: set golbal ssh config to correct path This adds the missing "etc" directory to the path for the global config file. * chore: update auth mode tooltip This just changes the text to be slightly more accurate to the current behavior. * feat: add box to disable waveshell install modal This hooks in to the existing don't show this again code that pops up when creating a modal. * refactor: remove install modal in remote creation There used to be a modal that popped up while installing a remote that informed the user that waveshell gets installed on their remote. Since we have a new modal that pops up at the time of install, the older modal can be removed. * fix: allow user to cancel ssh dial The new ssh code broke dial for invalid urls since the context did not cancel the dial or any associated user input. This change reconnects the context along with the context for installing waveshell. * style: widen the rconndetail modal The rconndetail modal is currently narrower than the xtermjs element which results in awkward scrolling if a line is long. This change makes the width auto so it can size itself as needed. * add a max-width for safety
This commit is contained in:
parent
0b9834171d
commit
2a5857bc3d
@ -13,7 +13,6 @@ export { NumberField } from "./numberfield";
|
||||
export { PasswordField } from "./passwordfield";
|
||||
export { ResizableSidebar } from "./resizablesidebar";
|
||||
export { SettingsError } from "./settingserror";
|
||||
export { ShowWaveShellInstallPrompt } from "./showwaveshellinstallprompt";
|
||||
export { Status } from "./status";
|
||||
export { TextField } from "./textfield";
|
||||
export { Toggle } from "./toggle";
|
||||
|
@ -1,28 +0,0 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { GlobalModel } from "@/models";
|
||||
import * as appconst from "@/app/appconst";
|
||||
|
||||
function ShowWaveShellInstallPrompt(callbackFn: () => void) {
|
||||
let message: string = `
|
||||
In order to use Wave's advanced features like unified history and persistent sessions, Wave installs a small, open-source helper program called WaveShell on your remote machine. WaveShell does not open any external ports and only communicates with your *local* Wave terminal instance over ssh. For more information please see [the docs](https://docs.waveterm.dev/reference/waveshell).
|
||||
`;
|
||||
message = message.trim();
|
||||
let prtn = GlobalModel.showAlert({
|
||||
message: message,
|
||||
confirm: true,
|
||||
markdown: true,
|
||||
confirmflag: appconst.ConfirmKey_HideShellPrompt,
|
||||
});
|
||||
prtn.then((confirm) => {
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
if (callbackFn) {
|
||||
callbackFn();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { ShowWaveShellInstallPrompt };
|
@ -7,16 +7,7 @@ import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "@/models";
|
||||
import {
|
||||
Modal,
|
||||
TextField,
|
||||
NumberField,
|
||||
InputDecoration,
|
||||
Dropdown,
|
||||
PasswordField,
|
||||
Tooltip,
|
||||
ShowWaveShellInstallPrompt,
|
||||
} from "@/elements";
|
||||
import { Modal, TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip } from "@/elements";
|
||||
import * as util from "@/util/util";
|
||||
|
||||
import "./createremoteconn.less";
|
||||
@ -73,12 +64,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleOk(): void {
|
||||
ShowWaveShellInstallPrompt(this.submitRemote);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
submitRemote(): void {
|
||||
handleSubmitRemote(): void {
|
||||
mobx.action(() => {
|
||||
this.errorStr.set(null);
|
||||
})();
|
||||
@ -275,7 +261,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
{ value: "none", label: "none" },
|
||||
{ value: "key", label: "key" },
|
||||
{ value: "password", label: "password" },
|
||||
{ value: "key+password", label: "key+password" },
|
||||
{ value: "key+password", label: "key+passphrase" },
|
||||
]}
|
||||
value={this.tempAuthMode.get()}
|
||||
onChange={(val: string) => {
|
||||
@ -288,17 +274,18 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
message={
|
||||
<ul>
|
||||
<li>
|
||||
<b>none</b> - no authentication, or authentication is already
|
||||
configured in your ssh config.
|
||||
<b>none</b> - no authentication details are stored.
|
||||
</li>
|
||||
<li>
|
||||
<b>key</b> - use a private key.
|
||||
<b>key</b> - provide a custom private key for authentication.
|
||||
</li>
|
||||
<li>
|
||||
<b>password</b> - use a password.
|
||||
<b>password</b> - provide a password (to save) for
|
||||
authentication.
|
||||
</li>
|
||||
<li>
|
||||
<b>key+password</b> - use a key with a passphrase.
|
||||
<b>key+passphrase</b> - provide a custom private key with a
|
||||
passphrase (to save) for authentication.
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
@ -374,7 +361,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
<div className="settings-field settings-error">Error: {this.getErrorStr()}</div>
|
||||
</If>
|
||||
</div>
|
||||
<Modal.Footer onCancel={this.model.closeModal} onOk={this.handleOk} okLabel="Connect" />
|
||||
<Modal.Footer onCancel={this.model.closeModal} onOk={this.handleSubmitRemote} okLabel="Connect" />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -3,13 +3,17 @@
|
||||
|
||||
.wave-modal-content {
|
||||
.wave-modal-body {
|
||||
padding: 20px 20px;
|
||||
padding: 0px 20px 0px 20px;
|
||||
|
||||
.wave-modal-dialog {
|
||||
padding: 20px 0px 20px 0px;
|
||||
|
||||
.userinput-query {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
|
@ -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 } from "@/elements";
|
||||
import { Modal, PasswordField, Markdown, Checkbox } from "@/elements";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
|
||||
|
||||
import "./userinput.less";
|
||||
@ -9,6 +9,7 @@ import "./userinput.less";
|
||||
export const UserInputModal = (userInputRequest: UserInputRequest) => {
|
||||
const [responseText, setResponseText] = React.useState("");
|
||||
const [countdown, setCountdown] = React.useState(Math.floor(userInputRequest.timeoutms / 1000));
|
||||
const checkboxStatus = React.useRef(false);
|
||||
|
||||
const handleSendCancel = React.useCallback(() => {
|
||||
GlobalModel.sendUserInput({
|
||||
@ -24,16 +25,19 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
|
||||
type: "userinputresp",
|
||||
requestid: userInputRequest.requestid,
|
||||
text: responseText,
|
||||
checkboxstat: checkboxStatus.current,
|
||||
});
|
||||
GlobalModel.remotesModel.closeModal();
|
||||
}, [responseText, userInputRequest]);
|
||||
|
||||
const handleSendConfirm = React.useCallback(
|
||||
(response: boolean) => {
|
||||
console.log(`checkbox ${checkboxStatus}\n\n`);
|
||||
GlobalModel.sendUserInput({
|
||||
type: "userinputresp",
|
||||
requestid: userInputRequest.requestid,
|
||||
confirm: response,
|
||||
checkboxstat: checkboxStatus.current,
|
||||
});
|
||||
GlobalModel.remotesModel.closeModal();
|
||||
},
|
||||
@ -71,6 +75,7 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
|
||||
<Modal className="userinput-modal">
|
||||
<Modal.Header onClose={handleSendCancel} title={userInputRequest.title + ` (${countdown}s)`} />
|
||||
<div className="wave-modal-body">
|
||||
<div className="wave-modal-dialog">
|
||||
<div className="userinput-query">
|
||||
<If condition={userInputRequest.markdown}>
|
||||
<Markdown text={userInputRequest.querytext} extraClassName="bottom-margin" />
|
||||
@ -89,6 +94,14 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
|
||||
</When>
|
||||
</Choose>
|
||||
</div>
|
||||
<If condition={userInputRequest.checkboxmsg != ""}>
|
||||
<Checkbox
|
||||
onChange={() => (checkboxStatus.current = !checkboxStatus.current)}
|
||||
label={userInputRequest.checkboxmsg}
|
||||
className="checkbox-text"
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
<Choose>
|
||||
<When condition={userInputRequest.responsetype == "text"}>
|
||||
<Modal.Footer onCancel={handleSendCancel} onOk={handleSendText} okLabel="Continue" />
|
||||
|
@ -1,5 +1,6 @@
|
||||
.rconndetail-modal {
|
||||
width: 631px;
|
||||
width: auto;
|
||||
max-width: 80vw;
|
||||
min-height: 565px;
|
||||
|
||||
.wave-modal-content {
|
||||
|
@ -8,7 +8,7 @@ import { boundMethod } from "autobind-decorator";
|
||||
import { If, For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, RemotesModel, GlobalCommandRunner } from "@/models";
|
||||
import { Button, Status, ShowWaveShellInstallPrompt } from "@/common/elements";
|
||||
import { Button, Status } from "@/common/elements";
|
||||
import * as util from "@/util/util";
|
||||
|
||||
import "./connections.less";
|
||||
@ -71,14 +71,9 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered
|
||||
GlobalModel.remotesModel.openAddModal({ remoteedit: true });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
importSshConfig(): void {
|
||||
GlobalCommandRunner.importSshConfig();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleImportSshConfig(): void {
|
||||
ShowWaveShellInstallPrompt(this.importSshConfig);
|
||||
GlobalCommandRunner.importSshConfig();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
|
2
src/types/custom.d.ts
vendored
2
src/types/custom.d.ts
vendored
@ -666,6 +666,7 @@ declare global {
|
||||
title: string;
|
||||
markdown: boolean;
|
||||
timeoutms: number;
|
||||
checkboxmsg: string;
|
||||
};
|
||||
|
||||
type UserInputResponsePacket = {
|
||||
@ -674,6 +675,7 @@ declare global {
|
||||
text?: string;
|
||||
confirm?: boolean;
|
||||
errormsg?: string;
|
||||
checkboxstat?: boolean;
|
||||
};
|
||||
|
||||
type RenderModeType = "normal" | "collapsed" | "expanded";
|
||||
|
@ -2202,7 +2202,7 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
|
||||
func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
|
||||
home := base.GetHomeDir()
|
||||
localConfig := filepath.Join(home, ".ssh", "config")
|
||||
systemConfig := filepath.Join("/", "ssh", "config")
|
||||
systemConfig := filepath.Join("/etc", "ssh", "config")
|
||||
sshConfigFiles := []string{localConfig, systemConfig}
|
||||
ssh_config.ReloadConfigs()
|
||||
hostPatterns, hostPatternsErr := resolveSshConfigPatterns(sshConfigFiles)
|
||||
|
@ -1202,14 +1202,54 @@ func (msh *MShellProc) RunInstall(autoInstall bool) {
|
||||
msh.WriteToPtyBuffer("*error: cannot install on archived remote\n")
|
||||
return
|
||||
}
|
||||
if autoInstall {
|
||||
|
||||
var makeClientCtx context.Context
|
||||
var makeClientCancelFn context.CancelFunc
|
||||
msh.WithLock(func() {
|
||||
makeClientCtx, makeClientCancelFn = context.WithCancel(context.Background())
|
||||
msh.MakeClientCancelFn = makeClientCancelFn
|
||||
msh.MakeClientDeadline = nil
|
||||
go msh.NotifyRemoteUpdate()
|
||||
})
|
||||
defer makeClientCancelFn()
|
||||
clientData, err := sstore.EnsureClientData(makeClientCtx)
|
||||
if err != nil {
|
||||
msh.WriteToPtyBuffer("*error: cannot obtain client data: %v", err)
|
||||
return
|
||||
}
|
||||
hideShellPrompt := clientData.ClientOpts.ConfirmFlags["hideshellprompt"]
|
||||
baseStatus := msh.GetStatus()
|
||||
|
||||
if baseStatus == StatusConnected {
|
||||
ctx, cancelFn := context.WithTimeout(makeClientCtx, 60*time.Second)
|
||||
defer cancelFn()
|
||||
request := &userinput.UserInputRequestType{
|
||||
ResponseType: "confirm",
|
||||
QueryText: "Waveshell is running on your connection and must be restarted to re-install. Would you like to continue?",
|
||||
Title: "Restart Waveshell",
|
||||
}
|
||||
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
|
||||
if err != nil {
|
||||
if err == context.Canceled {
|
||||
msh.WriteToPtyBuffer("installation canceled by user\n")
|
||||
} else {
|
||||
msh.WriteToPtyBuffer("timed out waiting for user input\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
if !response.Confirm {
|
||||
msh.WriteToPtyBuffer("installation canceled by user\n")
|
||||
return
|
||||
}
|
||||
} else if !hideShellPrompt {
|
||||
ctx, cancelFn := context.WithTimeout(makeClientCtx, 60*time.Second)
|
||||
defer cancelFn()
|
||||
request := &userinput.UserInputRequestType{
|
||||
ResponseType: "confirm",
|
||||
QueryText: "Waveshell must be reinstalled on the connection to continue. Would you like to install it?",
|
||||
Title: "Install Waveshell",
|
||||
CheckBoxMsg: "Don't show me this again",
|
||||
}
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancelFn()
|
||||
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
|
||||
if err != nil {
|
||||
var errMsg error
|
||||
@ -1234,29 +1274,25 @@ func (msh *MShellProc) RunInstall(autoInstall bool) {
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
baseStatus := msh.GetStatus()
|
||||
if baseStatus == StatusConnected {
|
||||
request := &userinput.UserInputRequestType{
|
||||
ResponseType: "confirm",
|
||||
QueryText: "Waveshell is running on your connection and must be restarted to re-install. Would you like to continue?",
|
||||
Title: "Restart Waveshell",
|
||||
}
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancelFn()
|
||||
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
|
||||
if response.CheckboxStat {
|
||||
clientData.ClientOpts.ConfirmFlags["hideshellprompt"] = true
|
||||
err = sstore.SetClientOpts(makeClientCtx, clientData.ClientOpts)
|
||||
if err != nil {
|
||||
if err == context.Canceled {
|
||||
msh.WriteToPtyBuffer("installation canceled by user\n")
|
||||
} else {
|
||||
msh.WriteToPtyBuffer("timed out waiting for user input\n")
|
||||
}
|
||||
msh.WriteToPtyBuffer("*error, %s\n", err)
|
||||
msh.setErrorStatus(err)
|
||||
return
|
||||
}
|
||||
if !response.Confirm {
|
||||
msh.WriteToPtyBuffer("installation canceled by user\n")
|
||||
|
||||
//reload updated clientdata before sending
|
||||
clientData, err = sstore.EnsureClientData(makeClientCtx)
|
||||
if err != nil {
|
||||
msh.WriteToPtyBuffer("*error, %s\n", err)
|
||||
msh.setErrorStatus(err)
|
||||
return
|
||||
}
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(*clientData)
|
||||
}
|
||||
}
|
||||
curStatus := msh.GetInstallStatus()
|
||||
if curStatus == StatusConnecting {
|
||||
@ -1267,14 +1303,14 @@ func (msh *MShellProc) RunInstall(autoInstall bool) {
|
||||
msh.WriteToPtyBuffer("*error: cannot install on a local remote\n")
|
||||
return
|
||||
}
|
||||
_, err := shellapi.MakeShellApi(packet.ShellType_bash)
|
||||
_, err = shellapi.MakeShellApi(packet.ShellType_bash)
|
||||
if err != nil {
|
||||
msh.WriteToPtyBuffer("*error: %v\n", err)
|
||||
return
|
||||
}
|
||||
if msh.Client == nil {
|
||||
remoteDisplayName := fmt.Sprintf("%s [%s]", remoteCopy.RemoteAlias, remoteCopy.RemoteCanonicalName)
|
||||
client, err := ConnectToClient(remoteCopy.SSHOpts, remoteDisplayName)
|
||||
client, err := ConnectToClient(makeClientCtx, remoteCopy.SSHOpts, remoteDisplayName)
|
||||
if err != nil {
|
||||
statusErr := fmt.Errorf("ssh cannot connect to client: %w", err)
|
||||
msh.setInstallErrorStatus(statusErr)
|
||||
@ -1481,7 +1517,7 @@ func (msh *MShellProc) getActiveShellTypes(ctx context.Context) ([]string, error
|
||||
return utilfn.CombineStrArrays(rtn, activeShells), nil
|
||||
}
|
||||
|
||||
func (msh *MShellProc) createWaveshellSession(remoteCopy sstore.RemoteType) (shexec.ConnInterface, error) {
|
||||
func (msh *MShellProc) createWaveshellSession(clientCtx context.Context, remoteCopy sstore.RemoteType) (shexec.ConnInterface, error) {
|
||||
msh.WithLock(func() {
|
||||
msh.Err = nil
|
||||
msh.ErrNoInitPk = false
|
||||
@ -1510,7 +1546,7 @@ func (msh *MShellProc) createWaveshellSession(remoteCopy sstore.RemoteType) (she
|
||||
wsSession = shexec.CmdWrap{Cmd: ecmd}
|
||||
} else if msh.Client == nil {
|
||||
remoteDisplayName := fmt.Sprintf("%s [%s]", remoteCopy.RemoteAlias, remoteCopy.RemoteCanonicalName)
|
||||
client, err := ConnectToClient(remoteCopy.SSHOpts, remoteDisplayName)
|
||||
client, err := ConnectToClient(clientCtx, remoteCopy.SSHOpts, remoteDisplayName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh cannot connect to client: %w", err)
|
||||
}
|
||||
@ -1559,16 +1595,6 @@ func (NewLauncher) Launch(msh *MShellProc, interactive bool) {
|
||||
msh.WriteToPtyBuffer("remote is trying to install, cancel install before trying to connect again\n")
|
||||
return
|
||||
}
|
||||
msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName)
|
||||
wsSession, err := msh.createWaveshellSession(remoteCopy)
|
||||
if err != nil {
|
||||
msh.WriteToPtyBuffer("*error, %s\n", err.Error())
|
||||
msh.setErrorStatus(err)
|
||||
msh.WithLock(func() {
|
||||
msh.Client = nil
|
||||
})
|
||||
return
|
||||
}
|
||||
var makeClientCtx context.Context
|
||||
var makeClientCancelFn context.CancelFunc
|
||||
msh.WithLock(func() {
|
||||
@ -1578,6 +1604,16 @@ func (NewLauncher) Launch(msh *MShellProc, interactive bool) {
|
||||
go msh.NotifyRemoteUpdate()
|
||||
})
|
||||
defer makeClientCancelFn()
|
||||
msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName)
|
||||
wsSession, err := msh.createWaveshellSession(makeClientCtx, remoteCopy)
|
||||
if err != nil {
|
||||
msh.WriteToPtyBuffer("*error, %s\n", err.Error())
|
||||
msh.setErrorStatus(err)
|
||||
msh.WithLock(func() {
|
||||
msh.Client = nil
|
||||
})
|
||||
return
|
||||
}
|
||||
cproc, err := shexec.MakeClientProc(makeClientCtx, wsSession)
|
||||
msh.WithLock(func() {
|
||||
msh.MakeClientCancelFn = nil
|
||||
|
@ -65,7 +65,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(sshKeywords *SshKeywords, passphrase string) func() ([]ssh.Signer, error) {
|
||||
func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords, passphrase string) func() ([]ssh.Signer, error) {
|
||||
var identityFiles []string
|
||||
existingKeys := make(map[string][]byte)
|
||||
|
||||
@ -124,7 +124,7 @@ func createPublicKeyCallback(sshKeywords *SshKeywords, passphrase string) func()
|
||||
QueryText: fmt.Sprintf("Enter passphrase for the SSH key: %s", identityFile),
|
||||
Title: "Publickey Auth + Passphrase",
|
||||
}
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second)
|
||||
defer cancelFn()
|
||||
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
|
||||
if err != nil {
|
||||
@ -150,11 +150,11 @@ func createDefaultPasswordCallbackPrompt(password string) func() (secret string,
|
||||
}
|
||||
}
|
||||
|
||||
func createInteractivePasswordCallbackPrompt(remoteDisplayName string) func() (secret string, err error) {
|
||||
func createInteractivePasswordCallbackPrompt(connCtx context.Context, remoteDisplayName string) func() (secret string, err error) {
|
||||
return func() (secret string, err error) {
|
||||
// limited to 15 seconds for some reason. this should be investigated more
|
||||
// in the future
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second)
|
||||
defer cancelFn()
|
||||
queryText := fmt.Sprintf(
|
||||
"Password Authentication requested from connection \n"+
|
||||
@ -174,13 +174,13 @@ func createInteractivePasswordCallbackPrompt(remoteDisplayName string) func() (s
|
||||
}
|
||||
}
|
||||
|
||||
func createCombinedPasswordCallbackPrompt(password string, remoteDisplayName string) func() (secret string, err error) {
|
||||
func createCombinedPasswordCallbackPrompt(connCtx context.Context, password string, remoteDisplayName string) func() (secret string, err error) {
|
||||
var once sync.Once
|
||||
return func() (secret string, err error) {
|
||||
var prompt func() (secret string, err error)
|
||||
once.Do(func() { prompt = createDefaultPasswordCallbackPrompt(password) })
|
||||
if prompt == nil {
|
||||
prompt = createInteractivePasswordCallbackPrompt(remoteDisplayName)
|
||||
prompt = createInteractivePasswordCallbackPrompt(connCtx, remoteDisplayName)
|
||||
}
|
||||
return prompt()
|
||||
}
|
||||
@ -199,14 +199,14 @@ func createNaiveKbdInteractiveChallenge(password string) func(name, instruction
|
||||
}
|
||||
}
|
||||
|
||||
func createInteractiveKbdInteractiveChallenge(remoteName string) func(name, instruction string, questions []string, echos []bool) (answers []string, err error) {
|
||||
func createInteractiveKbdInteractiveChallenge(connCtx context.Context, remoteName string) func(name, instruction string, questions []string, echos []bool) (answers []string, err error) {
|
||||
return func(name, instruction string, questions []string, echos []bool) (answers []string, err error) {
|
||||
if len(questions) != len(echos) {
|
||||
return nil, fmt.Errorf("bad response from server: questions has len %d, echos has len %d", len(questions), len(echos))
|
||||
}
|
||||
for i, question := range questions {
|
||||
echo := echos[i]
|
||||
answer, err := promptChallengeQuestion(question, echo, remoteName)
|
||||
answer, err := promptChallengeQuestion(connCtx, question, echo, remoteName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -216,10 +216,10 @@ func createInteractiveKbdInteractiveChallenge(remoteName string) func(name, inst
|
||||
}
|
||||
}
|
||||
|
||||
func promptChallengeQuestion(question string, echo bool, remoteName string) (answer string, err error) {
|
||||
func promptChallengeQuestion(connCtx context.Context, question string, echo bool, remoteName string) (answer string, err error) {
|
||||
// limited to 15 seconds for some reason. this should be investigated more
|
||||
// in the future
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second)
|
||||
defer cancelFn()
|
||||
queryText := fmt.Sprintf(
|
||||
"Keyboard Interactive Authentication requested from connection \n"+
|
||||
@ -238,13 +238,13 @@ func promptChallengeQuestion(question string, echo bool, remoteName string) (ans
|
||||
return response.Text, nil
|
||||
}
|
||||
|
||||
func createCombinedKbdInteractiveChallenge(password string, remoteName string) ssh.KeyboardInteractiveChallenge {
|
||||
func createCombinedKbdInteractiveChallenge(connCtx context.Context, password string, remoteName string) ssh.KeyboardInteractiveChallenge {
|
||||
var once sync.Once
|
||||
return func(name, instruction string, questions []string, echos []bool) (answers []string, err error) {
|
||||
var challenge ssh.KeyboardInteractiveChallenge
|
||||
once.Do(func() { challenge = createNaiveKbdInteractiveChallenge(password) })
|
||||
if challenge == nil {
|
||||
challenge = createInteractiveKbdInteractiveChallenge(remoteName)
|
||||
challenge = createInteractiveKbdInteractiveChallenge(connCtx, remoteName)
|
||||
}
|
||||
return challenge(name, instruction, questions, echos)
|
||||
}
|
||||
@ -505,7 +505,20 @@ func createHostKeyCallback(opts *sstore.SSHOpts) (ssh.HostKeyCallback, error) {
|
||||
return waveHostKeyCallback, nil
|
||||
}
|
||||
|
||||
func ConnectToClient(opts *sstore.SSHOpts, remoteDisplayName string) (*ssh.Client, error) {
|
||||
func DialContext(ctx context.Context, network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
|
||||
d := net.Dialer{Timeout: config.Timeout}
|
||||
conn, err := d.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -516,9 +529,9 @@ func ConnectToClient(opts *sstore.SSHOpts, remoteDisplayName string) (*ssh.Clien
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicKeyCallback := ssh.PublicKeysCallback(createPublicKeyCallback(sshKeywords, opts.SSHPassword))
|
||||
keyboardInteractive := ssh.KeyboardInteractive(createCombinedKbdInteractiveChallenge(opts.SSHPassword, remoteDisplayName))
|
||||
passwordCallback := ssh.PasswordCallback(createCombinedPasswordCallbackPrompt(opts.SSHPassword, remoteDisplayName))
|
||||
publicKeyCallback := ssh.PublicKeysCallback(createPublicKeyCallback(connCtx, sshKeywords, opts.SSHPassword))
|
||||
keyboardInteractive := ssh.KeyboardInteractive(createCombinedKbdInteractiveChallenge(connCtx, opts.SSHPassword, remoteDisplayName))
|
||||
passwordCallback := ssh.PasswordCallback(createCombinedPasswordCallbackPrompt(connCtx, opts.SSHPassword, remoteDisplayName))
|
||||
|
||||
// batch mode turns off interactive input. this means the number of
|
||||
// attemtps must drop to 1 with this setup
|
||||
@ -566,7 +579,7 @@ func ConnectToClient(opts *sstore.SSHOpts, remoteDisplayName string) (*ssh.Clien
|
||||
HostKeyCallback: hostKeyCallback,
|
||||
}
|
||||
networkAddr := sshKeywords.HostName + ":" + sshKeywords.Port
|
||||
return ssh.Dial("tcp", networkAddr, clientConfig)
|
||||
return DialContext(connCtx, "tcp", networkAddr, clientConfig)
|
||||
}
|
||||
|
||||
type SshKeywords struct {
|
||||
|
@ -21,6 +21,7 @@ type UserInputRequestType struct {
|
||||
Title string `json:"title"`
|
||||
Markdown bool `json:"markdown"`
|
||||
TimeoutMs int `json:"timeoutms"`
|
||||
CheckBoxMsg string `json:"checkboxmsg"`
|
||||
}
|
||||
|
||||
func (*UserInputRequestType) GetType() string {
|
||||
@ -44,6 +45,7 @@ type UserInputResponsePacketType struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
Confirm bool `json:"confirm,omitempty"`
|
||||
ErrorMsg string `json:"errormsg,omitempty"`
|
||||
CheckboxStat bool `json:"checkboxstat,omitempty"`
|
||||
}
|
||||
|
||||
func (*UserInputResponsePacketType) GetType() string {
|
||||
|
Loading…
Reference in New Issue
Block a user