) {
- const { required, onChange } = this.props;
+ const { required, onChange, isNumber } = this.props;
const inputValue = e.target.value;
+ if (isNumber && inputValue !== "" && !/^\d*$/.test(inputValue)) {
+ return;
+ }
+
// Check if value is empty and the field is required
if (required && !inputValue) {
this.setState({ error: true, hasContent: false });
diff --git a/src/app/common/modals/createremoteconn.tsx b/src/app/common/modals/createremoteconn.tsx
index 55872df56..7b181d677 100644
--- a/src/app/common/modals/createremoteconn.tsx
+++ b/src/app/common/modals/createremoteconn.tsx
@@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "@/models";
-import { Modal, TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip } from "@/elements";
+import { Modal, TextField, InputDecoration, Dropdown, PasswordField, Tooltip } from "@/elements";
import * as util from "@/util/util";
import "./createremoteconn.less";
@@ -236,11 +236,12 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
/>
-
diff --git a/src/electron/emain.ts b/src/electron/emain.ts
index c98fac3eb..f7e84d38f 100644
--- a/src/electron/emain.ts
+++ b/src/electron/emain.ts
@@ -419,6 +419,17 @@ function mainResizeHandler(_: any, win: Electron.BrowserWindow) {
});
}
+function mainPowerHandler(status: string) {
+ const url = new URL(getBaseHostPort() + "/api/power-monitor");
+ const fetchHeaders = getFetchHeaders();
+ const body = { status: status };
+ fetch(url, { method: "post", body: JSON.stringify(body), headers: fetchHeaders })
+ .then((resp) => handleJsonFetchResponse(url, resp))
+ .catch((err) => {
+ console.log("error setting power monitor state", err);
+ });
+}
+
function calcBounds(clientData: ClientDataType): Electron.Rectangle {
const primaryDisplay = electron.screen.getPrimaryDisplay();
const pdBounds = primaryDisplay.bounds;
@@ -946,3 +957,5 @@ function configureAutoUpdater(enabled: boolean) {
}
});
})();
+
+electron.powerMonitor.on("suspend", () => mainPowerHandler("suspend"));
diff --git a/src/models/commandrunner.ts b/src/models/commandrunner.ts
index 48fa3dc1d..d0b585c4a 100644
--- a/src/models/commandrunner.ts
+++ b/src/models/commandrunner.ts
@@ -467,6 +467,31 @@ class CommandRunner {
return GlobalModel.submitCommand("client", "setrightsidebar", null, kwargs, false);
}
+ setSudoPwStore(store: string): Promise {
+ let kwargs = {
+ nohist: "1",
+ sudopwstore: store,
+ };
+ return GlobalModel.submitCommand("client", "set", null, kwargs, false);
+ }
+
+ setSudoPwTimeout(timeout: string): Promise {
+ let kwargs = {
+ nohist: "1",
+ sudopwtimeout: timeout,
+ };
+ return GlobalModel.submitCommand("client", "set", null, kwargs, false);
+ }
+
+ setSudoPwClearOnSleep(clear: boolean): Promise {
+ let kwargs = {
+ nohist: "1",
+ sudopwclearonsleep: String(clear),
+ };
+ console.log(kwargs);
+ return GlobalModel.submitCommand("client", "set", null, kwargs, false);
+ }
+
editBookmark(bookmarkId: string, desc: string, cmdstr: string) {
let kwargs = {
nohist: "1",
diff --git a/src/models/model.ts b/src/models/model.ts
index 348f88f6b..361f729d7 100644
--- a/src/models/model.ts
+++ b/src/models/model.ts
@@ -455,6 +455,22 @@ class Model {
return this.termFontSize.get();
}
+ getSudoPwStore(): string {
+ let cdata = this.clientData.get();
+ return cdata?.feopts?.sudopwstore ?? appconst.DefaultSudoPwStore;
+ }
+
+ getSudoPwTimeout(): number {
+ let cdata = this.clientData.get();
+ const sudoPwTimeoutMs = cdata?.feopts?.sudopwtimeoutms ?? appconst.DefaultSudoPwTimeoutMs;
+ return sudoPwTimeoutMs / 1000 / 60;
+ }
+
+ getSudoPwClearOnSleep(): boolean {
+ let cdata = this.clientData.get();
+ return !cdata?.feopts?.nosudopwclearonsleep;
+ }
+
updateTermFontSizeVars() {
let lhe = this.recomputeLineHeightEnv();
mobx.action(() => {
diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts
index 7a9db4acb..7789fce4c 100644
--- a/src/types/custom.d.ts
+++ b/src/types/custom.d.ts
@@ -598,6 +598,9 @@ declare global {
termfontfamily: string;
theme: NativeThemeSource;
termthemesettings: TermThemeSettingsType;
+ sudopwstore: "on" | "off" | "notimeout";
+ sudopwtimeoutms: number;
+ nosudopwclearonsleep: boolean;
};
type ConfirmFlagsType = {
diff --git a/wavesrv/cmd/main-server.go b/wavesrv/cmd/main-server.go
index 3709ed46e..ef07a022e 100644
--- a/wavesrv/cmd/main-server.go
+++ b/wavesrv/cmd/main-server.go
@@ -209,6 +209,33 @@ func HandleSetWinSize(w http.ResponseWriter, r *http.Request) {
WriteJsonSuccess(w, true)
}
+func HandlePowerMonitor(w http.ResponseWriter, r *http.Request) {
+ decoder := json.NewDecoder(r.Body)
+ var body sstore.PowerMonitorEventType
+ err := decoder.Decode(&body)
+ if err != nil {
+ WriteJsonError(w, fmt.Errorf(ErrorDecodingJson, err))
+ return
+ }
+ cdata, err := sstore.EnsureClientData(r.Context())
+ if err != nil {
+ WriteJsonError(w, err)
+ return
+ }
+ switch body.Status {
+ case "suspend":
+ if !cdata.FeOpts.NoSudoPwClearOnSleep && cdata.FeOpts.SudoPwStore != "notimeout" {
+ for _, proc := range remote.GetRemoteMap() {
+ proc.ClearCachedSudoPw()
+ }
+ }
+ WriteJsonSuccess(w, true)
+ default:
+ WriteJsonError(w, fmt.Errorf("unknown status: %s", body.Status))
+ return
+ }
+}
+
// params: fg, active, open
func HandleLogActiveState(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
@@ -1149,6 +1176,7 @@ func main() {
gr.HandleFunc(bufferedpipe.BufferedPipeGetterUrl, AuthKeyWrapAllowHmac(bufferedpipe.HandleGetBufferedPipeOutput))
gr.HandleFunc("/api/get-client-data", AuthKeyWrap(HandleGetClientData))
gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize))
+ gr.HandleFunc("/api/power-monitor", AuthKeyWrap(HandlePowerMonitor))
gr.HandleFunc("/api/log-active-state", AuthKeyWrap(HandleLogActiveState))
gr.HandleFunc("/api/read-file", AuthKeyWrapAllowHmac(HandleReadFile))
gr.HandleFunc("/api/write-file", AuthKeyWrap(HandleWriteFile)).Methods("POST")
diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go
index 6ba5a56ee..3e31f6383 100644
--- a/wavesrv/pkg/cmdrunner/cmdrunner.go
+++ b/wavesrv/pkg/cmdrunner/cmdrunner.go
@@ -642,10 +642,17 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.Up
}
runPacket.Command = strings.TrimSpace(cmdStr)
runPacket.ReturnState = resolveBool(pk.Kwargs["rtnstate"], isRtnStateCmd)
+
+ clientData, err := sstore.EnsureClientData(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("cannot retrieve client data: %v", err)
+ }
+ feOpts := clientData.FeOpts
+
if sudoArg, ok := pk.Kwargs[KwArgSudo]; ok {
- runPacket.IsSudo = resolveBool(sudoArg, false)
+ runPacket.IsSudo = resolveBool(sudoArg, false) && feOpts.SudoPwStore != "off"
} else {
- runPacket.IsSudo = IsSudoCommand(cmdStr)
+ runPacket.IsSudo = IsSudoCommand(cmdStr) && feOpts.SudoPwStore != "off"
}
rcOpts := remote.RunCommandOpts{
SessionId: ids.SessionId,
@@ -5810,6 +5817,13 @@ func CheckOptionAlias(kwargs map[string]string, aliases ...string) (string, bool
return "", false
}
+func validateSudoPwStore(config string) error {
+ if utilfn.ContainsStr([]string{"on", "off", "notimeout"}, config) {
+ return nil
+ }
+ return fmt.Errorf("%s is not a config option", config)
+}
+
func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
clientData, err := sstore.EnsureClientData(ctx)
if err != nil {
@@ -5996,6 +6010,58 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
}
varsUpdated = append(varsUpdated, "webgl")
}
+ if sudoPwStoreStr, found := pk.Kwargs["sudopwstore"]; found {
+ err := validateSudoPwStore(sudoPwStoreStr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid sudo pw store, must be \"on\", \"off\", \"notimeout\": %v", err)
+ }
+ feOpts := clientData.FeOpts
+ feOpts.SudoPwStore = strings.ToLower(sudoPwStoreStr)
+ err = sstore.UpdateClientFeOpts(ctx, feOpts)
+ if err != nil {
+ return nil, fmt.Errorf("error updating client feopts: %v", err)
+ }
+ // clear all sudo pw if turning off
+ if feOpts.SudoPwStore == "off" {
+ for _, proc := range remote.GetRemoteMap() {
+ proc.ClearCachedSudoPw()
+ }
+ }
+ varsUpdated = append(varsUpdated, "sudopwstore")
+ }
+ if sudoPwTimeoutStr, found := pk.Kwargs["sudopwtimeout"]; found {
+ oldPwTimeout := clientData.FeOpts.SudoPwTimeoutMs / 1000 / 60 // ms to minutes
+ if oldPwTimeout == 0 {
+ oldPwTimeout = sstore.DefaultSudoTimeout
+ }
+ newSudoPwTimeout, err := resolveNonNegInt(sudoPwTimeoutStr, sstore.DefaultSudoTimeout)
+ if err != nil {
+ return nil, fmt.Errorf("invalid sudo pw timeout, must be a number greater than 0: %v", err)
+ }
+ if newSudoPwTimeout == 0 {
+ return nil, fmt.Errorf("invalid sudo pw timeout, must be a number greater than 0")
+ }
+ feOpts := clientData.FeOpts
+ feOpts.SudoPwTimeoutMs = newSudoPwTimeout * 60 * 1000 // minutes to ms
+ err = sstore.UpdateClientFeOpts(ctx, feOpts)
+ if err != nil {
+ return nil, fmt.Errorf("error updating client feopts: %v", err)
+ }
+ for _, proc := range remote.GetRemoteMap() {
+ proc.ChangeSudoTimeout(int64(newSudoPwTimeout - oldPwTimeout))
+ }
+ varsUpdated = append(varsUpdated, "sudopwtimeout")
+ }
+ if sudoPwClearOnSleepStr, found := pk.Kwargs["sudopwclearonsleep"]; found {
+ newSudoPwClearOnSleep := resolveBool(sudoPwClearOnSleepStr, true)
+ feOpts := clientData.FeOpts
+ feOpts.NoSudoPwClearOnSleep = !newSudoPwClearOnSleep
+ err = sstore.UpdateClientFeOpts(ctx, feOpts)
+ if err != nil {
+ return nil, fmt.Errorf("error updating client feopts: %v", err)
+ }
+ varsUpdated = append(varsUpdated, "sudopwclearonsleep")
+ }
if len(varsUpdated) == 0 {
return nil, fmt.Errorf("/client:set requires a value to set: %s", formatStrs([]string{"termfontsize", "termfontfamily", "openaiapitoken", "openaimodel", "openaibaseurl", "openaimaxtokens", "openaimaxchoices", "openaitimeout", "webgl"}, "or", false))
}
diff --git a/wavesrv/pkg/remote/remote.go b/wavesrv/pkg/remote/remote.go
index cd16910d0..c6a002424 100644
--- a/wavesrv/pkg/remote/remote.go
+++ b/wavesrv/pkg/remote/remote.go
@@ -2626,11 +2626,21 @@ func sendScreenUpdates(screens []*sstore.ScreenType) {
}
}
-func (msh *MShellProc) startSudoPwClearChecker() {
+func (msh *MShellProc) startSudoPwClearChecker(clientData *sstore.ClientData) {
+ ctx, cancelFn := context.WithCancel(context.Background())
+ defer cancelFn()
+ sudoPwStore := clientData.FeOpts.SudoPwStore
for {
+ clientData, err := sstore.EnsureClientData(ctx)
+ if err != nil {
+ log.Printf("*error: cannot obtain client data in sudo pw loop. using fallback: %v", err)
+ } else {
+ sudoPwStore = clientData.FeOpts.SudoPwStore
+ }
+
shouldExit := false
msh.WithLock(func() {
- if msh.sudoClearDeadline > 0 && time.Now().Unix() > msh.sudoClearDeadline {
+ if msh.sudoClearDeadline > 0 && time.Now().Unix() > msh.sudoClearDeadline && sudoPwStore != "notimeout" {
msh.sudoPw = nil
msh.sudoClearDeadline = 0
}
@@ -2668,13 +2678,25 @@ func (msh *MShellProc) sendSudoPassword(sudoPk *packet.SudoRequestPacketType) er
}
rawSecret = []byte(guiResponse.Text)
}
- //new
+
+ ctx, cancelFn := context.WithCancel(context.Background())
+ defer cancelFn()
+ clientData, err := sstore.EnsureClientData(ctx)
+ if err != nil {
+ return fmt.Errorf("*error: cannot obtain client data: %v", err)
+ }
+ sudoPwTimeout := clientData.FeOpts.SudoPwTimeoutMs / 1000 / 60
+ if sudoPwTimeout == 0 {
+ // 0 maps to default
+ sudoPwTimeout = sstore.DefaultSudoTimeout
+ }
+ pwTimeoutDur := time.Duration(sudoPwTimeout) * time.Minute
msh.WithLock(func() {
msh.sudoPw = rawSecret
if msh.sudoClearDeadline == 0 {
- go msh.startSudoPwClearChecker()
+ go msh.startSudoPwClearChecker(clientData)
}
- msh.sudoClearDeadline = time.Now().Add(SudoTimeoutTime).Unix()
+ msh.sudoClearDeadline = time.Now().Add(pwTimeoutDur).Unix()
})
srvPrivKey, err := ecdh.P256().GenerateKey(rand.Reader)
@@ -2767,6 +2789,15 @@ func (msh *MShellProc) ClearCachedSudoPw() {
})
}
+func (msh *MShellProc) ChangeSudoTimeout(deltaTime int64) {
+ msh.WithLock(func() {
+ if msh.sudoClearDeadline != 0 {
+ updated := msh.sudoClearDeadline + deltaTime*60
+ msh.sudoClearDeadline = max(0, updated)
+ }
+ })
+}
+
func (msh *MShellProc) ProcessPackets() {
defer msh.WithLock(func() {
if msh.Status == StatusConnected {
diff --git a/wavesrv/pkg/sstore/sstore.go b/wavesrv/pkg/sstore/sstore.go
index fb5c3d641..4aec42f68 100644
--- a/wavesrv/pkg/sstore/sstore.go
+++ b/wavesrv/pkg/sstore/sstore.go
@@ -43,6 +43,7 @@ const DBWALFileNameBackup = "backup.waveterm.db-wal"
const MaxWebShareLineCount = 50
const MaxWebShareScreenCount = 3
const MaxLineStateSize = 4 * 1024 // 4k for now, can raise if needed
+const DefaultSudoTimeout = 5
const DefaultSessionName = "default"
const LocalRemoteAlias = "local"
@@ -232,6 +233,10 @@ type ClientWinSizeType struct {
FullScreen bool `json:"fullscreen,omitempty"`
}
+type PowerMonitorEventType struct {
+ Status string `json:"status"`
+}
+
type SidebarValueType struct {
Collapsed bool `json:"collapsed"`
Width int `json:"width"`
@@ -250,10 +255,14 @@ type ClientOptsType struct {
}
type FeOptsType struct {
- TermFontSize int `json:"termfontsize,omitempty"`
- TermFontFamily string `json:"termfontfamily,omitempty"`
- Theme string `json:"theme,omitempty"`
- TermThemeSettings map[string]string `json:"termthemesettings"`
+ TermFontSize int `json:"termfontsize,omitempty"`
+ TermFontFamily string `json:"termfontfamily,omitempty"`
+ Theme string `json:"theme,omitempty"`
+ TermThemeSettings map[string]string `json:"termthemesettings"`
+ SudoPwStore string `json:"sudopwstore,omitempty"`
+ SudoPwTimeoutMs int `json:"sudopwtimeoutms,omitempty"`
+ SudoPwTimeout int `json:"sudopwtimeout,omitempty"`
+ NoSudoPwClearOnSleep bool `json:"nosudopwclearonsleep,omitempty"`
}
type ReleaseInfoType struct {