2022-06-10 09:35:24 +02:00
|
|
|
// Copyright 2022 Dashborg Inc
|
|
|
|
//
|
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
|
|
|
package base
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2022-07-02 02:37:37 +02:00
|
|
|
"io"
|
2022-06-10 09:35:24 +02:00
|
|
|
"io/fs"
|
|
|
|
"os"
|
2022-06-23 19:16:54 +02:00
|
|
|
"os/exec"
|
2022-06-10 09:35:24 +02:00
|
|
|
"path"
|
|
|
|
"path/filepath"
|
2022-06-25 08:42:00 +02:00
|
|
|
"strings"
|
2022-08-08 18:52:50 +02:00
|
|
|
"sync"
|
2022-06-27 21:03:47 +02:00
|
|
|
|
|
|
|
"github.com/google/uuid"
|
2022-06-10 09:35:24 +02:00
|
|
|
)
|
|
|
|
|
2022-07-02 02:37:37 +02:00
|
|
|
const HomeVarName = "HOME"
|
|
|
|
const DefaultMShellHome = "~/.mshell"
|
|
|
|
const DefaultMShellName = "mshell"
|
2022-06-23 19:16:54 +02:00
|
|
|
const MShellPathVarName = "MSHELL_PATH"
|
2022-07-02 02:37:37 +02:00
|
|
|
const MShellHomeVarName = "MSHELL_HOME"
|
2022-06-23 19:16:54 +02:00
|
|
|
const SSHCommandVarName = "SSH_COMMAND"
|
2022-07-02 02:37:37 +02:00
|
|
|
const SessionsDirBaseName = "sessions"
|
2022-06-28 07:39:16 +02:00
|
|
|
const MShellVersion = "0.1.0"
|
2022-07-02 02:37:37 +02:00
|
|
|
const RemoteIdFile = "remoteid"
|
2022-06-10 09:35:24 +02:00
|
|
|
|
2022-08-08 18:52:50 +02:00
|
|
|
var sessionDirCache = make(map[string]string)
|
|
|
|
var baseLock = &sync.Mutex{}
|
|
|
|
|
2022-06-10 09:35:24 +02:00
|
|
|
type CommandFileNames struct {
|
2022-06-11 06:37:21 +02:00
|
|
|
PtyOutFile string
|
|
|
|
StdinFifo string
|
|
|
|
RunnerOutFile string
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
|
|
|
|
2022-06-27 21:03:47 +02:00
|
|
|
type CommandKey string
|
|
|
|
|
|
|
|
func MakeCommandKey(sessionId string, cmdId string) CommandKey {
|
|
|
|
if sessionId == "" && cmdId == "" {
|
|
|
|
return CommandKey("")
|
|
|
|
}
|
|
|
|
return CommandKey(fmt.Sprintf("%s/%s", sessionId, cmdId))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ckey CommandKey) IsEmpty() bool {
|
|
|
|
return string(ckey) == ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ckey CommandKey) GetSessionId() string {
|
|
|
|
slashIdx := strings.Index(string(ckey), "/")
|
|
|
|
if slashIdx == -1 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return string(ckey[0:slashIdx])
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ckey CommandKey) GetCmdId() string {
|
|
|
|
slashIdx := strings.Index(string(ckey), "/")
|
|
|
|
if slashIdx == -1 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return string(ckey[slashIdx+1:])
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ckey CommandKey) Split() (string, string) {
|
|
|
|
fields := strings.SplitN(string(ckey), "/", 2)
|
|
|
|
if len(fields) < 2 {
|
|
|
|
return "", ""
|
|
|
|
}
|
|
|
|
return fields[0], fields[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ckey CommandKey) Validate(typeStr string) error {
|
|
|
|
if typeStr == "" {
|
|
|
|
typeStr = "ck"
|
|
|
|
}
|
|
|
|
if ckey == "" {
|
|
|
|
return fmt.Errorf("%s has empty commandkey", typeStr)
|
|
|
|
}
|
|
|
|
sessionId, cmdId := ckey.Split()
|
|
|
|
if sessionId == "" {
|
|
|
|
return fmt.Errorf("%s does not have sessionid", typeStr)
|
|
|
|
}
|
|
|
|
_, err := uuid.Parse(sessionId)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("%s has invalid sessionid '%s'", typeStr, sessionId)
|
|
|
|
}
|
|
|
|
if cmdId == "" {
|
|
|
|
return fmt.Errorf("%s does not have cmdid", typeStr)
|
|
|
|
}
|
|
|
|
_, err = uuid.Parse(cmdId)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("%s has invalid cmdid '%s'", typeStr, cmdId)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-15 07:16:58 +02:00
|
|
|
func GetHomeDir() string {
|
|
|
|
homeVar := os.Getenv(HomeVarName)
|
|
|
|
if homeVar == "" {
|
|
|
|
return "/"
|
|
|
|
}
|
|
|
|
return homeVar
|
|
|
|
}
|
|
|
|
|
2022-07-02 02:37:37 +02:00
|
|
|
func GetMShellHomeDir() string {
|
|
|
|
homeVar := os.Getenv(MShellHomeVarName)
|
|
|
|
if homeVar != "" {
|
|
|
|
return homeVar
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
2022-07-02 02:37:37 +02:00
|
|
|
return ExpandHomeDir(DefaultMShellHome)
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
|
|
|
|
2022-06-27 21:03:47 +02:00
|
|
|
func GetCommandFileNames(ck CommandKey) (*CommandFileNames, error) {
|
|
|
|
if err := ck.Validate("ck"); err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot get command files: %w", err)
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
2022-06-27 21:03:47 +02:00
|
|
|
sessionId, cmdId := ck.Split()
|
2022-06-10 09:35:24 +02:00
|
|
|
sdir, err := EnsureSessionDir(sessionId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
base := path.Join(sdir, cmdId)
|
|
|
|
return &CommandFileNames{
|
2022-06-11 06:37:21 +02:00
|
|
|
PtyOutFile: base + ".ptyout",
|
|
|
|
StdinFifo: base + ".stdin",
|
|
|
|
RunnerOutFile: base + ".runout",
|
2022-06-10 09:35:24 +02:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func CleanUpCmdFiles(sessionId string, cmdId string) error {
|
|
|
|
if cmdId == "" {
|
|
|
|
return fmt.Errorf("bad cmdid, cannot clean up")
|
|
|
|
}
|
|
|
|
sdir, err := EnsureSessionDir(sessionId)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
cmdFileGlob := path.Join(sdir, cmdId+".*")
|
|
|
|
matches, err := filepath.Glob(cmdFileGlob)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, file := range matches {
|
|
|
|
rmErr := os.Remove(file)
|
|
|
|
if err == nil && rmErr != nil {
|
|
|
|
err = rmErr
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func EnsureSessionDir(sessionId string) (string, error) {
|
|
|
|
if sessionId == "" {
|
|
|
|
return "", fmt.Errorf("Bad sessionid, cannot be empty")
|
|
|
|
}
|
2022-08-08 18:52:50 +02:00
|
|
|
baseLock.Lock()
|
|
|
|
sdir, ok := sessionDirCache[sessionId]
|
|
|
|
baseLock.Unlock()
|
|
|
|
if ok {
|
|
|
|
return sdir, nil
|
|
|
|
}
|
2022-07-02 02:37:37 +02:00
|
|
|
mhome := GetMShellHomeDir()
|
2022-08-08 18:52:50 +02:00
|
|
|
sdir = path.Join(mhome, SessionsDirBaseName, sessionId)
|
2022-06-10 09:35:24 +02:00
|
|
|
info, err := os.Stat(sdir)
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
|
|
err = os.MkdirAll(sdir, 0777)
|
|
|
|
if err != nil {
|
2022-08-17 01:26:06 +02:00
|
|
|
return "", fmt.Errorf("cannot make mshell session directory[%s]: %w", sdir, err)
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
|
|
|
info, err = os.Stat(sdir)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
if !info.IsDir() {
|
|
|
|
return "", fmt.Errorf("session dir '%s' must be a directory", sdir)
|
|
|
|
}
|
2022-08-08 18:52:50 +02:00
|
|
|
baseLock.Lock()
|
|
|
|
sessionDirCache[sessionId] = sdir
|
|
|
|
baseLock.Unlock()
|
2022-06-10 09:35:24 +02:00
|
|
|
return sdir, nil
|
|
|
|
}
|
|
|
|
|
2022-06-23 21:48:45 +02:00
|
|
|
func GetMShellPath() (string, error) {
|
2022-07-02 02:37:37 +02:00
|
|
|
msPath := os.Getenv(MShellPathVarName) // use MSHELL_PATH
|
2022-06-23 19:16:54 +02:00
|
|
|
if msPath != "" {
|
2022-06-23 21:48:45 +02:00
|
|
|
return exec.LookPath(msPath)
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
2022-07-02 02:37:37 +02:00
|
|
|
mhome := GetMShellHomeDir()
|
|
|
|
userMShellPath := path.Join(mhome, DefaultMShellName) // look in ~/.mshell
|
2022-06-23 21:48:45 +02:00
|
|
|
msPath, err := exec.LookPath(userMShellPath)
|
2022-07-02 02:37:37 +02:00
|
|
|
if err == nil {
|
2022-06-23 21:48:45 +02:00
|
|
|
return msPath, nil
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
2022-07-02 02:37:37 +02:00
|
|
|
return exec.LookPath(DefaultMShellName) // standard path lookup for 'mshell'
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
|
|
|
|
2022-07-02 02:37:37 +02:00
|
|
|
func GetMShellSessionsDir() (string, error) {
|
|
|
|
mhome := GetMShellHomeDir()
|
|
|
|
return path.Join(mhome, SessionsDirBaseName), nil
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
2022-06-25 08:42:00 +02:00
|
|
|
|
|
|
|
func ExpandHomeDir(pathStr string) string {
|
|
|
|
if pathStr != "~" && !strings.HasPrefix(pathStr, "~/") {
|
|
|
|
return pathStr
|
|
|
|
}
|
|
|
|
homeDir := GetHomeDir()
|
|
|
|
if pathStr == "~" {
|
|
|
|
return homeDir
|
|
|
|
}
|
|
|
|
return path.Join(homeDir, pathStr[2:])
|
|
|
|
}
|
2022-06-28 07:39:16 +02:00
|
|
|
|
|
|
|
func ValidGoArch(goos string, goarch string) bool {
|
|
|
|
return (goos == "darwin" || goos == "linux") && (goarch == "amd64" || goarch == "arm64")
|
|
|
|
}
|
|
|
|
|
|
|
|
func GoArchOptFile(goos string, goarch string) string {
|
|
|
|
return fmt.Sprintf("/opt/mshell/bin/mshell.%s.%s", goos, goarch)
|
|
|
|
}
|
2022-07-02 02:37:37 +02:00
|
|
|
|
|
|
|
func GetRemoteId() (string, error) {
|
|
|
|
mhome := GetMShellHomeDir()
|
2022-08-17 01:26:06 +02:00
|
|
|
homeInfo, err := os.Stat(mhome)
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
|
|
err = os.MkdirAll(mhome, 0777)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("cannot make mshell home directory[%s]: %w", mhome, err)
|
|
|
|
}
|
|
|
|
homeInfo, err = os.Stat(mhome)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("cannot stat mshell home directory[%s]: %w", mhome, err)
|
|
|
|
}
|
|
|
|
if !homeInfo.IsDir() {
|
|
|
|
return "", fmt.Errorf("mshell home directory[%s] is not a directory", mhome)
|
|
|
|
}
|
2022-07-02 02:37:37 +02:00
|
|
|
remoteIdFile := path.Join(mhome, RemoteIdFile)
|
|
|
|
fd, err := os.Open(remoteIdFile)
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
|
|
// write the file
|
|
|
|
remoteId := uuid.New().String()
|
|
|
|
err = os.WriteFile(remoteIdFile, []byte(remoteId), 0644)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("cannot write remoteid to '%s': %w", remoteIdFile, err)
|
|
|
|
}
|
|
|
|
return remoteId, nil
|
|
|
|
} else if err != nil {
|
|
|
|
return "", fmt.Errorf("cannot read remoteid file '%s': %w", remoteIdFile, err)
|
|
|
|
} else {
|
|
|
|
defer fd.Close()
|
|
|
|
contents, err := io.ReadAll(fd)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("cannot read remoteid file '%s': %w", remoteIdFile, err)
|
|
|
|
}
|
|
|
|
uuidStr := string(contents)
|
|
|
|
_, err = uuid.Parse(uuidStr)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("invalid uuid read from '%s': %w", remoteIdFile, err)
|
|
|
|
}
|
|
|
|
return uuidStr, nil
|
|
|
|
}
|
|
|
|
}
|