waveterm/wavesrv/cmd/main-server.go

1208 lines
35 KiB
Go
Raw Normal View History

2023-10-17 06:31:13 +02:00
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
2022-06-12 22:39:48 +02:00
package main
import (
2022-07-01 21:17:19 +02:00
"context"
"encoding/base64"
2022-06-12 22:39:48 +02:00
"encoding/json"
"errors"
2022-06-12 22:39:48 +02:00
"fmt"
"html"
"io"
"io/fs"
2022-10-31 20:40:45 +01:00
"log"
"mime/multipart"
2022-06-12 22:39:48 +02:00
"net/http"
"os"
"os/signal"
"path/filepath"
"regexp"
"runtime"
2022-07-15 10:57:45 +02:00
"runtime/debug"
"strconv"
2022-06-13 20:11:56 +02:00
"strings"
"sync"
"syscall"
2022-06-12 22:39:48 +02:00
"time"
2022-06-13 20:11:56 +02:00
"github.com/google/uuid"
2022-06-12 22:39:48 +02:00
"github.com/gorilla/mux"
zsh support (#227) adds zsh support to waveterm. big change, lots going on here. lots of other improvements and bug fixes added while debugging and building out the feature. Commits: * refactor shexec parser.go into new package shellenv. separate out bash specific parsing from generic functions * checkpoint * work on refactoring shexec. created two new packages shellapi (for bash/zsh specific stuff), and shellutil (shared between shellapi and shexec) * more refactoring * create shellapi interface to abstract bash specific functionality * more refactoring, move bash shell state parsing to shellapi * move makeRcFile to shellapi. remove all of the 'client' options CLI options from waveshell * get shellType passed through to server/single paths for waveshell * add a local shelltype detector * mock out a zshapi * move shelltype through more of the code * get a command to run via zsh * zsh can now switch directories. poc, needs cleanup * working on ShellState encoding differences between zsh/bash. Working on parsing zsh decls. move utilfn package into waveshell (shouldn't have been in wavesrv) * switch to use []byte for vardecl serialization + diffs * progress on zsh environment. still have issues reconciling init environment with trap environment * fix typeset argument parsing * parse promptvars, more zsh specific ignores * fix bug with promptvar not getting set (wrong check in FeState func) * add sdk (issue #188) to list of rtnstate commands * more zsh compatibility -- working with a larger ohmyzsh environment. ignore more variables, handle exit trap better. unique path/fpath. add a processtype variable to base. * must return a value * zsh alias parsing/restoring. diff changes (and rtnstate changes). introduces linediff v1. * force zmodload of zsh/parameter * starting work on zsh functions * need a v1 of mapdiff as well (to handle null chars) * pack/unpack of ints was wrong (one used int and one use uint). turned out we only ever encoded '0' so it worked. that also means it is safe to change unpack to unpackUInt * reworking for binary encoding of aliases and functions (because of zsh allows any character, including nulls, in names and values) * fixes, working on functions, issue with line endings * zsh functions. lots of ugliness here around dealing with line dicipline and cooked stty. new runcommand function to grab output from a non-tty fd. note that we still to run the actual command in a stty to get the proper output. * write uuid tempdir, cleanup with tmprcfilename code * hack in some simple zsh function declaration finding code for rtnstate. create function diff for rtnstate that supports zsh * make sure key order is constant so shell hashes are consistent * fix problems with state diffs to support new zsh formats. add diff/apply code to shellapi (moved from shellenv), that is now specific to zsh or bash * add log packet and new shellstate packets * switch to shellstate map that's also keyed by shelltype * add shelltype to remoteinstance * remove shell argument from waveshell * added new shelltype statemap to remote.go (msh), deal with fallout * move shellstate out of init packet, and move to an explicit reinit call. try to initialize all of the active shell states * change dont always store init state (only store on demand). initialize shell states on demand (if not already initialized). allow reset to change shells * add shellpref field to remote table. use to drive the default shell choice for new tabs * show shelltag on cmdinput, pass through ri and remote (defaultshellstate) * bump mshell version to v0.4 * better version validation for shellstate. also relax compatibility requirements for diffing states (shelltype + major version need to match) * better error handling, check shellstate compatibility during run (on waveshell server) * add extra separator for bash shellstate processing to deal with spurious output from rc files * special migration for v30 -- flag invalid bash shell states and show special button in UI to fix * format * remove zsh-decls (unused) * remove test code * remove debug print * fix typo
2024-01-17 01:11:04 +01:00
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
2023-10-16 22:30:10 +02:00
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
"github.com/wavetermdev/waveterm/waveshell/pkg/server"
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
"github.com/wavetermdev/waveterm/wavesrv/pkg/bufferedpipe"
2023-10-16 22:30:10 +02:00
"github.com/wavetermdev/waveterm/wavesrv/pkg/cmdrunner"
Simplified terminal theming (#570) * save work * reusable StyleBlock component * StyleBlock in elements dir * root level * ability to inherit root styles * change prop from classname to selector * selector should always be :root * remove selector prop from StyleBlock * working * cleanup * loadThemeStyles doesn't have to be async * revert changes in tabs2.less * remove old implementation * cleanup * remove file from another branch * fix issue where line in history view doesn't reflect the terminal theme * add key and value validation * add label to tab settings terminal theme dropdown * save work * save work * save work * working * trigger componentDidUpdate when switching tabs and sessions * cleanup * save work * save work * use UpdatePacket for theme changes as well * make methods cohesive * use themes coming from backend * reload terminal when styel block is unmounted and mounted * fix validation * re-render terminal when theme is updated * remove test styles * cleanup * more cleanup * revert unneeded change * more cleanup * fix type * more cleanup * render style blocks in the header instead of body using portal * add ability to reuse and dispose TermThemes instance and file watcher * remove comment * minor change * separate filewatcher as singleton * do not render app when term theme style blocks aren't rendered first * only render main when termstyles have been rendered already * add comment * use DoUpdate to send themes to front-end * support to watch subdirectories * added support for watch subdirectories * make watcher more flexible so it can be closed anywhere * cleanup * undo the app/main split * use TermThemesType in creating initial value for Themes field * simplify code * fix issue where dropdown label doesn't float when the theme selected is Inherit * remove unsed var * start watcher in main, merge themes (don't overwrite) on event. * ensure terminal-themes directory is created on startup * ah, wait for termThemes to be set (the connect packet needs to have been processed to proceed with rendering)
2024-04-24 08:22:35 +02:00
"github.com/wavetermdev/waveterm/wavesrv/pkg/configstore"
"github.com/wavetermdev/waveterm/wavesrv/pkg/ephemeral"
2023-10-16 22:30:10 +02:00
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
"github.com/wavetermdev/waveterm/wavesrv/pkg/releasechecker"
2023-10-16 22:30:10 +02:00
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
"github.com/wavetermdev/waveterm/wavesrv/pkg/rtnstate"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
2023-10-16 22:30:10 +02:00
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scws"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
Sudo Caching (#573) * feat: share sudo between pty sessions This is a first pass at a feature to cache the sudo password and share it between different pty sessions. This makes it possible to not require manual password entry every time sudo is used. * feat: allow error handling and canceling sudo cmds This adds the missing functionality that prevented failed sudo commands from automatically closing. * feat: restrict sudo caching to dev mode for now * modify fullCmdStr not pk.Command * refactor: condense ecdh encryptor creation This refactors the common pieces needed to create an encryptor from an ecdh key pair into a common function. * refactor: rename promptenc to waveenc * feat: add command to clear sudo password We currently do not provide use of the sudo -k and sudo -K commands to clear the sudo password. This adds a /sudo:clear command to handle it in the meantime. * feat: add kwarg to force sudo In cases where parsing for sudo doesn't work, this provides an alternate wave kwarg to use instead. It can be used with [sudo=1] at the beginning of a command. * refactor: simplify sudoArg parsing * feat: allow user to clear all sudo passwords This introduces the "all" kwarg for the sudo:clear command in order to clear all sudo passwords. * fix: handle deadline with real time Golang's time module uses monatomic time by default, but that is not desired for the password timeout since we want the timer to continue even if the computer is asleep. We now avoid this by directly comparing the unix timestamps. * fix: remove sudo restriction to dev mode This allows it to be used in regular builds as well. * fix: switch to password timeout without wait group This removes an unnecessary waiting period for sudo password entry. * fix: update deadline in sudo:clear This allows sudo:clear to cancel the goroutine for watching the password timer. * fix: pluralize sudo:clear message when all=1 This changes the output message for /sudo:clear to indicate multiple passwords cleared if the all=1 kwarg is used. * fix: use GetRemoteMap for getting remotes in clear The sudo:clear command was directly looping over the GlobalStore.Map which is not thread safe. Switched to GetRemoteMap which uses a lock internally. * fix: allow sudo metacmd to set sudo false This fixes the logic for resolving if a command is a sudo command. This change makes it possible for the sudo metacmd kwarg to force sudo to be false.
2024-04-17 01:58:17 +02:00
"github.com/wavetermdev/waveterm/wavesrv/pkg/waveenc"
2023-10-16 22:30:10 +02:00
"github.com/wavetermdev/waveterm/wavesrv/pkg/wsshell"
2022-06-12 22:39:48 +02:00
)
type WebFnType = func(http.ResponseWriter, *http.Request)
2022-06-12 22:39:48 +02:00
const HttpReadTimeout = 5 * time.Second
const HttpWriteTimeout = 21 * time.Second
const HttpMaxHeaderBytes = 60000
const HttpTimeoutDuration = 21 * time.Second
const MainServerAddr = "127.0.0.1:1619" // wavesrv, P=16, S=19, PS=1619
const WebSocketServerAddr = "127.0.0.1:1623" // wavesrv:websocket, P=16, W=23, PW=1623
const MainServerDevAddr = "127.0.0.1:8090"
const WebSocketServerDevAddr = "127.0.0.1:8091"
const WSStateReconnectTime = 30 * time.Second
const WSStatePacketChSize = 20
const InitialTelemetryWait = 30 * time.Second
const TelemetryTick = 10 * time.Minute
const TelemetryInterval = 4 * time.Hour
const MaxWriteFileMemSize = 20 * (1024 * 1024) // 20M
// these are set at build time
var WaveVersion = "v0.0.0"
var BuildTime = "0"
var GlobalLock = &sync.Mutex{}
var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState
var shutdownOnce sync.Once
var ContentTypeHeaderValidRe = regexp.MustCompile(`^\w+/[\w.+-]+$`)
2022-06-12 22:39:48 +02:00
2023-01-17 08:36:52 +01:00
type ClientActiveState struct {
Fg bool `json:"fg"`
Active bool `json:"active"`
Open bool `json:"open"`
}
// Error constants
const (
ErrorDecodingJson = "error decoding json: %w"
ErrorPanic = "panic: %v"
ErrorInvalidScreenId = "invalid screenid: %v"
ErrorInvalidLineId = "invalid lineid: %v"
)
// Header constants
const (
CacheControlHeaderKey = "Cache-Control"
CacheControlHeaderNoCache = "no-cache"
ContentTypeHeaderKey = "Content-Type"
ContentTypeJson = "application/json"
ContentTypeText = "text/plain"
)
func setWSState(state *scws.WSState) {
GlobalLock.Lock()
defer GlobalLock.Unlock()
WSStateMap[state.ClientId] = state
2022-06-12 22:39:48 +02:00
}
func getWSState(clientId string) *scws.WSState {
GlobalLock.Lock()
defer GlobalLock.Unlock()
return WSStateMap[clientId]
}
func removeWSStateAfterTimeout(clientId string, connectTime time.Time, waitDuration time.Duration) {
go func() {
time.Sleep(waitDuration)
GlobalLock.Lock()
defer GlobalLock.Unlock()
state := WSStateMap[clientId]
if state == nil || state.ConnectTime != connectTime {
return
}
delete(WSStateMap, clientId)
state.UnWatchScreen()
}()
}
2022-06-12 22:39:48 +02:00
func HandleWs(w http.ResponseWriter, r *http.Request) {
shell, err := wsshell.StartWS(w, r)
if err != nil {
2022-10-31 20:40:45 +01:00
log.Printf("WebSocket Upgrade Failed %T: %v\n", w, err)
2022-06-12 22:39:48 +02:00
return
}
defer shell.Conn.Close()
clientId := r.URL.Query().Get("clientid")
if clientId == "" {
close(shell.WriteChan)
return
}
state := getWSState(clientId)
if state == nil {
state = scws.MakeWSState(clientId, scbase.WaveAuthKey)
state.ReplaceShell(shell)
setWSState(state)
} else {
state.UpdateConnectTime()
state.ReplaceShell(shell)
}
stateConnectTime := state.GetConnectTime()
defer func() {
removeWSStateAfterTimeout(clientId, stateConnectTime, WSStateReconnectTime)
2022-06-12 22:39:48 +02:00
}()
2022-10-31 20:40:45 +01:00
log.Printf("WebSocket opened %s %s\n", state.ClientId, shell.RemoteAddr)
Simplified terminal theming (#570) * save work * reusable StyleBlock component * StyleBlock in elements dir * root level * ability to inherit root styles * change prop from classname to selector * selector should always be :root * remove selector prop from StyleBlock * working * cleanup * loadThemeStyles doesn't have to be async * revert changes in tabs2.less * remove old implementation * cleanup * remove file from another branch * fix issue where line in history view doesn't reflect the terminal theme * add key and value validation * add label to tab settings terminal theme dropdown * save work * save work * save work * working * trigger componentDidUpdate when switching tabs and sessions * cleanup * save work * save work * use UpdatePacket for theme changes as well * make methods cohesive * use themes coming from backend * reload terminal when styel block is unmounted and mounted * fix validation * re-render terminal when theme is updated * remove test styles * cleanup * more cleanup * revert unneeded change * more cleanup * fix type * more cleanup * render style blocks in the header instead of body using portal * add ability to reuse and dispose TermThemes instance and file watcher * remove comment * minor change * separate filewatcher as singleton * do not render app when term theme style blocks aren't rendered first * only render main when termstyles have been rendered already * add comment * use DoUpdate to send themes to front-end * support to watch subdirectories * added support for watch subdirectories * make watcher more flexible so it can be closed anywhere * cleanup * undo the app/main split * use TermThemesType in creating initial value for Themes field * simplify code * fix issue where dropdown label doesn't float when the theme selected is Inherit * remove unsed var * start watcher in main, merge themes (don't overwrite) on event. * ensure terminal-themes directory is created on startup * ah, wait for termThemes to be set (the connect packet needs to have been processed to proceed with rendering)
2024-04-24 08:22:35 +02:00
state.RunWSRead()
2022-06-12 22:39:48 +02:00
}
2022-06-18 00:30:42 +02:00
// todo: sync multiple writes to the same fifoName into a single go-routine and do liveness checking on fifo
// if this returns an error, likely the fifo is dead and the cmd should be marked as 'done'
func writeToFifo(fifoName string, data []byte) error {
rwfd, err := os.OpenFile(fifoName, os.O_RDWR, 0600)
if err != nil {
return err
}
defer rwfd.Close()
fifoWriter, err := os.OpenFile(fifoName, os.O_WRONLY, 0600) // blocking open (open won't block because of rwfd)
if err != nil {
return err
}
defer fifoWriter.Close()
// this *could* block if the fifo buffer is full
// unlikely because if the reader is dead, and len(data) < pipe size, then the buffer will be empty and will clear after rwfd is closed
_, err = fifoWriter.Write(data)
if err != nil {
return err
}
return nil
}
2022-09-25 09:26:33 +02:00
func HandleGetClientData(w http.ResponseWriter, r *http.Request) {
cdata, err := sstore.EnsureClientData(r.Context())
if err != nil {
WriteJsonError(w, err)
return
}
cdata = cdata.Clean()
2022-09-25 09:26:33 +02:00
WriteJsonSuccess(w, cdata)
}
func HandleSetWinSize(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var winSize sstore.ClientWinSizeType
err := decoder.Decode(&winSize)
if err != nil {
WriteJsonError(w, fmt.Errorf(ErrorDecodingJson, err))
2022-09-25 09:26:33 +02:00
return
}
err = sstore.SetWinSize(r.Context(), winSize)
if err != nil {
WriteJsonError(w, fmt.Errorf("error setting winsize: %w", err))
return
}
WriteJsonSuccess(w, true)
}
Sudo Config Gui (#603) * feat: add gui elements to configure ssh pw cache This adds a dropdown for on/off/notimeout, a number entry box for a timeout value, and a toggle for clearing when the computer sleeps. * fix: improve password timeout entry This makes the password timeout more consistent by using an inline settings element. It also creates the inline settings element to parse the input. * feat: turn sudo password caching on and off * feat: use configurable sudo timeout This makes it possible to control how long waveterm stores your sudo password. Note that if it changes, it immediately clears the cached passwords. * fix: clear existing sudo passwords if switched off When the sudo password store state is changed to "off", all existing passwords must immediately be cleared automatically. * feat: allow clearing sudo passwords on suspend This option makes it so the sudo passwords will be cleared when the computer falls asleep. It will never be used in the case where the password is set to never time out. * feat: allow notimeout to prevent sudo pw clear This option allows the sudo timeout to be ignored while it is selected. * feat: adjust current deadline based on user config This allows the deadline to update as changes to the config are happening. * fix: reject a sudopwtimeout of 0 on the backend * fix: use the default sudoPwTimeout for empty input * fix: specify the timeout length is minutes * fix: store sudopwtimeout in ms instead of minutes * fix: formatting the default sudo timeout By changing the order of operations, this no longer shows up as NaN if the default is used. * refactor: consolidate inlinesettingstextedit This removes the number variant and combines them into the same class with an option to switch between the two behaviors. * refactor: consolidate textfield and numberfield This removes the number variant of textfield. The textfield component can now act as a numberfield when the optional isNumber prop is true.
2024-04-26 03:19:43 +02:00
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
}
}
2023-01-17 08:36:52 +01:00
// params: fg, active, open
func HandleLogActiveState(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var activeState ClientActiveState
err := decoder.Decode(&activeState)
if err != nil {
WriteJsonError(w, fmt.Errorf(ErrorDecodingJson, err))
2023-01-17 08:36:52 +01:00
return
}
activity := telemetry.ActivityUpdate{}
2023-01-17 08:36:52 +01:00
if activeState.Fg {
activity.FgMinutes = 1
}
if activeState.Active {
activity.ActiveMinutes = 1
}
if activeState.Open {
activity.OpenMinutes = 1
}
2023-02-22 07:41:56 +01:00
activity.NumConns = remote.NumRemotes()
activity.NumWorkspaces, _ = sstore.NumSessions(r.Context())
activity.NumTabs, _ = sstore.NumScreens(r.Context())
err = telemetry.UpdateActivity(r.Context(), activity)
2023-01-17 08:36:52 +01:00
if err != nil {
WriteJsonError(w, fmt.Errorf("error updating activity: %w", err))
return
}
WriteJsonSuccess(w, true)
}
// params: screenid
func HandleGetScreenLines(w http.ResponseWriter, r *http.Request) {
2022-07-02 22:31:56 +02:00
qvals := r.URL.Query()
screenId := qvals.Get("screenid")
if _, err := uuid.Parse(screenId); err != nil {
WriteJsonError(w, fmt.Errorf("invalid screenid, err: %w", err))
2022-07-02 22:31:56 +02:00
return
}
screenLines, err := sstore.GetScreenLinesById(r.Context(), screenId)
2022-07-02 22:31:56 +02:00
if err != nil {
WriteJsonError(w, err)
return
}
2023-03-13 20:10:23 +01:00
WriteJsonSuccess(w, screenLines)
2022-07-02 22:31:56 +02:00
}
func HandleRtnState(w http.ResponseWriter, r *http.Request) {
defer func() {
r := recover()
if r == nil {
return
}
log.Printf("[error] in handlertnstate: %v\n", r)
debug.PrintStack()
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf(ErrorPanic, r)))
}()
qvals := r.URL.Query()
2023-03-21 03:20:57 +01:00
screenId := qvals.Get("screenid")
lineId := qvals.Get("lineid")
if screenId == "" || lineId == "" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("must specify screenid and lineid"))
return
}
2023-03-21 03:20:57 +01:00
if _, err := uuid.Parse(screenId); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf(ErrorInvalidScreenId, err)))
return
}
if _, err := uuid.Parse(lineId); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf(ErrorInvalidLineId, err)))
return
}
data, err := rtnstate.GetRtnStateDiff(r.Context(), screenId, lineId)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("cannot get rtnstate diff: %v", err)))
return
}
w.WriteHeader(http.StatusOK)
w.Write(data)
}
2022-09-15 08:10:35 +02:00
func HandleRemotePty(w http.ResponseWriter, r *http.Request) {
qvals := r.URL.Query()
remoteId := qvals.Get("remoteid")
if remoteId == "" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("must specify remoteid"))
2022-09-15 08:10:35 +02:00
return
}
if _, err := uuid.Parse(remoteId); err != nil {
w.WriteHeader(http.StatusInternalServerError)
2022-09-15 08:10:35 +02:00
w.Write([]byte(fmt.Sprintf("invalid remoteid: %v", err)))
return
}
realOffset, data, err := remote.ReadRemotePty(r.Context(), remoteId)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
2022-09-15 08:10:35 +02:00
w.Write([]byte(fmt.Sprintf("error reading ptyout file: %v", err)))
return
}
w.Header().Set("X-PtyDataOffset", strconv.FormatInt(realOffset, 10))
w.WriteHeader(http.StatusOK)
w.Write(data)
}
2022-07-03 21:01:56 +02:00
func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) {
2022-06-12 22:39:48 +02:00
qvals := r.URL.Query()
2023-03-21 03:20:57 +01:00
screenId := qvals.Get("screenid")
lineId := qvals.Get("lineid")
if screenId == "" || lineId == "" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("must specify screenid and lineid"))
2022-06-12 22:39:48 +02:00
return
}
2023-03-21 03:20:57 +01:00
if _, err := uuid.Parse(screenId); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf(ErrorInvalidScreenId, err)))
return
}
if _, err := uuid.Parse(lineId); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf(ErrorInvalidLineId, err)))
return
}
realOffset, data, err := sstore.ReadFullPtyOutFile(r.Context(), screenId, lineId)
2022-06-12 22:39:48 +02:00
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(html.EscapeString(fmt.Sprintf("error reading ptyout file: %v", err))))
2022-06-12 22:39:48 +02:00
return
}
w.Header().Set("X-PtyDataOffset", strconv.FormatInt(realOffset, 10))
2022-06-12 22:39:48 +02:00
w.WriteHeader(http.StatusOK)
w.Write(data)
2022-06-12 22:39:48 +02:00
}
type writeFileParamsType struct {
ScreenId string `json:"screenid"`
LineId string `json:"lineid"`
Path string `json:"path"`
UseTemp bool `json:"usetemp,omitempty"`
}
func parseWriteFileParams(r *http.Request) (*writeFileParamsType, multipart.File, error) {
err := r.ParseMultipartForm(MaxWriteFileMemSize)
if err != nil {
return nil, nil, fmt.Errorf("cannot parse multipart form data: %v", err)
}
form := r.MultipartForm
if len(form.Value["params"]) == 0 {
return nil, nil, fmt.Errorf("no params found")
}
paramsStr := form.Value["params"][0]
var params writeFileParamsType
err = json.Unmarshal([]byte(paramsStr), &params)
if err != nil {
return nil, nil, fmt.Errorf("bad params json: %v", err)
}
if len(form.File["data"]) == 0 {
return nil, nil, fmt.Errorf("no data found")
}
fileHeader := form.File["data"][0]
file, err := fileHeader.Open()
if err != nil {
return nil, nil, fmt.Errorf("error opening multipart data file: %v", err)
}
return &params, file, nil
}
func HandleWriteFile(w http.ResponseWriter, r *http.Request) {
defer func() {
r := recover()
if r == nil {
return
}
log.Printf("[error] in write-file: %v\n", r)
debug.PrintStack()
WriteJsonError(w, fmt.Errorf(ErrorPanic, r))
}()
w.Header().Set(CacheControlHeaderKey, CacheControlHeaderNoCache)
params, mpFile, err := parseWriteFileParams(r)
if err != nil {
WriteJsonError(w, fmt.Errorf("error parsing multipart form params: %w", err))
return
}
if params.ScreenId == "" || params.LineId == "" || params.Path == "" {
WriteJsonError(w, fmt.Errorf("invalid params, must set screenid, lineid, and path"))
return
}
if _, err := uuid.Parse(params.ScreenId); err != nil {
WriteJsonError(w, fmt.Errorf(ErrorInvalidScreenId, err))
return
}
if _, err := uuid.Parse(params.LineId); err != nil {
WriteJsonError(w, fmt.Errorf(ErrorInvalidLineId, err))
return
}
_, cmd, err := sstore.GetLineCmdByLineId(r.Context(), params.ScreenId, params.LineId)
if err != nil {
WriteJsonError(w, fmt.Errorf("cannot retrieve line/cmd: %v", err))
return
}
if cmd == nil {
WriteJsonError(w, fmt.Errorf("line not found"))
return
}
if cmd.Remote.RemoteId == "" {
WriteJsonError(w, fmt.Errorf("invalid line, no remote"))
return
}
wsh := remote.GetRemoteById(cmd.Remote.RemoteId)
if wsh == nil {
WriteJsonError(w, fmt.Errorf("invalid line, cannot resolve remote"))
return
}
rrState := wsh.GetRemoteRuntimeState()
2023-09-02 05:40:28 +02:00
fullPath, err := rrState.ExpandHomeDir(params.Path)
if err != nil {
WriteJsonError(w, fmt.Errorf("error expanding homedir: %v", err))
return
}
cwd := cmd.FeState["cwd"]
writePk := packet.MakeWriteFilePacket()
writePk.ReqId = uuid.New().String()
writePk.UseTemp = params.UseTemp
2023-09-02 05:40:28 +02:00
if filepath.IsAbs(fullPath) {
writePk.Path = fullPath
} else {
2023-09-02 05:40:28 +02:00
writePk.Path = filepath.Join(cwd, fullPath)
}
iter, err := wsh.PacketRpcIter(r.Context(), writePk)
if err != nil {
WriteJsonError(w, fmt.Errorf("error: %v", err))
return
}
// first packet should be WriteFileReady
readyIf, err := iter.Next(r.Context())
if err != nil {
WriteJsonError(w, fmt.Errorf("error while getting ready response: %w", err))
return
}
readyPk, ok := readyIf.(*packet.WriteFileReadyPacketType)
if !ok {
WriteJsonError(w, fmt.Errorf("bad ready packet received: %T", readyIf))
return
}
if readyPk.Error != "" {
WriteJsonError(w, fmt.Errorf("ready error: %s", readyPk.Error))
return
}
var buffer [server.MaxFileDataPacketSize]byte
bufSlice := buffer[:]
for {
dataPk := packet.MakeFileDataPacket(writePk.ReqId)
nr, err := io.ReadFull(mpFile, bufSlice)
if err == io.ErrUnexpectedEOF || err == io.EOF {
dataPk.Eof = true
} else if err != nil {
dataErr := fmt.Errorf("error reading file data: %v", err)
dataPk.Error = dataErr.Error()
wsh.SendFileData(dataPk)
WriteJsonError(w, dataErr)
return
}
if nr > 0 {
dataPk.Data = make([]byte, nr)
copy(dataPk.Data, bufSlice[0:nr])
}
wsh.SendFileData(dataPk)
if dataPk.Eof {
break
}
// slight throttle for sending packets
time.Sleep(10 * time.Millisecond)
}
doneIf, err := iter.Next(r.Context())
if err != nil {
WriteJsonError(w, fmt.Errorf("error while getting done response: %w", err))
return
}
donePk, ok := doneIf.(*packet.WriteFileDonePacketType)
if !ok {
WriteJsonError(w, fmt.Errorf("bad done packet received: %T", doneIf))
return
}
if donePk.Error != "" {
WriteJsonError(w, fmt.Errorf("dne error: %s", donePk.Error))
return
}
WriteJsonSuccess(w, nil)
}
func HandleReadFile(w http.ResponseWriter, r *http.Request) {
qvals := r.URL.Query()
screenId := qvals.Get("screenid")
lineId := qvals.Get("lineid")
path := qvals.Get("path") // validate path?
contentType := qvals.Get("mimetype") // force a mimetype
if screenId == "" || lineId == "" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("must specify sessionid, screenid, and lineid"))
return
}
if path == "" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("must specify path"))
return
}
if _, err := uuid.Parse(screenId); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf(ErrorInvalidScreenId, err)))
return
}
if _, err := uuid.Parse(lineId); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf(ErrorInvalidLineId, err)))
return
}
if contentType != "" && !ContentTypeHeaderValidRe.MatchString(contentType) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("invalid mimetype specified"))
return
}
_, cmd, err := sstore.GetLineCmdByLineId(r.Context(), screenId, lineId)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("invalid lineid: %v", err)))
return
}
if cmd == nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("invalid line, no cmd"))
return
}
if cmd.Remote.RemoteId == "" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("invalid line, no remote"))
return
}
wsh := remote.GetRemoteById(cmd.Remote.RemoteId)
if wsh == nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("invalid line, cannot resolve remote"))
return
}
rrState := wsh.GetRemoteRuntimeState()
2023-09-02 05:40:28 +02:00
fullPath, err := rrState.ExpandHomeDir(path)
if err != nil {
WriteJsonError(w, fmt.Errorf("error expanding homedir: %v", err))
return
}
streamPk := packet.MakeStreamFilePacket()
streamPk.ReqId = uuid.New().String()
cwd := cmd.FeState["cwd"]
if filepath.IsAbs(fullPath) {
streamPk.Path = fullPath
} else {
streamPk.Path = filepath.Join(cwd, fullPath)
}
iter, err := wsh.StreamFile(r.Context(), streamPk)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("error trying to stream file: %v", err)))
return
}
defer iter.Close()
respIf, err := iter.Next(r.Context())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("error getting streamfile response: %v", err)))
return
}
resp, ok := respIf.(*packet.StreamFileResponseType)
if !ok {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("bad response packet type: %T", respIf)))
return
}
if resp.Error != "" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("error response: %s", resp.Error)))
return
}
infoJson, _ := json.Marshal(resp.Info)
if contentType == "" && resp.Info.MimeType != "" {
contentType = resp.Info.MimeType
}
if contentType == "" {
contentType = "application/octet-stream"
}
w.Header().Set("X-FileInfo", base64.StdEncoding.EncodeToString(infoJson))
w.Header().Set(ContentTypeHeaderKey, contentType)
w.WriteHeader(http.StatusOK)
for {
dataPkIf, err := iter.Next(r.Context())
if err != nil {
log.Printf("error in read-file while getting data: %v\n", err)
break
}
if dataPkIf == nil {
break
}
dataPk, ok := dataPkIf.(*packet.FileDataPacketType)
if !ok {
log.Printf("error in read-file, invalid data packet type: %T", dataPkIf)
break
}
if dataPk.Error != "" {
log.Printf("in read-file, data packet error: %s", dataPk.Error)
break
}
w.Write(dataPk.Data)
}
}
2022-06-12 22:39:48 +02:00
func WriteJsonError(w http.ResponseWriter, errVal error) {
w.Header().Set(ContentTypeHeaderKey, ContentTypeJson)
w.WriteHeader(http.StatusOK)
2022-06-12 22:39:48 +02:00
errMap := make(map[string]interface{})
errMap["error"] = errVal.Error()
errorCode := base.GetErrorCode(errVal)
if errorCode != "" {
errMap["errorcode"] = errorCode
}
2022-06-12 22:39:48 +02:00
barr, _ := json.Marshal(errMap)
w.Write(barr)
}
func WriteJsonSuccess(w http.ResponseWriter, data interface{}) {
w.Header().Set(ContentTypeHeaderKey, ContentTypeJson)
2022-06-12 22:39:48 +02:00
rtnMap := make(map[string]interface{})
rtnMap["success"] = true
if data != nil {
rtnMap["data"] = data
}
barr, err := json.Marshal(rtnMap)
if err != nil {
WriteJsonError(w, err)
return
}
w.WriteHeader(http.StatusOK)
2022-06-12 22:39:48 +02:00
w.Write(barr)
}
func HandleRunCommand(w http.ResponseWriter, r *http.Request) {
2022-07-15 10:57:45 +02:00
defer func() {
r := recover()
if r == nil {
return
}
2022-10-31 20:40:45 +01:00
log.Printf("[error] in run-command: %v\n", r)
2022-07-15 10:57:45 +02:00
debug.PrintStack()
WriteJsonError(w, fmt.Errorf(ErrorPanic, r))
2022-07-15 10:57:45 +02:00
}()
w.Header().Set(CacheControlHeaderKey, CacheControlHeaderNoCache)
2022-06-12 22:39:48 +02:00
decoder := json.NewDecoder(r.Body)
2022-07-02 22:31:56 +02:00
var commandPk scpacket.FeCommandPacketType
err := decoder.Decode(&commandPk)
2022-06-12 22:39:48 +02:00
if err != nil {
WriteJsonError(w, fmt.Errorf(ErrorDecodingJson, err))
2022-06-12 22:39:48 +02:00
return
}
update, err := cmdrunner.HandleCommand(r.Context(), &commandPk)
2022-07-03 21:01:56 +02:00
if err != nil {
WriteJsonError(w, err)
2022-06-13 20:11:56 +02:00
return
}
if update != nil {
update.Clean()
}
WriteJsonSuccess(w, update)
2022-07-03 21:01:56 +02:00
}
func HandleRunEphemeralCommand(w http.ResponseWriter, r *http.Request) {
defer func() {
r := recover()
if r == nil {
return
}
log.Printf("[error] in run-ephemeral-command: %v\n", r)
debug.PrintStack()
WriteJsonError(w, fmt.Errorf(ErrorPanic, r))
}()
w.Header().Set(CacheControlHeaderKey, CacheControlHeaderNoCache)
decoder := json.NewDecoder(r.Body)
var commandPk scpacket.FeCommandPacketType
err := decoder.Decode(&commandPk)
if err != nil {
WriteJsonError(w, fmt.Errorf(ErrorDecodingJson, err))
return
}
log.Printf("Running ephemeral command: %v\n", commandPk)
if commandPk.EphemeralOpts == nil {
commandPk.EphemeralOpts = &ephemeral.EphemeralRunOpts{}
}
if commandPk.EphemeralOpts.TimeoutMs == 0 {
commandPk.EphemeralOpts.TimeoutMs = ephemeral.DefaultEphemeralTimeoutMs
}
// These need to be defined here so we can use the methods of the BufferedPipe that are not part of io.WriteCloser
var stdoutPipe, stderrPipe *bufferedpipe.BufferedPipe
if commandPk.EphemeralOpts.ExpectsResponse {
// Create new buffered pipes for stdout and stderr
stdoutPipe = bufferedpipe.NewBufferedPipe(ephemeral.DefaultEphemeralTimeoutDuration)
commandPk.EphemeralOpts.StdoutWriter = stdoutPipe
stderrPipe = bufferedpipe.NewBufferedPipe(ephemeral.DefaultEphemeralTimeoutDuration)
commandPk.EphemeralOpts.StderrWriter = stderrPipe
}
update, err := cmdrunner.HandleCommand(r.Context(), &commandPk)
if err != nil {
log.Printf("Error occurred while running ephemeral command: %v\n", err)
if commandPk.EphemeralOpts.ExpectsResponse {
log.Printf("Closing buffered pipes\n")
stdoutPipe.Close()
stderrPipe.Close()
}
WriteJsonError(w, err)
return
}
resp := scpacket.EphemeralCommandResponsePacketType{}
// No error occurred, so we can write the response to the client
if commandPk.EphemeralOpts.ExpectsResponse {
// If the client expects a response, we need to send the urls of the stdout and stderr outputs
stdoutUrl, err := stdoutPipe.GetOutputUrl()
if err != nil {
log.Printf("Error occurred while getting stdout url: %v\n", err)
WriteJsonError(w, err)
return
}
resp.StdoutUrl = stdoutUrl
stderrUrl, err := stderrPipe.GetOutputUrl()
if err != nil {
log.Printf("Error occurred while getting stderr url: %v\n", err)
WriteJsonError(w, err)
return
}
resp.StderrUrl = stderrUrl
}
WriteJsonSuccess(w, resp)
// With ephemeral commands, we can't send the update back directly, so we need to send it through the update bus
if update != nil {
log.Printf("Sending update to main update bus\n")
update.Clean()
scbus.MainUpdateBus.DoUpdate(update)
}
}
// Checks if the /config request is for a specific file or a directory. Passes the request to the appropriate handler.
func ConfigHandlerCheckIsDir(dirHandler http.Handler, fileHandler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
configPath := r.URL.Path
configBaseDir := filepath.Join(scbase.GetWaveHomeDir(), "config")
configFullPath, err := filepath.Abs(filepath.Join(scbase.GetWaveHomeDir(), configPath))
if err != nil || !strings.HasPrefix(configFullPath, configBaseDir) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("error: path is not in config folder"))
return
}
fstat, err := os.Stat(configFullPath)
if errors.Is(err, fs.ErrNotExist) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(fmt.Sprintf("file not found: %v", err)))
return
} else if err != nil {
w.WriteHeader(http.StatusInternalServerError)
2024-03-21 00:29:46 +01:00
w.Write([]byte(fmt.Sprintf("file stat err: %v", err)))
return
}
if fstat.IsDir() {
AuthKeyMiddleWare(dirHandler).ServeHTTP(w, r)
} else {
AuthKeyMiddleWare(fileHandler).ServeHTTP(w, r)
}
})
}
func AuthKeyMiddleWare(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqAuthKey := r.Header.Get("X-AuthKey")
w.Header().Set(CacheControlHeaderKey, CacheControlHeaderNoCache)
if reqAuthKey == "" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("no x-authkey header"))
return
}
if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("x-authkey header is invalid"))
return
}
next.ServeHTTP(w, r)
})
}
func AuthKeyWrapAllowHmac(fn WebFnType) WebFnType {
return func(w http.ResponseWriter, r *http.Request) {
reqAuthKey := r.Header.Get("X-AuthKey")
if reqAuthKey == "" {
// try hmac
qvals := r.URL.Query()
if !qvals.Has("hmac") {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("no x-authkey header"))
return
}
Sudo Caching (#573) * feat: share sudo between pty sessions This is a first pass at a feature to cache the sudo password and share it between different pty sessions. This makes it possible to not require manual password entry every time sudo is used. * feat: allow error handling and canceling sudo cmds This adds the missing functionality that prevented failed sudo commands from automatically closing. * feat: restrict sudo caching to dev mode for now * modify fullCmdStr not pk.Command * refactor: condense ecdh encryptor creation This refactors the common pieces needed to create an encryptor from an ecdh key pair into a common function. * refactor: rename promptenc to waveenc * feat: add command to clear sudo password We currently do not provide use of the sudo -k and sudo -K commands to clear the sudo password. This adds a /sudo:clear command to handle it in the meantime. * feat: add kwarg to force sudo In cases where parsing for sudo doesn't work, this provides an alternate wave kwarg to use instead. It can be used with [sudo=1] at the beginning of a command. * refactor: simplify sudoArg parsing * feat: allow user to clear all sudo passwords This introduces the "all" kwarg for the sudo:clear command in order to clear all sudo passwords. * fix: handle deadline with real time Golang's time module uses monatomic time by default, but that is not desired for the password timeout since we want the timer to continue even if the computer is asleep. We now avoid this by directly comparing the unix timestamps. * fix: remove sudo restriction to dev mode This allows it to be used in regular builds as well. * fix: switch to password timeout without wait group This removes an unnecessary waiting period for sudo password entry. * fix: update deadline in sudo:clear This allows sudo:clear to cancel the goroutine for watching the password timer. * fix: pluralize sudo:clear message when all=1 This changes the output message for /sudo:clear to indicate multiple passwords cleared if the all=1 kwarg is used. * fix: use GetRemoteMap for getting remotes in clear The sudo:clear command was directly looping over the GlobalStore.Map which is not thread safe. Switched to GetRemoteMap which uses a lock internally. * fix: allow sudo metacmd to set sudo false This fixes the logic for resolving if a command is a sudo command. This change makes it possible for the sudo metacmd kwarg to force sudo to be false.
2024-04-17 01:58:17 +02:00
hmacOk, err := waveenc.ValidateUrlHmac([]byte(scbase.WaveAuthKey), r.URL.Path, qvals)
if err != nil || !hmacOk {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("error validating hmac"))
return
}
// fallthrough (hmac is valid)
} else if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("x-authkey header is invalid"))
return
}
w.Header().Set(CacheControlHeaderKey, CacheControlHeaderNoCache)
fn(w, r)
}
}
func AuthKeyWrap(fn WebFnType) WebFnType {
return func(w http.ResponseWriter, r *http.Request) {
reqAuthKey := r.Header.Get("X-AuthKey")
if reqAuthKey == "" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("no x-authkey header"))
return
}
if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("x-authkey header is invalid"))
return
}
w.Header().Set(CacheControlHeaderKey, CacheControlHeaderNoCache)
fn(w, r)
}
}
func runWebSocketServer() {
gr := mux.NewRouter()
gr.HandleFunc("/ws", HandleWs)
serverAddr := WebSocketServerAddr
if scbase.IsDevMode() {
serverAddr = WebSocketServerDevAddr
}
server := &http.Server{
Addr: serverAddr,
ReadTimeout: HttpReadTimeout,
WriteTimeout: HttpWriteTimeout,
MaxHeaderBytes: HttpMaxHeaderBytes,
Handler: gr,
}
server.SetKeepAlivesEnabled(false)
log.Printf("Running websocket server on %s\n", serverAddr)
err := server.ListenAndServe()
if err != nil {
2022-10-31 20:40:45 +01:00
log.Printf("[error] trying to run websocket server: %v\n", err)
}
}
func test() error {
return nil
}
func sendTelemetryWrapper() {
defer func() {
r := recover()
if r == nil {
return
}
log.Printf("[error] in sendTelemetryWrapper: %v\n", r)
debug.PrintStack()
}()
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
err := pcloud.SendTelemetry(ctx, false)
if err != nil {
log.Printf("[error] sending telemetry: %v\n", err)
}
}
func checkNewReleaseWrapper() {
defer func() {
r := recover()
if r == nil {
return
}
log.Printf("[error] in checkNewReleaseWrapper: %v\n", r)
debug.PrintStack()
}()
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
_, err := releasechecker.CheckNewRelease(ctx, false)
if err != nil {
log.Printf("[error] checking for new release: %v\n", err)
return
}
}
func telemetryLoop() {
var nextSend int64
time.Sleep(InitialTelemetryWait)
for {
if time.Now().Unix() > nextSend {
nextSend = time.Now().Add(TelemetryInterval).Unix()
sendTelemetryWrapper()
checkNewReleaseWrapper()
}
time.Sleep(TelemetryTick)
}
}
// watch stdin, kill server if stdin is closed
func stdinReadWatch() {
buf := make([]byte, 1024)
for {
_, err := os.Stdin.Read(buf)
if err != nil {
doShutdown(fmt.Sprintf("stdin closed/error (%v)", err))
break
}
}
}
// ignore SIGHUP
func installSignalHandlers() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP)
go func() {
for sig := range sigCh {
doShutdown(fmt.Sprintf("got signal %v", sig))
break
}
}()
}
func doShutdown(reason string) {
shutdownOnce.Do(func() {
log.Printf("[wave] local server %v, start shutdown\n", reason)
shutdownActivityUpdate()
sendTelemetryWrapper()
log.Printf("[wave] closing db connection\n")
sstore.CloseDB()
log.Printf("[wave] *** shutting down local server\n")
Simplified terminal theming (#570) * save work * reusable StyleBlock component * StyleBlock in elements dir * root level * ability to inherit root styles * change prop from classname to selector * selector should always be :root * remove selector prop from StyleBlock * working * cleanup * loadThemeStyles doesn't have to be async * revert changes in tabs2.less * remove old implementation * cleanup * remove file from another branch * fix issue where line in history view doesn't reflect the terminal theme * add key and value validation * add label to tab settings terminal theme dropdown * save work * save work * save work * working * trigger componentDidUpdate when switching tabs and sessions * cleanup * save work * save work * use UpdatePacket for theme changes as well * make methods cohesive * use themes coming from backend * reload terminal when styel block is unmounted and mounted * fix validation * re-render terminal when theme is updated * remove test styles * cleanup * more cleanup * revert unneeded change * more cleanup * fix type * more cleanup * render style blocks in the header instead of body using portal * add ability to reuse and dispose TermThemes instance and file watcher * remove comment * minor change * separate filewatcher as singleton * do not render app when term theme style blocks aren't rendered first * only render main when termstyles have been rendered already * add comment * use DoUpdate to send themes to front-end * support to watch subdirectories * added support for watch subdirectories * make watcher more flexible so it can be closed anywhere * cleanup * undo the app/main split * use TermThemesType in creating initial value for Themes field * simplify code * fix issue where dropdown label doesn't float when the theme selected is Inherit * remove unsed var * start watcher in main, merge themes (don't overwrite) on event. * ensure terminal-themes directory is created on startup * ah, wait for termThemes to be set (the connect packet needs to have been processed to proceed with rendering)
2024-04-24 08:22:35 +02:00
watcher := configstore.GetWatcher()
if watcher != nil {
watcher.Close()
}
time.Sleep(1 * time.Second)
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
time.Sleep(5 * time.Second)
syscall.Kill(syscall.Getpid(), syscall.SIGKILL)
})
}
func configDirHandler(w http.ResponseWriter, r *http.Request) {
configPath := r.URL.Path
homeDir := scbase.GetWaveHomeDir()
configFullPath, err := filepath.Abs(filepath.Join(homeDir, configPath))
if err != nil || !strings.HasPrefix(configFullPath, homeDir) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("Invalid path: %v", err)))
return
}
dirFile, err := os.Open(configFullPath)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
2024-03-21 00:29:46 +01:00
w.Write([]byte(fmt.Sprintf("error opening specified dir: %v", err)))
return
}
entries, err := dirFile.Readdir(0)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
2024-03-21 00:29:46 +01:00
w.Write([]byte(fmt.Sprintf("error getting files: %v", err)))
return
}
var files []*packet.FileStatPacketType
for index := 0; index < len(entries); index++ {
curEntry := entries[index]
curFile := packet.MakeFileStatPacketFromFileInfo(curEntry, "", false)
files = append(files, curFile)
}
dirListJson, err := json.Marshal(files)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
2024-03-21 00:29:46 +01:00
w.Write([]byte(fmt.Sprintf("json err: %v", err)))
return
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
w.Write(dirListJson)
}
Simplified terminal theming (#570) * save work * reusable StyleBlock component * StyleBlock in elements dir * root level * ability to inherit root styles * change prop from classname to selector * selector should always be :root * remove selector prop from StyleBlock * working * cleanup * loadThemeStyles doesn't have to be async * revert changes in tabs2.less * remove old implementation * cleanup * remove file from another branch * fix issue where line in history view doesn't reflect the terminal theme * add key and value validation * add label to tab settings terminal theme dropdown * save work * save work * save work * working * trigger componentDidUpdate when switching tabs and sessions * cleanup * save work * save work * use UpdatePacket for theme changes as well * make methods cohesive * use themes coming from backend * reload terminal when styel block is unmounted and mounted * fix validation * re-render terminal when theme is updated * remove test styles * cleanup * more cleanup * revert unneeded change * more cleanup * fix type * more cleanup * render style blocks in the header instead of body using portal * add ability to reuse and dispose TermThemes instance and file watcher * remove comment * minor change * separate filewatcher as singleton * do not render app when term theme style blocks aren't rendered first * only render main when termstyles have been rendered already * add comment * use DoUpdate to send themes to front-end * support to watch subdirectories * added support for watch subdirectories * make watcher more flexible so it can be closed anywhere * cleanup * undo the app/main split * use TermThemesType in creating initial value for Themes field * simplify code * fix issue where dropdown label doesn't float when the theme selected is Inherit * remove unsed var * start watcher in main, merge themes (don't overwrite) on event. * ensure terminal-themes directory is created on startup * ah, wait for termThemes to be set (the connect packet needs to have been processed to proceed with rendering)
2024-04-24 08:22:35 +02:00
func configWatcher() {
watcher := configstore.GetWatcher()
if watcher != nil {
watcher.Start()
}
}
func startupActivityUpdate() {
activity := telemetry.ActivityUpdate{
NumConns: remote.NumRemotes(),
Startup: 1,
}
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
activity.NumWorkspaces, _ = sstore.NumSessions(ctx)
activity.NumTabs, _ = sstore.NumScreens(ctx)
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)
}
}
func shutdownActivityUpdate() {
activity := telemetry.ActivityUpdate{Shutdown: 1}
ctx, cancelFn := context.WithTimeout(context.Background(), 1*time.Second)
defer cancelFn()
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)
}
}
2022-06-12 22:39:48 +02:00
func main() {
2023-02-24 00:17:47 +01:00
scbase.BuildTime = BuildTime
scbase.WaveVersion = WaveVersion
zsh support (#227) adds zsh support to waveterm. big change, lots going on here. lots of other improvements and bug fixes added while debugging and building out the feature. Commits: * refactor shexec parser.go into new package shellenv. separate out bash specific parsing from generic functions * checkpoint * work on refactoring shexec. created two new packages shellapi (for bash/zsh specific stuff), and shellutil (shared between shellapi and shexec) * more refactoring * create shellapi interface to abstract bash specific functionality * more refactoring, move bash shell state parsing to shellapi * move makeRcFile to shellapi. remove all of the 'client' options CLI options from waveshell * get shellType passed through to server/single paths for waveshell * add a local shelltype detector * mock out a zshapi * move shelltype through more of the code * get a command to run via zsh * zsh can now switch directories. poc, needs cleanup * working on ShellState encoding differences between zsh/bash. Working on parsing zsh decls. move utilfn package into waveshell (shouldn't have been in wavesrv) * switch to use []byte for vardecl serialization + diffs * progress on zsh environment. still have issues reconciling init environment with trap environment * fix typeset argument parsing * parse promptvars, more zsh specific ignores * fix bug with promptvar not getting set (wrong check in FeState func) * add sdk (issue #188) to list of rtnstate commands * more zsh compatibility -- working with a larger ohmyzsh environment. ignore more variables, handle exit trap better. unique path/fpath. add a processtype variable to base. * must return a value * zsh alias parsing/restoring. diff changes (and rtnstate changes). introduces linediff v1. * force zmodload of zsh/parameter * starting work on zsh functions * need a v1 of mapdiff as well (to handle null chars) * pack/unpack of ints was wrong (one used int and one use uint). turned out we only ever encoded '0' so it worked. that also means it is safe to change unpack to unpackUInt * reworking for binary encoding of aliases and functions (because of zsh allows any character, including nulls, in names and values) * fixes, working on functions, issue with line endings * zsh functions. lots of ugliness here around dealing with line dicipline and cooked stty. new runcommand function to grab output from a non-tty fd. note that we still to run the actual command in a stty to get the proper output. * write uuid tempdir, cleanup with tmprcfilename code * hack in some simple zsh function declaration finding code for rtnstate. create function diff for rtnstate that supports zsh * make sure key order is constant so shell hashes are consistent * fix problems with state diffs to support new zsh formats. add diff/apply code to shellapi (moved from shellenv), that is now specific to zsh or bash * add log packet and new shellstate packets * switch to shellstate map that's also keyed by shelltype * add shelltype to remoteinstance * remove shell argument from waveshell * added new shelltype statemap to remote.go (msh), deal with fallout * move shellstate out of init packet, and move to an explicit reinit call. try to initialize all of the active shell states * change dont always store init state (only store on demand). initialize shell states on demand (if not already initialized). allow reset to change shells * add shellpref field to remote table. use to drive the default shell choice for new tabs * show shelltag on cmdinput, pass through ri and remote (defaultshellstate) * bump mshell version to v0.4 * better version validation for shellstate. also relax compatibility requirements for diffing states (shelltype + major version need to match) * better error handling, check shellstate compatibility during run (on waveshell server) * add extra separator for bash shellstate processing to deal with spurious output from rc files * special migration for v30 -- flag invalid bash shell states and show special button in UI to fix * format * remove zsh-decls (unused) * remove test code * remove debug print * fix typo
2024-01-17 01:11:04 +01:00
base.ProcessType = base.ProcessType_WaveSrv
wlog.GlobalSubsystem = base.ProcessType_WaveSrv
wlog.LogConsumer = wlog.LogWithLogger
2023-02-24 00:17:47 +01:00
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
if len(os.Args) >= 2 && os.Args[1] == "--test" {
2022-10-31 20:40:45 +01:00
log.Printf("running test fn\n")
err := test()
if err != nil {
2022-10-31 20:40:45 +01:00
log.Printf("[error] %v\n", err)
}
return
}
scHomeDir := scbase.GetWaveHomeDir()
log.Printf("[wave] *** starting wavesrv version %s+%s\n", scbase.WaveVersion, scbase.BuildTime)
log.Printf("[wave] homedir = %q\n", scHomeDir)
2022-10-31 20:40:45 +01:00
scLock, err := scbase.AcquireWaveLock()
if err != nil || scLock == nil {
log.Printf("[error] cannot acquire wave lock (another instance of wavesrv is likely running): %v\n", err)
return
}
if len(os.Args) >= 2 && strings.HasPrefix(os.Args[1], "--migrate") {
err := sstore.MigrateCommandOpts(os.Args[1:])
if err != nil {
2022-10-31 20:40:45 +01:00
log.Printf("[error] migrate cmd: %v\n", err)
}
return
}
err = scbase.InitializeWaveAuthKey()
if err != nil {
log.Printf("[error] %v\n", err)
return
}
Simplified terminal theming (#570) * save work * reusable StyleBlock component * StyleBlock in elements dir * root level * ability to inherit root styles * change prop from classname to selector * selector should always be :root * remove selector prop from StyleBlock * working * cleanup * loadThemeStyles doesn't have to be async * revert changes in tabs2.less * remove old implementation * cleanup * remove file from another branch * fix issue where line in history view doesn't reflect the terminal theme * add key and value validation * add label to tab settings terminal theme dropdown * save work * save work * save work * working * trigger componentDidUpdate when switching tabs and sessions * cleanup * save work * save work * use UpdatePacket for theme changes as well * make methods cohesive * use themes coming from backend * reload terminal when styel block is unmounted and mounted * fix validation * re-render terminal when theme is updated * remove test styles * cleanup * more cleanup * revert unneeded change * more cleanup * fix type * more cleanup * render style blocks in the header instead of body using portal * add ability to reuse and dispose TermThemes instance and file watcher * remove comment * minor change * separate filewatcher as singleton * do not render app when term theme style blocks aren't rendered first * only render main when termstyles have been rendered already * add comment * use DoUpdate to send themes to front-end * support to watch subdirectories * added support for watch subdirectories * make watcher more flexible so it can be closed anywhere * cleanup * undo the app/main split * use TermThemesType in creating initial value for Themes field * simplify code * fix issue where dropdown label doesn't float when the theme selected is Inherit * remove unsed var * start watcher in main, merge themes (don't overwrite) on event. * ensure terminal-themes directory is created on startup * ah, wait for termThemes to be set (the connect packet needs to have been processed to proceed with rendering)
2024-04-24 08:22:35 +02:00
_, err = scbase.EnsureConfigDirs()
if err != nil {
log.Printf("[error] ensuring config directory: %v\n", err)
return
}
err = sstore.TryMigrateUp()
if err != nil {
2022-10-31 20:40:45 +01:00
log.Printf("[error] migrate up: %v\n", err)
return
}
2022-09-25 09:26:33 +02:00
clientData, err := sstore.EnsureClientData(context.Background())
if err != nil {
2022-10-31 20:40:45 +01:00
log.Printf("[error] ensuring client data: %v\n", err)
return
}
2022-10-31 20:40:45 +01:00
log.Printf("userid = %s\n", clientData.UserId)
2022-07-01 21:17:19 +02:00
err = sstore.EnsureLocalRemote(context.Background())
if err != nil {
2022-10-31 20:40:45 +01:00
log.Printf("[error] ensuring local remote: %v\n", err)
2022-07-01 21:17:19 +02:00
return
}
err = remote.LoadRemotes(context.Background())
2022-06-12 22:39:48 +02:00
if err != nil {
2022-10-31 20:40:45 +01:00
log.Printf("[error] loading remotes: %v\n", err)
2022-06-12 22:39:48 +02:00
return
}
err = sstore.HangupAllRunningCmds(context.Background())
if err != nil {
2023-01-12 05:53:46 +01:00
log.Printf("[error] calling HUP on all running commands: %v\n", err)
}
err = sstore.ReInitFocus(context.Background())
if err != nil {
2023-03-15 00:37:22 +01:00
log.Printf("[error] resetting screen focus: %v\n", err)
}
log.Printf("PCLOUD_ENDPOINT=%s\n", pcloud.GetEndpoint())
startupActivityUpdate()
installSignalHandlers()
go telemetryLoop()
Simplified terminal theming (#570) * save work * reusable StyleBlock component * StyleBlock in elements dir * root level * ability to inherit root styles * change prop from classname to selector * selector should always be :root * remove selector prop from StyleBlock * working * cleanup * loadThemeStyles doesn't have to be async * revert changes in tabs2.less * remove old implementation * cleanup * remove file from another branch * fix issue where line in history view doesn't reflect the terminal theme * add key and value validation * add label to tab settings terminal theme dropdown * save work * save work * save work * working * trigger componentDidUpdate when switching tabs and sessions * cleanup * save work * save work * use UpdatePacket for theme changes as well * make methods cohesive * use themes coming from backend * reload terminal when styel block is unmounted and mounted * fix validation * re-render terminal when theme is updated * remove test styles * cleanup * more cleanup * revert unneeded change * more cleanup * fix type * more cleanup * render style blocks in the header instead of body using portal * add ability to reuse and dispose TermThemes instance and file watcher * remove comment * minor change * separate filewatcher as singleton * do not render app when term theme style blocks aren't rendered first * only render main when termstyles have been rendered already * add comment * use DoUpdate to send themes to front-end * support to watch subdirectories * added support for watch subdirectories * make watcher more flexible so it can be closed anywhere * cleanup * undo the app/main split * use TermThemesType in creating initial value for Themes field * simplify code * fix issue where dropdown label doesn't float when the theme selected is Inherit * remove unsed var * start watcher in main, merge themes (don't overwrite) on event. * ensure terminal-themes directory is created on startup * ah, wait for termThemes to be set (the connect packet needs to have been processed to proceed with rendering)
2024-04-24 08:22:35 +02:00
go configWatcher()
go stdinReadWatch()
go runWebSocketServer()
2023-03-27 03:48:43 +02:00
go func() {
time.Sleep(10 * time.Second)
pcloud.StartUpdateWriter()
}()
2022-06-12 22:39:48 +02:00
gr := mux.NewRouter()
gr.HandleFunc("/api/ptyout", AuthKeyWrap(HandleGetPtyOut))
gr.HandleFunc("/api/remote-pty", AuthKeyWrap(HandleRemotePty))
gr.HandleFunc("/api/rtnstate", AuthKeyWrap(HandleRtnState))
gr.HandleFunc("/api/get-screen-lines", AuthKeyWrap(HandleGetScreenLines))
gr.HandleFunc("/api/run-command", AuthKeyWrap(HandleRunCommand)).Methods("POST")
gr.HandleFunc("/api/run-ephemeral-command", AuthKeyWrap(HandleRunEphemeralCommand)).Methods("POST")
gr.HandleFunc(bufferedpipe.BufferedPipeGetterUrl, AuthKeyWrapAllowHmac(bufferedpipe.HandleGetBufferedPipeOutput))
gr.HandleFunc("/api/get-client-data", AuthKeyWrap(HandleGetClientData))
gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize))
Sudo Config Gui (#603) * feat: add gui elements to configure ssh pw cache This adds a dropdown for on/off/notimeout, a number entry box for a timeout value, and a toggle for clearing when the computer sleeps. * fix: improve password timeout entry This makes the password timeout more consistent by using an inline settings element. It also creates the inline settings element to parse the input. * feat: turn sudo password caching on and off * feat: use configurable sudo timeout This makes it possible to control how long waveterm stores your sudo password. Note that if it changes, it immediately clears the cached passwords. * fix: clear existing sudo passwords if switched off When the sudo password store state is changed to "off", all existing passwords must immediately be cleared automatically. * feat: allow clearing sudo passwords on suspend This option makes it so the sudo passwords will be cleared when the computer falls asleep. It will never be used in the case where the password is set to never time out. * feat: allow notimeout to prevent sudo pw clear This option allows the sudo timeout to be ignored while it is selected. * feat: adjust current deadline based on user config This allows the deadline to update as changes to the config are happening. * fix: reject a sudopwtimeout of 0 on the backend * fix: use the default sudoPwTimeout for empty input * fix: specify the timeout length is minutes * fix: store sudopwtimeout in ms instead of minutes * fix: formatting the default sudo timeout By changing the order of operations, this no longer shows up as NaN if the default is used. * refactor: consolidate inlinesettingstextedit This removes the number variant and combines them into the same class with an option to switch between the two behaviors. * refactor: consolidate textfield and numberfield This removes the number variant of textfield. The textfield component can now act as a numberfield when the optional isNumber prop is true.
2024-04-26 03:19:43 +02:00
gr.HandleFunc("/api/power-monitor", AuthKeyWrap(HandlePowerMonitor))
2023-01-17 08:36:52 +01:00
gr.HandleFunc("/api/log-active-state", AuthKeyWrap(HandleLogActiveState))
gr.HandleFunc("/api/read-file", AuthKeyWrapAllowHmac(HandleReadFile))
gr.HandleFunc("/api/write-file", AuthKeyWrap(HandleWriteFile)).Methods("POST")
configPath := filepath.Join(scbase.GetWaveHomeDir(), "config") + string(filepath.Separator)
log.Printf("[wave] config path: %q\n", configPath)
isFileHandler := http.StripPrefix("/config/", http.FileServer(http.Dir(configPath)))
isDirHandler := http.HandlerFunc(configDirHandler)
gr.PathPrefix("/config/").Handler(ConfigHandlerCheckIsDir(isDirHandler, isFileHandler))
serverAddr := MainServerAddr
if scbase.IsDevMode() {
serverAddr = MainServerDevAddr
}
2022-06-12 22:39:48 +02:00
server := &http.Server{
Addr: serverAddr,
2022-06-12 22:39:48 +02:00
ReadTimeout: HttpReadTimeout,
WriteTimeout: HttpWriteTimeout,
MaxHeaderBytes: HttpMaxHeaderBytes,
Handler: http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout"),
}
server.SetKeepAlivesEnabled(false)
log.Printf("Running main server on %s\n", serverAddr)
2022-06-12 22:39:48 +02:00
err = server.ListenAndServe()
if err != nil {
2022-10-31 20:40:45 +01:00
log.Printf("ERROR: %v\n", err)
2022-06-12 22:39:48 +02:00
}
runtime.KeepAlive(scLock)
2022-06-12 22:39:48 +02:00
}