SSH without using WSH (#1355)

This commit is contained in:
Sylvie Crowe 2024-11-27 16:52:00 -08:00 committed by GitHub
parent c37d292224
commit 24103213aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 474 additions and 251 deletions

View File

@ -27,10 +27,8 @@ func GenerateWshClient() error {
"github.com/wavetermdev/waveterm/pkg/wshutil",
"github.com/wavetermdev/waveterm/pkg/wshrpc",
"github.com/wavetermdev/waveterm/pkg/waveobj",
"github.com/wavetermdev/waveterm/pkg/wconfig",
"github.com/wavetermdev/waveterm/pkg/wps",
"github.com/wavetermdev/waveterm/pkg/vdom",
"github.com/wavetermdev/waveterm/pkg/telemetry",
})
wshDeclMap := wshrpc.GenerateWshCommandDeclMap()
for _, key := range utilfn.GetOrderedMapKeys(wshDeclMap) {

View File

@ -113,7 +113,7 @@ func telemetryLoop() {
}
func panicTelemetryHandler() {
activity := telemetry.ActivityUpdate{NumPanics: 1}
activity := wshrpc.ActivityUpdate{NumPanics: 1}
err := telemetry.UpdateActivity(context.Background(), activity)
if err != nil {
log.Printf("error updating activity (panicTelemetryHandler): %v\n", err)
@ -137,7 +137,7 @@ func sendTelemetryWrapper() {
}
func beforeSendActivityUpdate(ctx context.Context) {
activity := telemetry.ActivityUpdate{}
activity := wshrpc.ActivityUpdate{}
activity.NumTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx)
activity.NumBlocks, _ = wstore.DBGetCount[*waveobj.Block](ctx)
activity.Blocks, _ = wstore.DBGetBlockViewCounts(ctx)
@ -153,7 +153,7 @@ func beforeSendActivityUpdate(ctx context.Context) {
func startupActivityUpdate() {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
activity := telemetry.ActivityUpdate{Startup: 1}
activity := wshrpc.ActivityUpdate{Startup: 1}
err := telemetry.UpdateActivity(ctx, activity) // set at least one record into activity (don't use go routine wrap here)
if err != nil {
log.Printf("error updating startup activity: %v\n", err)
@ -163,7 +163,7 @@ func startupActivityUpdate() {
func shutdownActivityUpdate() {
ctx, cancelFn := context.WithTimeout(context.Background(), 1*time.Second)
defer cancelFn()
activity := telemetry.ActivityUpdate{Shutdown: 1}
activity := wshrpc.ActivityUpdate{Shutdown: 1}
err := telemetry.UpdateActivity(ctx, activity) // do NOT use the go routine wrap here (this needs to be synchronous)
if err != nil {
log.Printf("error updating shutdown activity: %v\n", err)

View File

@ -167,7 +167,7 @@ func connConnectRun(cmd *cobra.Command, args []string) error {
if err := validateConnectionName(connName); err != nil {
return err
}
err := wshclient.ConnConnectCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
err := wshclient.ConnConnectCommand(RpcClient, wshrpc.ConnRequest{Host: connName}, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil {
return fmt.Errorf("connecting connection: %w", err)
}

View File

@ -7,7 +7,6 @@ import (
"fmt"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)
@ -34,7 +33,7 @@ func setConfigRun(cmd *cobra.Command, args []string) (rtnErr error) {
if err != nil {
return err
}
commandData := wconfig.MetaSettingsType{MetaMapType: meta}
commandData := wshrpc.MetaSettingsType{MetaMapType: meta}
err = wshclient.SetConfigCommand(RpcClient, commandData, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil {
return fmt.Errorf("setting config: %w", err)

View File

@ -12,6 +12,8 @@ import (
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)
var identityFiles []string
var sshCmd = &cobra.Command{
Use: "ssh",
Short: "connect this terminal to a remote host",
@ -21,6 +23,7 @@ var sshCmd = &cobra.Command{
}
func init() {
sshCmd.Flags().StringArrayVarP(&identityFiles, "identityfile", "i", []string{}, "add an identity file for publickey authentication")
rootCmd.AddCommand(sshCmd)
}
@ -34,6 +37,16 @@ func sshRun(cmd *cobra.Command, args []string) (rtnErr error) {
if blockId == "" {
return fmt.Errorf("cannot determine blockid (not in JWT)")
}
// first, make a connection independent of the block
connOpts := wshrpc.ConnRequest{
Host: sshArg,
Keywords: wshrpc.ConnKeywords{
SshIdentityFile: identityFiles,
},
}
wshclient.ConnConnectCommand(RpcClient, connOpts, nil)
// now, with that made, it will be straightforward to connect
data := wshrpc.CommandSetMetaData{
ORef: waveobj.MakeORef(waveobj.OType_Block, blockId),
Meta: map[string]any{

View File

@ -341,7 +341,7 @@ const ConnStatusOverlay = React.memo(
}, [width, connStatus, setShowError]);
const handleTryReconnect = React.useCallback(() => {
const prtn = RpcApi.ConnConnectCommand(TabRpcClient, connName, { timeout: 60000 });
const prtn = RpcApi.ConnConnectCommand(TabRpcClient, { host: connName }, { timeout: 60000 });
prtn.catch((e) => console.log("error reconnecting", connName, e));
}, [connName]);
@ -673,7 +673,11 @@ const ChangeConnectionBlockModal = React.memo(
label: `Reconnect to ${connStatus.connection}`,
value: "",
onSelect: async (_: string) => {
const prtn = RpcApi.ConnConnectCommand(TabRpcClient, connStatus.connection, { timeout: 60000 });
const prtn = RpcApi.ConnConnectCommand(
TabRpcClient,
{ host: connStatus.connection },
{ timeout: 60000 }
);
prtn.catch((e) => console.log("error reconnecting", connStatus.connection, e));
},
};

View File

@ -45,11 +45,17 @@
.userinput-checkbox-container {
display: flex;
align-items: center;
flex-direction: column;
gap: 6px;
.userinput-checkbox {
accent-color: var(--accent-color);
.userinput-checkbox-row {
display: flex;
align-items: center;
gap: 6px;
.userinput-checkbox {
accent-color: var(--accent-color);
}
}
}
}

View File

@ -15,7 +15,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
const [countdown, setCountdown] = useState(Math.floor(userInputRequest.timeoutms / 1000));
const checkboxRef = useRef<HTMLInputElement>();
const handleSendCancel = useCallback(() => {
const handleSendErrResponse = useCallback(() => {
UserInputService.SendUserInputResponse({
type: "userinputresp",
requestid: userInputRequest.requestid,
@ -29,20 +29,24 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
type: "userinputresp",
requestid: userInputRequest.requestid,
text: responseText,
checkboxstat: checkboxRef.current?.checked ?? false,
checkboxstat: checkboxRef?.current?.checked ?? false,
});
modalsModel.popModal();
}, [responseText, userInputRequest]);
console.log("bar");
const handleSendConfirm = useCallback(() => {
UserInputService.SendUserInputResponse({
type: "userinputresp",
requestid: userInputRequest.requestid,
confirm: true,
checkboxstat: checkboxRef.current?.checked ?? false,
});
modalsModel.popModal();
}, [userInputRequest]);
const handleSendConfirm = useCallback(
(response: boolean) => {
UserInputService.SendUserInputResponse({
type: "userinputresp",
requestid: userInputRequest.requestid,
confirm: response,
checkboxstat: checkboxRef?.current?.checked ?? false,
});
modalsModel.popModal();
},
[userInputRequest]
);
const handleSubmit = useCallback(() => {
switch (userInputRequest.responsetype) {
@ -50,15 +54,16 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
handleSendText();
break;
case "confirm":
handleSendConfirm();
handleSendConfirm(true);
break;
}
}, [handleSendConfirm, handleSendText, userInputRequest.responsetype]);
console.log("baz");
const handleKeyDown = useCallback(
(waveEvent: WaveKeyboardEvent): boolean => {
if (keyutil.checkKeyPressed(waveEvent, "Escape")) {
handleSendCancel();
handleSendErrResponse();
return;
}
if (keyutil.checkKeyPressed(waveEvent, "Enter")) {
@ -66,7 +71,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
return true;
}
},
[handleSendCancel, handleSubmit]
[handleSendErrResponse, handleSubmit]
);
const queryText = useMemo(() => {
@ -75,6 +80,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
}
return <span className="userinput-text">{userInputRequest.querytext}</span>;
}, [userInputRequest.markdown, userInputRequest.querytext]);
console.log("foobarbaz");
const inputBox = useMemo(() => {
if (userInputRequest.responsetype === "confirm") {
@ -92,6 +98,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
/>
);
}, [userInputRequest.responsetype, userInputRequest.publictext, responseText, handleKeyDown, setResponseText]);
console.log("mem1");
const optionalCheckbox = useMemo(() => {
if (userInputRequest.checkboxmsg == "") {
@ -99,22 +106,25 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
}
return (
<div className="userinput-checkbox-container">
<input
type="checkbox"
id={`uicheckbox-${userInputRequest.requestid}`}
className="userinput-checkbox"
ref={checkboxRef}
/>
<label htmlFor={`uicheckbox-${userInputRequest.requestid}}`}>{userInputRequest.checkboxmsg}</label>
<div className="userinput-checkbox-row">
<input
type="checkbox"
id={`uicheckbox-${userInputRequest.requestid}`}
className="userinput-checkbox"
ref={checkboxRef}
/>
<label htmlFor={`uicheckbox-${userInputRequest.requestid}}`}>{userInputRequest.checkboxmsg}</label>
</div>
</div>
);
}, []);
console.log("mem2");
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
if (countdown <= 0) {
timeout = setTimeout(() => {
handleSendCancel();
handleSendErrResponse();
}, 300);
} else {
timeout = setTimeout(() => {
@ -123,9 +133,28 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
}
return () => clearTimeout(timeout);
}, [countdown]);
console.log("count");
const handleNegativeResponse = useCallback(() => {
switch (userInputRequest.responsetype) {
case "text":
handleSendErrResponse();
break;
case "confirm":
handleSendConfirm(false);
break;
}
}, [userInputRequest.responsetype, handleSendErrResponse, handleSendConfirm]);
console.log("before end");
return (
<Modal onOk={() => handleSubmit()} onCancel={() => handleSendCancel()} onClose={() => handleSendCancel()}>
<Modal
onOk={() => handleSubmit()}
onCancel={() => handleNegativeResponse()}
onClose={() => handleSendErrResponse()}
okLabel={userInputRequest.oklabel}
cancelLabel={userInputRequest.cancellabel}
>
<div className="userinput-header">{userInputRequest.title + ` (${countdown}s)`}</div>
<div className="userinput-body">
{queryText}

View File

@ -28,7 +28,7 @@ class RpcApiType {
}
// command "connconnect" [call]
ConnConnectCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
ConnConnectCommand(client: WshClient, data: ConnRequest, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("connconnect", data, opts);
}

View File

@ -5,7 +5,7 @@
declare global {
// telemetry.ActivityDisplayType
// wshrpc.ActivityDisplayType
type ActivityDisplayType = {
width: number;
height: number;
@ -13,7 +13,7 @@ declare global {
internal?: boolean;
};
// telemetry.ActivityUpdate
// wshrpc.ActivityUpdate
type ActivityUpdate = {
fgminutes?: number;
activeminutes?: number;
@ -275,9 +275,36 @@ declare global {
err: string;
};
// wshrpc.ConnKeywords
type ConnKeywords = {
wshenabled?: boolean;
askbeforewshinstall?: boolean;
"ssh:user"?: string;
"ssh:hostname"?: string;
"ssh:port"?: string;
"ssh:identityfile"?: string[];
"ssh:batchmode"?: boolean;
"ssh:pubkeyauthentication"?: boolean;
"ssh:passwordauthentication"?: boolean;
"ssh:kbdinteractiveauthentication"?: boolean;
"ssh:preferredauthentications"?: string[];
"ssh:addkeystoagent"?: boolean;
"ssh:identityagent"?: string;
"ssh:proxyjump"?: string[];
"ssh:userknownhostsfile"?: string[];
"ssh:globalknownhostsfile"?: string[];
};
// wshrpc.ConnRequest
type ConnRequest = {
host: string;
keywords?: ConnKeywords;
};
// wshrpc.ConnStatus
type ConnStatus = {
status: string;
wshenabled: boolean;
connection: string;
connected: boolean;
hasconnected: boolean;
@ -337,9 +364,11 @@ declare global {
type FullConfigType = {
settings: SettingsType;
mimetypes: {[key: string]: MimeTypeConfigType};
defaultwidgets: {[key: string]: WidgetConfigType};
widgets: {[key: string]: WidgetConfigType};
presets: {[key: string]: MetaType};
termthemes: {[key: string]: TermThemeType};
connections: {[key: string]: ConnKeywords};
configerrors: ConfigError[];
};
@ -608,6 +637,7 @@ declare global {
"telemetry:enabled"?: boolean;
"conn:*"?: boolean;
"conn:askbeforewshinstall"?: boolean;
"conn:wshenabled"?: boolean;
};
// waveobj.StickerClickOptsType
@ -701,6 +731,8 @@ declare global {
timeoutms: number;
checkboxmsg: string;
publictext: boolean;
oklabel?: string;
cancellabel?: string;
};
// userinput.UserInputResponse

View File

@ -296,7 +296,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
if err != nil {
return err
}
conn := conncontroller.GetConn(credentialCtx, opts, false)
conn := conncontroller.GetConn(credentialCtx, opts, false, &wshrpc.ConnKeywords{})
connStatus := conn.DeriveConnStatus()
if connStatus.Status != conncontroller.Status_Connected {
return fmt.Errorf("not connected, cannot start shellproc")
@ -538,7 +538,7 @@ func CheckConnStatus(blockId string) error {
if err != nil {
return fmt.Errorf("error parsing connection name: %w", err)
}
conn := conncontroller.GetConn(context.Background(), opts, false)
conn := conncontroller.GetConn(context.Background(), opts, false, &wshrpc.ConnKeywords{})
connStatus := conn.DeriveConnStatus()
if connStatus.Status != conncontroller.Status_Connected {
return fmt.Errorf("not connected: %s", connStatus.Status)

View File

@ -52,6 +52,7 @@ var activeConnCounter = &atomic.Int32{}
type SSHConn struct {
Lock *sync.Mutex
Status string
WshEnabled *atomic.Bool
Opts *remote.SSHOpts
Client *ssh.Client
SockName string
@ -290,6 +291,12 @@ type WshInstallOpts struct {
NoUserPrompt bool
}
type WshInstallSkipError struct{}
func (wise *WshInstallSkipError) Error() string {
return "skipping wsh installation"
}
func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName string, opts *WshInstallOpts) error {
if opts == nil {
opts = &WshInstallOpts{}
@ -325,12 +332,23 @@ func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName s
QueryText: queryText,
Title: title,
Markdown: true,
CheckBoxMsg: "Don't show me this again",
CheckBoxMsg: "Automatically install for all connections",
OkLabel: "Install wsh",
CancelLabel: "No wsh",
}
response, err := userinput.GetUserInput(ctx, request)
if err != nil || !response.Confirm {
if err != nil {
return err
}
if !response.Confirm {
meta := make(map[string]any)
meta["wshenabled"] = false
err = wconfig.SetConnectionsConfigValue(conn.GetName(), meta)
if err != nil {
log.Printf("warning: error writing to connections file: %v", err)
}
return &WshInstallSkipError{}
}
if response.CheckboxStat {
meta := waveobj.MetaMapType{
wconfig.ConfigKey_ConnAskBeforeWshInstall: false,
@ -371,7 +389,7 @@ func (conn *SSHConn) Reconnect(ctx context.Context) error {
if err != nil {
return err
}
return conn.Connect(ctx)
return conn.Connect(ctx, &wshrpc.ConnKeywords{})
}
func (conn *SSHConn) WaitForConnect(ctx context.Context) error {
@ -399,7 +417,7 @@ func (conn *SSHConn) WaitForConnect(ctx context.Context) error {
}
// does not return an error since that error is stored inside of SSHConn
func (conn *SSHConn) Connect(ctx context.Context) error {
func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords) error {
var connectAllowed bool
conn.WithLock(func() {
if conn.Status == Status_Connecting || conn.Status == Status_Connected {
@ -415,13 +433,13 @@ func (conn *SSHConn) Connect(ctx context.Context) error {
return fmt.Errorf("cannot connect to %q when status is %q", conn.GetName(), conn.GetStatus())
}
conn.FireConnChangeEvent()
err := conn.connectInternal(ctx)
err := conn.connectInternal(ctx, connFlags)
conn.WithLock(func() {
if err != nil {
conn.Status = Status_Error
conn.Error = err.Error()
conn.close_nolock()
telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
Conn: map[string]int{"ssh:connecterror": 1},
}, "ssh-connconnect")
} else {
@ -430,7 +448,7 @@ func (conn *SSHConn) Connect(ctx context.Context) error {
if conn.ActiveConnNum == 0 {
conn.ActiveConnNum = int(activeConnCounter.Add(1))
}
telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
Conn: map[string]int{"ssh:connect": 1},
}, "ssh-connconnect")
}
@ -445,8 +463,8 @@ func (conn *SSHConn) WithLock(fn func()) {
fn()
}
func (conn *SSHConn) connectInternal(ctx context.Context) error {
client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0)
func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error {
client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags)
if err != nil {
log.Printf("error: failed to connect to client %s: %s\n", conn.GetName(), err)
return err
@ -462,15 +480,40 @@ func (conn *SSHConn) connectInternal(ctx context.Context) error {
return err
}
config := wconfig.ReadFullConfig()
installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !config.Settings.ConnAskBeforeWshInstall})
if installErr != nil {
log.Printf("error: unable to install wsh shell extensions for %s: %v\n", conn.GetName(), err)
return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr)
enableWsh := config.Settings.ConnWshEnabled
askBeforeInstall := config.Settings.ConnAskBeforeWshInstall
connSettings, ok := config.Connections[conn.GetName()]
if ok {
if connSettings.WshEnabled != nil {
enableWsh = *connSettings.WshEnabled
}
if connSettings.AskBeforeWshInstall != nil {
askBeforeInstall = *connSettings.AskBeforeWshInstall
}
}
csErr := conn.StartConnServer()
if csErr != nil {
log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr)
return fmt.Errorf("conncontroller %s start wsh connserver error: %v", conn.GetName(), csErr)
if enableWsh {
installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !askBeforeInstall})
if errors.Is(installErr, &WshInstallSkipError{}) {
// skips are not true errors
conn.WithLock(func() {
conn.WshEnabled.Store(false)
})
} else if installErr != nil {
log.Printf("error: unable to install wsh shell extensions for %s: %v\n", conn.GetName(), err)
return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr)
} else {
conn.WshEnabled.Store(true)
}
if conn.WshEnabled.Load() {
csErr := conn.StartConnServer()
if csErr != nil {
log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr)
return fmt.Errorf("conncontroller %s start wsh connserver error: %v", conn.GetName(), csErr)
}
}
} else {
conn.WshEnabled.Store(false)
}
conn.HasWaiter.Store(true)
go conn.waitForDisconnect()
@ -504,16 +547,16 @@ func getConnInternal(opts *remote.SSHOpts) *SSHConn {
defer globalLock.Unlock()
rtn := clientControllerMap[*opts]
if rtn == nil {
rtn = &SSHConn{Lock: &sync.Mutex{}, Status: Status_Init, Opts: opts, HasWaiter: &atomic.Bool{}}
rtn = &SSHConn{Lock: &sync.Mutex{}, Status: Status_Init, WshEnabled: &atomic.Bool{}, Opts: opts, HasWaiter: &atomic.Bool{}}
clientControllerMap[*opts] = rtn
}
return rtn
}
func GetConn(ctx context.Context, opts *remote.SSHOpts, shouldConnect bool) *SSHConn {
func GetConn(ctx context.Context, opts *remote.SSHOpts, shouldConnect bool, connFlags *wshrpc.ConnKeywords) *SSHConn {
conn := getConnInternal(opts)
if conn.Client == nil && shouldConnect {
conn.Connect(ctx)
conn.Connect(ctx, connFlags)
}
return conn
}
@ -527,7 +570,7 @@ func EnsureConnection(ctx context.Context, connName string) error {
if err != nil {
return fmt.Errorf("error parsing connection name: %w", err)
}
conn := GetConn(ctx, connOpts, false)
conn := GetConn(ctx, connOpts, false, &wshrpc.ConnKeywords{})
if conn == nil {
return fmt.Errorf("connection not found: %s", connName)
}
@ -538,7 +581,7 @@ func EnsureConnection(ctx context.Context, connName string) error {
case Status_Connecting:
return conn.WaitForConnect(ctx)
case Status_Init, Status_Disconnected:
return conn.Connect(ctx)
return conn.Connect(ctx, &wshrpc.ConnKeywords{})
case Status_Error:
return fmt.Errorf("connection error: %s", connStatus.Error)
default:

View File

@ -17,7 +17,6 @@ import (
"os/exec"
"os/user"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@ -28,6 +27,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/userinput"
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
xknownhosts "golang.org/x/crypto/ssh/knownhosts"
@ -101,13 +101,13 @@ 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(connCtx context.Context, sshKeywords *SshKeywords, authSockSignersExt []ssh.Signer, agentClient agent.ExtendedAgent, debugInfo *ConnectionDebugInfo) func() ([]ssh.Signer, error) {
func createPublicKeyCallback(connCtx context.Context, sshKeywords *wshrpc.ConnKeywords, authSockSignersExt []ssh.Signer, agentClient agent.ExtendedAgent, debugInfo *ConnectionDebugInfo) func() ([]ssh.Signer, error) {
var identityFiles []string
existingKeys := make(map[string][]byte)
// checking the file early prevents us from needing to send a
// dummy signer if there's a problem with the signer
for _, identityFile := range sshKeywords.IdentityFile {
for _, identityFile := range sshKeywords.SshIdentityFile {
filePath, err := wavebase.ExpandHomeDir(identityFile)
if err != nil {
continue
@ -151,7 +151,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
if err == nil {
signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey)
if err == nil {
if sshKeywords.AddKeysToAgent && agentClient != nil {
if sshKeywords.SshAddKeysToAgent && agentClient != nil {
agentClient.Add(agent.AddedKey{
PrivateKey: unencryptedPrivateKey,
})
@ -165,7 +165,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
}
// batch mode deactivates user input
if sshKeywords.BatchMode {
if sshKeywords.SshBatchMode {
// skip this key and try with the next
return createDummySigner()
}
@ -194,7 +194,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
// skip this key and try with the next
return createDummySigner()
}
if sshKeywords.AddKeysToAgent && agentClient != nil {
if sshKeywords.SshAddKeysToAgent && agentClient != nil {
agentClient.Add(agent.AddedKey{
PrivateKey: unencryptedPrivateKey,
})
@ -333,7 +333,14 @@ func createUnknownKeyVerifier(knownHostsFile string, hostname string, remote str
return func() (*userinput.UserInputResponse, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelFn()
return userinput.GetUserInput(ctx, request)
resp, err := userinput.GetUserInput(ctx, request)
if err != nil {
return nil, err
}
if !resp.Confirm {
return nil, fmt.Errorf("user selected no")
}
return resp, nil
}
}
@ -357,7 +364,14 @@ func createMissingKnownHostsVerifier(knownHostsFile string, hostname string, rem
return func() (*userinput.UserInputResponse, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelFn()
return userinput.GetUserInput(ctx, request)
resp, err := userinput.GetUserInput(ctx, request)
if err != nil {
return nil, err
}
if !resp.Confirm {
return nil, fmt.Errorf("user selected no")
}
return resp, nil
}
}
@ -370,9 +384,9 @@ func lineContainsMatch(line []byte, matches [][]byte) bool {
return false
}
func createHostKeyCallback(sshKeywords *SshKeywords) (ssh.HostKeyCallback, HostKeyAlgorithms, error) {
globalKnownHostsFiles := sshKeywords.GlobalKnownHostsFile
userKnownHostsFiles := sshKeywords.UserKnownHostsFile
func createHostKeyCallback(sshKeywords *wshrpc.ConnKeywords) (ssh.HostKeyCallback, HostKeyAlgorithms, error) {
globalKnownHostsFiles := sshKeywords.SshGlobalKnownHostsFile
userKnownHostsFiles := sshKeywords.SshUserKnownHostsFile
osUser, err := user.Current()
if err != nil {
@ -536,12 +550,12 @@ func createHostKeyCallback(sshKeywords *SshKeywords) (ssh.HostKeyCallback, HostK
return waveHostKeyCallback, hostKeyAlgorithms, nil
}
func createClientConfig(connCtx context.Context, sshKeywords *SshKeywords, debugInfo *ConnectionDebugInfo) (*ssh.ClientConfig, error) {
remoteName := sshKeywords.User + "@" + xknownhosts.Normalize(sshKeywords.HostName+":"+sshKeywords.Port)
func createClientConfig(connCtx context.Context, sshKeywords *wshrpc.ConnKeywords, debugInfo *ConnectionDebugInfo) (*ssh.ClientConfig, error) {
remoteName := sshKeywords.SshUser + "@" + xknownhosts.Normalize(sshKeywords.SshHostName+":"+sshKeywords.SshPort)
var authSockSigners []ssh.Signer
var agentClient agent.ExtendedAgent
conn, err := net.Dial("unix", sshKeywords.IdentityAgent)
conn, err := net.Dial("unix", sshKeywords.SshIdentityAgent)
if err != nil {
log.Printf("Failed to open Identity Agent Socket: %v", err)
} else {
@ -555,20 +569,20 @@ func createClientConfig(connCtx context.Context, sshKeywords *SshKeywords, debug
// exclude gssapi-with-mic and hostbased until implemented
authMethodMap := map[string]ssh.AuthMethod{
"publickey": ssh.RetryableAuthMethod(publicKeyCallback, len(sshKeywords.IdentityFile)+len(authSockSigners)),
"publickey": ssh.RetryableAuthMethod(publicKeyCallback, len(sshKeywords.SshIdentityFile)+len(authSockSigners)),
"keyboard-interactive": ssh.RetryableAuthMethod(keyboardInteractive, 1),
"password": ssh.RetryableAuthMethod(passwordCallback, 1),
}
// note: batch mode turns off interactive input
authMethodActiveMap := map[string]bool{
"publickey": sshKeywords.PubkeyAuthentication,
"keyboard-interactive": sshKeywords.KbdInteractiveAuthentication && !sshKeywords.BatchMode,
"password": sshKeywords.PasswordAuthentication && !sshKeywords.BatchMode,
"publickey": sshKeywords.SshPubkeyAuthentication,
"keyboard-interactive": sshKeywords.SshKbdInteractiveAuthentication && !sshKeywords.SshBatchMode,
"password": sshKeywords.SshPasswordAuthentication && !sshKeywords.SshBatchMode,
}
var authMethods []ssh.AuthMethod
for _, authMethodName := range sshKeywords.PreferredAuthentications {
for _, authMethodName := range sshKeywords.SshPreferredAuthentications {
authMethodActive, ok := authMethodActiveMap[authMethodName]
if !ok || !authMethodActive {
continue
@ -585,9 +599,9 @@ func createClientConfig(connCtx context.Context, sshKeywords *SshKeywords, debug
return nil, err
}
networkAddr := sshKeywords.HostName + ":" + sshKeywords.Port
networkAddr := sshKeywords.SshHostName + ":" + sshKeywords.SshPort
return &ssh.ClientConfig{
User: sshKeywords.User,
User: sshKeywords.SshUser,
Auth: authMethods,
HostKeyCallback: hostKeyCallback,
HostKeyAlgorithms: hostKeyAlgorithms(networkAddr),
@ -616,7 +630,7 @@ func connectInternal(ctx context.Context, networkAddr string, clientConfig *ssh.
return ssh.NewClient(c, chans, reqs), nil
}
func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32) (*ssh.Client, int32, error) {
func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32, connFlags *wshrpc.ConnKeywords) (*ssh.Client, int32, error) {
debugInfo := &ConnectionDebugInfo{
CurrentClient: currentClient,
NextOpts: opts,
@ -631,12 +645,16 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
}
sshKeywords, err := combineSshKeywords(opts, sshConfigKeywords)
connFlags.SshUser = opts.SSHUser
connFlags.SshHostName = opts.SSHHost
connFlags.SshPort = fmt.Sprintf("%d", opts.SSHPort)
sshKeywords, err := combineSshKeywords(connFlags, sshConfigKeywords)
if err != nil {
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
}
for _, proxyName := range sshKeywords.ProxyJump {
for _, proxyName := range sshKeywords.SshProxyJump {
proxyOpts, err := ParseOpts(proxyName)
if err != nil {
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
@ -647,7 +665,8 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
jumpNum += 1
}
debugInfo.CurrentClient, jumpNum, err = ConnectToClient(connCtx, proxyOpts, debugInfo.CurrentClient, jumpNum)
// do not apply supplied keywords to proxies - ssh config must be used for that
debugInfo.CurrentClient, jumpNum, err = ConnectToClient(connCtx, proxyOpts, debugInfo.CurrentClient, jumpNum, &wshrpc.ConnKeywords{})
if err != nil {
// do not add a context on a recursive call
// (this can cause a recursive nested context that's arbitrarily deep)
@ -658,7 +677,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
if err != nil {
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
}
networkAddr := sshKeywords.HostName + ":" + sshKeywords.Port
networkAddr := sshKeywords.SshHostName + ":" + sshKeywords.SshPort
client, err := connectInternal(connCtx, networkAddr, clientConfig, debugInfo.CurrentClient)
if err != nil {
return client, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
@ -666,68 +685,51 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
return client, debugInfo.JumpNum, nil
}
type SshKeywords struct {
User string
HostName string
Port string
IdentityFile []string
BatchMode bool
PubkeyAuthentication bool
PasswordAuthentication bool
KbdInteractiveAuthentication bool
PreferredAuthentications []string
AddKeysToAgent bool
IdentityAgent string
ProxyJump []string
UserKnownHostsFile []string
GlobalKnownHostsFile []string
}
func combineSshKeywords(userProvidedOpts *wshrpc.ConnKeywords, configKeywords *wshrpc.ConnKeywords) (*wshrpc.ConnKeywords, error) {
sshKeywords := &wshrpc.ConnKeywords{}
func combineSshKeywords(opts *SSHOpts, configKeywords *SshKeywords) (*SshKeywords, error) {
sshKeywords := &SshKeywords{}
if opts.SSHUser != "" {
sshKeywords.User = opts.SSHUser
} else if configKeywords.User != "" {
sshKeywords.User = configKeywords.User
if userProvidedOpts.SshUser != "" {
sshKeywords.SshUser = userProvidedOpts.SshUser
} else if configKeywords.SshUser != "" {
sshKeywords.SshUser = configKeywords.SshUser
} else {
user, err := user.Current()
if err != nil {
return nil, fmt.Errorf("failed to get user for ssh: %+v", err)
}
sshKeywords.User = user.Username
sshKeywords.SshUser = user.Username
}
// we have to check the host value because of the weird way
// we store the pattern as the hostname for imported remotes
if configKeywords.HostName != "" {
sshKeywords.HostName = configKeywords.HostName
if configKeywords.SshHostName != "" {
sshKeywords.SshHostName = configKeywords.SshHostName
} else {
sshKeywords.HostName = opts.SSHHost
sshKeywords.SshHostName = userProvidedOpts.SshHostName
}
if opts.SSHPort != 0 && opts.SSHPort != 22 {
sshKeywords.Port = strconv.Itoa(opts.SSHPort)
} else if configKeywords.Port != "" && configKeywords.Port != "22" {
sshKeywords.Port = configKeywords.Port
if userProvidedOpts.SshPort != "0" && userProvidedOpts.SshPort != "22" {
sshKeywords.SshPort = userProvidedOpts.SshPort
} else if configKeywords.SshPort != "" && configKeywords.SshPort != "22" {
sshKeywords.SshPort = configKeywords.SshPort
} else {
sshKeywords.Port = "22"
sshKeywords.SshPort = "22"
}
sshKeywords.IdentityFile = configKeywords.IdentityFile
sshKeywords.SshIdentityFile = append(userProvidedOpts.SshIdentityFile, configKeywords.SshIdentityFile...)
// these are not officially supported in the waveterm frontend but can be configured
// in ssh config files
sshKeywords.BatchMode = configKeywords.BatchMode
sshKeywords.PubkeyAuthentication = configKeywords.PubkeyAuthentication
sshKeywords.PasswordAuthentication = configKeywords.PasswordAuthentication
sshKeywords.KbdInteractiveAuthentication = configKeywords.KbdInteractiveAuthentication
sshKeywords.PreferredAuthentications = configKeywords.PreferredAuthentications
sshKeywords.AddKeysToAgent = configKeywords.AddKeysToAgent
sshKeywords.IdentityAgent = configKeywords.IdentityAgent
sshKeywords.ProxyJump = configKeywords.ProxyJump
sshKeywords.UserKnownHostsFile = configKeywords.UserKnownHostsFile
sshKeywords.GlobalKnownHostsFile = configKeywords.GlobalKnownHostsFile
sshKeywords.SshBatchMode = configKeywords.SshBatchMode
sshKeywords.SshPubkeyAuthentication = configKeywords.SshPubkeyAuthentication
sshKeywords.SshPasswordAuthentication = configKeywords.SshPasswordAuthentication
sshKeywords.SshKbdInteractiveAuthentication = configKeywords.SshKbdInteractiveAuthentication
sshKeywords.SshPreferredAuthentications = configKeywords.SshPreferredAuthentications
sshKeywords.SshAddKeysToAgent = configKeywords.SshAddKeysToAgent
sshKeywords.SshIdentityAgent = configKeywords.SshIdentityAgent
sshKeywords.SshProxyJump = configKeywords.SshProxyJump
sshKeywords.SshUserKnownHostsFile = configKeywords.SshUserKnownHostsFile
sshKeywords.SshGlobalKnownHostsFile = configKeywords.SshGlobalKnownHostsFile
return sshKeywords, nil
}
@ -735,59 +737,60 @@ func combineSshKeywords(opts *SSHOpts, configKeywords *SshKeywords) (*SshKeyword
// note that a `var == "yes"` will default to false
// but `var != "no"` will default to true
// when given unexpected strings
func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
func findSshConfigKeywords(hostPattern string) (*wshrpc.ConnKeywords, error) {
WaveSshConfigUserSettings().ReloadConfigs()
sshKeywords := &SshKeywords{}
sshKeywords := &wshrpc.ConnKeywords{}
var err error
//config := wconfig.ReadFullConfig()
userRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "User")
if err != nil {
return nil, err
}
sshKeywords.User = trimquotes.TryTrimQuotes(userRaw)
sshKeywords.SshUser = trimquotes.TryTrimQuotes(userRaw)
hostNameRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "HostName")
if err != nil {
return nil, err
}
sshKeywords.HostName = trimquotes.TryTrimQuotes(hostNameRaw)
sshKeywords.SshHostName = trimquotes.TryTrimQuotes(hostNameRaw)
portRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "Port")
if err != nil {
return nil, err
}
sshKeywords.Port = trimquotes.TryTrimQuotes(portRaw)
sshKeywords.SshPort = trimquotes.TryTrimQuotes(portRaw)
identityFileRaw := WaveSshConfigUserSettings().GetAll(hostPattern, "IdentityFile")
for i := 0; i < len(identityFileRaw); i++ {
identityFileRaw[i] = trimquotes.TryTrimQuotes(identityFileRaw[i])
}
sshKeywords.IdentityFile = identityFileRaw
sshKeywords.SshIdentityFile = identityFileRaw
batchModeRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "BatchMode")
if err != nil {
return nil, err
}
sshKeywords.BatchMode = (strings.ToLower(trimquotes.TryTrimQuotes(batchModeRaw)) == "yes")
sshKeywords.SshBatchMode = (strings.ToLower(trimquotes.TryTrimQuotes(batchModeRaw)) == "yes")
// we currently do not support host-bound or unbound but will use yes when they are selected
pubkeyAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "PubkeyAuthentication")
if err != nil {
return nil, err
}
sshKeywords.PubkeyAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(pubkeyAuthenticationRaw)) != "no")
sshKeywords.SshPubkeyAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(pubkeyAuthenticationRaw)) != "no")
passwordAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "PasswordAuthentication")
if err != nil {
return nil, err
}
sshKeywords.PasswordAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(passwordAuthenticationRaw)) != "no")
sshKeywords.SshPasswordAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(passwordAuthenticationRaw)) != "no")
kbdInteractiveAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "KbdInteractiveAuthentication")
if err != nil {
return nil, err
}
sshKeywords.KbdInteractiveAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(kbdInteractiveAuthenticationRaw)) != "no")
sshKeywords.SshKbdInteractiveAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(kbdInteractiveAuthenticationRaw)) != "no")
// these are parsed as a single string and must be separated
// these are case sensitive in openssh so they are here too
@ -795,12 +798,12 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
if err != nil {
return nil, err
}
sshKeywords.PreferredAuthentications = strings.Split(trimquotes.TryTrimQuotes(preferredAuthenticationsRaw), ",")
sshKeywords.SshPreferredAuthentications = strings.Split(trimquotes.TryTrimQuotes(preferredAuthenticationsRaw), ",")
addKeysToAgentRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "AddKeysToAgent")
if err != nil {
return nil, err
}
sshKeywords.AddKeysToAgent = (strings.ToLower(trimquotes.TryTrimQuotes(addKeysToAgentRaw)) == "yes")
sshKeywords.SshAddKeysToAgent = (strings.ToLower(trimquotes.TryTrimQuotes(addKeysToAgentRaw)) == "yes")
identityAgentRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "IdentityAgent")
if err != nil {
@ -815,7 +818,7 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
if err != nil {
return nil, err
}
sshKeywords.IdentityAgent = agentPath
sshKeywords.SshIdentityAgent = agentPath
} else {
log.Printf("unable to find SSH_AUTH_SOCK: %v\n", err)
}
@ -824,7 +827,7 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
if err != nil {
return nil, err
}
sshKeywords.IdentityAgent = agentPath
sshKeywords.SshIdentityAgent = agentPath
}
proxyJumpRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "ProxyJump")
@ -837,12 +840,12 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
if proxyJumpName == "" || strings.ToLower(proxyJumpName) == "none" {
continue
}
sshKeywords.ProxyJump = append(sshKeywords.ProxyJump, proxyJumpName)
sshKeywords.SshProxyJump = append(sshKeywords.SshProxyJump, proxyJumpName)
}
rawUserKnownHostsFile, _ := WaveSshConfigUserSettings().GetStrict(hostPattern, "UserKnownHostsFile")
sshKeywords.UserKnownHostsFile = strings.Fields(rawUserKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes
sshKeywords.SshUserKnownHostsFile = strings.Fields(rawUserKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes
rawGlobalKnownHostsFile, _ := WaveSshConfigUserSettings().GetStrict(hostPattern, "GlobalKnownHostsFile")
sshKeywords.GlobalKnownHostsFile = strings.Fields(rawGlobalKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes
sshKeywords.SshGlobalKnownHostsFile = strings.Fields(rawGlobalKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes
return sshKeywords, nil
}

View File

@ -237,6 +237,47 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st
func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) {
client := conn.GetClient()
if !conn.WshEnabled.Load() {
// no wsh code
session, err := client.NewSession()
if err != nil {
return nil, err
}
remoteStdinRead, remoteStdinWriteOurs, err := os.Pipe()
if err != nil {
return nil, err
}
remoteStdoutReadOurs, remoteStdoutWrite, err := os.Pipe()
if err != nil {
return nil, err
}
pipePty := &PipePty{
remoteStdinWrite: remoteStdinWriteOurs,
remoteStdoutRead: remoteStdoutReadOurs,
}
if termSize.Rows == 0 || termSize.Cols == 0 {
termSize.Rows = shellutil.DefaultTermRows
termSize.Cols = shellutil.DefaultTermCols
}
if termSize.Rows <= 0 || termSize.Cols <= 0 {
return nil, fmt.Errorf("invalid term size: %v", termSize)
}
session.Stdin = remoteStdinRead
session.Stdout = remoteStdoutWrite
session.Stderr = remoteStdoutWrite
session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
sessionWrap := SessionWrap{session, "", pipePty, pipePty}
err = session.Shell()
if err != nil {
pipePty.Close()
return nil, err
}
return &ShellProc{Cmd: sessionWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
}
shellPath := cmdOpts.ShellPath
if shellPath == "" {
remoteShellPath, err := remote.DetectShell(client)

View File

@ -14,41 +14,12 @@ import (
"github.com/wavetermdev/waveterm/pkg/util/dbutil"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wstore"
)
const MaxTzNameLen = 50
type ActivityDisplayType struct {
Width int `json:"width"`
Height int `json:"height"`
DPR float64 `json:"dpr"`
Internal bool `json:"internal,omitempty"`
}
type ActivityUpdate struct {
FgMinutes int `json:"fgminutes,omitempty"`
ActiveMinutes int `json:"activeminutes,omitempty"`
OpenMinutes int `json:"openminutes,omitempty"`
NumTabs int `json:"numtabs,omitempty"`
NewTab int `json:"newtab,omitempty"`
NumBlocks int `json:"numblocks,omitempty"`
NumWindows int `json:"numwindows,omitempty"`
NumSSHConn int `json:"numsshconn,omitempty"`
NumWSLConn int `json:"numwslconn,omitempty"`
NumMagnify int `json:"nummagnify,omitempty"`
NumPanics int `json:"numpanics,omitempty"`
Startup int `json:"startup,omitempty"`
Shutdown int `json:"shutdown,omitempty"`
SetTabTheme int `json:"settabtheme,omitempty"`
BuildTime string `json:"buildtime,omitempty"`
Displays []ActivityDisplayType `json:"displays,omitempty"`
Renderers map[string]int `json:"renderers,omitempty"`
Blocks map[string]int `json:"blocks,omitempty"`
WshCmds map[string]int `json:"wshcmds,omitempty"`
Conn map[string]int `json:"conn,omitempty"`
}
type ActivityType struct {
Day string `json:"day"`
Uploaded bool `json:"-"`
@ -62,25 +33,25 @@ type ActivityType struct {
}
type TelemetryData struct {
ActiveMinutes int `json:"activeminutes"`
FgMinutes int `json:"fgminutes"`
OpenMinutes int `json:"openminutes"`
NumTabs int `json:"numtabs"`
NumBlocks int `json:"numblocks,omitempty"`
NumWindows int `json:"numwindows,omitempty"`
NumSSHConn int `json:"numsshconn,omitempty"`
NumWSLConn int `json:"numwslconn,omitempty"`
NumMagnify int `json:"nummagnify,omitempty"`
NewTab int `json:"newtab"`
NumStartup int `json:"numstartup,omitempty"`
NumShutdown int `json:"numshutdown,omitempty"`
NumPanics int `json:"numpanics,omitempty"`
SetTabTheme int `json:"settabtheme,omitempty"`
Displays []ActivityDisplayType `json:"displays,omitempty"`
Renderers map[string]int `json:"renderers,omitempty"`
Blocks map[string]int `json:"blocks,omitempty"`
WshCmds map[string]int `json:"wshcmds,omitempty"`
Conn map[string]int `json:"conn,omitempty"`
ActiveMinutes int `json:"activeminutes"`
FgMinutes int `json:"fgminutes"`
OpenMinutes int `json:"openminutes"`
NumTabs int `json:"numtabs"`
NumBlocks int `json:"numblocks,omitempty"`
NumWindows int `json:"numwindows,omitempty"`
NumSSHConn int `json:"numsshconn,omitempty"`
NumWSLConn int `json:"numwslconn,omitempty"`
NumMagnify int `json:"nummagnify,omitempty"`
NewTab int `json:"newtab"`
NumStartup int `json:"numstartup,omitempty"`
NumShutdown int `json:"numshutdown,omitempty"`
NumPanics int `json:"numpanics,omitempty"`
SetTabTheme int `json:"settabtheme,omitempty"`
Displays []wshrpc.ActivityDisplayType `json:"displays,omitempty"`
Renderers map[string]int `json:"renderers,omitempty"`
Blocks map[string]int `json:"blocks,omitempty"`
WshCmds map[string]int `json:"wshcmds,omitempty"`
Conn map[string]int `json:"conn,omitempty"`
}
func (tdata TelemetryData) Value() (driver.Value, error) {
@ -107,7 +78,7 @@ func AutoUpdateChannel() string {
}
// Wraps UpdateCurrentActivity, spawns goroutine, and logs errors
func GoUpdateActivityWrap(update ActivityUpdate, debugStr string) {
func GoUpdateActivityWrap(update wshrpc.ActivityUpdate, debugStr string) {
go func() {
defer panichandler.PanicHandlerNoTelemetry("GoUpdateActivityWrap")
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
@ -120,7 +91,7 @@ func GoUpdateActivityWrap(update ActivityUpdate, debugStr string) {
}()
}
func UpdateActivity(ctx context.Context, update ActivityUpdate) error {
func UpdateActivity(ctx context.Context, update wshrpc.ActivityUpdate) error {
now := time.Now()
dayStr := daystr.GetCurDayStr()
txErr := wstore.WithTx(ctx, func(tx *wstore.TxWrap) error {

View File

@ -61,7 +61,7 @@ var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
var errorRType = reflect.TypeOf((*error)(nil)).Elem()
var anyRType = reflect.TypeOf((*interface{})(nil)).Elem()
var metaRType = reflect.TypeOf((*waveobj.MetaMapType)(nil)).Elem()
var metaSettingsType = reflect.TypeOf((*wconfig.MetaSettingsType)(nil)).Elem()
var metaSettingsType = reflect.TypeOf((*wshrpc.MetaSettingsType)(nil)).Elem()
var uiContextRType = reflect.TypeOf((*waveobj.UIContext)(nil)).Elem()
var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem()
var updatesRtnRType = reflect.TypeOf(waveobj.UpdatesRtnType{})

View File

@ -25,6 +25,8 @@ type UserInputRequest struct {
TimeoutMs int `json:"timeoutms"`
CheckBoxMsg string `json:"checkboxmsg"`
PublicText bool `json:"publictext"`
OkLabel string `json:"oklabel,omitempty"`
CancelLabel string `json:"cancellabel,omitempty"`
}
type UserInputResponse struct {

View File

@ -7,6 +7,7 @@
"autoupdate:installonquit": true,
"autoupdate:intervalms": 3600000,
"conn:askbeforewshinstall": true,
"conn:wshenabled": true,
"editor:minimapenabled": true,
"web:defaulturl": "https://github.com/wavetermdev/waveterm",
"web:defaultsearch": "https://www.google.com/search?q={query}",

View File

@ -71,5 +71,6 @@ const (
ConfigKey_ConnClear = "conn:*"
ConfigKey_ConnAskBeforeWshInstall = "conn:askbeforewshinstall"
ConfigKey_ConnWshEnabled = "conn:wshenabled"
)

View File

@ -19,9 +19,11 @@ import (
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wconfig/defaultconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
)
const SettingsFile = "settings.json"
const ConnectionsFile = "connections.json"
const AnySchema = `
{
@ -30,25 +32,6 @@ const AnySchema = `
}
`
type MetaSettingsType struct {
waveobj.MetaMapType
}
func (m *MetaSettingsType) UnmarshalJSON(data []byte) error {
var metaMap waveobj.MetaMapType
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(&metaMap); err != nil {
return err
}
*m = MetaSettingsType{MetaMapType: metaMap}
return nil
}
func (m MetaSettingsType) MarshalJSON() ([]byte, error) {
return json.Marshal(m.MetaMapType)
}
type SettingsType struct {
AiClear bool `json:"ai:*,omitempty"`
AiPreset string `json:"ai:preset,omitempty"`
@ -115,6 +98,7 @@ type SettingsType struct {
ConnClear bool `json:"conn:*,omitempty"`
ConnAskBeforeWshInstall bool `json:"conn:askbeforewshinstall,omitempty"`
ConnWshEnabled bool `json:"conn:wshenabled,omitempty"`
}
type ConfigError struct {
@ -123,12 +107,14 @@ type ConfigError struct {
}
type FullConfigType struct {
Settings SettingsType `json:"settings" merge:"meta"`
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
Widgets map[string]WidgetConfigType `json:"widgets"`
Presets map[string]waveobj.MetaMapType `json:"presets"`
TermThemes map[string]TermThemeType `json:"termthemes"`
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"`
Settings SettingsType `json:"settings" merge:"meta"`
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
DefaultWidgets map[string]WidgetConfigType `json:"defaultwidgets"`
Widgets map[string]WidgetConfigType `json:"widgets"`
Presets map[string]waveobj.MetaMapType `json:"presets"`
TermThemes map[string]TermThemeType `json:"termthemes"`
Connections map[string]wshrpc.ConnKeywords `json:"connections"`
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"`
}
func goBackWS(barr []byte, offset int) int {
@ -401,12 +387,12 @@ func reindentJson(barr []byte, indentStr string) []byte {
if barr[0] != '{' && barr[0] != '[' {
return barr
}
if bytes.Contains(barr, []byte("\n")) {
if !bytes.Contains(barr, []byte("\n")) {
return barr
}
outputLines := bytes.Split(barr, []byte("\n"))
for i, line := range outputLines {
if i == 0 || i == len(outputLines)-1 {
if i == 0 {
continue
}
outputLines[i] = append([]byte(indentStr), line...)
@ -509,6 +495,25 @@ func SetBaseConfigValue(toMerge waveobj.MetaMapType) error {
return WriteWaveHomeConfigFile(SettingsFile, m)
}
func SetConnectionsConfigValue(connName string, toMerge waveobj.MetaMapType) error {
m, cerrs := ReadWaveHomeConfigFile(ConnectionsFile)
if len(cerrs) > 0 {
return fmt.Errorf("error reading config file: %v", cerrs[0])
}
if m == nil {
m = make(waveobj.MetaMapType)
}
connData := m.GetMap(connName)
if connData == nil {
connData = make(waveobj.MetaMapType)
}
for configKey, val := range toMerge {
connData[configKey] = val
}
m[connName] = connData
return WriteWaveHomeConfigFile(ConnectionsFile, m)
}
type WidgetConfigType struct {
DisplayOrder float64 `json:"display:order,omitempty"`
Icon string `json:"icon,omitempty"`

View File

@ -16,6 +16,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wstore"
)
@ -119,7 +120,7 @@ func CreateTab(ctx context.Context, windowId string, tabName string, activateTab
return "", fmt.Errorf("error setting active tab: %w", err)
}
}
telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{NewTab: 1}, "createtab")
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{NewTab: 1}, "createtab")
return tab.OID, nil
}
@ -282,7 +283,7 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
}
tctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
telemetry.UpdateActivity(tctx, telemetry.ActivityUpdate{
telemetry.UpdateActivity(tctx, wshrpc.ActivityUpdate{
Renderers: map[string]int{blockView: 1},
})
}()

View File

@ -9,14 +9,12 @@ import (
"github.com/wavetermdev/waveterm/pkg/wshutil"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/vdom"
"github.com/wavetermdev/waveterm/pkg/telemetry"
)
// command "activity", wshserver.ActivityCommand
func ActivityCommand(w *wshutil.WshRpc, data telemetry.ActivityUpdate, opts *wshrpc.RpcOpts) error {
func ActivityCommand(w *wshutil.WshRpc, data wshrpc.ActivityUpdate, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "activity", data, opts)
return err
}
@ -40,7 +38,7 @@ func BlockInfoCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) (*ws
}
// command "connconnect", wshserver.ConnConnectCommand
func ConnConnectCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
func ConnConnectCommand(w *wshutil.WshRpc, data wshrpc.ConnRequest, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "connconnect", data, opts)
return err
}
@ -290,7 +288,7 @@ func RouteUnannounceCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) error {
}
// command "setconfig", wshserver.SetConfigCommand
func SetConfigCommand(w *wshutil.WshRpc, data wconfig.MetaSettingsType, opts *wshrpc.RpcOpts) error {
func SetConfigCommand(w *wshutil.WshRpc, data wshrpc.MetaSettingsType, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "setconfig", data, opts)
return err
}

View File

@ -5,17 +5,17 @@
package wshrpc
import (
"bytes"
"context"
"encoding/json"
"log"
"os"
"reflect"
"github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/ijson"
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/vdom"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wps"
)
@ -133,11 +133,11 @@ type WshRpcInterface interface {
StreamWaveAiCommand(ctx context.Context, request OpenAiStreamRequest) chan RespOrErrorUnion[OpenAIPacketType]
StreamCpuDataCommand(ctx context.Context, request CpuDataRequest) chan RespOrErrorUnion[TimeSeriesData]
TestCommand(ctx context.Context, data string) error
SetConfigCommand(ctx context.Context, data wconfig.MetaSettingsType) error
SetConfigCommand(ctx context.Context, data MetaSettingsType) error
BlockInfoCommand(ctx context.Context, blockId string) (*BlockInfoData, error)
WaveInfoCommand(ctx context.Context) (*WaveInfoData, error)
WshActivityCommand(ct context.Context, data map[string]int) error
ActivityCommand(ctx context.Context, data telemetry.ActivityUpdate) error
ActivityCommand(ctx context.Context, data ActivityUpdate) error
GetVarCommand(ctx context.Context, data CommandVarData) (*CommandVarResponseData, error)
SetVarCommand(ctx context.Context, data CommandVarData) error
@ -146,7 +146,7 @@ type WshRpcInterface interface {
WslStatusCommand(ctx context.Context) ([]ConnStatus, error)
ConnEnsureCommand(ctx context.Context, connName string) error
ConnReinstallWshCommand(ctx context.Context, connName string) error
ConnConnectCommand(ctx context.Context, connName string) error
ConnConnectCommand(ctx context.Context, connRequest ConnRequest) error
ConnDisconnectCommand(ctx context.Context, connName string) error
ConnListCommand(ctx context.Context) ([]string, error)
WslListCommand(ctx context.Context) ([]string, error)
@ -440,6 +440,31 @@ type CommandRemoteWriteFileData struct {
CreateMode os.FileMode `json:"createmode,omitempty"`
}
type ConnKeywords struct {
WshEnabled *bool `json:"wshenabled,omitempty"`
AskBeforeWshInstall *bool `json:"askbeforewshinstall,omitempty"`
SshUser string `json:"ssh:user,omitempty"`
SshHostName string `json:"ssh:hostname,omitempty"`
SshPort string `json:"ssh:port,omitempty"`
SshIdentityFile []string `json:"ssh:identityfile,omitempty"`
SshBatchMode bool `json:"ssh:batchmode,omitempty"`
SshPubkeyAuthentication bool `json:"ssh:pubkeyauthentication,omitempty"`
SshPasswordAuthentication bool `json:"ssh:passwordauthentication,omitempty"`
SshKbdInteractiveAuthentication bool `json:"ssh:kbdinteractiveauthentication,omitempty"`
SshPreferredAuthentications []string `json:"ssh:preferredauthentications,omitempty"`
SshAddKeysToAgent bool `json:"ssh:addkeystoagent,omitempty"`
SshIdentityAgent string `json:"ssh:identityagent,omitempty"`
SshProxyJump []string `json:"ssh:proxyjump,omitempty"`
SshUserKnownHostsFile []string `json:"ssh:userknownhostsfile,omitempty"`
SshGlobalKnownHostsFile []string `json:"ssh:globalknownhostsfile,omitempty"`
}
type ConnRequest struct {
Host string `json:"host"`
Keywords ConnKeywords `json:"keywords,omitempty"`
}
const (
TimeSeries_Cpu = "cpu"
)
@ -449,8 +474,28 @@ type TimeSeriesData struct {
Values map[string]float64 `json:"values"`
}
type MetaSettingsType struct {
waveobj.MetaMapType
}
func (m *MetaSettingsType) UnmarshalJSON(data []byte) error {
var metaMap waveobj.MetaMapType
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(&metaMap); err != nil {
return err
}
*m = MetaSettingsType{MetaMapType: metaMap}
return nil
}
func (m MetaSettingsType) MarshalJSON() ([]byte, error) {
return json.Marshal(m.MetaMapType)
}
type ConnStatus struct {
Status string `json:"status"`
WshEnabled bool `json:"wshenabled"`
Connection string `json:"connection"`
Connected bool `json:"connected"`
HasConnected bool `json:"hasconnected"` // true if it has *ever* connected successfully
@ -522,3 +567,33 @@ type CommandVarResponseData struct {
Val string `json:"val"`
Exists bool `json:"exists"`
}
type ActivityDisplayType struct {
Width int `json:"width"`
Height int `json:"height"`
DPR float64 `json:"dpr"`
Internal bool `json:"internal,omitempty"`
}
type ActivityUpdate struct {
FgMinutes int `json:"fgminutes,omitempty"`
ActiveMinutes int `json:"activeminutes,omitempty"`
OpenMinutes int `json:"openminutes,omitempty"`
NumTabs int `json:"numtabs,omitempty"`
NewTab int `json:"newtab,omitempty"`
NumBlocks int `json:"numblocks,omitempty"`
NumWindows int `json:"numwindows,omitempty"`
NumSSHConn int `json:"numsshconn,omitempty"`
NumWSLConn int `json:"numwslconn,omitempty"`
NumMagnify int `json:"nummagnify,omitempty"`
NumPanics int `json:"numpanics,omitempty"`
Startup int `json:"startup,omitempty"`
Shutdown int `json:"shutdown,omitempty"`
SetTabTheme int `json:"settabtheme,omitempty"`
BuildTime string `json:"buildtime,omitempty"`
Displays []ActivityDisplayType `json:"displays,omitempty"`
Renderers map[string]int `json:"renderers,omitempty"`
Blocks map[string]int `json:"blocks,omitempty"`
WshCmds map[string]int `json:"wshcmds,omitempty"`
Conn map[string]int `json:"conn,omitempty"`
}

View File

@ -587,7 +587,7 @@ func (ws *WshServer) EventReadHistoryCommand(ctx context.Context, data wshrpc.Co
return events, nil
}
func (ws *WshServer) SetConfigCommand(ctx context.Context, data wconfig.MetaSettingsType) error {
func (ws *WshServer) SetConfigCommand(ctx context.Context, data wshrpc.MetaSettingsType) error {
log.Printf("SETCONFIG: %v\n", data)
return wconfig.SetBaseConfigValue(data.MetaMapType)
}
@ -623,14 +623,15 @@ func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string)
if err != nil {
return fmt.Errorf("error parsing connection name: %w", err)
}
conn := conncontroller.GetConn(ctx, connOpts, false)
conn := conncontroller.GetConn(ctx, connOpts, false, &wshrpc.ConnKeywords{})
if conn == nil {
return fmt.Errorf("connection not found: %s", connName)
}
return conn.Close()
}
func (ws *WshServer) ConnConnectCommand(ctx context.Context, connName string) error {
func (ws *WshServer) ConnConnectCommand(ctx context.Context, connRequest wshrpc.ConnRequest) error {
connName := connRequest.Host
if strings.HasPrefix(connName, "wsl://") {
distroName := strings.TrimPrefix(connName, "wsl://")
conn := wsl.GetWslConn(ctx, distroName, false)
@ -643,11 +644,11 @@ func (ws *WshServer) ConnConnectCommand(ctx context.Context, connName string) er
if err != nil {
return fmt.Errorf("error parsing connection name: %w", err)
}
conn := conncontroller.GetConn(ctx, connOpts, false)
conn := conncontroller.GetConn(ctx, connOpts, false, &connRequest.Keywords)
if conn == nil {
return fmt.Errorf("connection not found: %s", connName)
}
return conn.Connect(ctx)
return conn.Connect(ctx, &connRequest.Keywords)
}
func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName string) error {
@ -663,7 +664,7 @@ func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName strin
if err != nil {
return fmt.Errorf("error parsing connection name: %w", err)
}
conn := conncontroller.GetConn(ctx, connOpts, false)
conn := conncontroller.GetConn(ctx, connOpts, false, &wshrpc.ConnKeywords{})
if conn == nil {
return fmt.Errorf("connection not found: %s", connName)
}
@ -753,14 +754,14 @@ func (ws *WshServer) WshActivityCommand(ctx context.Context, data map[string]int
delete(data, key)
}
}
activityUpdate := telemetry.ActivityUpdate{
activityUpdate := wshrpc.ActivityUpdate{
WshCmds: data,
}
telemetry.GoUpdateActivityWrap(activityUpdate, "wsh-activity")
return nil
}
func (ws *WshServer) ActivityCommand(ctx context.Context, activity telemetry.ActivityUpdate) error {
func (ws *WshServer) ActivityCommand(ctx context.Context, activity wshrpc.ActivityUpdate) error {
telemetry.GoUpdateActivityWrap(activity, "wshrpc-activity")
return nil
}

View File

@ -394,7 +394,7 @@ func (conn *WslConn) Connect(ctx context.Context) error {
conn.Status = Status_Error
conn.Error = err.Error()
conn.close_nolock()
telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
Conn: map[string]int{"wsl:connecterror": 1},
}, "wsl-connconnect")
} else {
@ -403,7 +403,7 @@ func (conn *WslConn) Connect(ctx context.Context) error {
if conn.ActiveConnNum == 0 {
conn.ActiveConnNum = int(activeConnCounter.Add(1))
}
telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
Conn: map[string]int{"wsl:connect": 1},
}, "wsl-connconnect")
}