Compare commits

..

51 Commits

Author SHA1 Message Date
Red Adaya 1d0b7bd770 split models/model 2024-02-08 16:40:15 +08:00
Red Adaya a9b9ff3362 fix conflicts 2024-02-08 14:48:29 +08:00
Red Adaya d2829c466d fix ptyDataSource errors 2024-02-08 14:26:49 +08:00
Red Adaya f65e632c35 move model_old to models 2024-02-08 13:37:42 +08:00
Red Adaya acb3bd303b rename model to models 2024-02-08 13:37:02 +08:00
Red Adaya 3ad7feedad replace /model_old imports with /model 2024-02-08 13:34:46 +08:00
Red Adaya 24690ff931 add index.ts inside model dir 2024-02-08 09:35:52 +08:00
Red Adaya f510e1c904 cleanup connectionsview model 2024-02-08 09:23:44 +08:00
Red Adaya 35fa59aadd cleanup screenlinecontainer model 2024-02-08 09:20:30 +08:00
Red Adaya d414657252 cleanup session model 2024-02-08 09:16:02 +08:00
Red Adaya b59dd6960f cleanup screenlines model 2024-02-08 09:14:30 +08:00
Red Adaya 1ae74b767d cleanup screen model 2024-02-08 09:13:04 +08:00
Red Adaya 7fa862b402 cleanup remote model 2024-02-08 09:09:16 +08:00
Red Adaya 970c5c30fd cleanup plugins model 2024-02-08 09:07:26 +08:00
Red Adaya 2bcc8756e7 cleanup main model 2024-02-08 09:06:09 +08:00
Red Adaya 5ad26b10cd cleanup mainsidebar model 2024-02-08 09:02:31 +08:00
Red Adaya ef7acda1bb cleanup linecontainer model 2024-02-08 09:01:08 +08:00
Red Adaya 29cd754358 cleanup input model 2024-02-08 08:59:40 +08:00
Red Adaya 648d98f065 cleanup historyview model 2024-02-08 08:55:56 +08:00
Red Adaya 5770e482a1 cleanup commandrunner model 2024-02-08 08:44:01 +08:00
Red Adaya 23b3250f18 remove dayjs import from bookmarks model 2024-02-08 08:41:22 +08:00
Red Adaya b62977470b cleanup cmd model 2024-02-08 08:40:33 +08:00
Red Adaya 07f2794b1b cleanup clientsettingsview model 2024-02-08 08:37:56 +08:00
Red Adaya cf27a8baff cleanup bookmarks model 2024-02-08 08:37:17 +08:00
Red Adaya aace48d42a init 2024-02-08 08:35:30 +08:00
Red Adaya 15d02f3357 remove styles not related to Tooltip 2024-02-06 13:42:09 +08:00
Red Adaya 39d371681c remove styles not related to Toggle 2024-02-06 13:41:23 +08:00
Red Adaya 412c3ac5a2 remove styles not related to TextField 2024-02-06 13:37:15 +08:00
Red Adaya 36484621ec remove styles not related to Status 2024-02-06 13:35:15 +08:00
Red Adaya 72231ead1d SettingsError doesn't need styles cos it uses classnames in app.less 2024-02-06 13:33:42 +08:00
Red Adaya 81c1fd43f7 remove styles not related to ResizableSidebar 2024-02-06 13:31:20 +08:00
Red Adaya c9a73c2204 RemoteStatusLight no longer used. It's replaced by Status component. 2024-02-06 13:27:58 +08:00
Red Adaya 434f980b49 remove styles not related to PasswordField 2024-02-06 13:26:22 +08:00
Red Adaya 75d462764d NumberField doesn't need styles cos it extends TextField 2024-02-06 13:24:50 +08:00
Red Adaya c58c88af4e remove styles not related to modal 2024-02-06 13:23:32 +08:00
Red Adaya 05be301ea2 remove styles not related to markdown 2024-02-06 13:22:04 +08:00
Red Adaya 27e5a9154d LinkButton doesn't need styles cos it's extends Button component 2024-02-06 13:19:31 +08:00
Red Adaya a7d2148bc0 remove styles not related to InputDecoration 2024-02-06 10:28:39 +08:00
Red Adaya ead75880e1 remove styles not related to InlineSettingsTextEdit 2024-02-06 10:17:18 +08:00
Red Adaya 0bdb50cf91 fix import error 2024-02-06 10:12:32 +08:00
Red Adaya abf3448f07 InfoMessage conmponent no longer needed 2024-02-06 10:09:01 +08:00
Red Adaya 8988d26867 remove old connections 2024-02-06 09:50:57 +08:00
Red Adaya a084ca4801 IconButton doesn't need styles because it extends Button 2024-02-06 09:30:10 +08:00
Red Adaya 36e2578a03 remove styles not related to dropdown 2024-02-06 09:26:46 +08:00
Red Adaya a49487c035 renderCmdText doesn't need styles 2024-02-06 09:15:51 +08:00
Red Adaya 03296043fb remove styles not related to CmdStrCode 2024-02-06 09:10:35 +08:00
Red Adaya ec797f55ff remove styles not related to checkbox 2024-02-06 09:05:18 +08:00
Red Adaya 550461167b init 2024-02-05 23:08:59 +08:00
Red Adaya cf5d2ecddf Merge branch 'main' of github.com:wavetermdev/waveterm into split-components-in-common 2024-02-05 23:07:48 +08:00
Red Adaya c4128cd266 init 2024-02-02 23:26:33 +08:00
Red Adaya 17bb07a47d split settings modals 2024-02-02 16:50:44 +08:00
22 changed files with 128 additions and 1311 deletions
-1
View File
@@ -8,7 +8,6 @@ export const SESSION_SETTINGS = "sessionSettings";
export const LINE_SETTINGS = "lineSettings";
export const CLIENT_SETTINGS = "clientSettings";
export const TAB_SWITCHER = "tabSwitcher";
export const USER_INPUT = "userInput";
export const LineContainer_Main = "main";
export const LineContainer_History = "history";
-1
View File
@@ -5,7 +5,6 @@
margin-bottom: 10px;
font-family: @markdown-font;
font-size: 14px;
overflow-wrap: break-word;
code {
background-color: @markdown-highlight;
+1 -7
View File
@@ -60,13 +60,7 @@ class PasswordField extends TextField {
};
return (
<div
className={cn(`wave-textfield wave-password ${className || ""}`, {
focused: focused,
error: error,
"no-label": !label,
})}
>
<div className={cn(`wave-textfield wave-password ${className || ""}`, { focused: focused, error: error })}>
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
<div className="wave-textfield-inner">
<label
-1
View File
@@ -9,4 +9,3 @@ export { TabSwitcherModal } from "./tabswitcher";
export { SessionSettingsModal } from "./sessionsettings";
export { ScreenSettingsModal } from "./screensettings";
export { LineSettingsModal } from "./linesettings";
export { UserInputModal } from "./userinput";
+1 -1
View File
@@ -17,7 +17,7 @@ class ModalsProvider extends React.Component {
for (let i = 0; i < store.length; i++) {
let entry = store[i];
let Comp = entry.component;
rtn.push(<Comp key={entry.uniqueKey} {...entry.props} />);
rtn.push(<Comp key={entry.uniqueKey} />);
}
return <>{rtn}</>;
}
+10 -12
View File
@@ -12,21 +12,19 @@ import {
SessionSettingsModal,
ScreenSettingsModal,
LineSettingsModal,
UserInputModal,
} from "../modals";
import * as constants from "../../appconst";
const modalsRegistry: { [key: string]: React.ComponentType } = {
[constants.ABOUT]: AboutModal,
[constants.CREATE_REMOTE]: CreateRemoteConnModal,
[constants.VIEW_REMOTE]: ViewRemoteConnDetailModal,
[constants.EDIT_REMOTE]: EditRemoteConnModal,
[constants.ALERT]: AlertModal,
[constants.SCREEN_SETTINGS]: ScreenSettingsModal,
[constants.SESSION_SETTINGS]: SessionSettingsModal,
[constants.LINE_SETTINGS]: LineSettingsModal,
[constants.TAB_SWITCHER]: TabSwitcherModal,
[constants.USER_INPUT]: UserInputModal,
const modalsRegistry: { [key: string]: () => React.ReactElement } = {
[constants.ABOUT]: () => <AboutModal />,
[constants.CREATE_REMOTE]: () => <CreateRemoteConnModal />,
[constants.VIEW_REMOTE]: () => <ViewRemoteConnDetailModal />,
[constants.EDIT_REMOTE]: () => <EditRemoteConnModal />,
[constants.ALERT]: () => <AlertModal />,
[constants.SCREEN_SETTINGS]: () => <ScreenSettingsModal />,
[constants.SESSION_SETTINGS]: () => <SessionSettingsModal />,
[constants.LINE_SETTINGS]: () => <LineSettingsModal />,
[constants.TAB_SWITCHER]: () => <TabSwitcherModal />,
};
export { modalsRegistry };
-15
View File
@@ -1,15 +0,0 @@
@import "../../../app/common/themes/themes.less";
.userinput-modal {
width: 500px;
.wave-modal-content {
.wave-modal-body {
padding: 20px 20px;
.userinput-query {
margin-bottom: 10px;
}
}
}
}
-88
View File
@@ -1,88 +0,0 @@
import * as React from "react";
import { GlobalModel } from "../../../models";
import { Choose, When, If } from "tsx-control-statements";
import { Modal, PasswordField, Markdown } from "../elements";
import { UserInputRequest } from "../../../types/types";
import "./userinput.less";
export const UserInputModal = (userInputRequest: UserInputRequest) => {
const [responseText, setResponseText] = React.useState(null);
const [countdown, setCountdown] = React.useState(Math.floor(userInputRequest.timeoutms / 1000));
const closeModal = React.useCallback(() => {
GlobalModel.sendUserInput({
type: "userinputresp",
requestid: userInputRequest.requestid,
errormsg: "Canceled by the user",
});
GlobalModel.remotesModel.closeModal();
}, [responseText, userInputRequest]);
const handleSendText = React.useCallback(() => {
GlobalModel.sendUserInput({
type: "userinputresp",
requestid: userInputRequest.requestid,
text: responseText,
});
GlobalModel.remotesModel.closeModal();
}, [responseText, userInputRequest]);
const handleSendConfirm = React.useCallback(
(response: boolean) => {
GlobalModel.sendUserInput({
type: "userinputresp",
requestid: userInputRequest.requestid,
confirm: response,
});
GlobalModel.remotesModel.closeModal();
},
[userInputRequest]
);
React.useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
if (countdown == 0) {
timeout = setTimeout(() => {
GlobalModel.remotesModel.closeModal();
}, 300);
} else {
timeout = setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
}
return () => clearTimeout(timeout);
}, [countdown]);
return (
<Modal className="userinput-modal">
<Modal.Header onClose={closeModal} title={userInputRequest.title + ` (${countdown})`} />
<div className="wave-modal-body">
<div className="userinput-query">
<If condition={userInputRequest.markdown}>
<Markdown text={userInputRequest.querytext} />
</If>
<If condition={!userInputRequest.markdown}>{userInputRequest.querytext}</If>
</div>
<Choose>
<When condition={userInputRequest.responsetype == "text"}>
<PasswordField onChange={setResponseText} value={responseText} maxLength={400} />
</When>
</Choose>
</div>
<Choose>
<When condition={userInputRequest.responsetype == "text"}>
<Modal.Footer onCancel={closeModal} onOk={handleSendText} okLabel="Continue" />
</When>
<When condition={userInputRequest.responsetype == "confirm"}>
<Modal.Footer
onCancel={() => handleSendConfirm(false)}
onOk={() => handleSendConfirm(true)}
okLabel="Yes"
cancelLabel="No"
/>
</When>
</Choose>
</Modal>
);
};
+3 -3
View File
@@ -8,14 +8,14 @@ import { modalsRegistry } from "../app/common/modals/registry";
import { OArr } from "../types/types";
class ModalsModel {
store: OArr<ModalStoreEntry> = mobx.observable.array([], { name: "ModalsModel-store", deep: false });
store: OArr<ModalStoreEntry> = mobx.observable.array([], { name: "ModalsModel-store" });
pushModal(modalId: string, props?: any) {
pushModal(modalId: string) {
const modalFactory = modalsRegistry[modalId];
if (modalFactory && !this.store.some((modal) => modal.id === modalId)) {
mobx.action(() => {
this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4(), props });
this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4() });
})();
}
}
-10
View File
@@ -31,8 +31,6 @@ import {
RendererContext,
ClientDataType,
AlertMessageType,
UserInputRequest,
UserInputResponsePacket,
ScreenLinesType,
RemoteViewType,
CommandRtnType,
@@ -904,10 +902,6 @@ class Model {
this.getScreenById_single(snc.screenid)?.setNumRunningCmds(snc.num);
}
}
if ("userinputrequest" in update) {
let userInputRequest: UserInputRequest = update.userinputrequest;
this.modalsModel.pushModal(appconst.USER_INPUT, userInputRequest);
}
}
updateRemotes(remotes: RemoteType[]): void {
@@ -1315,10 +1309,6 @@ class Model {
this.ws.pushMessage(inputPacket);
}
sendUserInput(userInputResponsePacket: UserInputResponsePacket) {
this.ws.pushMessage(userInputResponsePacket);
}
sendCmdInputText(screenId: string, sp: StrWithPos) {
let pk: CmdInputTextPacketType = {
type: "cmdinputtext",
+3 -13
View File
@@ -51,8 +51,6 @@ import {
HistoryViewDataType,
AlertMessageType,
HistorySearchParams,
UserInputRequest,
UserInputResponsePacket,
FocusTypeStrs,
ScreenLinesType,
HistoryTypeStrs,
@@ -3378,14 +3376,14 @@ class RemotesModel {
}
class ModalsModel {
store: OArr<T.ModalStoreEntry> = mobx.observable.array([], { name: "ModalsModel-store", deep: false });
store: OArr<T.ModalStoreEntry> = mobx.observable.array([], { name: "ModalsModel-store" });
pushModal(modalId: string, props?: any) {
pushModal(modalId: string) {
const modalFactory = modalsRegistry[modalId];
if (modalFactory && !this.store.some((modal) => modal.id === modalId)) {
mobx.action(() => {
this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4(), props: props });
this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4() });
})();
}
}
@@ -4186,10 +4184,6 @@ class Model {
this.getScreenById_single(snc.screenid)?.setNumRunningCmds(snc.num);
}
}
if ("userinputrequest" in update) {
let userInputRequest: UserInputRequest = update.userinputrequest;
this.modalsModel.pushModal(appconst.USER_INPUT, userInputRequest);
}
}
updateRemotes(remotes: RemoteType[]): void {
@@ -4597,10 +4591,6 @@ class Model {
this.ws.pushMessage(inputPacket);
}
sendUserInput(userInputResponsePacket: UserInputResponsePacket) {
this.ws.pushMessage(userInputResponsePacket);
}
sendCmdInputText(screenId: string, sp: T.StrWithPos) {
let pk: T.CmdInputTextPacketType = {
type: "cmdinputtext",
-21
View File
@@ -333,7 +333,6 @@ type ModelUpdateType = {
alertmessage?: AlertMessageType;
screenstatusindicators?: ScreenStatusIndicatorUpdateType[];
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType[];
userinputrequest?: UserInputRequest;
};
type HistoryViewDataType = {
@@ -596,23 +595,6 @@ type HistorySearchParams = {
filterCmds?: boolean;
};
type UserInputRequest = {
requestid: string;
querytext: string;
responsetype: string;
title: string;
markdown: boolean;
timeoutms: number;
};
type UserInputResponsePacket = {
type: string;
requestid: string;
text?: string;
confirm?: boolean;
errormsg?: string;
};
type RenderModeType = "normal" | "collapsed" | "expanded";
type WebScreen = {
@@ -748,7 +730,6 @@ type ModalStoreEntry = {
id: string;
component: React.ComponentType;
uniqueKey: string;
props?: any;
};
type StrWithPos = {
@@ -828,8 +809,6 @@ export type {
RenderModeType,
AlertMessageType,
HistorySearchParams,
UserInputRequest,
UserInputResponsePacket,
ScreenLinesType,
FocusTypeStrs,
HistoryTypeStrs,
+6 -2
View File
@@ -156,12 +156,16 @@ func (p *PacketParser) trySendRpcResponse(pk PacketType) bool {
return false
}
p.Lock.Lock()
defer p.Lock.Unlock()
entry := p.RpcMap[respId]
p.Lock.Unlock()
if entry == nil {
return false
}
entry.RespCh <- respPk
// nonblocking send
select {
case entry.RespCh <- respPk:
default:
}
return true
}
File diff suppressed because it is too large Load Diff
-5
View File
@@ -24,11 +24,6 @@ const (
R_RemoteConnected = 16
)
const (
ConnectedRemote = "connected"
LocalRemote = "local"
)
type resolvedIds struct {
SessionId string
ScreenId string
+6 -13
View File
@@ -40,7 +40,7 @@ import (
"golang.org/x/mod/semver"
)
const UseSshLibrary = true
const UseSshLibrary = false
const RemoteTypeMShell = "mshell"
const DefaultTerm = "xterm-256color"
@@ -50,7 +50,6 @@ const RemoteTermRows = 8
const RemoteTermCols = 80
const PtyReadBufSize = 100
const RemoteConnectTimeout = 15 * time.Second
const RpcIterChannelSize = 100
var envVarsToStrip map[string]bool = map[string]bool{
"PROMPT": true,
@@ -666,12 +665,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState {
if vars["remoteuser"] == "root" || vars["sudo"] == "1" {
vars["isroot"] = "1"
}
varsCopy := make(map[string]string)
// deep copy so that concurrent calls don't collide on this data
for key, value := range vars {
varsCopy[key] = value
}
state.RemoteVars = varsCopy
state.RemoteVars = vars
state.ActiveShells = msh.StateMap.GetShells()
return state
}
@@ -1209,10 +1203,6 @@ func (msh *MShellProc) ReInit(ctx context.Context, shellType string) (*packet.Sh
return ssPk, nil
}
func (msh *MShellProc) WriteFile(ctx context.Context, writePk *packet.WriteFilePacketType) (*packet.RpcResponseIter, error) {
return msh.PacketRpcIter(ctx, writePk)
}
func (msh *MShellProc) StreamFile(ctx context.Context, streamPk *packet.StreamFilePacketType) (*packet.RpcResponseIter, error) {
return msh.PacketRpcIter(ctx, streamPk)
}
@@ -1896,6 +1886,7 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru
RunPacket: runPacket,
})
go pushNumRunningCmdsUpdate(&runPacket.CK, 1)
return cmd, func() { removeCmdWait(runPacket.CK) }, nil
}
@@ -1934,7 +1925,7 @@ func (msh *MShellProc) PacketRpcIter(ctx context.Context, pk packet.RpcPacketTyp
return nil, fmt.Errorf("PacketRpc passed nil packet")
}
reqId := pk.GetReqId()
msh.ServerProc.Output.RegisterRpcSz(reqId, RpcIterChannelSize)
msh.ServerProc.Output.RegisterRpc(reqId)
err := msh.ServerProc.Input.SendPacketCtx(ctx, pk)
if err != nil {
return nil, err
@@ -2073,6 +2064,8 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) {
// fall-through (nothing to do)
}
}
go pushNumRunningCmdsUpdate(&donePk.CK, -1)
sstore.MainBus.SendUpdate(update)
return
}
+19 -380
View File
@@ -1,117 +1,43 @@
// Copyright 2023-2024, Command Line Inc.
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package remote
import (
"bytes"
"context"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"log"
"net"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/kevinburke/ssh_config"
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
)
type UserInputCancelError struct {
Err error
}
func (uice UserInputCancelError) Error() string {
return uice.Err.Error()
}
func createPublicKeyAuth(identityFile string, passphrase string) (ssh.Signer, error) {
func createPublicKeyAuth(identityFile string, passphrase string) (ssh.AuthMethod, error) {
privateKey, err := os.ReadFile(base.ExpandHomeDir(identityFile))
if err != nil {
return nil, fmt.Errorf("failed to read ssh key file. err: %+v", err)
}
signer, err := ssh.ParsePrivateKey(privateKey)
if err == nil {
return signer, err
}
if _, ok := err.(*ssh.PassphraseMissingError); !ok {
return nil, fmt.Errorf("failed to parse private ssh key. err: %+v", err)
}
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(passphrase))
if err == nil {
return signer, err
}
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
log.Printf("qwerty: %+v", err)
return nil, fmt.Errorf("failed to parse private ssh key. err: %+v", err)
}
request := &sstore.UserInputRequestType{
ResponseType: "text",
QueryText: fmt.Sprintf("Enter passphrase for the SSH key: %s", identityFile),
Title: "Publickey Auth + Passphrase",
}
ctx, cancelFn := context.WithTimeout(context.Background(), 15*time.Second)
defer cancelFn()
response, err := sstore.MainBus.GetUserInput(ctx, request)
if err != nil {
return nil, UserInputCancelError{Err: err}
}
return ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(response.Text))
}
func createDefaultPasswordCallbackPrompt(password string) func() (secret string, err error) {
return func() (secret string, err error) {
// this should be modified to return an error if no password is stored
// but an empty password is not sufficient because some systems allow
// empty passwords
return password, nil
}
}
func createInteractivePasswordCallbackPrompt() 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(), 15*time.Second)
defer cancelFn()
request := &sstore.UserInputRequestType{
ResponseType: "text",
QueryText: "Password:",
Title: "Password Authentication",
if errors.Is(err, &ssh.PassphraseMissingError{}) {
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(passphrase))
if err != nil {
return nil, fmt.Errorf("failed to parse private ssh key with passphrase. err: %+v", err)
}
} else {
return nil, fmt.Errorf("failed to parse private ssh key. err: %+v", err)
}
response, err := sstore.MainBus.GetUserInput(ctx, request)
if err != nil {
return "", err
}
return response.Text, nil
}
return ssh.PublicKeys(signer), nil
}
func createCombinedPasswordCallbackPrompt(password 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()
}
return prompt()
}
}
func createNaiveKbdInteractiveChallenge(password 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) {
func createKeyboardInteractiveAuth(password string) ssh.AuthMethod {
challenge := func(name, instruction string, questions []string, echos []bool) (answers []string, err error) {
for _, q := range questions {
if strings.Contains(strings.ToLower(q), "password") {
answers = append(answers, password)
@@ -121,291 +47,7 @@ func createNaiveKbdInteractiveChallenge(password string) func(name, instruction
}
return answers, nil
}
}
func createInteractiveKbdInteractiveChallenge() 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)
if err != nil {
return nil, err
}
answers = append(answers, answer)
}
return answers, nil
}
}
func promptChallengeQuestion(question string, echo bool) (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(), 15*time.Second)
defer cancelFn()
request := &sstore.UserInputRequestType{
ResponseType: "text",
QueryText: question,
Title: "Keyboard Interactive Authentication",
}
response, err := sstore.MainBus.GetUserInput(ctx, request)
if err != nil {
return "", err
}
return response.Text, nil
}
func createCombinedKbdInteractiveChallenge(password 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()
}
return challenge(name, instruction, questions, echos)
}
}
func openKnownHostsForEdit(knownHostsFilename string) (*os.File, error) {
path, _ := filepath.Split(knownHostsFilename)
err := os.MkdirAll(path, 0700)
if err != nil {
return nil, err
}
return os.OpenFile(knownHostsFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
}
func writeToKnownHosts(knownHostsFile string, newLine string, getUserVerification func() (*scpacket.UserInputResponsePacketType, error)) error {
if getUserVerification == nil {
getUserVerification = func() (*scpacket.UserInputResponsePacketType, error) {
return &scpacket.UserInputResponsePacketType{
Type: "confirm",
Confirm: true,
}, nil
}
}
path, _ := filepath.Split(knownHostsFile)
err := os.MkdirAll(path, 0700)
if err != nil {
return err
}
f, err := os.OpenFile(knownHostsFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
// do not close writeable files with defer
// this file works, so let's ask the user for permission
response, err := getUserVerification()
if err != nil {
f.Close()
return UserInputCancelError{Err: err}
}
if !response.Confirm {
f.Close()
return UserInputCancelError{Err: fmt.Errorf("Canceled by the user")}
}
_, err = f.WriteString(newLine)
return f.Close()
}
func createUnknownKeyVerifier(knownHostsFile string, hostname string, remote string, key ssh.PublicKey) func() (*scpacket.UserInputResponsePacketType, error) {
base64Key := base64.StdEncoding.EncodeToString(key.Marshal())
queryText := fmt.Sprintf(
"The authenticity of host '%s (%s)' can't be established "+
"as it **does not exist in any checked known_hosts files**. "+
"The host you are attempting to connect to provides this %s key: \n"+
"%s.\n\n"+
"**Would you like to continue connecting?** If so, the key will be permanently "+
"added to the file %s "+
"to protect from future man-in-the-middle attacks.", hostname, remote, key.Type(), base64Key, knownHostsFile)
request := &sstore.UserInputRequestType{
ResponseType: "confirm",
QueryText: queryText,
Markdown: true,
Title: "Known Hosts Key Missing",
}
return func() (*scpacket.UserInputResponsePacketType, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelFn()
return sstore.MainBus.GetUserInput(ctx, request)
}
}
func createMissingKnownHostsVerifier(knownHostsFile string, hostname string, remote string, key ssh.PublicKey) func() (*scpacket.UserInputResponsePacketType, error) {
base64Key := base64.StdEncoding.EncodeToString(key.Marshal())
queryText := fmt.Sprintf(
"The authenticity of host '%s (%s)' can't be established "+
"as **no known_hosts files could be found**. "+
"The host you are attempting to connect to provides this %s key: \n"+
"%s.\n\n"+
"**Would you like to continue connecting?** If so: \n"+
"- %s will be created \n"+
"- the key will be added to %s\n\n"+
"This will protect from future man-in-the-middle attacks.", hostname, remote, key.Type(), base64Key, knownHostsFile, knownHostsFile)
request := &sstore.UserInputRequestType{
ResponseType: "confirm",
QueryText: queryText,
Markdown: true,
Title: "Known Hosts File Missing",
}
return func() (*scpacket.UserInputResponsePacketType, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelFn()
return sstore.MainBus.GetUserInput(ctx, request)
}
}
func lineContainsMatch(line []byte, matches [][]byte) bool {
for _, match := range matches {
if bytes.Contains(line, match) {
return true
}
}
return false
}
func createHostKeyCallback(opts *sstore.SSHOpts) (ssh.HostKeyCallback, error) {
rawUserKnownHostsFiles, _ := ssh_config.GetStrict(opts.SSHHost, "UserKnownHostsFile")
userKnownHostsFiles := strings.Fields(rawUserKnownHostsFiles) // TODO - smarter splitting escaped spaces and quotes
rawGlobalKnownHostsFiles, _ := ssh_config.GetStrict(opts.SSHHost, "GlobalKnownHostsFile")
globalKnownHostsFiles := strings.Fields(rawGlobalKnownHostsFiles) // TODO - smarter splitting escaped spaces and quotes
unexpandedKnownHostsFiles := append(userKnownHostsFiles, globalKnownHostsFiles...)
var knownHostsFiles []string
for _, filename := range unexpandedKnownHostsFiles {
knownHostsFiles = append(knownHostsFiles, base.ExpandHomeDir(filename))
}
// there are no good known hosts files
if len(knownHostsFiles) == 0 {
return nil, fmt.Errorf("no known_hosts files provided by ssh. defaults are overridden")
}
var unreadableFiles []string
// the library we use isn't very forgiving about files that are formatted
// incorrectly. if a problem file is found, it is removed from our list
// and we try again
var basicCallback ssh.HostKeyCallback
for basicCallback == nil && len(knownHostsFiles) > 0 {
var err error
basicCallback, err = knownhosts.New(knownHostsFiles...)
if serr, ok := err.(*os.PathError); ok {
badFile := serr.Path
unreadableFiles = append(unreadableFiles, badFile)
var okFiles []string
for _, filename := range knownHostsFiles {
if filename != badFile {
okFiles = append(okFiles, filename)
}
}
if len(okFiles) >= len(knownHostsFiles) {
return nil, fmt.Errorf("problem file (%s) doesn't exist. this should not be possible", badFile)
}
knownHostsFiles = okFiles
} else if err != nil {
// TODO handle obscure problems if possible
return nil, fmt.Errorf("known_hosts formatting error: %+v", err)
}
}
waveHostKeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
err := basicCallback(hostname, remote, key)
if err == nil {
// success
return nil
} else if _, ok := err.(*knownhosts.RevokedError); ok {
// revoked credentials are refused outright
return err
} else if _, ok := err.(*knownhosts.KeyError); !ok {
// this is an unknown error (note the !ok is opposite of usual)
return err
}
serr, _ := err.(*knownhosts.KeyError)
if len(serr.Want) == 0 {
// the key was not found
// try to write to a file that could be parsed
var err error
for _, filename := range knownHostsFiles {
newLine := knownhosts.Line([]string{knownhosts.Normalize(hostname)}, key)
getUserVerification := createUnknownKeyVerifier(filename, hostname, remote.String(), key)
err = writeToKnownHosts(filename, newLine, getUserVerification)
if err == nil {
break
}
if serr, ok := err.(UserInputCancelError); ok {
return serr
}
}
// try to write to a file that could not be read (file likely doesn't exist)
// should catch cases where there is no known_hosts file
if err != nil {
for _, filename := range unreadableFiles {
newLine := knownhosts.Line([]string{knownhosts.Normalize(hostname)}, key)
getUserVerification := createMissingKnownHostsVerifier(filename, hostname, remote.String(), key)
err = writeToKnownHosts(filename, newLine, getUserVerification)
if err == nil {
knownHostsFiles = []string{filename}
break
}
if serr, ok := err.(UserInputCancelError); ok {
return serr
}
}
}
if err != nil {
return err
}
} else {
// the key changed
correctKeyFingerprint := base64.StdEncoding.EncodeToString(key.Marshal())
var bulletListKnownHosts []string
for _, knownHostName := range knownHostsFiles {
withBulletPoint := "- " + knownHostName
bulletListKnownHosts = append(bulletListKnownHosts, withBulletPoint)
}
var offendingKeysFmt []string
for _, badKey := range serr.Want {
formattedKey := "- " + base64.StdEncoding.EncodeToString(badKey.Key.Marshal())
offendingKeysFmt = append(offendingKeysFmt, formattedKey)
}
alertText := fmt.Sprintf("**WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!**\n\n"+
"If this is not expected, it is possible that someone could be trying to "+
"eavesdrop on you via a man-in-the-middle attack. "+
"Alternatively, the host you are connecting to may have changed its key. "+
"The %s key sent by the remote hist has the fingerprint: \n"+
"%s\n\n"+
"If you are sure this is correct, please update your known_hosts files to "+
"remove the lines with the offending before trying to connect again. \n"+
"**Known Hosts Files** \n"+
"%s\n\n"+
"**Offending Keys** \n"+
"%s", key.Type(), correctKeyFingerprint, strings.Join(bulletListKnownHosts, " \n"), strings.Join(offendingKeysFmt, " \n"))
update := &sstore.ModelUpdate{AlertMessage: &sstore.AlertMessageType{
Markdown: true,
Title: "Known Hosts Key Changed",
Message: alertText,
}}
sstore.MainBus.SendUpdate(update)
return fmt.Errorf("remote host identification has changed")
}
updatedCallback, err := knownhosts.New(knownHostsFiles...)
if err != nil {
return err
}
// try one final time
return updatedCallback(hostname, remote, key)
}
return waveHostKeyCallback, nil
return ssh.KeyboardInteractive(challenge)
}
func ConnectToClient(opts *sstore.SSHOpts) (*ssh.Client, error) {
@@ -418,17 +60,14 @@ func ConnectToClient(opts *sstore.SSHOpts) (*ssh.Client, error) {
identityFile = configIdentity
}
hostKeyCallback, err := createHostKeyCallback(opts)
if err != nil {
return nil, err
}
hostKeyCallback := ssh.InsecureIgnoreHostKey()
var authMethods []ssh.AuthMethod
publicKeySigner, err := createPublicKeyAuth(identityFile, opts.SSHPassword)
publicKeyAuth, err := createPublicKeyAuth(identityFile, opts.SSHPassword)
if err == nil {
authMethods = append(authMethods, ssh.PublicKeys(publicKeySigner))
authMethods = append(authMethods, publicKeyAuth)
}
authMethods = append(authMethods, ssh.RetryableAuthMethod(ssh.KeyboardInteractive(createCombinedKbdInteractiveChallenge(opts.SSHPassword)), 2))
authMethods = append(authMethods, ssh.RetryableAuthMethod(ssh.PasswordCallback(createCombinedPasswordCallbackPrompt(opts.SSHPassword)), 2))
authMethods = append(authMethods, createKeyboardInteractiveAuth(opts.SSHPassword))
authMethods = append(authMethods, ssh.Password(opts.SSHPassword))
configUser, _ := ssh_config.GetStrict(opts.SSHHost, "User")
configHostName, _ := ssh_config.GetStrict(opts.SSHHost, "HostName")
+12 -89
View File
@@ -6,84 +6,20 @@ package scpacket
import (
"fmt"
"reflect"
"regexp"
"strings"
"github.com/alessio/shellescape"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
)
var RemoteNameRe = regexp.MustCompile("^\\*?[a-zA-Z0-9_-]+$")
type RemotePtrType struct {
OwnerId string `json:"ownerid"`
RemoteId string `json:"remoteid"`
Name string `json:"name"`
}
func (r RemotePtrType) IsSessionScope() bool {
return strings.HasPrefix(r.Name, "*")
}
func (rptr *RemotePtrType) GetDisplayName(baseDisplayName string) string {
name := baseDisplayName
if rptr == nil {
return name
}
if rptr.Name != "" {
name = name + ":" + rptr.Name
}
if rptr.OwnerId != "" {
name = "@" + rptr.OwnerId + ":" + name
}
return name
}
func (r RemotePtrType) Validate() error {
if r.OwnerId != "" {
if _, err := uuid.Parse(r.OwnerId); err != nil {
return fmt.Errorf("invalid ownerid format: %v", err)
}
}
if r.RemoteId != "" {
if _, err := uuid.Parse(r.RemoteId); err != nil {
return fmt.Errorf("invalid remoteid format: %v", err)
}
}
if r.Name != "" {
ok := RemoteNameRe.MatchString(r.Name)
if !ok {
return fmt.Errorf("invalid remote name")
}
}
return nil
}
func (r RemotePtrType) MakeFullRemoteRef() string {
if r.RemoteId == "" {
return ""
}
if r.OwnerId == "" && r.Name == "" {
return r.RemoteId
}
if r.OwnerId != "" && r.Name == "" {
return fmt.Sprintf("@%s:%s", r.OwnerId, r.RemoteId)
}
if r.OwnerId == "" && r.Name != "" {
return fmt.Sprintf("%s:%s", r.RemoteId, r.Name)
}
return fmt.Sprintf("@%s:%s:%s", r.OwnerId, r.RemoteId, r.Name)
}
const FeCommandPacketStr = "fecmd"
const WatchScreenPacketStr = "watchscreen"
const FeInputPacketStr = "feinput"
const RemoteInputPacketStr = "remoteinput"
const CmdInputTextPacketStr = "cmdinputtext"
const UserInputResponsePacketStr = "userinputresp"
type FeCommandPacketType struct {
Type string `json:"type"`
@@ -119,20 +55,20 @@ func (pk *FeCommandPacketType) GetRawStr() string {
}
type UIContextType struct {
SessionId string `json:"sessionid"`
ScreenId string `json:"screenid"`
Remote *RemotePtrType `json:"remote,omitempty"`
WinSize *packet.WinSize `json:"winsize,omitempty"`
Build string `json:"build,omitempty"`
SessionId string `json:"sessionid"`
ScreenId string `json:"screenid"`
Remote *sstore.RemotePtrType `json:"remote,omitempty"`
WinSize *packet.WinSize `json:"winsize,omitempty"`
Build string `json:"build,omitempty"`
}
type FeInputPacketType struct {
Type string `json:"type"`
CK base.CommandKey `json:"ck"`
Remote RemotePtrType `json:"remote"`
InputData64 string `json:"inputdata64"`
SigName string `json:"signame,omitempty"`
WinSize *packet.WinSize `json:"winsize,omitempty"`
Type string `json:"type"`
CK base.CommandKey `json:"ck"`
Remote sstore.RemotePtrType `json:"remote"`
InputData64 string `json:"inputdata64"`
SigName string `json:"signame,omitempty"`
WinSize *packet.WinSize `json:"winsize,omitempty"`
}
type RemoteInputPacketType struct {
@@ -156,21 +92,12 @@ type CmdInputTextPacketType struct {
Text utilfn.StrWithPos `json:"text"`
}
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"`
}
func init() {
packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(FeCommandPacketType{}))
packet.RegisterPacketType(WatchScreenPacketStr, reflect.TypeOf(WatchScreenPacketType{}))
packet.RegisterPacketType(FeInputPacketStr, reflect.TypeOf(FeInputPacketType{}))
packet.RegisterPacketType(RemoteInputPacketStr, reflect.TypeOf(RemoteInputPacketType{}))
packet.RegisterPacketType(CmdInputTextPacketStr, reflect.TypeOf(CmdInputTextPacketType{}))
packet.RegisterPacketType(UserInputResponsePacketStr, reflect.TypeOf(UserInputResponsePacketType{}))
}
func (*CmdInputTextPacketType) GetType() string {
@@ -212,7 +139,3 @@ func MakeRemoteInputPacket() *RemoteInputPacketType {
func (*RemoteInputPacketType) GetType() string {
return RemoteInputPacketStr
}
func (*UserInputResponsePacketType) GetType() string {
return UserInputResponsePacketStr
}
-12
View File
@@ -281,18 +281,6 @@ func (ws *WSState) processMessage(msgBytes []byte) error {
sstore.ScreenMemSetCmdInputText(cmdInputPk.ScreenId, cmdInputPk.Text, cmdInputPk.SeqNum)
return nil
}
if pk.GetType() == scpacket.UserInputResponsePacketStr {
userInputRespPk := pk.(*scpacket.UserInputResponsePacketType)
uich, ok := sstore.MainBus.GetUserInputChannel(userInputRespPk.RequestId)
if !ok {
return fmt.Errorf("received User Input Response with invalid Id (%s): %v\n", userInputRespPk.RequestId, err)
}
select {
case uich <- userInputRespPk:
default:
}
return nil
}
return fmt.Errorf("got ws bad message: %v", pk.GetType())
}
-1
View File
@@ -950,7 +950,6 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.C
// This is not a fatal error, so just log it
log.Printf("error setting status indicator level after done packet: %v\n", err)
}
IncrementNumRunningCmds_Update(update, screenId, -1)
return update, nil
}

Some files were not shown because too many files have changed in this diff Show More