app: handle launch method in New function

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
Ophestra 2024-09-12 20:53:33 +09:00
parent 8223a9ee66
commit b0aff89166
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
6 changed files with 125 additions and 52 deletions

View File

@ -2,8 +2,6 @@ package main
import ( import (
"flag" "flag"
"git.ophivana.moe/cat/fortify/internal/app"
) )
var ( var (
@ -38,9 +36,6 @@ func init() {
flag.BoolVar(&mustDBus, "dbus", false, "Proxy D-Bus connection") flag.BoolVar(&mustDBus, "dbus", false, "Proxy D-Bus connection")
flag.BoolVar(&mustPulse, "pulse", false, "Share PulseAudio socket and cookie") 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(&flagVerbose, "v", false, "Verbose output")
flag.BoolVar(&printVersion, "V", false, "Print version") flag.BoolVar(&printVersion, "V", false, "Print version")
} }

View File

@ -17,15 +17,9 @@ const (
sudoAskPass = "SUDO_ASKPASS" sudoAskPass = "SUDO_ASKPASS"
) )
const ( const (
LaunchMethodSudo = iota LaunchMethodSudo uint8 = iota
LaunchMethodBwrap
LaunchMethodMachineCtl LaunchMethodMachineCtl
launchOptionLength
)
var (
// LaunchOptions is set in main's cli.go
LaunchOptions [launchOptionLength]bool
) )
func (a *App) Run() { func (a *App) Run() {
@ -34,34 +28,20 @@ func (a *App) Run() {
a.AppendEnv(term, t) a.AppendEnv(term, t)
} }
commandBuilder := a.commandBuilderSudo var commandBuilder func() (args []string)
var toolPath string switch a.launchOption {
case LaunchMethodSudo:
// dependency checks commandBuilder = a.commandBuilderSudo
const sudoFallback = "Falling back to 'sudo', some desktop integration features may not work" case LaunchMethodBwrap:
if LaunchOptions[LaunchMethodMachineCtl] && !LaunchOptions[LaunchMethodSudo] { // sudo argument takes priority commandBuilder = a.commandBuilderBwrap
if !util.SdBooted() { case LaunchMethodMachineCtl:
fmt.Println("This system was not booted through systemd") commandBuilder = a.commandBuilderMachineCtl
fmt.Println(sudoFallback) default:
} else if machineCtlPath, ok := util.Which("machinectl"); !ok { panic("unreachable")
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
} }
if system.V.Verbose { cmd := exec.Command(a.toolPath, commandBuilder()...)
fmt.Printf("Selected launcher '%s'\n", toolPath)
}
cmd := exec.Command(toolPath, commandBuilder()...)
cmd.Env = []string{} cmd.Env = []string{}
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@ -122,6 +102,12 @@ func (a *App) commandBuilderSudo() (args []string) {
return return
} }
func (a *App) commandBuilderBwrap() (args []string) {
// TODO: build bwrap command
state.Fatal("bwrap")
panic("unreachable")
}
func (a *App) commandBuilderMachineCtl() (args []string) { func (a *App) commandBuilderMachineCtl() (args []string) {
args = make([]string, 0, 9+len(a.env)) args = make([]string, 0, 9+len(a.env))

View File

@ -9,13 +9,19 @@ import (
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/util"
) )
type App struct { type App struct {
launchOptionText string
uid int uid int
env []string env []string
command []string command []string
launchOption uint8
toolPath string
enablements state.Enablements enablements state.Enablements
*user.User *user.User
@ -23,6 +29,10 @@ type App struct {
// so don't treat it as if it is // so don't treat it as if it is
} }
func (a *App) LaunchOption() uint8 {
return a.launchOption
}
func (a *App) setEnablement(e state.Enablement) { func (a *App) setEnablement(e state.Enablement) {
if a.enablements.Has(e) { if a.enablements.Has(e) {
panic("enablement " + e.String() + " set twice") panic("enablement " + e.String() + " set twice")
@ -31,8 +41,8 @@ func (a *App) setEnablement(e state.Enablement) {
a.enablements |= e.Mask() a.enablements |= e.Mask()
} }
func New(userName string, args []string) *App { func New(userName string, args []string, launchOptionText string) *App {
a := &App{command: args} a := &App{command: args, launchOptionText: launchOptionText}
if u, err := user.Lookup(userName); err != nil { if u, err := user.Lookup(userName); err != nil {
if errors.As(err, new(user.UnknownUserError)) { if errors.As(err, new(user.UnknownUserError)) {
@ -57,6 +67,47 @@ func New(userName string, args []string) *App {
if system.V.Verbose { if system.V.Verbose {
fmt.Println("Running as user", a.Username, "("+a.Uid+"),", "command:", a.command) 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 return a

12
internal/util/early.go Normal file
View File

@ -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
}
}()

View File

@ -8,7 +8,6 @@ import (
"path" "path"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/system"
) )
const ( const (
@ -22,19 +21,16 @@ const (
) )
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html // SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
func SdBooted() bool { func SdBooted() (bool, error) {
_, err := os.Stat(systemdCheckPath) _, err := os.Stat(systemdCheckPath)
if err != nil { if err != nil {
if system.V.Verbose { if errors.Is(err, fs.ErrNotExist) {
if errors.Is(err, fs.ErrNotExist) { err = nil
fmt.Println("System not booted through systemd")
} else {
fmt.Println("Error accessing", systemdCheckPath+":", err.Error())
}
} }
return false return false, err
} }
return true
return true, nil
} }
// DiscoverPulseCookie try various standard methods to discover the current user's PulseAudio authentication cookie // DiscoverPulseCookie try various standard methods to discover the current user's PulseAudio authentication cookie

37
main.go
View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"os" "os"
"path"
"strconv" "strconv"
"syscall" "syscall"
@ -15,6 +16,7 @@ import (
"git.ophivana.moe/cat/fortify/internal/app" "git.ophivana.moe/cat/fortify/internal/app"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/util"
) )
var ( var (
@ -24,8 +26,19 @@ var (
dbusSession *dbus.Config dbusSession *dbus.Config
dbusSystem *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() { func tryVersion() {
if printVersion { if printVersion {
fmt.Println(Version) fmt.Println(Version)
@ -44,7 +57,7 @@ func main() {
tryLicense() tryLicense()
system.Retrieve(flagVerbose) system.Retrieve(flagVerbose)
a = app.New(userName, flag.Args()) a = app.New(userName, flag.Args(), launchOptionText)
state.Set(*a.User, a.Command(), a.UID()) state.Set(*a.User, a.Command(), a.UID())
// parse D-Bus config file if applicable // parse D-Bus config file if applicable
@ -87,6 +100,26 @@ func main() {
state.Fatal("Error creating shared directory:", err) 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 // warn about target user home directory ownership
if stat, err := os.Stat(a.HomeDir); err != nil { if stat, err := os.Stat(a.HomeDir); err != nil {
if system.V.Verbose { if system.V.Verbose {
@ -117,7 +150,7 @@ func main() {
state.Fatal(fmt.Sprintf("Path '%s' is not a directory", system.V.Runtime)) state.Fatal(fmt.Sprintf("Path '%s' is not a directory", system.V.Runtime))
} else { } else {
if err = acl.UpdatePerm(system.V.Runtime, a.UID(), acl.Execute); err != nil { 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 { } else {
state.RegisterRevertPath(system.V.Runtime) state.RegisterRevertPath(system.V.Runtime)
} }