From 2a5857bc3d31551a8db84515452e7d8b92e9cc12 Mon Sep 17 00:00:00 2001
From: Sylvie Crowe <107814465+oneirocosm@users.noreply.github.com>
Date: Thu, 7 Mar 2024 22:37:00 -0800
Subject: [PATCH] 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
---
src/app/common/elements/index.tsx | 1 -
.../elements/showwaveshellinstallprompt.tsx | 28 -----
src/app/common/modals/createremoteconn.tsx | 33 ++----
src/app/common/modals/userinput.less | 10 +-
src/app/common/modals/userinput.tsx | 47 +++++---
.../common/modals/viewremoteconndetail.less | 3 +-
src/app/connections/connections.tsx | 9 +-
src/types/custom.d.ts | 2 +
wavesrv/pkg/cmdrunner/cmdrunner.go | 2 +-
wavesrv/pkg/remote/remote.go | 112 ++++++++++++------
wavesrv/pkg/remote/sshclient.go | 47 +++++---
wavesrv/pkg/userinput/userinput.go | 12 +-
12 files changed, 165 insertions(+), 141 deletions(-)
delete mode 100644 src/app/common/elements/showwaveshellinstallprompt.tsx
diff --git a/src/app/common/elements/index.tsx b/src/app/common/elements/index.tsx
index 30895855a..7096d3284 100644
--- a/src/app/common/elements/index.tsx
+++ b/src/app/common/elements/index.tsx
@@ -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";
diff --git a/src/app/common/elements/showwaveshellinstallprompt.tsx b/src/app/common/elements/showwaveshellinstallprompt.tsx
deleted file mode 100644
index 647cd74d4..000000000
--- a/src/app/common/elements/showwaveshellinstallprompt.tsx
+++ /dev/null
@@ -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 };
diff --git a/src/app/common/modals/createremoteconn.tsx b/src/app/common/modals/createremoteconn.tsx
index 297e67973..56f4e6ab5 100644
--- a/src/app/common/modals/createremoteconn.tsx
+++ b/src/app/common/modals/createremoteconn.tsx
@@ -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={
-
- none - no authentication, or authentication is already
- configured in your ssh config.
+ none - no authentication details are stored.
-
- key - use a private key.
+ key - provide a custom private key for authentication.
-
- password - use a password.
+ password - provide a password (to save) for
+ authentication.
-
- key+password - use a key with a passphrase.
+ key+passphrase - provide a custom private key with a
+ passphrase (to save) for authentication.
}
@@ -374,7 +361,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
Error: {this.getErrorStr()}
-
+
);
}
diff --git a/src/app/common/modals/userinput.less b/src/app/common/modals/userinput.less
index f86c11ca0..fd6b3e6fa 100644
--- a/src/app/common/modals/userinput.less
+++ b/src/app/common/modals/userinput.less
@@ -3,10 +3,14 @@
.wave-modal-content {
.wave-modal-body {
- padding: 20px 20px;
+ padding: 0px 20px 0px 20px;
- .userinput-query {
- margin-bottom: 10px;
+ .wave-modal-dialog {
+ padding: 20px 0px 20px 0px;
+
+ .userinput-query {
+ margin-bottom: 10px;
+ }
}
}
}
diff --git a/src/app/common/modals/userinput.tsx b/src/app/common/modals/userinput.tsx
index 47a3c55a8..19e0bbee0 100644
--- a/src/app/common/modals/userinput.tsx
+++ b/src/app/common/modals/userinput.tsx
@@ -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,23 +75,32 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
-
-
-
-
-
{userInputRequest.querytext}
+
+
+
+
+
+ {userInputRequest.querytext}
+
+
+
+ handleTextKeyDown(e)}
+ />
+
+
-
-
- handleTextKeyDown(e)}
- />
-
-
+
+ (checkboxStatus.current = !checkboxStatus.current)}
+ label={userInputRequest.checkboxmsg}
+ className="checkbox-text"
+ />
+
diff --git a/src/app/common/modals/viewremoteconndetail.less b/src/app/common/modals/viewremoteconndetail.less
index aee472d89..44d474178 100644
--- a/src/app/common/modals/viewremoteconndetail.less
+++ b/src/app/common/modals/viewremoteconndetail.less
@@ -1,5 +1,6 @@
.rconndetail-modal {
- width: 631px;
+ width: auto;
+ max-width: 80vw;
min-height: 565px;
.wave-modal-content {
diff --git a/src/app/connections/connections.tsx b/src/app/connections/connections.tsx
index f5c715b8f..9cc734620 100644
--- a/src/app/connections/connections.tsx
+++ b/src/app/connections/connections.tsx
@@ -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
diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts
index 0c5018b87..4395ca3bc 100644
--- a/src/types/custom.d.ts
+++ b/src/types/custom.d.ts
@@ -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";
diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go
index 9291203aa..3af7379ba 100644
--- a/wavesrv/pkg/cmdrunner/cmdrunner.go
+++ b/wavesrv/pkg/cmdrunner/cmdrunner.go
@@ -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)
diff --git a/wavesrv/pkg/remote/remote.go b/wavesrv/pkg/remote/remote.go
index 9d51f9d73..07f61dec9 100644
--- a/wavesrv/pkg/remote/remote.go
+++ b/wavesrv/pkg/remote/remote.go
@@ -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,28 +1274,24 @@ 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 err != nil {
- if err == context.Canceled {
- msh.WriteToPtyBuffer("installation canceled by user\n")
- } else {
- msh.WriteToPtyBuffer("timed out waiting for user input\n")
+ if response.CheckboxStat {
+ clientData.ClientOpts.ConfirmFlags["hideshellprompt"] = true
+ err = sstore.SetClientOpts(makeClientCtx, clientData.ClientOpts)
+ if err != nil {
+ msh.WriteToPtyBuffer("*error, %s\n", err)
+ msh.setErrorStatus(err)
+ return
}
- return
- }
- if !response.Confirm {
- msh.WriteToPtyBuffer("installation canceled by user\n")
- return
+
+ //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()
@@ -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
diff --git a/wavesrv/pkg/remote/sshclient.go b/wavesrv/pkg/remote/sshclient.go
index ed770ca27..7eff78ef3 100644
--- a/wavesrv/pkg/remote/sshclient.go
+++ b/wavesrv/pkg/remote/sshclient.go
@@ -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 {
diff --git a/wavesrv/pkg/userinput/userinput.go b/wavesrv/pkg/userinput/userinput.go
index 0ab0fe410..50212acb6 100644
--- a/wavesrv/pkg/userinput/userinput.go
+++ b/wavesrv/pkg/userinput/userinput.go
@@ -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 {
@@ -39,11 +40,12 @@ const UserInputResponsePacketStr = "userinputresp"
// An RpcResponse for user input requests
type UserInputResponsePacketType struct {
- Type string `json:"type"`
- RequestId string `json:"requestid"`
- Text string `json:"text,omitempty"`
- Confirm bool `json:"confirm,omitempty"`
- ErrorMsg string `json:"errormsg,omitempty"`
+ Type string `json:"type"`
+ RequestId string `json:"requestid"`
+ Text string `json:"text,omitempty"`
+ Confirm bool `json:"confirm,omitempty"`
+ ErrorMsg string `json:"errormsg,omitempty"`
+ CheckboxStat bool `json:"checkboxstat,omitempty"`
}
func (*UserInputResponsePacketType) GetType() string {