diff --git a/flag.go b/flag.go index 8fbdc6b..aa67908 100644 --- a/flag.go +++ b/flag.go @@ -2,8 +2,6 @@ package main import ( "flag" - - "git.ophivana.moe/cat/fortify/internal/app" ) var ( @@ -38,9 +36,6 @@ func init() { flag.BoolVar(&mustDBus, "dbus", false, "Proxy D-Bus connection") flag.BoolVar(&mustPulse, "pulse", false, "Share PulseAudio socket and cookie") - flag.BoolVar(&app.LaunchOptions[app.LaunchMethodSudo], "sudo", false, "Use 'sudo' to switch user") - flag.BoolVar(&app.LaunchOptions[app.LaunchMethodMachineCtl], "machinectl", true, "Use 'machinectl' to switch user") - flag.BoolVar(&flagVerbose, "v", false, "Verbose output") flag.BoolVar(&printVersion, "V", false, "Print version") } diff --git a/internal/app/run.go b/internal/app/run.go index b5eec82..0871e89 100644 --- a/internal/app/run.go +++ b/internal/app/run.go @@ -17,15 +17,9 @@ const ( sudoAskPass = "SUDO_ASKPASS" ) const ( - LaunchMethodSudo = iota + LaunchMethodSudo uint8 = iota + LaunchMethodBwrap LaunchMethodMachineCtl - - launchOptionLength -) - -var ( - // LaunchOptions is set in main's cli.go - LaunchOptions [launchOptionLength]bool ) func (a *App) Run() { @@ -34,34 +28,20 @@ func (a *App) Run() { a.AppendEnv(term, t) } - commandBuilder := a.commandBuilderSudo + var commandBuilder func() (args []string) - var toolPath string - - // dependency checks - const sudoFallback = "Falling back to 'sudo', some desktop integration features may not work" - if LaunchOptions[LaunchMethodMachineCtl] && !LaunchOptions[LaunchMethodSudo] { // sudo argument takes priority - if !util.SdBooted() { - fmt.Println("This system was not booted through systemd") - fmt.Println(sudoFallback) - } else if machineCtlPath, ok := util.Which("machinectl"); !ok { - fmt.Println("Did not find 'machinectl' in PATH") - fmt.Println(sudoFallback) - } else { - toolPath = machineCtlPath - commandBuilder = a.commandBuilderMachineCtl - } - } else if sudoPath, ok := util.Which("sudo"); !ok { - state.Fatal("Did not find 'sudo' in PATH") - } else { - toolPath = sudoPath + switch a.launchOption { + case LaunchMethodSudo: + commandBuilder = a.commandBuilderSudo + case LaunchMethodBwrap: + commandBuilder = a.commandBuilderBwrap + case LaunchMethodMachineCtl: + commandBuilder = a.commandBuilderMachineCtl + default: + panic("unreachable") } - if system.V.Verbose { - fmt.Printf("Selected launcher '%s'\n", toolPath) - } - - cmd := exec.Command(toolPath, commandBuilder()...) + cmd := exec.Command(a.toolPath, commandBuilder()...) cmd.Env = []string{} cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -122,6 +102,12 @@ func (a *App) commandBuilderSudo() (args []string) { return } +func (a *App) commandBuilderBwrap() (args []string) { + // TODO: build bwrap command + state.Fatal("bwrap") + panic("unreachable") +} + func (a *App) commandBuilderMachineCtl() (args []string) { args = make([]string, 0, 9+len(a.env)) diff --git a/internal/app/setup.go b/internal/app/setup.go index 82765df..d8378cf 100644 --- a/internal/app/setup.go +++ b/internal/app/setup.go @@ -9,13 +9,19 @@ import ( "git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/system" + "git.ophivana.moe/cat/fortify/internal/util" ) type App struct { + launchOptionText string + uid int env []string command []string + launchOption uint8 + toolPath string + enablements state.Enablements *user.User @@ -23,6 +29,10 @@ type App struct { // so don't treat it as if it is } +func (a *App) LaunchOption() uint8 { + return a.launchOption +} + func (a *App) setEnablement(e state.Enablement) { if a.enablements.Has(e) { panic("enablement " + e.String() + " set twice") @@ -31,8 +41,8 @@ func (a *App) setEnablement(e state.Enablement) { a.enablements |= e.Mask() } -func New(userName string, args []string) *App { - a := &App{command: args} +func New(userName string, args []string, launchOptionText string) *App { + a := &App{command: args, launchOptionText: launchOptionText} if u, err := user.Lookup(userName); err != nil { if errors.As(err, new(user.UnknownUserError)) { @@ -57,6 +67,47 @@ func New(userName string, args []string) *App { if system.V.Verbose { fmt.Println("Running as user", a.Username, "("+a.Uid+"),", "command:", a.command) + if util.SdBootedV { + fmt.Println("System booted with systemd as init system (PID 1).") + } + } + + switch a.launchOptionText { + case "sudo": + a.launchOption = LaunchMethodSudo + if sudoPath, ok := util.Which("sudo"); !ok { + fmt.Println("Did not find 'sudo' in PATH") + os.Exit(1) + } else { + a.toolPath = sudoPath + } + case "bubblewrap": + a.launchOption = LaunchMethodBwrap + if bwrapPath, ok := util.Which("bwrap"); !ok { + fmt.Println("Did not find 'bwrap' in PATH") + os.Exit(1) + } else { + a.toolPath = bwrapPath + } + case "systemd": + a.launchOption = LaunchMethodMachineCtl + if !util.SdBootedV { + fmt.Println("System has not been booted with systemd as init system (PID 1).") + os.Exit(1) + } + + if machineCtlPath, ok := util.Which("machinectl"); !ok { + fmt.Println("Did not find 'machinectl' in PATH") + } else { + a.toolPath = machineCtlPath + } + default: + fmt.Println("invalid launch method") + os.Exit(1) + } + + if system.V.Verbose { + fmt.Println("Determined launch method to be", a.launchOptionText, "with tool at", a.toolPath) } return a diff --git a/internal/util/early.go b/internal/util/early.go new file mode 100644 index 0000000..ba7375c --- /dev/null +++ b/internal/util/early.go @@ -0,0 +1,12 @@ +package util + +import "fmt" + +var SdBootedV = func() bool { + if v, err := SdBooted(); err != nil { + fmt.Println("warn: read systemd marker:", err) + return false + } else { + return v + } +}() diff --git a/internal/util/std.go b/internal/util/std.go index 5c37a0d..4f304fb 100644 --- a/internal/util/std.go +++ b/internal/util/std.go @@ -8,7 +8,6 @@ import ( "path" "git.ophivana.moe/cat/fortify/internal/state" - "git.ophivana.moe/cat/fortify/internal/system" ) const ( @@ -22,19 +21,16 @@ const ( ) // SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html -func SdBooted() bool { +func SdBooted() (bool, error) { _, 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()) - } + if errors.Is(err, fs.ErrNotExist) { + err = nil } - return false + return false, err } - return true + + return true, nil } // DiscoverPulseCookie try various standard methods to discover the current user's PulseAudio authentication cookie diff --git a/main.go b/main.go index a15dc06..1411b91 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "fmt" "io/fs" "os" + "path" "strconv" "syscall" @@ -15,6 +16,7 @@ import ( "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" ) var ( @@ -24,8 +26,19 @@ var ( dbusSession *dbus.Config dbusSystem *dbus.Config + + launchOptionText string ) +func init() { + methodHelpString := "Method of launching the child process, can be one of \"sudo\", \"bubblewrap\"" + if util.SdBootedV { + methodHelpString += ", \"systemd\"" + } + + flag.StringVar(&launchOptionText, "method", "sudo", methodHelpString) +} + func tryVersion() { if printVersion { fmt.Println(Version) @@ -44,7 +57,7 @@ func main() { tryLicense() system.Retrieve(flagVerbose) - a = app.New(userName, flag.Args()) + a = app.New(userName, flag.Args(), launchOptionText) state.Set(*a.User, a.Command(), a.UID()) // parse D-Bus config file if applicable @@ -87,6 +100,26 @@ func main() { state.Fatal("Error creating shared directory:", err) } + if a.LaunchOption() == app.LaunchMethodSudo { + // ensure child runtime directory (e.g. `/tmp/fortify.%d/%d.share`) + cr := path.Join(system.V.Share, a.Uid+".share") + if err := os.Mkdir(cr, 0700); err != nil && !errors.Is(err, fs.ErrExist) { + state.Fatal("Error creating child runtime directory:", err) + } else { + if err = acl.UpdatePerm(cr, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil { + state.Fatal("Error preparing child runtime directory:", err) + } else { + state.RegisterRevertPath(cr) + } + a.AppendEnv("XDG_RUNTIME_DIR", cr) + a.AppendEnv("XDG_SESSION_CLASS", "user") + a.AppendEnv("XDG_SESSION_TYPE", "tty") + if system.V.Verbose { + fmt.Printf("Child runtime data dir '%s' configured\n", cr) + } + } + } + // warn about target user home directory ownership if stat, err := os.Stat(a.HomeDir); err != nil { if system.V.Verbose { @@ -117,7 +150,7 @@ func main() { 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) + state.Fatal("Error preparing runtime directory:", err) } else { state.RegisterRevertPath(system.V.Runtime) }