198 lines
4.3 KiB
Go
198 lines
4.3 KiB
Go
package session
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"latchd/config"
|
|
)
|
|
|
|
type PostLoginEnv struct {
|
|
Kind string // "x11", "wayland", "tty"
|
|
Title string
|
|
XinitrcPath string
|
|
ScriptPath string
|
|
DesktopNames string // for XDG_CURRENT_DESKTOP (colon-separated)
|
|
SessionDesktop string // for XDG_SESSION_DESKTOP
|
|
}
|
|
|
|
type SpawnedEnv struct {
|
|
Kind string
|
|
Server *exec.Cmd
|
|
Client *exec.Cmd
|
|
logFd *os.File
|
|
}
|
|
|
|
const logLimit = 67_108_864
|
|
|
|
type limitWriter struct {
|
|
w io.Writer
|
|
n int64
|
|
max int64
|
|
}
|
|
|
|
func (l *limitWriter) Write(p []byte) (int, error) {
|
|
if l.n >= l.max {
|
|
return len(p), nil
|
|
}
|
|
rem := l.max - l.n
|
|
if int64(len(p)) > rem {
|
|
p = p[:rem]
|
|
}
|
|
n, err := l.w.Write(p)
|
|
l.n += int64(n)
|
|
return n, err
|
|
}
|
|
|
|
func Spawn(env *PostLoginEnv, uid, gid uint32, groups []uint32, cfg *config.Config) (*SpawnedEnv, error) {
|
|
var loginFlag string
|
|
switch cfg.ShellLoginFlag {
|
|
case config.ShellLoginShort:
|
|
loginFlag = "-l"
|
|
case config.ShellLoginLong:
|
|
loginFlag = "--login"
|
|
}
|
|
|
|
switch env.Kind {
|
|
case "x11":
|
|
return spawnX11(env, uid, gid, groups, cfg, loginFlag)
|
|
case "wayland":
|
|
return spawnWayland(env, uid, gid, groups, cfg, loginFlag)
|
|
default:
|
|
return spawnTTY(env, uid, gid, groups, cfg)
|
|
}
|
|
}
|
|
|
|
func spawnX11(env *PostLoginEnv, uid, gid uint32, groups []uint32, cfg *config.Config, loginFlag string) (*SpawnedEnv, error) {
|
|
server, err := setupX(uid, gid, groups, cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("X setup: %w", err)
|
|
}
|
|
|
|
args := shellCmd(cfg.SystemShell, loginFlag, cfg.X11.SetupPath+" "+env.XinitrcPath)
|
|
client := privCmd(args, uid, gid, groups)
|
|
|
|
logF := openLog(cfg.ClientLogPath)
|
|
if logF != nil {
|
|
lw := &limitWriter{w: logF, max: logLimit}
|
|
client.Stdout = lw
|
|
client.Stderr = lw
|
|
}
|
|
|
|
if err := client.Start(); err != nil {
|
|
return nil, fmt.Errorf("X11 client: %w", err)
|
|
}
|
|
return &SpawnedEnv{Kind: "x11", Server: server, Client: client, logFd: logF}, nil
|
|
}
|
|
|
|
func spawnWayland(env *PostLoginEnv, uid, gid uint32, groups []uint32, cfg *config.Config, loginFlag string) (*SpawnedEnv, error) {
|
|
args := shellCmd(cfg.SystemShell, loginFlag, env.ScriptPath)
|
|
client := privCmd(args, uid, gid, groups)
|
|
|
|
logF := openLog(cfg.ClientLogPath)
|
|
if logF != nil {
|
|
lw := &limitWriter{w: logF, max: logLimit}
|
|
client.Stdout = lw
|
|
client.Stderr = lw
|
|
}
|
|
|
|
if err := client.Start(); err != nil {
|
|
return nil, fmt.Errorf("Wayland: %w", err)
|
|
}
|
|
return &SpawnedEnv{Kind: "wayland", Client: client, logFd: logF}, nil
|
|
}
|
|
|
|
func spawnTTY(env *PostLoginEnv, uid, gid uint32, groups []uint32, cfg *config.Config) (*SpawnedEnv, error) {
|
|
shell := userShell(int(uid))
|
|
_ = env
|
|
_ = cfg
|
|
|
|
client := privCmd([]string{shell}, uid, gid, groups)
|
|
client.Stdin = os.Stdin
|
|
client.Stdout = os.Stdout
|
|
client.Stderr = os.Stderr
|
|
|
|
if err := client.Start(); err != nil {
|
|
return nil, fmt.Errorf("TTY: %w", err)
|
|
}
|
|
return &SpawnedEnv{Kind: "tty", Client: client}, nil
|
|
}
|
|
|
|
func (s *SpawnedEnv) Wait() error {
|
|
defer func() {
|
|
if s.logFd != nil {
|
|
s.logFd.Close()
|
|
}
|
|
}()
|
|
if s.Kind == "x11" && s.Server != nil {
|
|
defer func() {
|
|
s.Server.Process.Signal(syscall.SIGTERM)
|
|
s.Server.Wait()
|
|
}()
|
|
}
|
|
if s.Client != nil {
|
|
return s.Client.Wait()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SpawnedEnv) PID() int {
|
|
if s.Client != nil && s.Client.Process != nil {
|
|
return s.Client.Process.Pid
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func shellCmd(shell, loginFlag, command string) []string {
|
|
a := []string{shell}
|
|
if loginFlag != "" {
|
|
a = append(a, loginFlag)
|
|
}
|
|
return append(a, "-c", command)
|
|
}
|
|
|
|
func privCmd(args []string, uid, gid uint32, groups []uint32) *exec.Cmd {
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
Credential: &syscall.Credential{
|
|
Uid: uid,
|
|
Gid: gid,
|
|
Groups: groups,
|
|
},
|
|
Setsid: true,
|
|
Setctty: false,
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func openLog(path string) *os.File {
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return f
|
|
}
|
|
|
|
func userShell(uid int) string {
|
|
data, err := os.ReadFile("/etc/passwd")
|
|
if err != nil {
|
|
return "/bin/sh"
|
|
}
|
|
uidStr := strconv.Itoa(uid)
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
parts := strings.Split(line, ":")
|
|
if len(parts) >= 7 && parts[2] == uidStr && parts[6] != "" {
|
|
sh := parts[6]
|
|
if sh != "/sbin/nologin" && sh != "/usr/sbin/nologin" && sh != "/bin/false" {
|
|
return sh
|
|
}
|
|
}
|
|
}
|
|
return "/bin/sh"
|
|
}
|