208 lines
6.0 KiB
Go
208 lines
6.0 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"syscall"
|
|
|
|
"git.ophivana.moe/cat/fortify/internal/acl"
|
|
"git.ophivana.moe/cat/fortify/internal/app"
|
|
"git.ophivana.moe/cat/fortify/internal/state"
|
|
"git.ophivana.moe/cat/fortify/internal/system"
|
|
"git.ophivana.moe/cat/fortify/internal/util"
|
|
"git.ophivana.moe/cat/fortify/internal/xcb"
|
|
)
|
|
|
|
var (
|
|
Version = "impure"
|
|
a *app.App
|
|
)
|
|
|
|
func tryVersion() {
|
|
if printVersion {
|
|
fmt.Println(Version)
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
const (
|
|
term = "TERM"
|
|
display = "DISPLAY"
|
|
|
|
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
|
|
waylandDisplay = "WAYLAND_DISPLAY"
|
|
)
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
// launcher payload early exit
|
|
app.Early(printVersion)
|
|
|
|
// version/license command early exit
|
|
tryVersion()
|
|
tryLicense()
|
|
|
|
system.Retrieve(flagVerbose)
|
|
a = app.New(userName, flag.Args())
|
|
state.Set(*a.User, a.Command(), a.UID())
|
|
|
|
// ensure RunDir (e.g. `/run/user/%d/fortify`)
|
|
if err := os.Mkdir(system.V.RunDir, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
|
state.Fatal("Error creating runtime directory:", err)
|
|
}
|
|
|
|
// state query command early exit
|
|
state.Early()
|
|
|
|
// ensure Share (e.g. `/tmp/fortify.%d`)
|
|
// acl is unnecessary as this directory is world executable
|
|
if err := os.Mkdir(system.V.Share, 0701); err != nil && !errors.Is(err, fs.ErrExist) {
|
|
state.Fatal("Error creating shared directory:", err)
|
|
}
|
|
|
|
// warn about target user home directory ownership
|
|
if stat, err := os.Stat(a.HomeDir); err != nil {
|
|
if system.V.Verbose {
|
|
switch {
|
|
case errors.Is(err, fs.ErrPermission):
|
|
fmt.Printf("User %s home directory %s is not accessible", a.Username, a.HomeDir)
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
fmt.Printf("User %s home directory %s does not exist", a.Username, a.HomeDir)
|
|
default:
|
|
fmt.Printf("Error stat user %s home directory %s: %s", a.Username, a.HomeDir, err)
|
|
}
|
|
}
|
|
return
|
|
} else {
|
|
// FreeBSD: not cross-platform
|
|
if u := strconv.Itoa(int(stat.Sys().(*syscall.Stat_t).Uid)); u != a.Uid {
|
|
fmt.Printf("User %s home directory %s has incorrect ownership (expected UID %s, found %s)", a.Username, a.HomeDir, a.Uid, u)
|
|
}
|
|
}
|
|
|
|
// ensure runtime directory ACL (e.g. `/run/user/%d`)
|
|
if s, err := os.Stat(system.V.Runtime); err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
state.Fatal("Runtime directory does not exist")
|
|
}
|
|
state.Fatal("Error accessing runtime directory:", err)
|
|
} else if !s.IsDir() {
|
|
state.Fatal(fmt.Sprintf("Path '%s' is not a directory", system.V.Runtime))
|
|
} else {
|
|
if err = acl.UpdatePerm(system.V.Runtime, a.UID(), acl.Execute); err != nil {
|
|
state.Fatal("Error preparing runtime dir:", err)
|
|
} else {
|
|
state.RegisterRevertPath(system.V.Runtime)
|
|
}
|
|
if system.V.Verbose {
|
|
fmt.Printf("Runtime data dir '%s' configured\n", system.V.Runtime)
|
|
}
|
|
}
|
|
|
|
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
|
|
if w, ok := os.LookupEnv(waylandDisplay); !ok {
|
|
if system.V.Verbose {
|
|
fmt.Println("Wayland: WAYLAND_DISPLAY not set, skipping")
|
|
}
|
|
} else {
|
|
// add environment variable for new process
|
|
wp := path.Join(system.V.Runtime, w)
|
|
a.AppendEnv(waylandDisplay, wp)
|
|
if err := acl.UpdatePerm(wp, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil {
|
|
state.Fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err)
|
|
} else {
|
|
state.RegisterRevertPath(wp)
|
|
}
|
|
if system.V.Verbose {
|
|
fmt.Printf("Wayland socket '%s' configured\n", w)
|
|
}
|
|
}
|
|
|
|
// discovery X11 and grant user permission via the `ChangeHosts` command
|
|
if d, ok := os.LookupEnv(display); !ok {
|
|
if system.V.Verbose {
|
|
fmt.Println("X11: DISPLAY not set, skipping")
|
|
}
|
|
} else {
|
|
// add environment variable for new process
|
|
a.AppendEnv(display, d)
|
|
|
|
if system.V.Verbose {
|
|
fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", a.Username, d)
|
|
}
|
|
if err := xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+a.Username); err != nil {
|
|
state.Fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err)
|
|
} else {
|
|
state.XcbActionComplete()
|
|
}
|
|
}
|
|
|
|
// ensure PulseAudio directory ACL (e.g. `/run/user/%d/pulse`)
|
|
pulse := path.Join(system.V.Runtime, "pulse")
|
|
pulseS := path.Join(pulse, "native")
|
|
if s, err := os.Stat(pulse); err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
state.Fatal("Error accessing PulseAudio directory:", err)
|
|
}
|
|
if mustPulse {
|
|
state.Fatal("PulseAudio is unavailable")
|
|
}
|
|
if system.V.Verbose {
|
|
fmt.Printf("PulseAudio dir '%s' not found, skipping\n", pulse)
|
|
}
|
|
} else {
|
|
// add environment variable for new process
|
|
a.AppendEnv(util.PulseServer, "unix:"+pulseS)
|
|
if err = acl.UpdatePerm(pulse, a.UID(), acl.Execute); err != nil {
|
|
state.Fatal("Error preparing PulseAudio:", err)
|
|
} else {
|
|
state.RegisterRevertPath(pulse)
|
|
}
|
|
|
|
// ensure PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
|
|
if s, err = os.Stat(pulseS); err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
state.Fatal("PulseAudio directory found but socket does not exist")
|
|
}
|
|
state.Fatal("Error accessing PulseAudio socket:", err)
|
|
} else {
|
|
if m := s.Mode(); m&0o006 != 0o006 {
|
|
state.Fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
|
|
}
|
|
}
|
|
|
|
// Publish current user's pulse-cookie for target user
|
|
pulseCookieSource := util.DiscoverPulseCookie()
|
|
pulseCookieFinal := path.Join(system.V.Share, "pulse-cookie")
|
|
a.AppendEnv(util.PulseCookie, pulseCookieFinal)
|
|
if system.V.Verbose {
|
|
fmt.Printf("Publishing PulseAudio cookie '%s' to '%s'\n", pulseCookieSource, pulseCookieFinal)
|
|
}
|
|
if err = util.CopyFile(pulseCookieFinal, pulseCookieSource); err != nil {
|
|
state.Fatal("Error copying PulseAudio cookie:", err)
|
|
}
|
|
if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil {
|
|
state.Fatal("Error publishing PulseAudio cookie:", err)
|
|
} else {
|
|
state.RegisterRevertPath(pulseCookieFinal)
|
|
}
|
|
|
|
if system.V.Verbose {
|
|
fmt.Printf("PulseAudio dir '%s' configured\n", pulse)
|
|
}
|
|
}
|
|
|
|
// pass $TERM to launcher
|
|
if t, ok := os.LookupEnv(term); ok {
|
|
a.AppendEnv(term, t)
|
|
}
|
|
|
|
a.Run()
|
|
}
|