waveterm/waveshell/pkg/packet/packet.go
Sylvie Crowe 2913babea7
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-16 16:58:17 -07:00

1379 lines
37 KiB
Go

// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package packet
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"reflect"
"sync"
"time"
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
)
// single : <init, >run, >cmddata, >cmddone, <cmdstart, <>data, <>dataack, <cmddone
// single(detached): <init, >run, >cmddata, >cmddone, <cmdstart
// server : <init, >run, >cmddata, >cmddone, <cmdstart, <>data, <>dataack, <cmddone
// >cd, >getcmd, >untailcmd, >input, <resp
// all : <>error, <>message, <>ping, <raw
//
// >streamfile, <streamfileresp, <filedata*
// >writefile, <writefileready, >filedata*, <writefiledone
const MaxCompGenValues = 100
var GlobalDebug = false
const (
RunPacketStr = "run" // rpc
PingPacketStr = "ping"
InitPacketStr = "init"
DataPacketStr = "data" // command
DataAckPacketStr = "dataack" // command
CmdStartPacketStr = "cmdstart" // rpc-response
CmdDonePacketStr = "cmddone" // command
DataEndPacketStr = "dataend"
ResponsePacketStr = "resp" // rpc-response
DonePacketStr = "done"
MessagePacketStr = "message"
GetCmdPacketStr = "getcmd" // rpc
UntailCmdPacketStr = "untailcmd" // rpc
CdPacketStr = "cd" // rpc
RawPacketStr = "raw"
SpecialInputPacketStr = "sinput" // command
CompGenPacketStr = "compgen" // rpc
ReInitPacketStr = "reinit" // rpc
CmdFinalPacketStr = "cmdfinal" // command, pushed at the "end" of a command (fail-safe for no cmddone)
StreamFilePacketStr = "streamfile" // rpc
StreamFileResponseStr = "streamfileresp" // rpc-response
WriteFilePacketStr = "writefile" // rpc
WriteFileReadyPacketStr = "writefileready" // rpc-response
WriteFileDonePacketStr = "writefiledone" // rpc-response
FileDataPacketStr = "filedata"
FileStatPacketStr = "filestat"
LogPacketStr = "log" // logging packet (sent from waveshell back to server)
ShellStatePacketStr = "shellstate"
RpcInputPacketStr = "rpcinput" // rpc-followup
SudoRequestPacketStr = "sudorequest"
SudoResponsePacketStr = "sudoresponse"
OpenAIPacketStr = "openai" // other
OpenAICloudReqStr = "openai-cloudreq"
)
const (
ShellType_bash = "bash"
ShellType_zsh = "zsh"
)
const (
EC_InvalidCwd = "ERRCWD"
EC_CmdNotRunning = "CMDNOTRUNNING"
)
const PacketSenderQueueSize = 20
const PacketEOFStr = "EOF"
var TypeStrToFactory map[string]reflect.Type
const OpenAICmdInfoChatGreetingMessage = "Hello, may I help you with this command? \n(Ctrl-Space: open, ESC: close, Ctrl+L: clear chat buffer, Up/Down: select code blocks, Enter: to copy a selected code block to the command input)"
func init() {
TypeStrToFactory = make(map[string]reflect.Type)
TypeStrToFactory[RunPacketStr] = reflect.TypeOf(RunPacketType{})
TypeStrToFactory[PingPacketStr] = reflect.TypeOf(PingPacketType{})
TypeStrToFactory[ResponsePacketStr] = reflect.TypeOf(ResponsePacketType{})
TypeStrToFactory[DonePacketStr] = reflect.TypeOf(DonePacketType{})
TypeStrToFactory[MessagePacketStr] = reflect.TypeOf(MessagePacketType{})
TypeStrToFactory[CmdStartPacketStr] = reflect.TypeOf(CmdStartPacketType{})
TypeStrToFactory[CmdDonePacketStr] = reflect.TypeOf(CmdDonePacketType{})
TypeStrToFactory[GetCmdPacketStr] = reflect.TypeOf(GetCmdPacketType{})
TypeStrToFactory[UntailCmdPacketStr] = reflect.TypeOf(UntailCmdPacketType{})
TypeStrToFactory[InitPacketStr] = reflect.TypeOf(InitPacketType{})
TypeStrToFactory[CdPacketStr] = reflect.TypeOf(CdPacketType{})
TypeStrToFactory[RawPacketStr] = reflect.TypeOf(RawPacketType{})
TypeStrToFactory[SpecialInputPacketStr] = reflect.TypeOf(SpecialInputPacketType{})
TypeStrToFactory[DataPacketStr] = reflect.TypeOf(DataPacketType{})
TypeStrToFactory[DataAckPacketStr] = reflect.TypeOf(DataAckPacketType{})
TypeStrToFactory[DataEndPacketStr] = reflect.TypeOf(DataEndPacketType{})
TypeStrToFactory[CompGenPacketStr] = reflect.TypeOf(CompGenPacketType{})
TypeStrToFactory[ReInitPacketStr] = reflect.TypeOf(ReInitPacketType{})
TypeStrToFactory[CmdFinalPacketStr] = reflect.TypeOf(CmdFinalPacketType{})
TypeStrToFactory[StreamFilePacketStr] = reflect.TypeOf(StreamFilePacketType{})
TypeStrToFactory[StreamFileResponseStr] = reflect.TypeOf(StreamFileResponseType{})
TypeStrToFactory[OpenAIPacketStr] = reflect.TypeOf(OpenAIPacketType{})
TypeStrToFactory[FileDataPacketStr] = reflect.TypeOf(FileDataPacketType{})
TypeStrToFactory[WriteFilePacketStr] = reflect.TypeOf(WriteFilePacketType{})
TypeStrToFactory[WriteFileReadyPacketStr] = reflect.TypeOf(WriteFileReadyPacketType{})
TypeStrToFactory[WriteFileDonePacketStr] = reflect.TypeOf(WriteFileDonePacketType{})
TypeStrToFactory[LogPacketStr] = reflect.TypeOf(LogPacketType{})
TypeStrToFactory[ShellStatePacketStr] = reflect.TypeOf(ShellStatePacketType{})
TypeStrToFactory[FileStatPacketStr] = reflect.TypeOf(FileStatPacketType{})
TypeStrToFactory[RpcInputPacketStr] = reflect.TypeOf(RpcInputPacketType{})
TypeStrToFactory[SudoRequestPacketStr] = reflect.TypeOf(SudoRequestPacketType{})
TypeStrToFactory[SudoResponsePacketStr] = reflect.TypeOf(SudoResponsePacketType{})
var _ RpcPacketType = (*RunPacketType)(nil)
var _ RpcPacketType = (*GetCmdPacketType)(nil)
var _ RpcPacketType = (*UntailCmdPacketType)(nil)
var _ RpcPacketType = (*CdPacketType)(nil)
var _ RpcPacketType = (*CompGenPacketType)(nil)
var _ RpcPacketType = (*ReInitPacketType)(nil)
var _ RpcPacketType = (*StreamFilePacketType)(nil)
var _ RpcPacketType = (*WriteFilePacketType)(nil)
var _ RpcResponsePacketType = (*CmdStartPacketType)(nil)
var _ RpcResponsePacketType = (*ResponsePacketType)(nil)
var _ RpcResponsePacketType = (*StreamFileResponseType)(nil)
var _ RpcResponsePacketType = (*FileDataPacketType)(nil)
var _ RpcResponsePacketType = (*WriteFileReadyPacketType)(nil)
var _ RpcResponsePacketType = (*WriteFileDonePacketType)(nil)
var _ RpcResponsePacketType = (*ShellStatePacketType)(nil)
var _ RpcFollowUpPacketType = (*FileDataPacketType)(nil)
var _ RpcFollowUpPacketType = (*RpcInputPacketType)(nil)
var _ CommandPacketType = (*DataPacketType)(nil)
var _ CommandPacketType = (*DataAckPacketType)(nil)
var _ CommandPacketType = (*CmdDonePacketType)(nil)
var _ CommandPacketType = (*SpecialInputPacketType)(nil)
var _ CommandPacketType = (*CmdFinalPacketType)(nil)
var _ CommandPacketType = (*SudoResponsePacketType)(nil)
}
func RegisterPacketType(typeStr string, rtype reflect.Type) {
TypeStrToFactory[typeStr] = rtype
}
func MakePacket(packetType string) (PacketType, error) {
rtype := TypeStrToFactory[packetType]
if rtype == nil {
return nil, fmt.Errorf("invalid packet type '%s'", packetType)
}
rtn := reflect.New(rtype)
return rtn.Interface().(PacketType), nil
}
type PingPacketType struct {
Type string `json:"type"`
}
func (*PingPacketType) GetType() string {
return PingPacketStr
}
func MakePingPacket() *PingPacketType {
return &PingPacketType{Type: PingPacketStr}
}
type RpcInputPacketType struct {
Type string `json:"type"`
ReqId string `json:"reqid"`
Data []byte `json:"data"`
}
func (*RpcInputPacketType) GetType() string {
return RpcInputPacketStr
}
func (p *RpcInputPacketType) GetAssociatedReqId() string {
return p.ReqId
}
func MakeRpcInputPacket(reqId string) *RpcInputPacketType {
return &RpcInputPacketType{Type: RpcInputPacketStr, ReqId: reqId}
}
// these packets can travel either direction
// so it is both a RpcResponsePacketType and an RpcFollowUpPacketType
type FileDataPacketType struct {
Type string `json:"type"`
RespId string `json:"respid"`
Data []byte `json:"data"`
Eof bool `json:"eof,omitempty"`
Error string `json:"error,omitempty"`
}
func (*FileDataPacketType) GetType() string {
return FileDataPacketStr
}
func MakeFileDataPacket(reqId string) *FileDataPacketType {
return &FileDataPacketType{
Type: FileDataPacketStr,
RespId: reqId,
}
}
func (p *FileDataPacketType) GetAssociatedReqId() string {
return p.RespId
}
func (p *FileDataPacketType) GetResponseId() string {
return p.RespId
}
func (p *FileDataPacketType) GetResponseDone() bool {
return p.Eof || p.Error != ""
}
type DataPacketType struct {
Type string `json:"type"`
CK base.CommandKey `json:"ck"`
FdNum int `json:"fdnum"`
Data64 string `json:"data64"` // base64 encoded
Eof bool `json:"eof,omitempty"`
Error string `json:"error,omitempty"`
}
func (*DataPacketType) GetType() string {
return DataPacketStr
}
func (p *DataPacketType) GetCK() base.CommandKey {
return p.CK
}
func B64DecodedLen(b64 string) int {
if len(b64) < 4 {
return 0 // we use padded strings, so < 4 is always 0
}
realLen := 3 * (len(b64) / 4)
if b64[len(b64)-1] == '=' {
realLen--
}
if b64[len(b64)-2] == '=' {
realLen--
}
return realLen
}
func (p *DataPacketType) String() string {
eofStr := ""
if p.Eof {
eofStr = ", eof"
}
errStr := ""
if p.Error != "" {
errStr = fmt.Sprintf(", err=%s", p.Error)
}
return fmt.Sprintf("data[fd=%d, len=%d%s%s]", p.FdNum, B64DecodedLen(p.Data64), eofStr, errStr)
}
func MakeDataPacket() *DataPacketType {
return &DataPacketType{Type: DataPacketStr}
}
type DataEndPacketType struct {
Type string `json:"type"`
CK base.CommandKey `json:"ck"`
}
func MakeDataEndPacket(ck base.CommandKey) *DataEndPacketType {
return &DataEndPacketType{Type: DataEndPacketStr, CK: ck}
}
func (*DataEndPacketType) GetType() string {
return DataEndPacketStr
}
type DataAckPacketType struct {
Type string `json:"type"`
CK base.CommandKey `json:"ck"`
FdNum int `json:"fdnum"`
AckLen int `json:"acklen"`
Error string `json:"error,omitempty"`
}
func (*DataAckPacketType) GetType() string {
return DataAckPacketStr
}
func (p *DataAckPacketType) GetCK() base.CommandKey {
return p.CK
}
func (p *DataAckPacketType) String() string {
errStr := ""
if p.Error != "" {
errStr = fmt.Sprintf(" err=%s", p.Error)
}
return fmt.Sprintf("ack[fd=%d, acklen=%d%s]", p.FdNum, p.AckLen, errStr)
}
func MakeDataAckPacket() *DataAckPacketType {
return &DataAckPacketType{Type: DataAckPacketStr}
}
type WinSize struct {
Rows int `json:"rows"`
Cols int `json:"cols"`
}
// SigNum gets sent to process via a signal
// WinSize, if set, will run TIOCSWINSZ to set size, and then send SIGWINCH
type SpecialInputPacketType struct {
Type string `json:"type"`
CK base.CommandKey `json:"ck"`
SigName string `json:"signame,omitempty"` // passed to unix.SignalNum (needs 'SIG' prefix, e.g. "SIGTERM"), also accepts a number (e.g. "9")
WinSize *WinSize `json:"winsize,omitempty"`
}
func (*SpecialInputPacketType) GetType() string {
return SpecialInputPacketStr
}
func (p *SpecialInputPacketType) GetCK() base.CommandKey {
return p.CK
}
func MakeSpecialInputPacket() *SpecialInputPacketType {
return &SpecialInputPacketType{Type: SpecialInputPacketStr}
}
type UntailCmdPacketType struct {
Type string `json:"type"`
ReqId string `json:"reqid"`
CK base.CommandKey `json:"ck"`
}
func (*UntailCmdPacketType) GetType() string {
return UntailCmdPacketStr
}
func (p *UntailCmdPacketType) GetReqId() string {
return p.ReqId
}
func MakeUntailCmdPacket() *UntailCmdPacketType {
return &UntailCmdPacketType{Type: UntailCmdPacketStr}
}
type GetCmdPacketType struct {
Type string `json:"type"`
ReqId string `json:"reqid"`
CK base.CommandKey `json:"ck"`
PtyPos int64 `json:"ptypos"`
RunPos int64 `json:"runpos"`
Tail bool `json:"tail,omitempty"`
PtyOnly bool `json:"ptyonly,omitempty"`
}
func (*GetCmdPacketType) GetType() string {
return GetCmdPacketStr
}
func (p *GetCmdPacketType) GetReqId() string {
return p.ReqId
}
func MakeGetCmdPacket() *GetCmdPacketType {
return &GetCmdPacketType{Type: GetCmdPacketStr}
}
type CdPacketType struct {
Type string `json:"type"`
ReqId string `json:"reqid"`
Dir string `json:"dir"`
}
func (*CdPacketType) GetType() string {
return CdPacketStr
}
func (p *CdPacketType) GetReqId() string {
return p.ReqId
}
func MakeCdPacket() *CdPacketType {
return &CdPacketType{Type: CdPacketStr}
}
type ReInitPacketType struct {
Type string `json:"type"`
ShellType string `json:"shelltype"`
ReqId string `json:"reqid"`
}
func (*ReInitPacketType) GetType() string {
return ReInitPacketStr
}
func (p *ReInitPacketType) GetReqId() string {
return p.ReqId
}
func MakeReInitPacket() *ReInitPacketType {
return &ReInitPacketType{Type: ReInitPacketStr}
}
type FileStatPacketType struct {
Type string `json:"type"`
Name string `json:"name"`
Size int64 `json:"size"`
ModTs time.Time `json:"modts"`
IsDir bool `json:"isdir"`
Perm int `json:"perm"`
ModeStr string `json:"modestr"`
Error string `json:"error"`
Done bool `json:"done"`
RespId string `json:"respid"`
Path string `json:"path"`
}
func (*FileStatPacketType) GetType() string {
return FileStatPacketStr
}
func (p *FileStatPacketType) GetResponseDone() bool {
return p.Done
}
func (p *FileStatPacketType) GetResponseId() string {
return p.RespId
}
func MakeFileStatPacketType() *FileStatPacketType {
return &FileStatPacketType{Type: FileStatPacketStr}
}
func MakeFileStatPacketFromFileInfo(finfo fs.FileInfo, err string, done bool) *FileStatPacketType {
resp := MakeFileStatPacketType()
resp.Error = err
resp.Done = done
resp.IsDir = finfo.IsDir()
resp.Name = finfo.Name()
resp.Size = finfo.Size()
resp.ModTs = finfo.ModTime()
resp.Perm = int(finfo.Mode().Perm())
resp.ModeStr = finfo.Mode().String()
return resp
}
type StreamFilePacketType struct {
Type string `json:"type"`
ReqId string `json:"reqid"`
Path string `json:"path"`
ByteRange []int64 `json:"byterange"` // works like the http "Range" header (multiple ranges are not allowed)
StatOnly bool `json:"statonly,omitempty"` // set if you just want the stat response (no data returned)
}
func (*StreamFilePacketType) GetType() string {
return StreamFilePacketStr
}
func (p *StreamFilePacketType) GetReqId() string {
return p.ReqId
}
func MakeStreamFilePacket() *StreamFilePacketType {
return &StreamFilePacketType{Type: StreamFilePacketStr}
}
type FileInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
ModTs int64 `json:"modts"`
IsDir bool `json:"isdir,omitempty"`
Perm int `json:"perm"`
MimeType string `json:"mimetype,omitempty"`
NotFound bool `json:"notfound,omitempty"` // when NotFound is set, Perm will be set to permission for directory
}
type StreamFileResponseType struct {
Type string `json:"type"`
RespId string `json:"respid"`
Done bool `json:"done,omitempty"`
Info *FileInfo `json:"info,omitempty"`
Error string `json:"error,omitempty"`
}
func (*StreamFileResponseType) GetType() string {
return StreamFileResponseStr
}
func (p *StreamFileResponseType) GetResponseId() string {
return p.RespId
}
func (p *StreamFileResponseType) GetResponseDone() bool {
return p.Done
}
func MakeStreamFileResponse(respId string) *StreamFileResponseType {
return &StreamFileResponseType{
Type: StreamFileResponseStr,
RespId: respId,
}
}
type CompGenPacketType struct {
Type string `json:"type"`
ReqId string `json:"reqid"`
Prefix string `json:"prefix"`
CompType string `json:"comptype"`
Cwd string `json:"cwd"`
}
func IsValidCompGenType(t string) bool {
return (t == "file" || t == "command" || t == "directory" || t == "variable")
}
func (*CompGenPacketType) GetType() string {
return CompGenPacketStr
}
func (p *CompGenPacketType) GetReqId() string {
return p.ReqId
}
func MakeCompGenPacket() *CompGenPacketType {
return &CompGenPacketType{Type: CompGenPacketStr}
}
type ResponsePacketType struct {
Type string `json:"type"`
RespId string `json:"respid"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
ErrorCode string `json:"errorcode,omitempty"` // can be used for structured errors
Data interface{} `json:"data,omitempty"`
}
func (*ResponsePacketType) GetType() string {
return ResponsePacketStr
}
func (p *ResponsePacketType) GetResponseId() string {
return p.RespId
}
func (*ResponsePacketType) GetResponseDone() bool {
return true
}
func (p *ResponsePacketType) Err() error {
if p == nil {
return fmt.Errorf("no response received")
}
if !p.Success {
if p.Error != "" {
if p.ErrorCode != "" {
return &base.CodedError{ErrorCode: p.ErrorCode, Err: errors.New(p.Error)}
}
return errors.New(p.Error)
}
return fmt.Errorf("rpc failed")
}
return nil
}
func (p *ResponsePacketType) String() string {
if p.Success {
return "response[success]"
}
return fmt.Sprintf("response[error:%s]", p.Error)
}
func MakeErrorResponsePacket(reqId string, err error) *ResponsePacketType {
if codedErr, ok := err.(*base.CodedError); ok {
return &ResponsePacketType{Type: ResponsePacketStr, RespId: reqId, Error: codedErr.Err.Error(), ErrorCode: codedErr.ErrorCode}
}
return &ResponsePacketType{Type: ResponsePacketStr, RespId: reqId, Error: err.Error()}
}
func MakeResponsePacket(reqId string, data interface{}) *ResponsePacketType {
return &ResponsePacketType{Type: ResponsePacketStr, RespId: reqId, Success: true, Data: data}
}
type RawPacketType struct {
Type string `json:"type"`
Data string `json:"data"`
}
func (*RawPacketType) GetType() string {
return RawPacketStr
}
func (p *RawPacketType) String() string {
return fmt.Sprintf("raw[%s]", p.Data)
}
func MakeRawPacket(val string) *RawPacketType {
return &RawPacketType{Type: RawPacketStr, Data: val}
}
type LogPacketType struct {
Type string `json:"type"`
Entry wlog.LogEntry `json:"entry"`
}
func (*LogPacketType) GetType() string {
return LogPacketStr
}
func (p *LogPacketType) String() string {
return "log"
}
func MakeLogPacket(entry wlog.LogEntry) *LogPacketType {
return &LogPacketType{Type: LogPacketStr, Entry: entry}
}
type ShellStatePacketType struct {
Type string `json:"type"`
ShellType string `json:"shelltype"`
RespId string `json:"respid,omitempty"`
State *ShellState `json:"state"`
Stats *ShellStateStats `json:"stats"`
Error string `json:"error,omitempty"`
}
func (*ShellStatePacketType) GetType() string {
return ShellStatePacketStr
}
func (p *ShellStatePacketType) String() string {
return fmt.Sprintf("shellstate[%s]", p.ShellType)
}
func (p *ShellStatePacketType) GetResponseId() string {
return p.RespId
}
func (p *ShellStatePacketType) GetResponseDone() bool {
return true
}
func MakeShellStatePacket() *ShellStatePacketType {
return &ShellStatePacketType{Type: ShellStatePacketStr}
}
type MessagePacketType struct {
Type string `json:"type"`
CK base.CommandKey `json:"ck,omitempty"`
Message string `json:"message"`
}
func (*MessagePacketType) GetType() string {
return MessagePacketStr
}
func (p *MessagePacketType) String() string {
return fmt.Sprintf("messsage[%s]", p.Message)
}
func MakeMessagePacket(message string) *MessagePacketType {
return &MessagePacketType{Type: MessagePacketStr, Message: message}
}
func FmtMessagePacket(fmtStr string, args ...interface{}) *MessagePacketType {
message := fmt.Sprintf(fmtStr, args...)
return &MessagePacketType{Type: MessagePacketStr, Message: message}
}
type InitPacketType struct {
Type string `json:"type"`
RespId string `json:"respid,omitempty"`
Version string `json:"version"`
BuildTime string `json:"buildtime,omitempty"`
MShellHomeDir string `json:"mshellhomedir,omitempty"`
HomeDir string `json:"homedir,omitempty"`
User string `json:"user,omitempty"`
HostName string `json:"hostname,omitempty"`
NotFound bool `json:"notfound,omitempty"`
UName string `json:"uname,omitempty"`
Shell string `json:"shell,omitempty"`
RemoteId string `json:"remoteid,omitempty"`
}
func (*InitPacketType) GetType() string {
return InitPacketStr
}
func (pk *InitPacketType) GetResponseId() string {
return pk.RespId
}
func (pk *InitPacketType) GetResponseDone() bool {
return true
}
func MakeInitPacket() *InitPacketType {
return &InitPacketType{Type: InitPacketStr}
}
type DonePacketType struct {
Type string `json:"type"`
}
func (*DonePacketType) GetType() string {
return DonePacketStr
}
func MakeDonePacket() *DonePacketType {
return &DonePacketType{Type: DonePacketStr}
}
type CmdFinalPacketType struct {
Type string `json:"type"`
Ts int64 `json:"ts"`
CK base.CommandKey `json:"ck"`
Error string `json:"error"`
}
func (*CmdFinalPacketType) GetType() string {
return CmdFinalPacketStr
}
func (pk *CmdFinalPacketType) GetCK() base.CommandKey {
return pk.CK
}
func MakeCmdFinalPacket(ck base.CommandKey) *CmdFinalPacketType {
return &CmdFinalPacketType{Type: CmdFinalPacketStr, CK: ck}
}
type CmdDonePacketType struct {
Type string `json:"type"`
Ts int64 `json:"ts"`
CK base.CommandKey `json:"ck"`
ExitCode int `json:"exitcode"`
DurationMs int64 `json:"durationms"`
FinalState *ShellState `json:"finalstate,omitempty"`
FinalStateDiff *ShellStateDiff `json:"finalstatediff,omitempty"`
FinalStateBasePtr *ShellStatePtr `json:"finalstatebaseptr,omitempty"`
}
func (*CmdDonePacketType) GetType() string {
return CmdDonePacketStr
}
func (p *CmdDonePacketType) GetCK() base.CommandKey {
return p.CK
}
func MakeCmdDonePacket(ck base.CommandKey) *CmdDonePacketType {
return &CmdDonePacketType{Type: CmdDonePacketStr, CK: ck}
}
type CmdStartPacketType struct {
Type string `json:"type"`
RespId string `json:"respid,omitempty"`
Ts int64 `json:"ts"`
CK base.CommandKey `json:"ck"`
Pid int `json:"pid,omitempty"`
MShellPid int `json:"mshellpid,omitempty"`
}
func (*CmdStartPacketType) GetType() string {
return CmdStartPacketStr
}
func (p *CmdStartPacketType) GetResponseId() string {
return p.RespId
}
func (*CmdStartPacketType) GetResponseDone() bool {
return true
}
func MakeCmdStartPacket(reqId string) *CmdStartPacketType {
return &CmdStartPacketType{Type: CmdStartPacketStr, RespId: reqId}
}
type TermOpts struct {
Rows int `json:"rows"`
Cols int `json:"cols"`
Term string `json:"term"`
MaxPtySize int64 `json:"maxptysize,omitempty"`
FlexRows bool `json:"flexrows,omitempty"`
}
type RemoteFd struct {
FdNum int `json:"fdnum"`
Read bool `json:"read"`
Write bool `json:"write"`
DupStdin bool `json:"-"`
}
type RunDataType struct {
FdNum int `json:"fdnum"`
DataLen int `json:"datalen"`
Data []byte `json:"-"`
}
type RunPacketType struct {
Type string `json:"type"`
ReqId string `json:"reqid"`
CK base.CommandKey `json:"ck"`
ShellType string `json:"shelltype"` // added in Wave v0.6.0 (either "bash" or "zsh") (set by remote.go)
Command string `json:"command"`
State *ShellState `json:"state,omitempty"`
StatePtr *ShellStatePtr `json:"stateptr,omitempty"` // added in Wave v0.7.2
StateComplete bool `json:"statecomplete,omitempty"` // set to true if state is complete (the default env should not be set)
UsePty bool `json:"usepty,omitempty"` // Set to true if the command should be run in a pty. This will write all output to the stdout file descriptor with PTY formatting.
TermOpts *TermOpts `json:"termopts,omitempty"`
Fds []RemoteFd `json:"fds,omitempty"`
RunData []RunDataType `json:"rundata,omitempty"`
Detached bool `json:"detached,omitempty"`
ReturnState bool `json:"returnstate,omitempty"`
IsSudo bool `json:"issudo,omitempty"`
}
func (*RunPacketType) GetType() string {
return RunPacketStr
}
func (p *RunPacketType) GetReqId() string {
return p.ReqId
}
func MakeRunPacket() *RunPacketType {
return &RunPacketType{Type: RunPacketStr}
}
type OpenAIUsageType struct {
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
TotalTokens int `json:"total_tokens,omitempty"`
}
type OpenAICmdInfoPacketOutputType struct {
Model string `json:"model,omitempty"`
Created int64 `json:"created,omitempty"`
FinishReason string `json:"finish_reason,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
}
type OpenAIPacketType struct {
Type string `json:"type"`
Model string `json:"model,omitempty"`
Created int64 `json:"created,omitempty"`
FinishReason string `json:"finish_reason,omitempty"`
Usage *OpenAIUsageType `json:"usage,omitempty"`
Index int `json:"index,omitempty"`
Text string `json:"text,omitempty"`
Error string `json:"error,omitempty"`
}
func (*OpenAIPacketType) GetType() string {
return OpenAIPacketStr
}
func MakeOpenAIPacket() *OpenAIPacketType {
return &OpenAIPacketType{Type: OpenAIPacketStr}
}
type BarePacketType struct {
Type string `json:"type"`
}
type WriteFilePacketType struct {
Type string `json:"type"`
ReqId string `json:"reqid"`
UseTemp bool `json:"usetemp,omitempty"`
Path string `json:"path"`
}
func (*WriteFilePacketType) GetType() string {
return WriteFilePacketStr
}
func (p *WriteFilePacketType) GetReqId() string {
return p.ReqId
}
func MakeWriteFilePacket() *WriteFilePacketType {
return &WriteFilePacketType{Type: WriteFilePacketStr}
}
type WriteFileReadyPacketType struct {
Type string `json:"type"`
RespId string `json:"reqid"`
Error string `json:"error,omitempty"`
}
func (*WriteFileReadyPacketType) GetType() string {
return WriteFileReadyPacketStr
}
func (p *WriteFileReadyPacketType) GetResponseId() string {
return p.RespId
}
func (p *WriteFileReadyPacketType) GetResponseDone() bool {
return p.Error != ""
}
func MakeWriteFileReadyPacket(reqId string) *WriteFileReadyPacketType {
return &WriteFileReadyPacketType{
Type: WriteFileReadyPacketStr,
RespId: reqId,
}
}
type WriteFileDonePacketType struct {
Type string `json:"type"`
RespId string `json:"reqid"`
Error string `json:"error,omitempty"`
}
func (*WriteFileDonePacketType) GetType() string {
return WriteFileDonePacketStr
}
func (p *WriteFileDonePacketType) GetResponseId() string {
return p.RespId
}
func (p *WriteFileDonePacketType) GetResponseDone() bool {
return true
}
func MakeWriteFileDonePacket(reqId string) *WriteFileDonePacketType {
return &WriteFileDonePacketType{
Type: WriteFileDonePacketStr,
RespId: reqId,
}
}
type OpenAICmdInfoChatMessage struct {
MessageID int `json:"messageid"`
IsAssistantResponse bool `json:"isassistantresponse,omitempty"`
AssistantResponse *OpenAICmdInfoPacketOutputType `json:"assistantresponse,omitempty"`
UserQuery string `json:"userquery,omitempty"`
UserEngineeredQuery string `json:"userengineeredquery,omitempty"`
}
type OpenAIPromptMessageType struct {
Role string `json:"role"`
Content string `json:"content"`
Name string `json:"name,omitempty"`
}
type OpenAICloudReqPacketType struct {
Type string `json:"type"`
ClientId string `json:"clientid"`
Prompt []OpenAIPromptMessageType `json:"prompt"`
MaxTokens int `json:"maxtokens,omitempty"`
MaxChoices int `json:"maxchoices,omitempty"`
}
func (*OpenAICloudReqPacketType) GetType() string {
return OpenAICloudReqStr
}
func MakeOpenAICloudReqPacket() *OpenAICloudReqPacketType {
return &OpenAICloudReqPacketType{
Type: OpenAICloudReqStr,
}
}
type SudoRequestPacketType struct {
Type string `json:"type"`
CK base.CommandKey `json:"ck"`
ShellPubKey []byte `json:"shellpubkey"`
SudoStatus string `json:"sudostatus"`
ErrStr string `json:"errstr"`
}
func (*SudoRequestPacketType) GetType() string {
return SudoRequestPacketStr
}
func (p *SudoRequestPacketType) GetCK() base.CommandKey {
return p.CK
}
func MakeSudoRequestPacket(ck base.CommandKey, pubKey []byte, sudoStatus string) *SudoRequestPacketType {
return &SudoRequestPacketType{
Type: SudoRequestPacketStr,
CK: ck,
ShellPubKey: pubKey,
SudoStatus: sudoStatus,
}
}
type SudoResponsePacketType struct {
Type string `json:"type"`
CK base.CommandKey `json:"ck"`
Secret []byte `json:"secret"`
SrvPubKey []byte `json:"srvpubkey"`
}
func (*SudoResponsePacketType) GetType() string {
return SudoResponsePacketStr
}
func (p *SudoResponsePacketType) GetCK() base.CommandKey {
return p.CK
}
func MakeSudoResponsePacket(ck base.CommandKey, secret []byte, srvPubKey []byte) *SudoResponsePacketType {
return &SudoResponsePacketType{
Type: SudoResponsePacketStr,
CK: ck,
Secret: secret,
SrvPubKey: srvPubKey,
}
}
type PacketType interface {
GetType() string
}
func AsString(pk PacketType) string {
if pk == nil {
return "nil"
}
if s, ok := pk.(fmt.Stringer); ok {
return s.String()
}
return fmt.Sprintf("%s[]", pk.GetType())
}
type RpcPacketType interface {
GetType() string
GetReqId() string
}
type RpcResponsePacketType interface {
GetType() string
GetResponseId() string
GetResponseDone() bool
}
type CommandPacketType interface {
GetType() string
GetCK() base.CommandKey
}
// RpcPackets initiate an Rpc. these can be part of the data passed back and forth
type RpcFollowUpPacketType interface {
GetType() string
GetAssociatedReqId() string
}
type ModelUpdatePacketType struct {
Type string `json:"type"`
Updates []any `json:"updates"`
}
func AsExtType(pk PacketType) string {
if rpcPacket, ok := pk.(RpcPacketType); ok {
return fmt.Sprintf("%s[%s]", rpcPacket.GetType(), rpcPacket.GetReqId())
} else if cmdPacket, ok := pk.(CommandPacketType); ok {
return fmt.Sprintf("%s[%s]", cmdPacket.GetType(), cmdPacket.GetCK())
} else {
return pk.GetType()
}
}
func ParseJsonPacket(jsonBuf []byte) (PacketType, error) {
var bareCmd BarePacketType
err := json.Unmarshal(jsonBuf, &bareCmd)
if err != nil {
return nil, err
}
if bareCmd.Type == "" {
return nil, fmt.Errorf("received packet with no type")
}
pk, err := MakePacket(bareCmd.Type)
if err != nil {
return nil, err
}
err = json.Unmarshal(jsonBuf, pk)
if err != nil {
return nil, fmt.Errorf("unmarshaling %q packet: %v", bareCmd.Type, err)
}
return pk, nil
}
type SendError struct {
IsWriteError bool // fatal
IsMarshalError bool // not fatal
PacketType string
Err error
}
func (e *SendError) Unwrap() error {
return e.Err
}
func (e *SendError) Error() string {
if e.IsMarshalError {
return fmt.Sprintf("SendPacket marshal-error '%s' packet: %v", e.PacketType, e.Err)
} else if e.IsWriteError {
return fmt.Sprintf("SendPacket write-error packet[%s]: %v", e.PacketType, e.Err)
} else {
return e.Err.Error()
}
}
func MarshalPacket(packet PacketType) ([]byte, error) {
if packet == nil {
return nil, fmt.Errorf("invalid nil packet")
}
jsonBytes, err := json.Marshal(packet)
if err != nil {
return nil, &SendError{IsMarshalError: true, PacketType: packet.GetType(), Err: err}
}
var outBuf bytes.Buffer
outBuf.WriteByte('\n')
outBuf.WriteString(fmt.Sprintf("##%d", len(jsonBytes)))
outBuf.Write(jsonBytes)
outBuf.WriteByte('\n')
outBytes := outBuf.Bytes()
return outBytes, nil
}
func SendPacket(w io.Writer, packet PacketType) error {
if packet == nil {
return nil
}
outBytes, err := MarshalPacket(packet)
if err != nil {
return err
}
if GlobalDebug {
base.Logf("SEND> %s\n", AsString(packet))
}
_, err = w.Write(outBytes)
if err != nil {
return &SendError{IsWriteError: true, PacketType: packet.GetType(), Err: err}
}
return nil
}
type PacketSender struct {
Lock *sync.Mutex
SendCh chan PacketType
Done bool
DoneCh chan bool
ErrHandler func(*PacketSender, PacketType, error)
ExitErr error
}
func MakePacketSender(output io.Writer, errHandler func(*PacketSender, PacketType, error)) *PacketSender {
sender := &PacketSender{
Lock: &sync.Mutex{},
SendCh: make(chan PacketType, PacketSenderQueueSize),
DoneCh: make(chan bool),
ErrHandler: errHandler,
}
go func() {
defer close(sender.DoneCh)
defer sender.Close()
for pk := range sender.SendCh {
err := SendPacket(output, pk)
if err != nil {
sender.goHandleError(pk, err)
if serr, ok := err.(*SendError); ok && serr.IsMarshalError {
// marshaler errors are recoverable
continue
}
// write errors are not recoverable
sender.Lock.Lock()
sender.ExitErr = err
sender.Lock.Unlock()
return
}
}
}()
return sender
}
func (sender *PacketSender) SendLogPacket(entry wlog.LogEntry) {
sender.SendPacket(MakeLogPacket(entry))
}
func (sender *PacketSender) goHandleError(pk PacketType, err error) {
sender.Lock.Lock()
defer sender.Lock.Unlock()
if sender.ErrHandler != nil {
go sender.ErrHandler(sender, pk, err)
}
}
func MakeChannelPacketSender(packetCh chan PacketType) *PacketSender {
sender := &PacketSender{
Lock: &sync.Mutex{},
SendCh: make(chan PacketType, PacketSenderQueueSize),
DoneCh: make(chan bool),
}
go func() {
defer close(sender.DoneCh)
defer sender.Close()
for pk := range sender.SendCh {
packetCh <- pk
}
}()
return sender
}
func (sender *PacketSender) Close() {
sender.Lock.Lock()
defer sender.Lock.Unlock()
if sender.Done {
return
}
sender.Done = true
close(sender.SendCh)
}
// returns ExitErr if set
func (sender *PacketSender) WaitForDone() error {
<-sender.DoneCh
sender.Lock.Lock()
defer sender.Lock.Unlock()
return sender.ExitErr
}
// this is "advisory", as there is a race condition between the loop closing and setting Done.
// that's okay because that's an impossible race condition anyway (you could enqueue the packet
// and then the connection dies, or it dies half way, etc.). this just stops blindly adding
// packets forever when the loop is done.
func (sender *PacketSender) checkStatus() error {
sender.Lock.Lock()
defer sender.Lock.Unlock()
if sender.Done {
return fmt.Errorf("cannot send packet, sender write loop is closed")
}
return nil
}
func (sender *PacketSender) SendPacketCtx(ctx context.Context, pk PacketType) error {
err := sender.checkStatus()
if err != nil {
return err
}
select {
case sender.SendCh <- pk:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (sender *PacketSender) SendPacket(pk PacketType) error {
if pk == nil {
log.Printf("tried to send nil packet\n")
return fmt.Errorf("tried to send nil packet")
}
if pk.GetType() == "" {
log.Printf("tried to send invalid packet: %T\n", pk)
return fmt.Errorf("tried to send packet without a type: %T", pk)
}
err := sender.checkStatus()
if err != nil {
return err
}
sender.SendCh <- pk
return nil
}
func (sender *PacketSender) SendErrorResponse(reqId string, err error) error {
pk := MakeErrorResponsePacket(reqId, err)
return sender.SendPacket(pk)
}
func (sender *PacketSender) SendResponse(reqId string, data interface{}) error {
pk := MakeResponsePacket(reqId, data)
return sender.SendPacket(pk)
}
func (sender *PacketSender) SendMessageFmt(fmtStr string, args ...interface{}) error {
return sender.SendPacket(MakeMessagePacket(fmt.Sprintf(fmtStr, args...)))
}
type UnknownPacketReporter interface {
UnknownPacket(pk PacketType)
}
type DefaultUPR struct{}
func (DefaultUPR) UnknownPacket(pk PacketType) {
if pk.GetType() == RawPacketStr {
rawPacket := pk.(*RawPacketType)
fmt.Fprintf(os.Stderr, "%s\n", rawPacket.Data)
} else if pk.GetType() == CmdStartPacketStr {
return // do nothing
} else {
wlog.Logf("[upr] invalid packet received '%s'", AsExtType(pk))
}
}
type MessageUPR struct {
CK base.CommandKey
Sender *PacketSender
}
func (upr MessageUPR) UnknownPacket(pk PacketType) {
msg := FmtMessagePacket("[error] invalid packet received %s", AsString(pk))
msg.CK = upr.CK
upr.Sender.SendPacket(msg)
}
// todo: clean hanging entries in RunMap when in server mode
type RunPacketBuilder struct {
RunMap map[base.CommandKey]*RunPacketType
}
func MakeRunPacketBuilder() *RunPacketBuilder {
return &RunPacketBuilder{
RunMap: make(map[base.CommandKey]*RunPacketType),
}
}
// returns (consumed, fullRunPacket)
func (b *RunPacketBuilder) ProcessPacket(pk PacketType) (bool, *RunPacketType) {
if pk.GetType() == RunPacketStr {
runPacket := pk.(*RunPacketType)
if len(runPacket.RunData) == 0 {
return true, runPacket
}
b.RunMap[runPacket.CK] = runPacket
return true, nil
}
if pk.GetType() == DataEndPacketStr {
endPacket := pk.(*DataEndPacketType)
runPacket := b.RunMap[endPacket.CK] // might be nil
delete(b.RunMap, endPacket.CK)
return true, runPacket
}
if pk.GetType() == DataPacketStr {
dataPacket := pk.(*DataPacketType)
runPacket := b.RunMap[dataPacket.CK]
if runPacket == nil {
return false, nil
}
for idx, runData := range runPacket.RunData {
if runData.FdNum == dataPacket.FdNum {
// can ignore error, will get caught later with RunData.DataLen check
realData, _ := base64.StdEncoding.DecodeString(dataPacket.Data64)
runData.Data = append(runData.Data, realData...)
runPacket.RunData[idx] = runData
break
}
}
return true, nil
}
return false, nil
}