From d8f76f3b2594db4687c0203c4f2be8d3e4ef7740 Mon Sep 17 00:00:00 2001 From: Ophestra Umiker Date: Wed, 4 Sep 2024 01:20:12 +0900 Subject: [PATCH] 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 --- .gitignore | 2 +- README.md | 83 ------- cli.go | 41 +--- flake.nix | 6 +- go.mod | 2 +- acl.go => internal/acl/c.go | 79 +------ internal/acl/export.go | 86 +++++++ internal/app/builder.go | 13 ++ internal/app/launch.go | 152 +++++++++++++ internal/app/setup.go | 124 ++++++++++ internal/state/exit.go | 68 ++++++ internal/state/register.go | 12 + state.go => internal/state/track.go | 65 +----- internal/state/value.go | 21 ++ internal/system/retrieve.go | 28 +++ internal/system/value.go | 17 ++ internal/util/simple.go | 39 ++++ internal/util/std.go | 75 ++++++ internal/xcb/c.go | 33 +++ internal/xcb/export.go | 47 ++++ launcher.go | 72 ------ main.go | 342 ++++++++-------------------- util.go | 100 -------- x11.go | 72 ------ 24 files changed, 830 insertions(+), 749 deletions(-) delete mode 100644 README.md rename acl.go => internal/acl/c.go (50%) create mode 100644 internal/acl/export.go create mode 100644 internal/app/builder.go create mode 100644 internal/app/launch.go create mode 100644 internal/app/setup.go create mode 100644 internal/state/exit.go create mode 100644 internal/state/register.go rename state.go => internal/state/track.go (55%) create mode 100644 internal/state/value.go create mode 100644 internal/system/retrieve.go create mode 100644 internal/system/value.go create mode 100644 internal/util/simple.go create mode 100644 internal/util/std.go create mode 100644 internal/xcb/c.go create mode 100644 internal/xcb/export.go delete mode 100644 launcher.go delete mode 100644 util.go delete mode 100644 x11.go diff --git a/.gitignore b/.gitignore index ae08513..4b84d41 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ *.dll *.so *.dylib -/ego +/fortify # Test binary, built with `go test -c` *.test diff --git a/README.md b/README.md deleted file mode 100644 index 055e48b..0000000 --- a/README.md +++ /dev/null @@ -1,83 +0,0 @@ -ego (the Go side) -================= - -[![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/cat/ego.svg)](https://pkg.go.dev/git.ophivana.moe/cat/ego) - -> Do all your games need access to your documents, browser history, SSH private keys? -> -> ... No? Just run `ego steam`! - -**Ego** is a tool to run Linux desktop applications under a different local user. Currently -integrates with Wayland, Xorg, PulseAudio and xdg-desktop-portal. You may think of it as `xhost` -for Wayland and PulseAudio. This is done using filesystem ACLs and X11 host access control. - -Disclaimer: **DO NOT RUN UNTRUSTED PROGRAMS VIA EGO.** However, using ego is more secure than -running applications directly under your primary user. - -Differences ------------ -* Written in Go -* Tracks process states -* Cleans up after last process exits -* Argv preservation in machinectl mode -* Has no dependencies other than the two C libraries - -Manual setup ------------- -Ego aims to come with sane defaults and be easy to set up. - -**Requirements:** -* Sudo -* A C compiler -* [Go](https://go.dev/doc/install) -* `libacl.so` library (Debian/Ubuntu: libacl1-dev; Fedora: libacl-devel; Arch: acl) -* `libxcb.so` library (Debian/Ubuntu: libxcb1-dev; Fedora: libxcb-devel; Arch: libxcb) - -**Recommended:** (Not needed when using `--sudo` mode, but some desktop functionality may not work). -* `machinectl` command (Debian/Ubuntu/Fedora: systemd-container; Arch: systemd) -* `xdg-desktop-portal-gtk` (Debian/Ubuntu/Fedora/Arch: xdg-desktop-portal-gtk) - -**Installation:** - -1. Run in repository worktree: - - go build -v -ldflags '-s -w' - sudo cp ego /usr/local/bin/ - -2. Create local user named "ego": [1] - - sudo useradd ego --uid 155 --create-home - -3. That's all, try it: - - ego xdg-open . - -[1] No extra groups are needed by the ego user. -UID below 1000 hides this user on the login screen. - -### Avoid password prompt -If using "machinectl" mode (default if available), you need the rather new systemd version >=247 -and polkit >=0.106 to do this securely. - -Create file `/etc/polkit-1/rules.d/50-ego-machinectl.rules`, polkit will automatically load it -(replace `$USER` with your own username): - -```js -polkit.addRule(function(action, subject) { - if (action.id == "org.freedesktop.machine1.host-shell" && - action.lookup("user") == "ego" && - subject.user == "$USER") { - return polkit.Result.YES; - } -}); -``` - -##### sudo mode -For sudo, add the following to `/etc/sudoers` (replace `$USER` with your own username): - - $USER ALL=(ego) NOPASSWD:ALL - -Appendix --------- -Ego is licensed under the MIT License (see the `LICENSE` file). -The original Ego was created by Marti Raudsepp under the repository https://github.com/intgr/ego \ No newline at end of file diff --git a/cli.go b/cli.go index ca1b41b..e5466f0 100644 --- a/cli.go +++ b/cli.go @@ -1,50 +1,23 @@ package main import ( - "errors" "flag" - "fmt" - "os" - "os/user" + + "git.ophivana.moe/cat/fortify/internal/system" ) var ( userName string - methodFlags [2]bool printVersion bool mustPulse bool + flagVerbose bool ) func init() { - flag.StringVar(&userName, "u", "ego", "Specify a username") - flag.BoolVar(&methodFlags[0], "sudo", false, "Use 'sudo' to change user") - flag.BoolVar(&methodFlags[1], "bare", false, "Use 'machinectl' but skip xdg-desktop-portal setup") + flag.StringVar(&userName, "u", "chronos", "Specify a username") + flag.BoolVar(&system.MethodFlags[0], "sudo", false, "Use 'sudo' to change user") + flag.BoolVar(&system.MethodFlags[1], "bare", false, "Use 'machinectl' but skip xdg-desktop-portal setup") flag.BoolVar(&mustPulse, "pulse", false, "Treat unavailable PulseAudio as fatal") - flag.BoolVar(&verbose, "v", false, "Verbose output") + flag.BoolVar(&flagVerbose, "v", false, "Verbose output") flag.BoolVar(&printVersion, "V", false, "Print version") } - -func copyArgs() { - tryLauncher() - tryVersion() - tryLicense() - - command = flag.Args() - - if u, err := user.Lookup(userName); err != nil { - if errors.As(err, new(user.UnknownUserError)) { - fmt.Println("unknown user", userName) - } else { - // unreachable - panic(err) - } - - os.Exit(1) - } else { - ego = u - } - - if verbose { - fmt.Println("Running as user", ego.Username, "("+ego.Uid+"),", "command:", command) - } -} diff --git a/flake.nix b/flake.nix index 8c8e419..1e31f3b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "ego development environment"; + description = "fortify development environment"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/24.05"; @@ -30,11 +30,11 @@ mkShell { packages = [ (buildGoModule rec { - pname = "ego"; + pname = "fortify"; version = "0.0.0-flake"; src = ./.; - vendorHash = null; # we have no dependencies :3 + vendorHash = null; # we have no Go dependencies :3 ldflags = [ "-s" diff --git a/go.mod b/go.mod index c3d7971..e5903ee 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module git.ophivana.moe/cat/ego +module git.ophivana.moe/cat/fortify go 1.22 diff --git a/acl.go b/internal/acl/c.go similarity index 50% rename from acl.go rename to internal/acl/c.go index 34e51a4..89bc046 100644 --- a/acl.go +++ b/internal/acl/c.go @@ -1,4 +1,4 @@ -package main +package acl import ( "errors" @@ -13,88 +13,11 @@ import ( //#cgo linux LDFLAGS: -lacl import "C" -const ( - aclRead = C.ACL_READ - aclWrite = C.ACL_WRITE - aclExecute = C.ACL_EXECUTE - - aclTypeDefault = C.ACL_TYPE_DEFAULT - aclTypeAccess = C.ACL_TYPE_ACCESS - - aclUndefinedTag = C.ACL_UNDEFINED_TAG - aclUserObj = C.ACL_USER_OBJ - aclUser = C.ACL_USER - aclGroupObj = C.ACL_GROUP_OBJ - aclGroup = C.ACL_GROUP - aclMask = C.ACL_MASK - aclOther = C.ACL_OTHER -) - type acl struct { val C.acl_t freed bool } -func aclUpdatePerm(path string, uid int, perms ...C.acl_perm_t) error { - // read acl from file - a, err := aclGetFile(path, aclTypeAccess) - if err != nil { - return err - } - // free acl on return if get is successful - defer a.free() - - // remove existing entry - if err = a.removeEntry(aclUser, uid); err != nil { - return err - } - - // create new entry if perms are passed - if len(perms) > 0 { - // create new acl entry - var e C.acl_entry_t - if _, err = C.acl_create_entry(&a.val, &e); err != nil { - return err - } - - // get perm set of new entry - var p C.acl_permset_t - if _, err = C.acl_get_permset(e, &p); err != nil { - return err - } - - // add target perms - for _, perm := range perms { - if _, err = C.acl_add_perm(p, perm); err != nil { - return err - } - } - - // set perm set to new entry - if _, err = C.acl_set_permset(e, p); err != nil { - return err - } - - // set user tag to new entry - if _, err = C.acl_set_tag_type(e, aclUser); err != nil { - return err - } - - // set qualifier (uid) to new entry - if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil { - return err - } - } - - // calculate mask after update - if _, err = C.acl_calc_mask(&a.val); err != nil { - return err - } - - // write acl to file - return a.setFile(path, aclTypeAccess) -} - func aclGetFile(path string, t C.acl_type_t) (*acl, error) { p := C.CString(path) a, err := C.acl_get_file(p, t) diff --git a/internal/acl/export.go b/internal/acl/export.go new file mode 100644 index 0000000..aeb8ed9 --- /dev/null +++ b/internal/acl/export.go @@ -0,0 +1,86 @@ +package acl + +import "unsafe" + +//#include +//#include +//#include +//#cgo linux LDFLAGS: -lacl +import "C" + +const ( + Read = C.ACL_READ + Write = C.ACL_WRITE + Execute = C.ACL_EXECUTE + + TypeDefault = C.ACL_TYPE_DEFAULT + TypeAccess = C.ACL_TYPE_ACCESS + + UndefinedTag = C.ACL_UNDEFINED_TAG + UserObj = C.ACL_USER_OBJ + User = C.ACL_USER + GroupObj = C.ACL_GROUP_OBJ + Group = C.ACL_GROUP + Mask = C.ACL_MASK + Other = C.ACL_OTHER +) + +func UpdatePerm(path string, uid int, perms ...C.acl_perm_t) error { + // read acl from file + a, err := aclGetFile(path, TypeAccess) + if err != nil { + return err + } + // free acl on return if get is successful + defer a.free() + + // remove existing entry + if err = a.removeEntry(User, uid); err != nil { + return err + } + + // create new entry if perms are passed + if len(perms) > 0 { + // create new acl entry + var e C.acl_entry_t + if _, err = C.acl_create_entry(&a.val, &e); err != nil { + return err + } + + // get perm set of new entry + var p C.acl_permset_t + if _, err = C.acl_get_permset(e, &p); err != nil { + return err + } + + // add target perms + for _, perm := range perms { + if _, err = C.acl_add_perm(p, perm); err != nil { + return err + } + } + + // set perm set to new entry + if _, err = C.acl_set_permset(e, p); err != nil { + return err + } + + // set user tag to new entry + if _, err = C.acl_set_tag_type(e, User); err != nil { + return err + } + + // set qualifier (uid) to new entry + if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil { + return err + } + } + + // calculate mask after update + if _, err = C.acl_calc_mask(&a.val); err != nil { + return err + } + + // write acl to file + return a.setFile(path, TypeAccess) +} diff --git a/internal/app/builder.go b/internal/app/builder.go new file mode 100644 index 0000000..2bce43b --- /dev/null +++ b/internal/app/builder.go @@ -0,0 +1,13 @@ +package app + +func (a *App) Command() []string { + return a.command +} + +func (a *App) UID() int { + return a.uid +} + +func (a *App) AppendEnv(k, v string) { + a.env = append(a.env, k+"="+v) +} diff --git a/internal/app/launch.go b/internal/app/launch.go new file mode 100644 index 0000000..c6f7b77 --- /dev/null +++ b/internal/app/launch.go @@ -0,0 +1,152 @@ +package app + +import ( + "bytes" + "encoding/base64" + "encoding/gob" + "fmt" + "os" + "strings" + "syscall" + + "git.ophivana.moe/cat/fortify/internal/state" + "git.ophivana.moe/cat/fortify/internal/system" + "git.ophivana.moe/cat/fortify/internal/util" +) + +const ( + sudoAskPass = "SUDO_ASKPASS" + launcherPayload = "FORTIFY_LAUNCHER_PAYLOAD" +) + +func (a *App) launcherPayloadEnv() string { + r := &bytes.Buffer{} + enc := base64.NewEncoder(base64.StdEncoding, r) + + if err := gob.NewEncoder(enc).Encode(a.command); err != nil { + state.Fatal("Error encoding launcher payload:", err) + } + + _ = enc.Close() + return launcherPayload + "=" + r.String() +} + +// Early hidden launcher path +func Early(printVersion bool) { + if printVersion { + if r, ok := os.LookupEnv(launcherPayload); ok { + dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(r)) + + var argv []string + if err := gob.NewDecoder(dec).Decode(&argv); err != nil { + fmt.Println("Error decoding launcher payload:", err) + os.Exit(1) + } + + if err := os.Unsetenv(launcherPayload); err != nil { + fmt.Println("Error unsetting launcher payload:", err) + // not fatal, do not fail + } + + var p string + + if len(argv) > 0 { + if p, ok = util.Which(argv[0]); !ok { + fmt.Printf("Did not find '%s' in PATH\n", argv[0]) + os.Exit(1) + } + } else { + if p, ok = os.LookupEnv("SHELL"); !ok { + fmt.Println("No command was specified and $SHELL was unset") + os.Exit(1) + } + } + + if err := syscall.Exec(p, argv, os.Environ()); err != nil { + fmt.Println("Error executing launcher payload:", err) + os.Exit(1) + } + + // unreachable + os.Exit(1) + return + } + } +} + +func (a *App) launchBySudo() (args []string) { + args = make([]string, 0, 4+len(a.env)+len(a.command)) + + // -Hiu $USER + args = append(args, "-Hiu", a.Username) + + // -A? + if _, ok := os.LookupEnv(sudoAskPass); ok { + if system.V.Verbose { + fmt.Printf("%s set, adding askpass flag\n", sudoAskPass) + } + args = append(args, "-A") + } + + // environ + args = append(args, a.env...) + + // -- $@ + args = append(args, "--") + args = append(args, a.command...) + + return +} + +func (a *App) launchByMachineCtl(bare bool) (args []string) { + args = make([]string, 0, 9+len(a.env)) + + // shell --uid=$USER + args = append(args, "shell", "--uid="+a.Username) + + // --quiet + if !system.V.Verbose { + args = append(args, "--quiet") + } + + // environ + envQ := make([]string, len(a.env)+1) + for i, e := range a.env { + envQ[i] = "-E" + e + } + envQ[len(a.env)] = "-E" + a.launcherPayloadEnv() + args = append(args, envQ...) + + // -- .host + args = append(args, "--", ".host") + + // /bin/sh -c + if sh, ok := util.Which("sh"); !ok { + state.Fatal("Did not find 'sh' in PATH") + } else { + args = append(args, sh, "-c") + } + + if len(a.command) == 0 { // execute shell if command is not provided + a.command = []string{"$SHELL"} + } + + innerCommand := strings.Builder{} + + if !bare { + innerCommand.WriteString("dbus-update-activation-environment --systemd") + for _, e := range a.env { + innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0]) + } + innerCommand.WriteString("; systemctl --user start xdg-desktop-portal-gtk; ") + } + + if executable, err := os.Executable(); err != nil { + state.Fatal("Error reading executable path:", err) + } else { + innerCommand.WriteString("exec " + executable + " -V") + } + args = append(args, innerCommand.String()) + + return +} diff --git a/internal/app/setup.go b/internal/app/setup.go new file mode 100644 index 0000000..7b5cd2e --- /dev/null +++ b/internal/app/setup.go @@ -0,0 +1,124 @@ +package app + +import ( + "errors" + "fmt" + "os" + "os/exec" + "os/user" + "strconv" + + "git.ophivana.moe/cat/fortify/internal/state" + "git.ophivana.moe/cat/fortify/internal/system" + "git.ophivana.moe/cat/fortify/internal/util" +) + +type App struct { + uid int + env []string + command []string + + *user.User +} + +func (a *App) Run() { + f := a.launchBySudo + m, b := false, false + switch { + case system.MethodFlags[0]: // sudo + case system.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 !util.SdBooted() { + fmt.Println("This system was not booted through systemd") + fmt.Println(sudoFallback) + } else if tp, ok := util.Which("machinectl"); !ok { + fmt.Println("Did not find 'machinectl' in PATH") + fmt.Println(sudoFallback) + } else { + toolPath = tp + f = func() []string { return a.launchByMachineCtl(b) } + } + } else if tp, ok := util.Which("sudo"); !ok { + state.Fatal("Did not find 'sudo' in PATH") + } else { + toolPath = tp + } + + if system.V.Verbose { + fmt.Printf("Selected launcher '%s' bare=%t\n", toolPath, b) + } + + cmd := exec.Command(toolPath, f()...) + cmd.Env = a.env + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Dir = system.V.RunDir + + if system.V.Verbose { + fmt.Println("Executing:", cmd) + } + + if err := cmd.Start(); err != nil { + state.Fatal("Error starting process:", err) + } + + if err := state.SaveProcess(a.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) { + state.Fatal("Error running process:", err) + } + } + + if system.V.Verbose { + fmt.Println("Process exited with exit code", r) + } + state.BeforeExit() + os.Exit(r) +} + +func New(userName string, args []string) *App { + a := &App{command: args} + + if u, err := user.Lookup(userName); err != nil { + if errors.As(err, new(user.UnknownUserError)) { + fmt.Println("unknown user", userName) + } else { + // unreachable + panic(err) + } + + // too early for fatal + os.Exit(1) + } else { + a.User = u + } + + if u, err := strconv.Atoi(a.Uid); err != nil { + // usually unreachable + panic("uid parse") + } else { + a.uid = u + } + + if system.V.Verbose { + fmt.Println("Running as user", a.Username, "("+a.Uid+"),", "command:", a.command) + } + + return a +} diff --git a/internal/state/exit.go b/internal/state/exit.go new file mode 100644 index 0000000..e0e2c04 --- /dev/null +++ b/internal/state/exit.go @@ -0,0 +1,68 @@ +package state + +import ( + "errors" + "fmt" + "io/fs" + "os" + + "git.ophivana.moe/cat/fortify/internal/acl" + "git.ophivana.moe/cat/fortify/internal/system" + "git.ophivana.moe/cat/fortify/internal/xcb" +) + +func Fatal(msg ...any) { + fmt.Println(msg...) + BeforeExit() + os.Exit(1) +} + +func BeforeExit() { + if u == nil { + fmt.Println("warn: beforeExit called before app init") + return + } + + if statePath == "" { + if system.V.Verbose { + fmt.Println("State path is unset") + } + } else { + if err := os.Remove(statePath); err != nil && !errors.Is(err, fs.ErrNotExist) { + fmt.Println("Error removing state file:", err) + } + } + + if d, err := readLaunchers(); err != nil { + fmt.Println("Error reading active launchers:", err) + os.Exit(1) + } else if len(d) > 0 { + // other launchers are still active + if system.V.Verbose { + fmt.Printf("Found %d active launchers, exiting without cleaning up\n", len(d)) + } + return + } + + if system.V.Verbose { + fmt.Println("No other launchers active, will clean up") + } + + if xcbActionComplete { + if system.V.Verbose { + fmt.Printf("X11: Removing XHost entry SI:localuser:%s\n", u.Username) + } + if err := xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+u.Username); err != nil { + fmt.Println("Error removing XHost entry:", err) + } + } + + for _, candidate := range cleanupCandidate { + if err := acl.UpdatePerm(candidate, uid); err != nil { + fmt.Printf("Error stripping ACL entry from '%s': %s\n", candidate, err) + } + if system.V.Verbose { + fmt.Printf("Stripped ACL entry for user '%s' from '%s'\n", u.Username, candidate) + } + } +} diff --git a/internal/state/register.go b/internal/state/register.go new file mode 100644 index 0000000..2fff565 --- /dev/null +++ b/internal/state/register.go @@ -0,0 +1,12 @@ +package state + +func RegisterRevertPath(p string) { + cleanupCandidate = append(cleanupCandidate, p) +} + +func XcbActionComplete() { + if xcbActionComplete { + Fatal("xcb inserted twice") + } + xcbActionComplete = true +} diff --git a/state.go b/internal/state/track.go similarity index 55% rename from state.go rename to internal/state/track.go index 2204708..913d684 100644 --- a/state.go +++ b/internal/state/track.go @@ -1,4 +1,4 @@ -package main +package state import ( "encoding/gob" @@ -10,6 +10,8 @@ import ( "os/exec" "path" "strconv" + + "git.ophivana.moe/cat/fortify/internal/system" ) // we unfortunately have to assume there are never races between processes @@ -33,7 +35,7 @@ func init() { flag.BoolVar(&stateActionEarly, "state", false, "query state value of current active launchers") } -func tryState() { +func Early() { if !stateActionEarly { return } @@ -52,13 +54,9 @@ func tryState() { os.Exit(0) } -func registerRevertPath(p string) { - cleanupCandidate = append(cleanupCandidate, p) -} - -// called after process start, before wait -func registerProcess(uid string, cmd *exec.Cmd) error { - statePath = path.Join(runDir, uid, strconv.Itoa(cmd.Process.Pid)) +// SaveProcess called after process start, before wait +func SaveProcess(uid string, cmd *exec.Cmd) error { + statePath = path.Join(system.V.RunDir, uid, strconv.Itoa(cmd.Process.Pid)) state := launcherState{ PID: cmd.Process.Pid, Launcher: cmd.Path, @@ -66,7 +64,7 @@ func registerProcess(uid string, cmd *exec.Cmd) error { Command: command, } - if err := os.Mkdir(path.Join(runDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) { + if err := os.Mkdir(path.Join(system.V.RunDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) { return err } @@ -86,7 +84,7 @@ func registerProcess(uid string, cmd *exec.Cmd) error { func readLaunchers() ([]*launcherState, error) { var f *os.File var r []*launcherState - launcherPrefix := path.Join(runDir, ego.Uid) + launcherPrefix := path.Join(system.V.RunDir, u.Uid) if pl, err := os.ReadDir(launcherPrefix); err != nil { return nil, err @@ -115,48 +113,3 @@ func readLaunchers() ([]*launcherState, error) { return r, nil } - -func beforeExit() { - if err := os.Remove(statePath); err != nil && !errors.Is(err, fs.ErrNotExist) { - fmt.Println("Error removing state file:", err) - } - - if a, err := readLaunchers(); err != nil { - fmt.Println("Error reading active launchers:", err) - os.Exit(1) - } else if len(a) > 0 { - // other launchers are still active - if verbose { - fmt.Printf("Found %d active launchers, exiting without cleaning up\n", len(a)) - } - return - } - - if verbose { - fmt.Println("No other launchers active, will clean up") - } - - if xcbActionComplete { - if verbose { - fmt.Printf("X11: Removing XHost entry SI:localuser:%s\n", ego.Username) - } - if err := changeHosts(xcbHostModeDelete, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil { - fmt.Println("Error removing XHost entry:", err) - } - } - - for _, candidate := range cleanupCandidate { - if err := aclUpdatePerm(candidate, uid); err != nil { - fmt.Printf("Error stripping ACL entry from '%s': %s\n", candidate, err) - } - if verbose { - fmt.Printf("Stripped ACL entry for user '%s' from '%s'\n", ego.Username, candidate) - } - } -} - -func fatal(msg ...any) { - fmt.Println(msg...) - beforeExit() - os.Exit(1) -} diff --git a/internal/state/value.go b/internal/state/value.go new file mode 100644 index 0000000..cb20818 --- /dev/null +++ b/internal/state/value.go @@ -0,0 +1,21 @@ +package state + +import ( + "os/user" +) + +var ( + u *user.User + uid int + command []string +) + +func Set(val user.User, c []string, d int) { + if u != nil { + panic("state set twice") + } + + u = &val + command = c + uid = d +} diff --git a/internal/system/retrieve.go b/internal/system/retrieve.go new file mode 100644 index 0000000..d9f0e3d --- /dev/null +++ b/internal/system/retrieve.go @@ -0,0 +1,28 @@ +package system + +import ( + "fmt" + "os" + "path" + "strconv" +) + +func Retrieve(verbose bool) { + if V != nil { + panic("system info retrieved twice") + } + + v := &Values{Share: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())), Verbose: verbose} + + if r, ok := os.LookupEnv(xdgRuntimeDir); !ok { + fmt.Println("Env variable", xdgRuntimeDir, "unset") + + // too early for fatal + os.Exit(1) + } else { + v.Runtime = r + v.RunDir = path.Join(v.Runtime, "fortify") + } + + V = v +} diff --git a/internal/system/value.go b/internal/system/value.go new file mode 100644 index 0000000..aa4da51 --- /dev/null +++ b/internal/system/value.go @@ -0,0 +1,17 @@ +package system + +const ( + xdgRuntimeDir = "XDG_RUNTIME_DIR" +) + +type Values struct { + Share string + Runtime string + RunDir string + Verbose bool +} + +var ( + V *Values + MethodFlags [2]bool +) diff --git a/internal/util/simple.go b/internal/util/simple.go new file mode 100644 index 0000000..08ce23f --- /dev/null +++ b/internal/util/simple.go @@ -0,0 +1,39 @@ +package util + +import ( + "io" + "os" + "os/exec" +) + +func Which(file string) (string, bool) { + p, err := exec.LookPath(file) + return p, err == nil +} + +func CopyFile(dst, src string) error { + srcD, err := os.Open(src) + if err != nil { + return err + } + defer func() { + if srcD.Close() != nil { + // unreachable + panic("src file closed prematurely") + } + }() + + dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer func() { + if dstD.Close() != nil { + // unreachable + panic("dst file closed prematurely") + } + }() + + _, err = io.Copy(dstD, srcD) + return err +} diff --git a/internal/util/std.go b/internal/util/std.go new file mode 100644 index 0000000..5c37a0d --- /dev/null +++ b/internal/util/std.go @@ -0,0 +1,75 @@ +package util + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path" + + "git.ophivana.moe/cat/fortify/internal/state" + "git.ophivana.moe/cat/fortify/internal/system" +) + +const ( + systemdCheckPath = "/run/systemd/system" + + home = "HOME" + xdgConfigHome = "XDG_CONFIG_HOME" + + PulseServer = "PULSE_SERVER" + PulseCookie = "PULSE_COOKIE" +) + +// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html +func SdBooted() bool { + _, err := os.Stat(systemdCheckPath) + if err != nil { + if system.V.Verbose { + if errors.Is(err, fs.ErrNotExist) { + fmt.Println("System not booted through systemd") + } else { + fmt.Println("Error accessing", systemdCheckPath+":", err.Error()) + } + } + return false + } + return true +} + +// DiscoverPulseCookie try various standard methods to discover the current user's PulseAudio authentication cookie +func DiscoverPulseCookie() string { + if p, ok := os.LookupEnv(PulseCookie); ok { + return p + } + + if p, ok := os.LookupEnv(home); ok { + p = path.Join(p, ".pulse-cookie") + if s, err := os.Stat(p); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + state.Fatal("Error accessing PulseAudio cookie:", err) + // unreachable + return p + } + } else if !s.IsDir() { + return p + } + } + + if p, ok := os.LookupEnv(xdgConfigHome); ok { + p = path.Join(p, "pulse", "cookie") + if s, err := os.Stat(p); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + state.Fatal("Error accessing PulseAudio cookie:", err) + // unreachable + return p + } + } else if !s.IsDir() { + return p + } + } + + state.Fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)", + PulseCookie, xdgConfigHome, home)) + return "" +} diff --git a/internal/xcb/c.go b/internal/xcb/c.go new file mode 100644 index 0000000..8fe59d8 --- /dev/null +++ b/internal/xcb/c.go @@ -0,0 +1,33 @@ +package xcb + +import ( + "errors" +) + +//#include +//#include +//#cgo linux LDFLAGS: -lxcb +import "C" + +func xcbHandleConnectionError(c *C.xcb_connection_t) error { + if errno := C.xcb_connection_has_error(c); errno != 0 { + switch errno { + case C.XCB_CONN_ERROR: + return errors.New("connection error") + case C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED: + return errors.New("extension not supported") + case C.XCB_CONN_CLOSED_MEM_INSUFFICIENT: + return errors.New("memory not available") + case C.XCB_CONN_CLOSED_REQ_LEN_EXCEED: + return errors.New("request length exceeded") + case C.XCB_CONN_CLOSED_PARSE_ERR: + return errors.New("invalid display string") + case C.XCB_CONN_CLOSED_INVALID_SCREEN: + return errors.New("server has no screen matching display") + default: + return errors.New("generic X11 failure") + } + } else { + return nil + } +} diff --git a/internal/xcb/export.go b/internal/xcb/export.go new file mode 100644 index 0000000..72a7413 --- /dev/null +++ b/internal/xcb/export.go @@ -0,0 +1,47 @@ +package xcb + +//#include +//#include +//#cgo linux LDFLAGS: -lxcb +import "C" +import ( + "errors" + "unsafe" +) + +const ( + HostModeInsert = C.XCB_HOST_MODE_INSERT + HostModeDelete = C.XCB_HOST_MODE_DELETE + + FamilyInternet = C.XCB_FAMILY_INTERNET + FamilyDecnet = C.XCB_FAMILY_DECNET + FamilyChaos = C.XCB_FAMILY_CHAOS + FamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED + FamilyInternet6 = C.XCB_FAMILY_INTERNET_6 +) + +func ChangeHosts(mode, family C.uint8_t, address string) error { + var c *C.xcb_connection_t + c = C.xcb_connect(nil, nil) + defer C.xcb_disconnect(c) + + if err := xcbHandleConnectionError(c); err != nil { + return err + } + + addr := C.CString(address) + cookie := C.xcb_change_hosts_checked(c, mode, family, C.ushort(len(address)), (*C.uchar)(unsafe.Pointer(addr))) + C.free(unsafe.Pointer(addr)) + + if err := xcbHandleConnectionError(c); err != nil { + return err + } + + e := C.xcb_request_check(c, cookie) + if e != nil { + defer C.free(unsafe.Pointer(e)) + return errors.New("xcb_change_hosts() failed") + } + + return nil +} diff --git a/launcher.go b/launcher.go deleted file mode 100644 index 68d2cb8..0000000 --- a/launcher.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "bytes" - "encoding/base64" - "encoding/gob" - "fmt" - "os" - "strings" - "syscall" -) - -const ( - // hidden path for main to act as a launcher - egoLauncher = "EGO_LAUNCHER" -) - -// hidden launcher path -func tryLauncher() { - if printVersion { - if r, ok := os.LookupEnv(egoLauncher); ok { - // egoLauncher variable contains launcher payload - dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(r)) - - var argv []string - if err := gob.NewDecoder(dec).Decode(&argv); err != nil { - fmt.Println("Error decoding launcher payload:", err) - os.Exit(1) - } - - if err := os.Unsetenv(egoLauncher); err != nil { - fmt.Println("Error unsetting launcher payload:", err) - // not fatal, do not fail - } - - var p string - - if len(argv) > 0 { - if p, ok = which(argv[0]); !ok { - fmt.Printf("Did not find '%s' in PATH\n", argv[0]) - os.Exit(1) - } - } else { - if p, ok = os.LookupEnv("SHELL"); !ok { - fmt.Println("No command was specified and $SHELL was unset") - os.Exit(1) - } - } - - if err := syscall.Exec(p, argv, os.Environ()); err != nil { - fmt.Println("Error executing launcher payload:", err) - os.Exit(1) - } - - // unreachable - os.Exit(1) - return - } - } -} - -func launcherPayloadEnv() string { - r := &bytes.Buffer{} - enc := base64.NewEncoder(base64.StdEncoding, r) - - if err := gob.NewEncoder(enc).Encode(command); err != nil { - fatal("Error encoding launcher payload:", err) - } - - _ = enc.Close() - return egoLauncher + "=" + r.String() -} diff --git a/main.go b/main.go index 4a719d8..102252b 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) + + // 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) } - if r, ok := os.LookupEnv(xdgRuntimeDir); !ok { - fatal("Env variable", xdgRuntimeDir, "unset") - } else { - runtime = r - runDir = path.Join(runtime, "ego") + // 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) } - // state query command - tryState() - - // 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) + a.AppendEnv(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") - } - args = append(args, innerCommand.String()) - - return + a.Run() } diff --git a/util.go b/util.go deleted file mode 100644 index 1a50743..0000000 --- a/util.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io" - "io/fs" - "os" - "os/exec" - "path" -) - -const ( - systemdCheckPath = "/run/systemd/system" -) - -// https://www.freedesktop.org/software/systemd/man/sd_booted.html -func sdBooted() bool { - _, err := os.Stat(systemdCheckPath) - if err != nil { - if verbose { - if errors.Is(err, fs.ErrNotExist) { - fmt.Println("System not booted through systemd") - } else { - fmt.Println("Error accessing", systemdCheckPath+":", err.Error()) - } - } - return false - } - return true -} - -// Try various ways to discover the current user's PulseAudio authentication cookie. -func discoverPulseCookie() string { - if p, ok := os.LookupEnv(pulseCookie); ok { - return p - } - - if p, ok := os.LookupEnv(home); ok { - p = path.Join(p, ".pulse-cookie") - if s, err := os.Stat(p); err != nil { - if !errors.Is(err, fs.ErrNotExist) { - fatal("Error accessing PulseAudio cookie:", err) - // unreachable - return p - } - } else if !s.IsDir() { - return p - } - } - - if p, ok := os.LookupEnv(xdgConfigHome); ok { - p = path.Join(p, "pulse", "cookie") - if s, err := os.Stat(p); err != nil { - if !errors.Is(err, fs.ErrNotExist) { - fatal("Error accessing PulseAudio cookie:", err) - // unreachable - return p - } - } else if !s.IsDir() { - return p - } - } - - fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)", - pulseCookie, xdgConfigHome, home)) - return "" -} - -func which(file string) (string, bool) { - p, err := exec.LookPath(file) - return p, err == nil -} - -func copyFile(dst, src string) error { - srcD, err := os.Open(src) - if err != nil { - return err - } - defer func() { - if srcD.Close() != nil { - // unreachable - panic("src file closed prematurely") - } - }() - - dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return err - } - defer func() { - if dstD.Close() != nil { - // unreachable - panic("dst file closed prematurely") - } - }() - - _, err = io.Copy(dstD, srcD) - return err -} diff --git a/x11.go b/x11.go deleted file mode 100644 index d25155e..0000000 --- a/x11.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import "C" -import ( - "errors" - "unsafe" -) - -//#include -//#include -//#cgo linux LDFLAGS: -lxcb -import "C" - -const ( - xcbHostModeInsert = C.XCB_HOST_MODE_INSERT - xcbHostModeDelete = C.XCB_HOST_MODE_DELETE - - xcbFamilyInternet = C.XCB_FAMILY_INTERNET - xcbFamilyDecnet = C.XCB_FAMILY_DECNET - xcbFamilyChaos = C.XCB_FAMILY_CHAOS - xcbFamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED - xcbFamilyInternet6 = C.XCB_FAMILY_INTERNET_6 -) - -func changeHosts(mode, family C.uint8_t, address string) error { - var c *C.xcb_connection_t - c = C.xcb_connect(nil, nil) - defer C.xcb_disconnect(c) - - if err := xcbHandleConnectionError(c); err != nil { - return err - } - - addr := C.CString(address) - cookie := C.xcb_change_hosts_checked(c, mode, family, C.ushort(len(address)), (*C.uchar)(unsafe.Pointer(addr))) - C.free(unsafe.Pointer(addr)) - - if err := xcbHandleConnectionError(c); err != nil { - return err - } - - e := C.xcb_request_check(c, cookie) - if e != nil { - defer C.free(unsafe.Pointer(e)) - return errors.New("xcb_change_hosts() failed") - } - - return nil -} - -func xcbHandleConnectionError(c *C.xcb_connection_t) error { - if errno := C.xcb_connection_has_error(c); errno != 0 { - switch errno { - case C.XCB_CONN_ERROR: - return errors.New("connection error") - case C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED: - return errors.New("extension not supported") - case C.XCB_CONN_CLOSED_MEM_INSUFFICIENT: - return errors.New("memory not available") - case C.XCB_CONN_CLOSED_REQ_LEN_EXCEED: - return errors.New("request length exceeded") - case C.XCB_CONN_CLOSED_PARSE_ERR: - return errors.New("invalid display string") - case C.XCB_CONN_CLOSED_INVALID_SCREEN: - return errors.New("server has no screen matching display") - default: - return errors.New("generic X11 failure") - } - } else { - return nil - } -}