// Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package shellutil import ( "encoding/base64" "encoding/json" "fmt" "sync" "time" "github.com/wavetermdev/waveterm/pkg/wshrpc" ) var tokenSwapMap map[string]*TokenSwapEntry = make(map[string]*TokenSwapEntry) var tokenMapLock = &sync.Mutex{} type TokenSwapEntry struct { Token string `json:"token"` SockName string `json:"sockname,omitempty"` RpcContext *wshrpc.RpcContext `json:"rpccontext,omitempty"` Env map[string]string `json:"env,omitempty"` ScriptText string `json:"scripttext,omitempty"` Exp time.Time `json:"-"` } type UnpackedTokenType struct { Token string `json:"token"` // uuid SockName string `json:"sockname,omitempty"` RpcContext *wshrpc.RpcContext `json:"rpccontext,omitempty"` } func (t *UnpackedTokenType) Pack() (string, error) { // convert to json, and then base64 encode barr, err := json.Marshal(t) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(barr), nil } func UnpackSwapToken(token string) (*UnpackedTokenType, error) { // base64 decode, then convert from json barr, err := base64.StdEncoding.DecodeString(token) if err != nil { return nil, err } var unpacked UnpackedTokenType err = json.Unmarshal(barr, &unpacked) if err != nil { return nil, err } return &unpacked, nil } func (t *TokenSwapEntry) PackForClient() (string, error) { unpackedToken := &UnpackedTokenType{ Token: t.Token, SockName: t.SockName, RpcContext: t.RpcContext, } return unpackedToken.Pack() } func removeExpiredTokens() { now := time.Now() tokenMapLock.Lock() defer tokenMapLock.Unlock() for k, v := range tokenSwapMap { if v.Exp.Before(now) { delete(tokenSwapMap, k) } } } func AddTokenSwapEntry(entry *TokenSwapEntry) error { removeExpiredTokens() if entry.Token == "" { return fmt.Errorf("token cannot be empty") } tokenMapLock.Lock() defer tokenMapLock.Unlock() if _, ok := tokenSwapMap[entry.Token]; ok { return fmt.Errorf("token already exists: %s", entry.Token) } tokenSwapMap[entry.Token] = entry return nil } func GetAndRemoveTokenSwapEntry(token string) *TokenSwapEntry { removeExpiredTokens() tokenMapLock.Lock() defer tokenMapLock.Unlock() if entry, ok := tokenSwapMap[token]; ok { delete(tokenSwapMap, token) return entry } return nil } func encodeEnvVarsForBash(env map[string]string) (string, error) { var encoded string for k, v := range env { // validate key if !IsValidEnvVarName(k) { return "", fmt.Errorf("invalid env var name: %q", k) } encoded += fmt.Sprintf("export %s=%s\n", k, HardQuote(v)) } return encoded, nil } func encodeEnvVarsForFish(env map[string]string) (string, error) { var encoded string for k, v := range env { // validate key if !IsValidEnvVarName(k) { return "", fmt.Errorf("invalid env var name: %q", k) } encoded += fmt.Sprintf("set -x %s %s\n", k, HardQuoteFish(v)) } return encoded, nil } func encodeEnvVarsForPowerShell(env map[string]string) (string, error) { var encoded string for k, v := range env { // validate key if !IsValidEnvVarName(k) { return "", fmt.Errorf("invalid env var name: %q", k) } encoded += fmt.Sprintf("$env:%s = %s\n", k, HardQuotePowerShell(v)) } return encoded, nil } func EncodeEnvVarsForShell(shellType string, env map[string]string) (string, error) { switch shellType { case ShellType_bash, ShellType_zsh: return encodeEnvVarsForBash(env) case ShellType_fish: return encodeEnvVarsForFish(env) case ShellType_pwsh: return encodeEnvVarsForPowerShell(env) default: return "", fmt.Errorf("unknown or unsupported shell type for env var encoding: %s", shellType) } }