diff options
Diffstat (limited to 'internal/app')
| -rw-r--r-- | internal/app/builder.go | 13 | ||||
| -rw-r--r-- | internal/app/launch.go | 152 | ||||
| -rw-r--r-- | internal/app/setup.go | 124 |
3 files changed, 289 insertions, 0 deletions
diff --git a/internal/app/builder.go b/internal/app/builder.go new file mode 100644 index 00000000..2bce43b4 --- /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 00000000..c6f7b773 --- /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 00000000..7b5cd2ec --- /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 +} |
