aboutsummaryrefslogtreecommitdiffhomepage
path: root/main.go
diff options
context:
space:
mode:
authorOphestra Umiker <cat@ophivana.moe>2024-09-04 01:20:12 +0900
committerOphestra Umiker <cat@ophivana.moe>2024-09-04 01:20:12 +0900
commitd8f76f3b2594db4687c0203c4f2be8d3e4ef7740 (patch)
treeb1d8bfb5250d71094e397f8afa87710de1f1ca2f /main.go
parent7e6eb82195c650bc34829e5cca2d2296e78c0707 (diff)
rename to fortify and restructure
More sandbox features will be added and this will no longer track ego's features and behaviour. Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
Diffstat (limited to 'main.go')
-rw-r--r--main.go342
1 files changed, 93 insertions, 249 deletions
diff --git a/main.go b/main.go
index 4a719d85..102252b4 100644
--- a/main.go
+++ b/main.go
@@ -6,15 +6,22 @@ import (
"fmt"
"io/fs"
"os"
- "os/exec"
- "os/user"
"path"
"strconv"
- "strings"
"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"
+var (
+ Version = "impure"
+ a *app.App
+)
func tryVersion() {
if printVersion {
@@ -23,25 +30,9 @@ func tryVersion() {
}
}
-var (
- ego *user.User
- uid int
- env []string
- command []string
- verbose bool
- runtime string
- runDir string
-)
-
const (
- term = "TERM"
- home = "HOME"
- sudoAskPass = "SUDO_ASKPASS"
- xdgRuntimeDir = "XDG_RUNTIME_DIR"
- xdgConfigHome = "XDG_CONFIG_HOME"
- display = "DISPLAY"
- pulseServer = "PULSE_SERVER"
- pulseCookie = "PULSE_COOKIE"
+ term = "TERM"
+ display = "DISPLAY"
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
waylandDisplay = "WAYLAND_DISPLAY"
@@ -49,315 +40,168 @@ const (
func main() {
flag.Parse()
- copyArgs()
- if u, err := strconv.Atoi(ego.Uid); err != nil {
- // usually unreachable
- panic("ego uid parse")
- } else {
- uid = u
- }
+ // launcher payload early exit
+ app.Early(printVersion)
- if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
- fatal("Env variable", xdgRuntimeDir, "unset")
- } else {
- runtime = r
- runDir = path.Join(runtime, "ego")
+ // 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
- tryState()
+ // 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)
+ }
- // Report warning if user home directory does not exist or has wrong ownership
- if stat, err := os.Stat(ego.HomeDir); err != nil {
- if verbose {
+ // 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", ego.Username, ego.HomeDir)
+ 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", ego.Username, ego.HomeDir)
+ 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", ego.Username, ego.HomeDir, err)
+ 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 != ego.Uid {
- fmt.Printf("User %s home directory %s has incorrect ownership (expected UID %s, found %s)", ego.Username, ego.HomeDir, ego.Uid, u)
+ 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)
}
}
- // Add execute perm to runtime dir, e.g. `/run/user/%d`
- if s, err := os.Stat(runtime); err != nil {
+ // 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) {
- fatal("Runtime directory does not exist")
+ state.Fatal("Runtime directory does not exist")
}
- fatal("Error accessing runtime directory:", err)
+ state.Fatal("Error accessing runtime directory:", err)
} else if !s.IsDir() {
- fatal(fmt.Sprintf("Path '%s' is not a directory", runtime))
+ state.Fatal(fmt.Sprintf("Path '%s' is not a directory", system.V.Runtime))
} else {
- if err = aclUpdatePerm(runtime, uid, aclExecute); err != nil {
- fatal("Error preparing runtime dir:", err)
+ if err = acl.UpdatePerm(system.V.Runtime, a.UID(), acl.Execute); err != nil {
+ state.Fatal("Error preparing runtime dir:", err)
} else {
- registerRevertPath(runtime)
+ state.RegisterRevertPath(system.V.Runtime)
}
- if verbose {
- fmt.Printf("Runtime data dir '%s' configured\n", runtime)
+ if system.V.Verbose {
+ fmt.Printf("Runtime data dir '%s' configured\n", system.V.Runtime)
}
}
- // Create runtime dir for Ego itself (e.g. `/run/user/%d/ego`) and make it readable for target
- if err := os.Mkdir(runDir, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
- fatal("Error creating Ego runtime dir:", err)
- }
- if err := aclUpdatePerm(runDir, uid, aclExecute); err != nil {
- fatal("Error preparing Ego runtime dir:", err)
- } else {
- registerRevertPath(runDir)
- }
-
- // Add rwx permissions to Wayland socket (e.g. `/run/user/%d/wayland-0`)
+ // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
if w, ok := os.LookupEnv(waylandDisplay); !ok {
- if verbose {
+ if system.V.Verbose {
fmt.Println("Wayland: WAYLAND_DISPLAY not set, skipping")
}
} else {
// add environment variable for new process
- env = append(env, waylandDisplay+"="+path.Join(runtime, w))
- wp := path.Join(runtime, w)
- if err := aclUpdatePerm(wp, uid, aclRead, aclWrite, aclExecute); err != nil {
- fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err)
+ 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 {
- registerRevertPath(wp)
+ state.RegisterRevertPath(wp)
}
- if verbose {
+ if system.V.Verbose {
fmt.Printf("Wayland socket '%s' configured\n", w)
}
}
- // Detect `DISPLAY` and grant permissions via X11 protocol `ChangeHosts` command
+ // discovery X11 and grant user permission via the `ChangeHosts` command
if d, ok := os.LookupEnv(display); !ok {
- if verbose {
+ if system.V.Verbose {
fmt.Println("X11: DISPLAY not set, skipping")
}
} else {
// add environment variable for new process
- env = append(env, display+"="+d)
+ a.AppendEnv(display, d)
- if verbose {
- fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", ego.Username, d)
+ if system.V.Verbose {
+ fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", a.Username, d)
}
- if err := changeHosts(xcbHostModeInsert, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil {
- fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err)
+ 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 {
- xcbActionComplete = true
+ state.XcbActionComplete()
}
}
- // Add execute permissions to PulseAudio directory (e.g. `/run/user/%d/pulse`)
- pulse := path.Join(runtime, "pulse")
+ // 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) {
- fatal("Error accessing PulseAudio directory:", err)
+ state.Fatal("Error accessing PulseAudio directory:", err)
}
if mustPulse {
- fatal("PulseAudio is unavailable")
+ state.Fatal("PulseAudio is unavailable")
}
- if verbose {
+ if system.V.Verbose {
fmt.Printf("PulseAudio dir '%s' not found, skipping\n", pulse)
}
} else {
// add environment variable for new process
- env = append(env, pulseServer+"=unix:"+pulseS)
- if err = aclUpdatePerm(pulse, uid, aclExecute); err != nil {
- fatal("Error preparing PulseAudio:", err)
+ a.AppendEnv(util.PulseServer, "unix:"+pulseS)
+ if err = acl.UpdatePerm(pulse, a.UID(), acl.Execute); err != nil {
+ state.Fatal("Error preparing PulseAudio:", err)
} else {
- registerRevertPath(pulse)
+ state.RegisterRevertPath(pulse)
}
- // Ensure permissions of PulseAudio socket `/run/user/%d/pulse/native`
+ // 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) {
- fatal("PulseAudio directory found but socket does not exist")
+ state.Fatal("PulseAudio directory found but socket does not exist")
}
- fatal("Error accessing PulseAudio socket:", err)
+ state.Fatal("Error accessing PulseAudio socket:", err)
} else {
if m := s.Mode(); m&0o006 != 0o006 {
- fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
+ state.Fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
}
}
// Publish current user's pulse-cookie for target user
- pulseCookieSource := discoverPulseCookie()
- env = append(env, pulseCookie+"="+pulseCookieSource)
- pulseCookieFinal := path.Join(runDir, "pulse-cookie")
- if verbose {
+ 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 = copyFile(pulseCookieFinal, pulseCookieSource); err != nil {
- fatal("Error copying PulseAudio cookie:", err)
+ if err = util.CopyFile(pulseCookieFinal, pulseCookieSource); err != nil {
+ state.Fatal("Error copying PulseAudio cookie:", err)
}
- if err = aclUpdatePerm(pulseCookieFinal, uid, aclRead); err != nil {
- fatal("Error publishing PulseAudio cookie:", err)
+ if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil {
+ state.Fatal("Error publishing PulseAudio cookie:", err)
} else {
- registerRevertPath(pulseCookieFinal)
+ state.RegisterRevertPath(pulseCookieFinal)
}
- if verbose {
+ if system.V.Verbose {
fmt.Printf("PulseAudio dir '%s' configured\n", pulse)
}
}
// pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok {
- env = append(env, term+"="+t)
- }
-
- f := launchBySudo
- m, b := false, false
- switch {
- case methodFlags[0]: // sudo
- case methodFlags[1]: // bare
- m, b = true, true
- default: // machinectl
- m, b = true, false
- }
-
- var toolPath string
-
- // dependency checks
- const sudoFallback = "Falling back to 'sudo', some desktop integration features may not work"
- if m {
- if !sdBooted() {
- fmt.Println("This system was not booted through systemd")
- fmt.Println(sudoFallback)
- } else if tp, ok := which("machinectl"); !ok {
- fmt.Println("Did not find 'machinectl' in PATH")
- fmt.Println(sudoFallback)
- } else {
- toolPath = tp
- f = func() []string { return launchByMachineCtl(b) }
- }
- } else if tp, ok := which("sudo"); !ok {
- fatal("Did not find 'sudo' in PATH")
- } else {
- toolPath = tp
- }
-
- if verbose {
- fmt.Printf("Selected launcher '%s' bare=%t\n", toolPath, b)
- }
-
- cmd := exec.Command(toolPath, f()...)
- cmd.Env = env
- cmd.Stdin = os.Stdin
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Dir = runDir
-
- if verbose {
- fmt.Println("Executing:", cmd)
- }
-
- if err := cmd.Start(); err != nil {
- fatal("Error starting process:", err)
- }
-
- if err := registerProcess(ego.Uid, cmd); err != nil {
- // process already started, shouldn't be fatal
- fmt.Println("Error registering process:", err)
- }
-
- var r int
- if err := cmd.Wait(); err != nil {
- var exitError *exec.ExitError
- if !errors.As(err, &exitError) {
- fatal("Error running process:", err)
- }
- }
-
- if verbose {
- fmt.Println("Process exited with exit code", r)
- }
- beforeExit()
- os.Exit(r)
-}
-
-func launchBySudo() (args []string) {
- args = make([]string, 0, 4+len(env)+len(command))
-
- // -Hiu $USER
- args = append(args, "-Hiu", ego.Username)
-
- // -A?
- if _, ok := os.LookupEnv(sudoAskPass); ok {
- if verbose {
- fmt.Printf("%s set, adding askpass flag\n", sudoAskPass)
- }
- args = append(args, "-A")
- }
-
- // environ
- args = append(args, env...)
-
- // -- $@
- args = append(args, "--")
- args = append(args, command...)
-
- return
-}
-
-func launchByMachineCtl(bare bool) (args []string) {
- args = make([]string, 0, 9+len(env))
-
- // shell --uid=$USER
- args = append(args, "shell", "--uid="+ego.Username)
-
- // --quiet
- if !verbose {
- args = append(args, "--quiet")
- }
-
- // environ
- envQ := make([]string, len(env)+1)
- for i, e := range env {
- envQ[i] = "-E" + e
- }
- envQ[len(env)] = "-E" + launcherPayloadEnv()
- args = append(args, envQ...)
-
- // -- .host
- args = append(args, "--", ".host")
-
- // /bin/sh -c
- if sh, ok := which("sh"); !ok {
- fatal("Did not find 'sh' in PATH")
- } else {
- args = append(args, sh, "-c")
- }
-
- if len(command) == 0 { // execute shell if command is not provided
- command = []string{"$SHELL"}
- }
-
- innerCommand := strings.Builder{}
-
- if !bare {
- innerCommand.WriteString("dbus-update-activation-environment --systemd")
- for _, e := range env {
- innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0])
- }
- innerCommand.WriteString("; systemctl --user start xdg-desktop-portal-gtk; ")
- }
-
- if executable, err := os.Executable(); err != nil {
- fatal("Error reading executable path:", err)
- } else {
- innerCommand.WriteString("exec " + executable + " -V")
+ a.AppendEnv(term, t)
}
- args = append(args, innerCommand.String())
- return
+ a.Run()
}