Compare commits

...

14 Commits

Author SHA1 Message Date
Sylvia Crowe 00c2ab9fc4 merge branch 'main' into ssh-extra-fixes 2024-03-14 16:20:55 -07:00
Sylvia Crowe 81294820c8 feat: add connection segfault protection
A user recently reported a crash on a failed connection. This handles
these errors more gracefully than the previous system and prints the
errors to the connection prompt.
2024-03-14 16:19:32 -07:00
Evan Simkowitz c546464751 Add scripthaus to path for Build Helper workflow (#458) 2024-03-14 12:23:35 -07:00
Evan Simkowitz 3f7c48a9ae Add blur for line actions (#455) 2024-03-14 12:19:21 -07:00
dependabot[bot] 21bf07bbf8 Bump follow-redirects from 1.15.4 to 1.15.6 (#457)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-14 12:18:11 -07:00
Evan Simkowitz fd05e11d3a Fix CodeQL checks for Go (#456)
* Fix CodeQL checks for Go

* add scripthaus checkout

* fix

* fix

* add scripthaus to path

* test go setup before init
2024-03-14 12:12:51 -07:00
Mike Sawka 8ac7d8c241 small cleanup of sizing for media plugin, null checks (#454)
* clean up media viewer size, null check, check for fileurl

* make video responsive to container
2024-03-14 10:32:37 -07:00
june 3f988c0e6b Add support for svg images to imageview command (#453) 2024-03-14 09:55:52 -07:00
Sylvia Crowe bb8f5dc660 chore: remove legacy remote launcher
The Legacy Launch function was kept as a way to maintain the old ssh
system while adding the new one. Now that the new system has been
released with minimal issues, it is safe to fully remove the legacy
launcher.
2024-03-14 01:07:54 -07:00
Mike Sawka 0f2f6c9cc0 quick media viewer (using new hmac read-file urls) (#451) 2024-03-14 00:49:05 -07:00
Mike Sawka 697f7c0758 fix hmac implementation/typo. read WaveAuthKey and store in scbase instead of main-server (#450) 2024-03-14 00:22:59 -07:00
Sylvia Crowe c3917444a2 merge branch 'main' into ssh-extra-fixes 2024-03-13 18:08:29 -07:00
Sylvia Crowe 2cc86f39d6 feat: relax user requirement for canonical name
Previously, the canonical name required a user as a part of it. This
change makes it possible to omit the username if desired. In this case,
the connection will default to your current user.
2024-03-08 00:32:46 -08:00
Sylvia Crowe af22fd38af feat: don't hide nonsecret kbd-interactive answers
Some Kbd-Interactive challenges are marked with echo true. For these, it
makes sense to use a TextField in place of a PasswordField. This change
differentiates between the two options.
2024-03-07 23:32:15 -08:00
20 changed files with 266 additions and 213 deletions
+2 -1
View File
@@ -41,6 +41,7 @@ jobs:
cd scripthaus;
go get ./...;
CGO_ENABLED=1 go build -o scripthaus cmd/main.go
echo $PWD >> $GITHUB_PATH
- uses: actions/setup-node@v4
with:
node-version: ${{env.NODE_VERSION}}
@@ -53,7 +54,7 @@ jobs:
- name: Install Yarn Dependencies
run: yarn --frozen-lockfile
- name: Build ${{ matrix.platform }}/${{ matrix.arch }}
run: ./scripthaus/scripthaus run ${{ matrix.scripthaus }}
run: scripthaus run ${{ matrix.scripthaus }}
env:
GOARCH: ${{ matrix.arch }}
CSC_LINK: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE}}
+32 -1
View File
@@ -47,6 +47,32 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Checkout Scripthaus (Go only)
if: matrix.language == 'go'
uses: actions/checkout@v4
with:
repository: scripthaus-dev/scripthaus
path: scripthaus
- name: Setup Go (Go only)
uses: actions/setup-go@v5
if: matrix.language == 'go'
with:
go-version: stable
cache-dependency-path: |
wavesrv/go.sum
waveshell/go.sum
scripthaus/go.sum
- name: Install Scripthaus (Go only)
if: matrix.language == 'go'
run: |
go work use ./scripthaus;
cd scripthaus;
go get ./...;
CGO_ENABLED=1 go build -o scripthaus cmd/main.go
echo $PWD >> $GITHUB_PATH
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
@@ -61,9 +87,14 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- name: Autobuild (not Go)
if: matrix.language != 'go'
uses: github/codeql-action/autobuild@v3
- name: Build (Go only)
if: matrix.language == 'go'
run: scripthaus run build-backend
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+14 -5
View File
@@ -1,7 +1,7 @@
import * as React from "react";
import { GlobalModel } from "@/models";
import { Choose, When, If } from "tsx-control-statements/components";
import { Modal, PasswordField, Markdown, Checkbox } from "@/elements";
import { Modal, PasswordField, TextField, Markdown, Checkbox } from "@/elements";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import "./userinput.less";
@@ -82,8 +82,17 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
</If>
<If condition={!userInputRequest.markdown}>{userInputRequest.querytext}</If>
</div>
<Choose>
<When condition={userInputRequest.responsetype == "text"}>
<If condition={userInputRequest.responsetype == "text"}>
<If condition={userInputRequest.publictext}>
<TextField
onChange={setResponseText}
value={responseText}
maxLength={400}
autoFocus={true}
onKeyDown={(e) => handleTextKeyDown(e)}
/>
</If>
<If condition={!userInputRequest.publictext}>
<PasswordField
onChange={setResponseText}
value={responseText}
@@ -91,8 +100,8 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
autoFocus={true}
onKeyDown={(e) => handleTextKeyDown(e)}
/>
</When>
</Choose>
</If>
</If>
</div>
<If condition={userInputRequest.checkboxmsg != ""}>
<Checkbox
+1
View File
@@ -126,6 +126,7 @@
&:hover .line-actions {
background-color: var(--line-actions-bg-color);
backdrop-filter: blur(8px);
.line-icon {
visibility: visible;
}
+4 -1
View File
@@ -48,12 +48,15 @@ class SimpleImageRenderer extends React.Component<
return (
<div className="image-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
<div className="load-error-text">
ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found
ERROR: file {dataBlob?.name ? JSON.stringify(dataBlob.name) : ""} not found
</div>
</div>
);
}
if (this.objUrl == null) {
if (dataBlob.name?.endsWith(".svg")) {
dataBlob = new Blob([dataBlob], { type: "image/svg+xml" }) as ExtBlob;
}
this.objUrl = URL.createObjectURL(dataBlob);
}
let opts = this.props.opts;
+14
View File
@@ -0,0 +1,14 @@
.media-renderer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: var(--termpad);
video {
object-fit: contain;
object-position: center;
height: 100%;
width: auto;
}
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import * as util from "@/util/util";
import { GlobalModel } from "@/models";
import "./media.less";
@mobxReact.observer
class SimpleMediaRenderer extends React.Component<
{ data: ExtBlob; context: RendererContext; opts: RendererOpts; savedHeight: number; lineState: LineStateType },
{}
> {
objUrl: string = null;
componentWillUnmount() {
if (this.objUrl != null) {
URL.revokeObjectURL(this.objUrl);
}
}
render() {
let dataBlob = this.props.data;
if (dataBlob == null || dataBlob.notFound) {
return (
<div className="media-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
<div className="load-error-text">
ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found
</div>
</div>
);
}
let fileUrl = this.props.lineState["wave:fileurl"];
if (util.isBlank(fileUrl)) {
return (
<div className="media-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
<div className="load-error-text">
ERROR: no fileurl found (please use `mediaview` to view media files)
</div>
</div>
);
}
let fullVideoUrl = GlobalModel.getBaseHostPort() + fileUrl;
const opts = this.props.opts;
const height = opts.idealSize.height - 10;
const width = opts.maxSize.width - 10;
return (
<div className="media-renderer" style={{ height: height, width: width }}>
<video controls>
<source src={fullVideoUrl} />
</video>
</div>
);
}
}
export { SimpleMediaRenderer };
+1 -1
View File
@@ -32,7 +32,7 @@ class SimplePdfRenderer extends React.Component<
);
}
if (this.objUrl == null) {
const pdfBlob = new File([dataBlob], "test.pdf", { type: "application/pdf" });
const pdfBlob = new File([dataBlob], dataBlob.name ?? "file.pdf", { type: "application/pdf" });
this.objUrl = URL.createObjectURL(pdfBlob);
}
const opts = this.props.opts;
+11
View File
@@ -8,6 +8,7 @@ import { SimpleMustacheRenderer } from "./mustache/mustache";
import { CSVRenderer } from "./csv/csv";
import { OpenAIRenderer, OpenAIRendererModel } from "./openai/openai";
import { SimplePdfRenderer } from "./pdf/pdf";
import { SimpleMediaRenderer } from "./media/media";
import { isBlank } from "@/util/util";
import { sprintf } from "sprintf-js";
@@ -89,6 +90,16 @@ const PluginConfigs: RendererPluginType[] = [
mimeTypes: ["application/pdf"],
simpleComponent: SimplePdfRenderer,
},
{
name: "media",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["video/*", "audio/*"],
simpleComponent: SimpleMediaRenderer,
},
];
class PluginModelClass {
+1
View File
@@ -667,6 +667,7 @@ declare global {
markdown: boolean;
timeoutms: number;
checkboxmsg: string;
publictext: boolean;
};
type UserInputResponsePacket = {
+6 -8
View File
@@ -74,7 +74,6 @@ var BuildTime = "0"
var GlobalLock = &sync.Mutex{}
var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState
var GlobalAuthKey string
var shutdownOnce sync.Once
var ContentTypeHeaderValidRe = regexp.MustCompile(`^\w+/[\w.+-]+$`)
@@ -140,7 +139,7 @@ func HandleWs(w http.ResponseWriter, r *http.Request) {
}
state := getWSState(clientId)
if state == nil {
state = scws.MakeWSState(clientId, GlobalAuthKey)
state = scws.MakeWSState(clientId, scbase.WaveAuthKey)
state.ReplaceShell(shell)
setWSState(state)
} else {
@@ -687,7 +686,7 @@ func AuthKeyMiddleWare(next http.Handler) http.Handler {
w.Write([]byte("no x-authkey header"))
return
}
if reqAuthKey != GlobalAuthKey {
if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(500)
w.Write([]byte("x-authkey header is invalid"))
return
@@ -707,14 +706,14 @@ func AuthKeyWrapAllowHmac(fn WebFnType) WebFnType {
w.Write([]byte("no x-authkey header"))
return
}
hmacOk, err := promptenc.ValidateUrlHmac([]byte(GlobalAuthKey), r.URL.Path, qvals)
hmacOk, err := promptenc.ValidateUrlHmac([]byte(scbase.WaveAuthKey), r.URL.Path, qvals)
if err != nil || !hmacOk {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error validating hmac")))
return
}
// fallthrough (hmac is valid)
} else if reqAuthKey != GlobalAuthKey {
} else if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(500)
w.Write([]byte("x-authkey header is invalid"))
return
@@ -733,7 +732,7 @@ func AuthKeyWrap(fn WebFnType) WebFnType {
w.Write([]byte("no x-authkey header"))
return
}
if reqAuthKey != GlobalAuthKey {
if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(500)
w.Write([]byte("x-authkey header is invalid"))
return
@@ -890,12 +889,11 @@ func main() {
}
return
}
authKey, err := scbase.ReadWaveAuthKey()
err = scbase.InitializeWaveAuthKey()
if err != nil {
log.Printf("[error] %v\n", err)
return
}
GlobalAuthKey = authKey
err = sstore.TryMigrateUp()
if err != nil {
log.Printf("[error] migrate up: %v\n", err)
+74 -14
View File
@@ -37,6 +37,7 @@ import (
"github.com/wavetermdev/waveterm/wavesrv/pkg/comp"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
"github.com/wavetermdev/waveterm/wavesrv/pkg/promptenc"
"github.com/wavetermdev/waveterm/wavesrv/pkg/releasechecker"
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote/openai"
@@ -125,7 +126,7 @@ var SetVarScopes = []SetVarScope{
{ScopeName: "remote", VarNames: []string{}},
}
var userHostRe = regexp.MustCompile(`^(sudo@)?([a-zA-Z0-9][a-zA-Z0-9._@\\-]*)@([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
var userHostRe = regexp.MustCompile(`^(sudo@)?([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
var remoteAliasRe = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$")
var rendererRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.:-]*$")
@@ -280,6 +281,7 @@ func init() {
registerCmdFn("mdview", MarkdownViewCommand)
registerCmdFn("markdownview", MarkdownViewCommand)
registerCmdFn("pdfview", PdfViewCommand)
registerCmdFn("mediaview", MediaViewCommand)
registerCmdFn("csvview", CSVViewCommand)
}
@@ -1794,6 +1796,7 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType, isLocal b
return nil, fmt.Errorf("invalid format of user@host argument")
}
sudoStr, remoteUser, remoteHost, remotePortStr := m[1], m[2], m[3], m[4]
remoteUser = strings.Trim(remoteUser, "@")
var uhPort int
if remotePortStr != "" {
var err error
@@ -1835,7 +1838,11 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType, isLocal b
return nil, fmt.Errorf("invalid port argument, \"%d\" is not in the range of 1 to 65535", portVal)
}
sshOpts.SSHPort = portVal
canonicalName = remoteUser + "@" + remoteHost
if remoteUser == "" {
canonicalName = remoteHost
} else {
canonicalName = remoteUser + "@" + remoteHost
}
if portVal != 0 && portVal != 22 {
canonicalName = canonicalName + ":" + strconv.Itoa(portVal)
}
@@ -2134,17 +2141,17 @@ func createSshImportSummary(changeList map[string][]string) string {
func NewHostInfo(hostName string) (*HostInfoType, error) {
userName, _ := ssh_config.GetStrict(hostName, "User")
if userName == "" {
// we cannot store a remote with a missing user
// in the current setup
return nil, fmt.Errorf("could not parse \"%s\" - no User in config\n", hostName)
var canonicalName string
if userName != "" {
canonicalName = userName + "@" + hostName
} else {
canonicalName = hostName
}
canonicalName := userName + "@" + hostName
// check if user and host are okay
// check if canonicalname is okay
m := userHostRe.FindStringSubmatch(canonicalName)
if m == nil || m[2] == "" || m[3] == "" {
return nil, fmt.Errorf("could not parse \"%s\" - %s did not fit user@host requirement\n", hostName, canonicalName)
if m == nil {
return nil, fmt.Errorf("could not parse \"%s\" - %s did not fit user@host requirement", hostName, canonicalName)
}
portStr, _ := ssh_config.GetStrict(hostName, "Port")
@@ -2155,10 +2162,10 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
portVal, err = strconv.Atoi(portStr)
if err != nil {
// do not make assumptions about port if incorrectly configured
return nil, fmt.Errorf("could not parse \"%s\" (%s) - %s could not be converted to a valid port\n", hostName, canonicalName, portStr)
return nil, fmt.Errorf("could not parse \"%s\" (%s) - %s could not be converted to a valid port", hostName, canonicalName, portStr)
}
if portVal <= 0 || portVal > 65535 {
return nil, fmt.Errorf("could not parse port \"%d\": number is not valid for a port\n", portVal)
return nil, fmt.Errorf("could not parse port \"%d\": number is not valid for a port", portVal)
}
}
identityFile, _ := ssh_config.GetStrict(hostName, "IdentityFile")
@@ -4861,11 +4868,12 @@ func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
if pk.Args[0] == "" {
return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk))
}
filePath := pk.Args[0]
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
if err != nil {
return nil, err
}
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), pk.Args[0])
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), filePath)
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
@@ -4874,7 +4882,7 @@ func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
// set the line state
lineState := make(map[string]any)
lineState[sstore.LineState_Source] = "file"
lineState[sstore.LineState_File] = pk.Args[0]
lineState[sstore.LineState_File] = filePath
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "image", lineState)
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
@@ -4915,6 +4923,58 @@ func PdfViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbu
return update, nil
}
func MakeReadFileUrl(screenId string, lineId string, filePath string) (string, error) {
qvals := make(url.Values)
qvals.Set("screenid", screenId)
qvals.Set("lineid", lineId)
qvals.Set("path", filePath)
qvals.Set("nonce", uuid.New().String())
hmacStr, err := promptenc.ComputeUrlHmac([]byte(scbase.WaveAuthKey), "/api/read-file", qvals)
if err != nil {
return "", fmt.Errorf("error computing hmac-url: %v", err)
}
qvals.Set("hmac", hmacStr)
return "/api/read-file?" + qvals.Encode(), nil
}
func MediaViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
if len(pk.Args) == 0 {
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
}
// TODO more error checking on filename format?
if pk.Args[0] == "" {
return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk))
}
fileName := pk.Args[0]
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
if err != nil {
return nil, err
}
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), fileName)
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
return nil, err
}
// compute hmac read-file URL
readFileUrl, err := MakeReadFileUrl(ids.ScreenId, cmd.LineId, fileName)
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
return nil, fmt.Errorf("error making read-file url: %v", err)
}
// set the line state
lineState := make(map[string]any)
lineState[sstore.LineState_FileUrl] = readFileUrl
lineState[sstore.LineState_File] = fileName
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "media", lineState)
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
return nil, err
}
update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive))
return update, nil
}
func MarkdownViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
if len(pk.Args) == 0 {
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
+1
View File
@@ -36,6 +36,7 @@ var BareMetaCmds = []BareMetaCmdDecl{
{"mdview", "markdownview"},
{"csvview", "csvview"},
{"pdfview", "pdfview"},
{"mediaview", "mediaview"},
}
const (
+2 -2
View File
@@ -12,7 +12,7 @@ import (
)
func ComputeUrlHmac(key []byte, baseUrl string, qvals url.Values) (string, error) {
if qvals.Has("nonce") {
if !qvals.Has("nonce") {
return "", fmt.Errorf("nonce is required for hmac")
}
if qvals.Has("hmac") {
@@ -37,7 +37,7 @@ func ValidateUrlHmac(key []byte, baseUrl string, qvalsOrig url.Values) (bool, er
qvals := copyUrlValues(qvalsOrig)
hmacStr := qvals.Get("hmac")
if hmacStr == "" {
return false, fmt.Errorf("no hmac key found"))
return false, fmt.Errorf("no hmac key found")
}
qvals.Del("hmac")
encStr := baseUrl + "?" + qvals.Encode()
+20 -167
View File
@@ -15,6 +15,7 @@ import (
"os/exec"
"path"
"regexp"
"runtime/debug"
"strconv"
"strings"
"sync"
@@ -44,8 +45,6 @@ import (
"golang.org/x/mod/semver"
)
const UseSshLibrary = true
const RemoteTypeMShell = "mshell"
const DefaultTerm = "xterm-256color"
const DefaultMaxPtySize = 1024 * 1024
@@ -129,12 +128,6 @@ type pendingStateKey struct {
RemotePtr sstore.RemotePtrType
}
// for conditional launch method based on ssh library in use
// remove once ssh library is stabilized
type Launcher interface {
Launch(*MShellProc, bool)
}
type MShellProc struct {
Lock *sync.Mutex
Remote *sstore.RemoteType
@@ -163,7 +156,6 @@ type MShellProc struct {
RunningCmds map[base.CommandKey]*RunCmdType
PendingStateCmds map[pendingStateKey]base.CommandKey // key=[remoteinstance name]
launcher Launcher // for conditional launch method based on ssh library in use. remove once ssh library is stabilized
Client *ssh.Client
}
@@ -188,12 +180,6 @@ func CanComplete(remoteType string) bool {
}
}
// for conditional launch method based on ssh library in use
// remove once ssh library is stabilized
func (msh *MShellProc) Launch(interactive bool) {
msh.launcher.Launch(msh, interactive)
}
func (msh *MShellProc) GetStatus() string {
msh.Lock.Lock()
defer msh.Lock.Unlock()
@@ -711,14 +697,8 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc {
RunningCmds: make(map[base.CommandKey]*RunCmdType),
PendingStateCmds: make(map[pendingStateKey]base.CommandKey),
StateMap: server.MakeShellStateMap(),
launcher: LegacyLauncher{}, // for conditional launch method based on ssh library in use. remove once ssh library is stabilized
DataPosMap: utilfn.MakeSyncMap[base.CommandKey, int64](),
}
// for conditional launch method based on ssh library in use
// remove once ssh library is stabilized
if UseSshLibrary {
rtn.launcher = NewLauncher{}
}
rtn.WriteToPtyBuffer("console for connection [%s]\n", r.GetName())
return rtn
@@ -1201,6 +1181,15 @@ func (msh *MShellProc) WaitAndSendPassword(pw string) {
}
func (msh *MShellProc) RunInstall(autoInstall bool) {
defer func() {
if r := recover(); r != nil {
errMsg := fmt.Errorf("this should not happen. if it does, please reach out to us in our discord or open an issue on our github\n\n"+
"error:\n%v\n\nstack trace:\n%s", r, string(debug.Stack()))
log.Printf("fatal error, %s\n", errMsg)
msh.WriteToPtyBuffer("*fatal error, %s\n", errMsg)
msh.setErrorStatus(errMsg)
}
}()
remoteCopy := msh.GetRemoteCopy()
if remoteCopy.Archived {
msh.WriteToPtyBuffer("*error: cannot install on archived remote\n")
@@ -1574,12 +1563,16 @@ func (msh *MShellProc) createWaveshellSession(clientCtx context.Context, remoteC
return wsSession, nil
}
// for conditional launch method based on ssh library in use
// remove once ssh library is stabilized
type NewLauncher struct{}
// func (msh *MShellProc) LaunchNew(interactive bool) {
func (NewLauncher) Launch(msh *MShellProc, interactive bool) {
func (msh *MShellProc) Launch(interactive bool) {
defer func() {
if r := recover(); r != nil {
errMsg := fmt.Errorf("this should not happen. if it does, please reach out to us in our discord or open an issue on our github\n\n"+
"error:\n%v\n\nstack trace:\n%s", r, string(debug.Stack()))
log.Printf("fatal error, %s\n", errMsg)
msh.WriteToPtyBuffer("*fatal error, %s\n", errMsg)
msh.setErrorStatus(errMsg)
}
}()
remoteCopy := msh.GetRemoteCopy()
if remoteCopy.Archived {
msh.WriteToPtyBuffer("cannot launch archived remote\n")
@@ -1687,146 +1680,6 @@ func (NewLauncher) Launch(msh *MShellProc, interactive bool) {
go msh.NotifyRemoteUpdate()
}
// for conditional launch method based on ssh library in use
// remove once ssh library is stabilized
type LegacyLauncher struct{}
// func (msh *MShellProc) LaunchLegacy(interactive bool) {
func (LegacyLauncher) Launch(msh *MShellProc, interactive bool) {
remoteCopy := msh.GetRemoteCopy()
if remoteCopy.Archived {
msh.WriteToPtyBuffer("cannot launch archived remote\n")
return
}
curStatus := msh.GetStatus()
if curStatus == StatusConnected {
msh.WriteToPtyBuffer("remote is already connected (no action taken)\n")
return
}
if curStatus == StatusConnecting {
msh.WriteToPtyBuffer("remote is already connecting, disconnect before trying to connect again\n")
return
}
sapi, err := shellapi.MakeShellApi(msh.GetShellType())
if err != nil {
msh.WriteToPtyBuffer("*error, %v\n", err)
return
}
istatus := msh.GetInstallStatus()
if istatus == StatusConnecting {
msh.WriteToPtyBuffer("remote is trying to install, cancel install before trying to connect again\n")
return
}
if remoteCopy.SSHOpts.SSHPort != 0 && remoteCopy.SSHOpts.SSHPort != 22 {
msh.WriteToPtyBuffer("connecting to %s (port %d)...\n", remoteCopy.RemoteCanonicalName, remoteCopy.SSHOpts.SSHPort)
} else {
msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName)
}
sshOpts := convertSSHOpts(remoteCopy.SSHOpts)
sshOpts.SSHErrorsToTty = true
if remoteCopy.ConnectMode != sstore.ConnectModeManual && remoteCopy.SSHOpts.SSHPassword == "" && !interactive {
sshOpts.BatchMode = true
}
var cmdStr string
if sshOpts.SSHHost == "" && remoteCopy.Local {
var err error
cmdStr, err = MakeLocalMShellCommandStr(remoteCopy.IsSudo())
if err != nil {
msh.WriteToPtyBuffer("*error, cannot find local mshell binary: %v\n", err)
return
}
} else {
cmdStr = MakeServerCommandStr()
}
ecmd := sshOpts.MakeSSHExecCmd(cmdStr, sapi)
cmdPty, err := msh.addControllingTty(ecmd)
if err != nil {
statusErr := fmt.Errorf("cannot attach controlling tty to mshell command: %w", err)
msh.WriteToPtyBuffer("*error, %s\n", statusErr.Error())
msh.setErrorStatus(statusErr)
return
}
defer func() {
if len(ecmd.ExtraFiles) > 0 {
ecmd.ExtraFiles[len(ecmd.ExtraFiles)-1].Close()
}
}()
go msh.RunPtyReadLoop(cmdPty)
if remoteCopy.SSHOpts.SSHPassword != "" {
go msh.WaitAndSendPassword(remoteCopy.SSHOpts.SSHPassword)
}
var makeClientCtx context.Context
var makeClientCancelFn context.CancelFunc
msh.WithLock(func() {
deadlineTime := time.Now().Add(RemoteConnectTimeout)
makeClientCtx, makeClientCancelFn = context.WithDeadline(context.Background(), deadlineTime)
defer makeClientCancelFn()
msh.Err = nil
msh.ErrNoInitPk = false
msh.Status = StatusConnecting
msh.MakeClientCancelFn = makeClientCancelFn
msh.MakeClientDeadline = &deadlineTime
go msh.NotifyRemoteUpdate()
})
go msh.watchClientDeadlineTime()
cproc, err := shexec.MakeClientProc(makeClientCtx, shexec.CmdWrap{Cmd: ecmd})
msh.WithLock(func() {
msh.MakeClientCancelFn = nil
msh.MakeClientDeadline = nil
msh.StateMap.Clear()
// no notify here, because we'll call notify in either case below
})
if err == context.DeadlineExceeded {
msh.WriteToPtyBuffer("*connect timeout\n")
msh.setErrorStatus(errors.New("connect timeout"))
return
} else if err == context.Canceled {
msh.WriteToPtyBuffer("*forced disconnection\n")
msh.WithLock(func() {
msh.Status = StatusDisconnected
go msh.NotifyRemoteUpdate()
})
return
} else if serr, ok := err.(shexec.WaveshellLaunchError); ok {
msh.WithLock(func() {
msh.UName = serr.InitPk.UName
if semver.Compare(serr.InitPk.Version, scbase.MShellVersion) < 0 {
// only set NeedsMShellUpgrade if we got an InitPk
msh.NeedsMShellUpgrade = true
}
msh.InitPkShellType = serr.InitPk.Shell
})
msh.WriteToPtyBuffer("*error, %s\n", serr.Error())
msh.setErrorStatus(serr)
go msh.tryAutoInstall()
return
} else if err != nil {
msh.WriteToPtyBuffer("*error, %s\n", serr.Error())
msh.setErrorStatus(err)
return
}
msh.updateRemoteStateVars(context.Background(), msh.RemoteId, cproc.InitPk)
msh.WithLock(func() {
msh.ServerProc = cproc
msh.Status = StatusConnected
})
go func() {
exitErr := cproc.Cmd.Wait()
exitCode := shexec.GetExitCode(exitErr)
msh.WithLock(func() {
if msh.Status == StatusConnected || msh.Status == StatusConnecting {
msh.Status = StatusDisconnected
go msh.NotifyRemoteUpdate()
}
})
msh.WriteToPtyBuffer("*disconnected exitcode=%d\n", exitCode)
}()
go msh.ProcessPackets()
msh.initActiveShells()
go msh.NotifyRemoteUpdate()
}
func (msh *MShellProc) initActiveShells() {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
+1
View File
@@ -230,6 +230,7 @@ func promptChallengeQuestion(connCtx context.Context, question string, echo bool
QueryText: queryText,
Markdown: true,
Title: "Keyboard Interactive Authentication",
PublicText: echo,
}
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
if err != nil {
+17 -10
View File
@@ -38,6 +38,9 @@ const WaveAppPathVarName = "WAVETERM_APP_PATH"
const WaveAuthKeyFileName = "waveterm.authkey"
const MShellVersion = "v0.5.0"
// initialized by InitialzeWaveAuthKey (called by main-server)
var WaveAuthKey string
var SessionDirCache = make(map[string]string)
var ScreenDirCache = make(map[string]string)
var BaseLock = &sync.Mutex{}
@@ -108,25 +111,28 @@ func MShellBinaryReader(version string, goos string, goarch string) (io.ReadClos
return fd, nil
}
func createWaveAuthKeyFile(fileName string) (string, error) {
// also sets WaveAuthKey
func createWaveAuthKeyFile(fileName string) error {
fd, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return "", err
return err
}
defer fd.Close()
keyStr := GenWaveUUID()
_, err = fd.Write([]byte(keyStr))
if err != nil {
return "", err
return err
}
return keyStr, nil
WaveAuthKey = keyStr
return nil
}
func ReadWaveAuthKey() (string, error) {
// sets WaveAuthKey
func InitializeWaveAuthKey() error {
homeDir := GetWaveHomeDir()
err := ensureDir(homeDir)
if err != nil {
return "", fmt.Errorf("cannot find/create WAVETERM_HOME directory %q", homeDir)
return fmt.Errorf("cannot find/create WAVETERM_HOME directory %q", homeDir)
}
fileName := path.Join(homeDir, WaveAuthKeyFileName)
fd, err := os.Open(fileName)
@@ -134,19 +140,20 @@ func ReadWaveAuthKey() (string, error) {
return createWaveAuthKeyFile(fileName)
}
if err != nil {
return "", fmt.Errorf("error opening wave authkey:%s: %v", fileName, err)
return fmt.Errorf("error opening wave authkey:%s: %v", fileName, err)
}
defer fd.Close()
buf, err := io.ReadAll(fd)
if err != nil {
return "", fmt.Errorf("error reading wave authkey:%s: %v", fileName, err)
return fmt.Errorf("error reading wave authkey:%s: %v", fileName, err)
}
keyStr := string(buf)
_, err = uuid.Parse(keyStr)
if err != nil {
return "", fmt.Errorf("invalid authkey:%s format: %v", fileName, err)
return fmt.Errorf("invalid authkey:%s format: %v", fileName, err)
}
return keyStr, nil
WaveAuthKey = keyStr
return nil
}
func AcquireWaveLock() (*os.File, error) {
+1
View File
@@ -63,6 +63,7 @@ const (
const (
LineState_Source = "prompt:source"
LineState_File = "prompt:file"
LineState_FileUrl = "wave:fileurl"
LineState_Min = "wave:min"
LineState_Template = "template"
LineState_Mode = "mode"
+1
View File
@@ -22,6 +22,7 @@ type UserInputRequestType struct {
Markdown bool `json:"markdown"`
TimeoutMs int `json:"timeoutms"`
CheckBoxMsg string `json:"checkboxmsg"`
PublicText bool `json:"publictext"`
}
func (*UserInputRequestType) GetType() string {
+3 -3
View File
@@ -4016,9 +4016,9 @@ fn.name@1.x.x:
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
follow-redirects@^1.0.0:
version "1.15.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
foreground-child@^3.1.0:
version "3.1.1"