package sstore import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "database/sql/driver" "fmt" "log" "os" "os/user" "path" "regexp" "strings" "sync" "time" "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/sawka/txwrap" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/scbase" _ "github.com/mattn/go-sqlite3" ) const LineTypeCmd = "cmd" const LineTypeText = "text" const LineNoHeight = -1 const DBFileName = "prompt.db" const DBFileNameBackup = "backup.prompt.db" const DefaultSessionName = "default" const LocalRemoteAlias = "local" const DefaultCwd = "~" const ( MainViewSession = "session" MainViewBookmarks = "bookmarks" MainViewHistory = "history" ) const ( CmdStatusRunning = "running" CmdStatusDetached = "detached" CmdStatusError = "error" CmdStatusDone = "done" CmdStatusHangup = "hangup" CmdStatusWaiting = "waiting" ) const ( ShareModeLocal = "local" ShareModePrivate = "private" ShareModeView = "view" ShareModeShared = "shared" ) const ( ConnectModeStartup = "startup" ConnectModeAuto = "auto" ConnectModeManual = "manual" ) const ( RemoteTypeSsh = "ssh" ) const ( ScreenFocusInput = "input" ScreenFocusCmd = "cmd" ScreenFocusCmdFg = "cmd-fg" ) const MaxTzNameLen = 50 var globalDBLock = &sync.Mutex{} var globalDB *sqlx.DB var globalDBErr error func GetDBName() string { scHome := scbase.GetPromptHomeDir() return path.Join(scHome, DBFileName) } func GetDBBackupName() string { scHome := scbase.GetPromptHomeDir() return path.Join(scHome, DBFileNameBackup) } func IsValidConnectMode(mode string) bool { return mode == ConnectModeStartup || mode == ConnectModeAuto || mode == ConnectModeManual } func GetDB(ctx context.Context) (*sqlx.DB, error) { if txwrap.IsTxWrapContext(ctx) { return nil, fmt.Errorf("cannot call GetDB from within a running transaction") } globalDBLock.Lock() defer globalDBLock.Unlock() if globalDB == nil && globalDBErr == nil { dbName := GetDBName() globalDB, globalDBErr = sqlx.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_journal_mode=WAL&_busy_timeout=5000", dbName)) if globalDBErr != nil { globalDBErr = fmt.Errorf("opening db[%s]: %w", dbName, globalDBErr) log.Printf("[db] error: %v\n", globalDBErr) } else { log.Printf("[db] successfully opened db %s\n", dbName) } } return globalDB, globalDBErr } func CloseDB() { globalDBLock.Lock() defer globalDBLock.Unlock() if globalDB == nil { return } err := globalDB.Close() if err != nil { log.Printf("[db] error closing database: %v\n", err) } globalDB = nil } type ClientWinSizeType struct { Width int `json:"width"` Height int `json:"height"` Top int `json:"top"` Left int `json:"left"` FullScreen bool `json:"fullscreen,omitempty"` } type ActivityUpdate struct { FgMinutes int ActiveMinutes int OpenMinutes int NumCommands int ClickShared int HistoryView int BookmarksView int NumConns int BuildTime string } type ActivityType struct { Day string `json:"day"` Uploaded bool `json:"-"` TData TelemetryData `json:"tdata"` TzName string `json:"tzname"` TzOffset int `json:"tzoffset"` ClientVersion string `json:"clientversion"` ClientArch string `json:"clientarch"` BuildTime string `json:"buildtime"` OSRelease string `json:"osrelease"` } type TelemetryData struct { NumCommands int `json:"numcommands"` ActiveMinutes int `json:"activeminutes"` FgMinutes int `json:"fgminutes"` OpenMinutes int `json:"openminutes"` ClickShared int `json:"clickshared,omitempty"` HistoryView int `json:"historyview,omitempty"` BookmarksView int `json:"bookmarksview,omitempty"` NumConns int `json:"numconns"` } func (tdata TelemetryData) Value() (driver.Value, error) { return quickValueJson(tdata) } func (tdata *TelemetryData) Scan(val interface{}) error { return quickScanJson(tdata, val) } type ClientOptsType struct { NoTelemetry bool `json:"notelemetry,omitempty"` } type FeOptsType struct { TermFontSize int `json:"termfontsize,omitempty"` } type ClientData struct { ClientId string `json:"clientid"` UserId string `json:"userid"` UserPrivateKeyBytes []byte `json:"-"` UserPublicKeyBytes []byte `json:"-"` UserPrivateKey *ecdsa.PrivateKey `json:"-" dbmap:"-"` UserPublicKey *ecdsa.PublicKey `json:"-" dbmap:"-"` ActiveSessionId string `json:"activesessionid"` WinSize ClientWinSizeType `json:"winsize"` ClientOpts ClientOptsType `json:"clientopts"` FeOpts FeOptsType `json:"feopts"` } func (ClientData) UseDBMap() {} type CloudAclType struct { UserId string `json:"userid"` Role string `json:"role"` } type SessionType struct { SessionId string `json:"sessionid"` Name string `json:"name"` SessionIdx int64 `json:"sessionidx"` ActiveScreenId string `json:"activescreenid"` ShareMode string `json:"sharemode"` NotifyNum int64 `json:"notifynum"` Archived bool `json:"archived,omitempty"` ArchivedTs int64 `json:"archivedts,omitempty"` Remotes []*RemoteInstance `json:"remotes"` // only for updates Remove bool `json:"remove,omitempty"` Full bool `json:"full,omitempty"` } type CloudSessionType struct { SessionId string ViewKey string WriteKey string EncKey string EncType string Vts int64 Acl []*CloudAclType } func (cs *CloudSessionType) ToMap() map[string]any { m := make(map[string]any) m["sessionid"] = cs.SessionId m["viewkey"] = cs.ViewKey m["writekey"] = cs.WriteKey m["enckey"] = cs.EncKey m["enctype"] = cs.EncType m["vts"] = cs.Vts m["acl"] = quickJsonArr(cs.Acl) return m } func (cs *CloudSessionType) FromMap(m map[string]interface{}) bool { quickSetStr(&cs.SessionId, m, "sessionid") quickSetStr(&cs.ViewKey, m, "viewkey") quickSetStr(&cs.WriteKey, m, "writekey") quickSetStr(&cs.EncKey, m, "enckey") quickSetStr(&cs.EncType, m, "enctype") quickSetInt64(&cs.Vts, m, "vts") quickSetJsonArr(&cs.Acl, m, "acl") return true } type CloudUpdate struct { UpdateId string Ts int64 UpdateType string UpdateKeys []string } type SessionStatsType struct { SessionId string `json:"sessionid"` NumScreens int `json:"numscreens"` NumArchivedScreens int `json:"numarchivedscreens"` NumLines int `json:"numlines"` NumCmds int `json:"numcmds"` DiskStats SessionDiskSizeType `json:"diskstats"` } var RemoteNameRe = regexp.MustCompile("^\\*?[a-zA-Z0-9_-]+$") type RemotePtrType struct { OwnerId string `json:"ownerid"` RemoteId string `json:"remoteid"` Name string `json:"name"` } func (r RemotePtrType) IsSessionScope() bool { return strings.HasPrefix(r.Name, "*") } func (rptr *RemotePtrType) GetDisplayName(baseDisplayName string) string { name := baseDisplayName if rptr == nil { return name } if rptr.Name != "" { name = name + ":" + rptr.Name } if rptr.OwnerId != "" { name = "@" + rptr.OwnerId + ":" + name } return name } func (r RemotePtrType) Validate() error { if r.OwnerId != "" { if _, err := uuid.Parse(r.OwnerId); err != nil { return fmt.Errorf("invalid ownerid format: %v", err) } } if r.RemoteId != "" { if _, err := uuid.Parse(r.RemoteId); err != nil { return fmt.Errorf("invalid remoteid format: %v", err) } } if r.Name != "" { ok := RemoteNameRe.MatchString(r.Name) if !ok { return fmt.Errorf("invalid remote name") } } return nil } func (r RemotePtrType) MakeFullRemoteRef() string { if r.RemoteId == "" { return "" } if r.OwnerId == "" && r.Name == "" { return r.RemoteId } if r.OwnerId != "" && r.Name == "" { return fmt.Sprintf("@%s:%s", r.OwnerId, r.RemoteId) } if r.OwnerId == "" && r.Name != "" { return fmt.Sprintf("%s:%s", r.RemoteId, r.Name) } return fmt.Sprintf("@%s:%s:%s", r.OwnerId, r.RemoteId, r.Name) } func (h *HistoryItemType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["historyid"] = h.HistoryId rtn["ts"] = h.Ts rtn["userid"] = h.UserId rtn["sessionid"] = h.SessionId rtn["screenid"] = h.ScreenId rtn["lineid"] = h.LineId rtn["haderror"] = h.HadError rtn["cmdid"] = h.CmdId rtn["cmdstr"] = h.CmdStr rtn["remoteownerid"] = h.Remote.OwnerId rtn["remoteid"] = h.Remote.RemoteId rtn["remotename"] = h.Remote.Name rtn["ismetacmd"] = h.IsMetaCmd rtn["incognito"] = h.Incognito return rtn } func (h *HistoryItemType) FromMap(m map[string]interface{}) bool { quickSetStr(&h.HistoryId, m, "historyid") quickSetInt64(&h.Ts, m, "ts") quickSetStr(&h.UserId, m, "userid") quickSetStr(&h.SessionId, m, "sessionid") quickSetStr(&h.ScreenId, m, "screenid") quickSetStr(&h.LineId, m, "lineid") quickSetBool(&h.HadError, m, "haderror") quickSetStr(&h.CmdId, m, "cmdid") quickSetStr(&h.CmdStr, m, "cmdstr") quickSetStr(&h.Remote.OwnerId, m, "remoteownerid") quickSetStr(&h.Remote.RemoteId, m, "remoteid") quickSetStr(&h.Remote.Name, m, "remotename") quickSetBool(&h.IsMetaCmd, m, "ismetacmd") quickSetStr(&h.HistoryNum, m, "historynum") quickSetInt64(&h.LineNum, m, "linenum") quickSetBool(&h.Incognito, m, "incognito") return true } type ScreenOptsType struct { TabColor string `json:"tabcolor,omitempty"` PTerm string `json:"pterm,omitempty"` } type ScreenLinesType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` Lines []*LineType `json:"lines" dbmap:"-"` Cmds []*CmdType `json:"cmds" dbmap:"-"` } func (ScreenLinesType) UseDBMap() {} type ScreenType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` Name string `json:"name"` ScreenIdx int64 `json:"screenidx"` ScreenOpts ScreenOptsType `json:"screenopts"` OwnerId string `json:"ownerid"` ShareMode string `json:"sharemode"` CurRemote RemotePtrType `json:"curremote"` NextLineNum int64 `json:"nextlinenum"` SelectedLine int64 `json:"selectedline"` Anchor ScreenAnchorType `json:"anchor"` FocusType string `json:"focustype"` Archived bool `json:"archived,omitempty"` ArchivedTs int64 `json:"archivedts,omitempty"` // only for updates Full bool `json:"full,omitempty"` Remove bool `json:"remove,omitempty"` } func (s *ScreenType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["sessionid"] = s.SessionId rtn["screenid"] = s.ScreenId rtn["name"] = s.Name rtn["screenidx"] = s.ScreenIdx rtn["screenopts"] = quickJson(s.ScreenOpts) rtn["ownerid"] = s.OwnerId rtn["sharemode"] = s.ShareMode rtn["curremoteownerid"] = s.CurRemote.OwnerId rtn["curremoteid"] = s.CurRemote.RemoteId rtn["curremotename"] = s.CurRemote.Name rtn["nextlinenum"] = s.NextLineNum rtn["selectedline"] = s.SelectedLine rtn["anchor"] = quickJson(s.Anchor) rtn["focustype"] = s.FocusType rtn["archived"] = s.Archived rtn["archivedts"] = s.ArchivedTs return rtn } func (s *ScreenType) FromMap(m map[string]interface{}) bool { quickSetStr(&s.SessionId, m, "sessionid") quickSetStr(&s.ScreenId, m, "screenid") quickSetStr(&s.Name, m, "name") quickSetInt64(&s.ScreenIdx, m, "screenidx") quickSetJson(&s.ScreenOpts, m, "screenopts") quickSetStr(&s.OwnerId, m, "ownerid") quickSetStr(&s.ShareMode, m, "sharemode") quickSetStr(&s.CurRemote.OwnerId, m, "curremoteownerid") quickSetStr(&s.CurRemote.RemoteId, m, "curremoteid") quickSetStr(&s.CurRemote.Name, m, "curremotename") quickSetInt64(&s.NextLineNum, m, "nextlinenum") quickSetInt64(&s.SelectedLine, m, "selectedline") quickSetJson(&s.Anchor, m, "anchor") quickSetStr(&s.FocusType, m, "focustype") quickSetBool(&s.Archived, m, "archived") quickSetInt64(&s.ArchivedTs, m, "archivedts") return true } const ( LayoutFull = "full" ) type LayoutType struct { Type string `json:"type"` Parent string `json:"parent,omitempty"` ZIndex int64 `json:"zindex,omitempty"` Float bool `json:"float,omitempty"` Top string `json:"top,omitempty"` Bottom string `json:"bottom,omitempty"` Left string `json:"left,omitempty"` Right string `json:"right,omitempty"` Width string `json:"width,omitempty"` Height string `json:"height,omitempty"` } func (l *LayoutType) Scan(val interface{}) error { return quickScanJson(l, val) } func (l LayoutType) Value() (driver.Value, error) { return quickValueJson(l) } type ScreenAnchorType struct { AnchorLine int `json:"anchorline,omitempty"` AnchorOffset int `json:"anchoroffset,omitempty"` } type HistoryItemType struct { HistoryId string `json:"historyid"` Ts int64 `json:"ts"` UserId string `json:"userid"` SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` LineId string `json:"lineid"` HadError bool `json:"haderror"` CmdId string `json:"cmdid"` CmdStr string `json:"cmdstr"` Remote RemotePtrType `json:"remote"` IsMetaCmd bool `json:"ismetacmd"` Incognito bool `json:"incognito,omitempty"` // only for updates Remove bool `json:"remove"` // transient (string because of different history orderings) HistoryNum string `json:"historynum"` LineNum int64 `json:"linenum"` } type HistoryQueryOpts struct { Offset int MaxItems int FromTs int64 SearchText string SessionId string RemoteId string ScreenId string NoMeta bool RawOffset int FilterFn func(*HistoryItemType) bool } type HistoryQueryResult struct { MaxItems int Items []*HistoryItemType Offset int // the offset shown to user RawOffset int // internal offset HasMore bool NextRawOffset int // internal offset used by pager for next query prevItems int // holds number of items skipped by RawOffset } type TermOpts struct { Rows int64 `json:"rows"` Cols int64 `json:"cols"` FlexRows bool `json:"flexrows,omitempty"` MaxPtySize int64 `json:"maxptysize,omitempty"` } func (opts *TermOpts) Scan(val interface{}) error { return quickScanJson(opts, val) } func (opts TermOpts) Value() (driver.Value, error) { return quickValueJson(opts) } type ShellStatePtr struct { BaseHash string DiffHashArr []string } func (ssptr *ShellStatePtr) IsEmpty() bool { if ssptr == nil || ssptr.BaseHash == "" { return true } return false } type RemoteInstance struct { RIId string `json:"riid"` Name string `json:"name"` SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` RemoteOwnerId string `json:"remoteownerid"` RemoteId string `json:"remoteid"` FeState FeStateType `json:"festate"` StateBaseHash string `json:"-"` StateDiffHashArr []string `json:"-"` // only for updates Remove bool `json:"remove,omitempty"` } type StateBase struct { BaseHash string Version string Ts int64 Data []byte } type StateDiff struct { DiffHash string Ts int64 BaseHash string DiffHashArr []string Data []byte } func (sd *StateDiff) FromMap(m map[string]interface{}) bool { quickSetStr(&sd.DiffHash, m, "diffhash") quickSetInt64(&sd.Ts, m, "ts") quickSetStr(&sd.BaseHash, m, "basehash") quickSetJsonArr(&sd.DiffHashArr, m, "diffhasharr") quickSetBytes(&sd.Data, m, "data") return true } func (sd *StateDiff) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["diffhash"] = sd.DiffHash rtn["ts"] = sd.Ts rtn["basehash"] = sd.BaseHash rtn["diffhasharr"] = quickJsonArr(sd.DiffHashArr) rtn["data"] = sd.Data return rtn } type FeStateType struct { Cwd string `json:"cwd"` // maybe later we can add some vars } func FeStateFromShellState(state *packet.ShellState) *FeStateType { if state == nil { return nil } return &FeStateType{Cwd: state.Cwd} } func (ri *RemoteInstance) FromMap(m map[string]interface{}) bool { quickSetStr(&ri.RIId, m, "riid") quickSetStr(&ri.Name, m, "name") quickSetStr(&ri.SessionId, m, "sessionid") quickSetStr(&ri.ScreenId, m, "screenid") quickSetStr(&ri.RemoteOwnerId, m, "remoteownerid") quickSetStr(&ri.RemoteId, m, "remoteid") quickSetJson(&ri.FeState, m, "festate") quickSetStr(&ri.StateBaseHash, m, "statebasehash") quickSetJsonArr(&ri.StateDiffHashArr, m, "statediffhasharr") return true } func (ri *RemoteInstance) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["riid"] = ri.RIId rtn["name"] = ri.Name rtn["sessionid"] = ri.SessionId rtn["screenid"] = ri.ScreenId rtn["remoteownerid"] = ri.RemoteOwnerId rtn["remoteid"] = ri.RemoteId rtn["festate"] = quickJson(ri.FeState) rtn["statebasehash"] = ri.StateBaseHash rtn["statediffhasharr"] = quickJsonArr(ri.StateDiffHashArr) return rtn } type LineType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` UserId string `json:"userid"` LineId string `json:"lineid"` Ts int64 `json:"ts"` LineNum int64 `json:"linenum"` LineNumTemp bool `json:"linenumtemp,omitempty"` LineLocal bool `json:"linelocal"` LineType string `json:"linetype"` Renderer string `json:"renderer,omitempty"` Text string `json:"text,omitempty"` CmdId string `json:"cmdid,omitempty"` Ephemeral bool `json:"ephemeral,omitempty"` ContentHeight int64 `json:"contentheight,omitempty"` Star bool `json:"star,omitempty"` Bookmarked bool `json:"bookmarked,omitempty"` Pinned bool `json:"pinned,omitempty"` Archived bool `json:"archived,omitempty"` Remove bool `json:"remove,omitempty"` } type PlaybookType struct { PlaybookId string `json:"playbookid"` PlaybookName string `json:"playbookname"` Description string `json:"description"` EntryIds []string `json:"entryids"` // this is not persisted to DB, just for transport to FE Entries []*PlaybookEntry `json:"entries"` } func (p *PlaybookType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["playbookid"] = p.PlaybookId rtn["playbookname"] = p.PlaybookName rtn["description"] = p.Description rtn["entryids"] = quickJsonArr(p.EntryIds) return rtn } func (p *PlaybookType) FromMap(m map[string]interface{}) bool { quickSetStr(&p.PlaybookId, m, "playbookid") quickSetStr(&p.PlaybookName, m, "playbookname") quickSetStr(&p.Description, m, "description") quickSetJsonArr(&p.Entries, m, "entries") return true } // reorders p.Entries to match p.EntryIds func (p *PlaybookType) OrderEntries() { if len(p.Entries) == 0 { return } m := make(map[string]*PlaybookEntry) for _, entry := range p.Entries { m[entry.EntryId] = entry } newList := make([]*PlaybookEntry, 0, len(p.EntryIds)) for _, entryId := range p.EntryIds { entry := m[entryId] if entry != nil { newList = append(newList, entry) } } p.Entries = newList } // removes from p.EntryIds (not from p.Entries) func (p *PlaybookType) RemoveEntry(entryIdToRemove string) { if len(p.EntryIds) == 0 { return } newList := make([]string, 0, len(p.EntryIds)-1) for _, entryId := range p.EntryIds { if entryId == entryIdToRemove { continue } newList = append(newList, entryId) } p.EntryIds = newList } type PlaybookEntry struct { PlaybookId string `json:"playbookid"` EntryId string `json:"entryid"` Alias string `json:"alias"` CmdStr string `json:"cmdstr"` UpdatedTs int64 `json:"updatedts"` CreatedTs int64 `json:"createdts"` Description string `json:"description"` Remove bool `json:"remove,omitempty"` } type BookmarkType struct { BookmarkId string `json:"bookmarkid"` CreatedTs int64 `json:"createdts"` CmdStr string `json:"cmdstr"` Alias string `json:"alias,omitempty"` Tags []string `json:"tags"` Description string `json:"description"` Cmds []base.CommandKey `json:"cmds"` OrderIdx int64 `json:"orderidx"` Remove bool `json:"remove,omitempty"` } func (bm *BookmarkType) GetSimpleKey() string { return bm.BookmarkId } func (bm *BookmarkType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["bookmarkid"] = bm.BookmarkId rtn["createdts"] = bm.CreatedTs rtn["cmdstr"] = bm.CmdStr rtn["alias"] = bm.Alias rtn["description"] = bm.Description rtn["tags"] = quickJsonArr(bm.Tags) return rtn } func (bm *BookmarkType) FromMap(m map[string]interface{}) bool { quickSetStr(&bm.BookmarkId, m, "bookmarkid") quickSetInt64(&bm.CreatedTs, m, "createdts") quickSetStr(&bm.Alias, m, "alias") quickSetStr(&bm.CmdStr, m, "cmdstr") quickSetStr(&bm.Description, m, "description") quickSetJsonArr(&bm.Tags, m, "tags") return true } type ResolveItem struct { Name string Num int Id string Hidden bool } type SSHOpts struct { Local bool `json:"local,omitempty"` SSHHost string `json:"sshhost"` SSHUser string `json:"sshuser"` SSHOptsStr string `json:"sshopts,omitempty"` SSHIdentity string `json:"sshidentity,omitempty"` SSHPort int `json:"sshport,omitempty"` SSHPassword string `json:"sshpassword,omitempty"` } type RemoteOptsType struct { Color string `json:"color"` } func (opts *RemoteOptsType) Scan(val interface{}) error { return quickScanJson(opts, val) } func (opts RemoteOptsType) Value() (driver.Value, error) { return quickValueJson(opts) } type RemoteType struct { RemoteId string `json:"remoteid"` PhysicalId string `json:"physicalid"` RemoteType string `json:"remotetype"` RemoteAlias string `json:"remotealias"` RemoteCanonicalName string `json:"remotecanonicalname"` RemoteSudo bool `json:"remotesudo"` RemoteUser string `json:"remoteuser"` RemoteHost string `json:"remotehost"` ConnectMode string `json:"connectmode"` AutoInstall bool `json:"autoinstall"` SSHOpts *SSHOpts `json:"sshopts"` RemoteOpts *RemoteOptsType `json:"remoteopts"` LastConnectTs int64 `json:"lastconnectts"` Archived bool `json:"archived"` RemoteIdx int64 `json:"remoteidx"` Local bool `json:"local"` } func (r *RemoteType) GetName() string { if r.RemoteAlias != "" { return r.RemoteAlias } return r.RemoteCanonicalName } type CmdDoneInfo struct { Ts int64 `json:"ts"` ExitCode int64 `json:"exitcode"` DurationMs int64 `json:"durationms"` } type CmdType struct { SessionId string `json:"sessionid"` CmdId string `json:"cmdid"` Remote RemotePtrType `json:"remote"` CmdStr string `json:"cmdstr"` FeState FeStateType `json:"festate"` StatePtr ShellStatePtr `json:"state"` TermOpts TermOpts `json:"termopts"` OrigTermOpts TermOpts `json:"origtermopts"` Status string `json:"status"` StartPk *packet.CmdStartPacketType `json:"startpk,omitempty"` DoneInfo *CmdDoneInfo `json:"doneinfo,omitempty"` RunOut []packet.PacketType `json:"runout,omitempty"` RtnState bool `json:"rtnstate,omitempty"` RtnStatePtr ShellStatePtr `json:"rtnstateptr,omitempty"` Remove bool `json:"remove,omitempty"` } func (r *RemoteType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["remoteid"] = r.RemoteId rtn["physicalid"] = r.PhysicalId rtn["remotetype"] = r.RemoteType rtn["remotealias"] = r.RemoteAlias rtn["remotecanonicalname"] = r.RemoteCanonicalName rtn["remotesudo"] = r.RemoteSudo rtn["remoteuser"] = r.RemoteUser rtn["remotehost"] = r.RemoteHost rtn["connectmode"] = r.ConnectMode rtn["autoinstall"] = r.AutoInstall rtn["sshopts"] = quickJson(r.SSHOpts) rtn["remoteopts"] = quickJson(r.RemoteOpts) rtn["lastconnectts"] = r.LastConnectTs rtn["archived"] = r.Archived rtn["remoteidx"] = r.RemoteIdx rtn["local"] = r.Local return rtn } func (r *RemoteType) FromMap(m map[string]interface{}) bool { quickSetStr(&r.RemoteId, m, "remoteid") quickSetStr(&r.PhysicalId, m, "physicalid") quickSetStr(&r.RemoteType, m, "remotetype") quickSetStr(&r.RemoteAlias, m, "remotealias") quickSetStr(&r.RemoteCanonicalName, m, "remotecanonicalname") quickSetBool(&r.RemoteSudo, m, "remotesudo") quickSetStr(&r.RemoteUser, m, "remoteuser") quickSetStr(&r.RemoteHost, m, "remotehost") quickSetStr(&r.ConnectMode, m, "connectmode") quickSetBool(&r.AutoInstall, m, "autoinstall") quickSetJson(&r.SSHOpts, m, "sshopts") quickSetJson(&r.RemoteOpts, m, "remoteopts") quickSetInt64(&r.LastConnectTs, m, "lastconnectts") quickSetBool(&r.Archived, m, "archived") quickSetInt64(&r.RemoteIdx, m, "remoteidx") quickSetBool(&r.Local, m, "local") return true } func (cmd *CmdType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["sessionid"] = cmd.SessionId rtn["cmdid"] = cmd.CmdId rtn["remoteownerid"] = cmd.Remote.OwnerId rtn["remoteid"] = cmd.Remote.RemoteId rtn["remotename"] = cmd.Remote.Name rtn["cmdstr"] = cmd.CmdStr rtn["festate"] = quickJson(cmd.FeState) rtn["statebasehash"] = cmd.StatePtr.BaseHash rtn["statediffhasharr"] = quickJsonArr(cmd.StatePtr.DiffHashArr) rtn["termopts"] = quickJson(cmd.TermOpts) rtn["origtermopts"] = quickJson(cmd.OrigTermOpts) rtn["status"] = cmd.Status rtn["startpk"] = quickJson(cmd.StartPk) rtn["doneinfo"] = quickJson(cmd.DoneInfo) rtn["runout"] = quickJson(cmd.RunOut) rtn["rtnstate"] = cmd.RtnState rtn["rtnbasehash"] = cmd.RtnStatePtr.BaseHash rtn["rtndiffhasharr"] = quickJsonArr(cmd.RtnStatePtr.DiffHashArr) return rtn } func (cmd *CmdType) FromMap(m map[string]interface{}) bool { quickSetStr(&cmd.SessionId, m, "sessionid") quickSetStr(&cmd.CmdId, m, "cmdid") quickSetStr(&cmd.Remote.OwnerId, m, "remoteownerid") quickSetStr(&cmd.Remote.RemoteId, m, "remoteid") quickSetStr(&cmd.Remote.Name, m, "remotename") quickSetStr(&cmd.CmdStr, m, "cmdstr") quickSetJson(&cmd.FeState, m, "festate") quickSetStr(&cmd.StatePtr.BaseHash, m, "statebasehash") quickSetJsonArr(&cmd.StatePtr.DiffHashArr, m, "statediffhasharr") quickSetJson(&cmd.TermOpts, m, "termopts") quickSetJson(&cmd.OrigTermOpts, m, "origtermopts") quickSetStr(&cmd.Status, m, "status") quickSetJson(&cmd.StartPk, m, "startpk") quickSetJson(&cmd.DoneInfo, m, "doneinfo") quickSetJson(&cmd.RunOut, m, "runout") quickSetBool(&cmd.RtnState, m, "rtnstate") quickSetStr(&cmd.RtnStatePtr.BaseHash, m, "rtnbasehash") quickSetJsonArr(&cmd.RtnStatePtr.DiffHashArr, m, "rtndiffhasharr") return true } func makeNewLineCmd(sessionId string, screenId string, userId string, cmdId string, renderer string) *LineType { rtn := &LineType{} rtn.SessionId = sessionId rtn.ScreenId = screenId rtn.UserId = userId rtn.LineId = scbase.GenPromptUUID() rtn.Ts = time.Now().UnixMilli() rtn.LineLocal = true rtn.LineType = LineTypeCmd rtn.CmdId = cmdId rtn.ContentHeight = LineNoHeight rtn.Renderer = renderer return rtn } func makeNewLineText(sessionId string, screenId string, userId string, text string) *LineType { rtn := &LineType{} rtn.SessionId = sessionId rtn.ScreenId = screenId rtn.UserId = userId rtn.LineId = scbase.GenPromptUUID() rtn.Ts = time.Now().UnixMilli() rtn.LineLocal = true rtn.LineType = LineTypeText rtn.Text = text rtn.ContentHeight = LineNoHeight return rtn } func AddCommentLine(ctx context.Context, sessionId string, screenId string, userId string, commentText string) (*LineType, error) { rtnLine := makeNewLineText(sessionId, screenId, userId, commentText) err := InsertLine(ctx, rtnLine, nil) if err != nil { return nil, err } return rtnLine, nil } func AddCmdLine(ctx context.Context, sessionId string, screenId string, userId string, cmd *CmdType, renderer string) (*LineType, error) { rtnLine := makeNewLineCmd(sessionId, screenId, userId, cmd.CmdId, renderer) err := InsertLine(ctx, rtnLine, cmd) if err != nil { return nil, err } return rtnLine, nil } func EnsureLocalRemote(ctx context.Context) error { physicalId, err := base.GetRemoteId() if err != nil { return fmt.Errorf("getting local physical remoteid: %w", err) } remote, err := GetLocalRemote(ctx) if err != nil { return fmt.Errorf("getting local remote from db: %w", err) } if remote != nil { return nil } hostName, err := os.Hostname() if err != nil { return fmt.Errorf("getting hostname: %w", err) } user, err := user.Current() if err != nil { return fmt.Errorf("getting user: %w", err) } // create the local remote localRemote := &RemoteType{ RemoteId: scbase.GenPromptUUID(), PhysicalId: physicalId, RemoteType: RemoteTypeSsh, RemoteAlias: LocalRemoteAlias, RemoteCanonicalName: fmt.Sprintf("%s@%s", user.Username, hostName), RemoteSudo: false, RemoteUser: user.Username, RemoteHost: hostName, ConnectMode: ConnectModeStartup, AutoInstall: true, SSHOpts: &SSHOpts{Local: true}, Local: true, } err = UpsertRemote(ctx, localRemote) if err != nil { return err } log.Printf("[db] added local remote '%s', id=%s\n", localRemote.RemoteCanonicalName, localRemote.RemoteId) sudoRemote := &RemoteType{ RemoteId: scbase.GenPromptUUID(), PhysicalId: "", RemoteType: RemoteTypeSsh, RemoteAlias: "sudo", RemoteCanonicalName: fmt.Sprintf("sudo@%s@%s", user.Username, hostName), RemoteSudo: true, RemoteUser: "root", RemoteHost: hostName, ConnectMode: ConnectModeManual, AutoInstall: true, SSHOpts: &SSHOpts{Local: true}, RemoteOpts: &RemoteOptsType{Color: "red"}, Local: true, } err = UpsertRemote(ctx, sudoRemote) if err != nil { return err } log.Printf("[db] added sudo remote '%s', id=%s\n", sudoRemote.RemoteCanonicalName, sudoRemote.RemoteId) return nil } func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { session, err := GetSessionByName(ctx, DefaultSessionName) if err != nil { return nil, err } if session != nil { return session, nil } _, err = InsertSessionWithName(ctx, DefaultSessionName, ShareModeLocal, true) if err != nil { return nil, err } return GetSessionByName(ctx, DefaultSessionName) } func createClientData(tx *TxWrap) error { curve := elliptic.P384() pkey, err := ecdsa.GenerateKey(curve, rand.Reader) if err != nil { return fmt.Errorf("generating P-834 key: %w", err) } pkBytes, err := x509.MarshalECPrivateKey(pkey) if err != nil { return fmt.Errorf("marshaling (pkcs8) private key bytes: %w", err) } pubBytes, err := x509.MarshalPKIXPublicKey(&pkey.PublicKey) if err != nil { return fmt.Errorf("marshaling (pkix) public key bytes: %w", err) } c := ClientData{ ClientId: uuid.New().String(), UserId: uuid.New().String(), UserPrivateKeyBytes: pkBytes, UserPublicKeyBytes: pubBytes, ActiveSessionId: "", WinSize: ClientWinSizeType{}, } query := `INSERT INTO client ( clientid, userid, activesessionid, userpublickeybytes, userprivatekeybytes, winsize) VALUES (:clientid,:userid,:activesessionid,:userpublickeybytes,:userprivatekeybytes,:winsize)` tx.NamedExec(query, ToDBMap(c)) log.Printf("create new clientid[%s] userid[%s] with public/private keypair\n", c.ClientId, c.UserId) return nil } func EnsureClientData(ctx context.Context) (*ClientData, error) { rtn, err := WithTxRtn(ctx, func(tx *TxWrap) (*ClientData, error) { query := `SELECT count(*) FROM client` count := tx.GetInt(query) if count > 1 { return nil, fmt.Errorf("invalid client database, multiple (%d) rows in client table", count) } if count == 0 { createErr := createClientData(tx) if createErr != nil { return nil, createErr } } cdata := GetMappable[*ClientData](tx, `SELECT * FROM client`) if cdata == nil { return nil, fmt.Errorf("no client data found") } return cdata, nil }) if err != nil { return nil, err } if rtn.UserId == "" { return nil, fmt.Errorf("invalid client data (no userid)") } if len(rtn.UserPrivateKeyBytes) == 0 || len(rtn.UserPublicKeyBytes) == 0 { return nil, fmt.Errorf("invalid client data (no public/private keypair)") } rtn.UserPrivateKey, err = x509.ParseECPrivateKey(rtn.UserPrivateKeyBytes) if err != nil { return nil, fmt.Errorf("invalid client data, cannot parse private key: %w", err) } pubKey, err := x509.ParsePKIXPublicKey(rtn.UserPublicKeyBytes) if err != nil { return nil, fmt.Errorf("invalid client data, cannot parse public key: %w", err) } var ok bool rtn.UserPublicKey, ok = pubKey.(*ecdsa.PublicKey) if !ok { return nil, fmt.Errorf("invalid client data, wrong public key type: %T", pubKey) } return rtn, nil } func SetClientOpts(ctx context.Context, clientOpts ClientOptsType) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE client SET clientopts = ?` tx.Exec(query, quickJson(clientOpts)) return nil }) return txErr }