2022-04-01 20:28:51 +00:00
|
|
|
package execuser
|
|
|
|
|
|
2022-05-04 14:14:12 +00:00
|
|
|
import (
|
2024-05-16 11:39:30 +00:00
|
|
|
"bufio"
|
|
|
|
|
"bytes"
|
2022-06-10 21:52:24 +00:00
|
|
|
"errors"
|
2022-05-04 14:14:12 +00:00
|
|
|
"fmt"
|
2024-05-16 11:39:30 +00:00
|
|
|
"io"
|
2022-05-04 14:14:12 +00:00
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
2024-05-16 11:39:30 +00:00
|
|
|
"regexp"
|
2025-02-05 17:00:13 +00:00
|
|
|
"sort"
|
2022-05-04 14:14:12 +00:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2025-06-27 13:41:13 +00:00
|
|
|
userpkg "github.com/fleetdm/fleet/v4/orbit/pkg/user"
|
2022-05-04 14:14:12 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// run uses sudo to run the given path as login user.
|
2024-10-31 19:24:42 +00:00
|
|
|
func run(path string, opts eopts) (lastLogs string, err error) {
|
2024-11-20 16:44:40 +00:00
|
|
|
args, err := getUserAndDisplayArgs(path, opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("get args: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
args = append(args,
|
|
|
|
|
// Append the packaged libayatana-appindicator3 libraries path to LD_LIBRARY_PATH.
|
|
|
|
|
//
|
|
|
|
|
// Fleet Desktop doesn't use libayatana-appindicator3 since 1.18.3, but we need to
|
|
|
|
|
// keep this to support older versions of Fleet Desktop.
|
|
|
|
|
fmt.Sprintf("LD_LIBRARY_PATH=%s:%s", filepath.Dir(path), os.ExpandEnv("$LD_LIBRARY_PATH")),
|
|
|
|
|
path,
|
|
|
|
|
)
|
|
|
|
|
|
2024-11-22 19:30:13 +00:00
|
|
|
if len(opts.args) > 0 {
|
|
|
|
|
for _, arg := range opts.args {
|
|
|
|
|
args = append(args, arg[0], arg[1])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-20 16:44:40 +00:00
|
|
|
cmd := exec.Command("sudo", args...)
|
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
|
log.Printf("cmd=%s", cmd.String())
|
|
|
|
|
|
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
|
|
|
return "", fmt.Errorf("open path %q: %w", path, err)
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// run uses sudo to run the given path as login user and waits for the process to finish.
|
|
|
|
|
func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
|
|
|
|
|
args, err := getUserAndDisplayArgs(path, opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, -1, fmt.Errorf("get args: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
args = append(args, path)
|
|
|
|
|
|
|
|
|
|
if len(opts.args) > 0 {
|
|
|
|
|
for _, arg := range opts.args {
|
|
|
|
|
args = append(args, arg[0], arg[1])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-05 15:44:16 +00:00
|
|
|
// Prefix with "timeout" and "sudo" if applicable
|
|
|
|
|
var cmdArgs []string
|
|
|
|
|
if opts.timeout > 0 {
|
|
|
|
|
cmdArgs = append(cmdArgs, "timeout", fmt.Sprintf("%ds", int(opts.timeout.Seconds())))
|
|
|
|
|
}
|
|
|
|
|
cmdArgs = append(cmdArgs, "sudo")
|
|
|
|
|
cmdArgs = append(cmdArgs, args...)
|
|
|
|
|
|
|
|
|
|
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) // #nosec G204
|
|
|
|
|
|
2024-11-20 16:44:40 +00:00
|
|
|
log.Printf("cmd=%s", cmd.String())
|
|
|
|
|
|
|
|
|
|
output, err = cmd.Output()
|
|
|
|
|
if err != nil {
|
|
|
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
|
|
|
exitCode = exitErr.ExitCode()
|
|
|
|
|
return output, exitCode, fmt.Errorf("%q exited with code %d: %w", path, exitCode, err)
|
|
|
|
|
}
|
|
|
|
|
return output, -1, fmt.Errorf("%q error: %w", path, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return output, exitCode, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-05 15:02:03 +00:00
|
|
|
func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
|
|
|
|
|
args, err := getUserAndDisplayArgs(path, opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("get args: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
args = append(args, path)
|
|
|
|
|
|
|
|
|
|
if len(opts.args) > 0 {
|
|
|
|
|
for _, arg := range opts.args {
|
|
|
|
|
args = append(args, arg[0], arg[1])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := exec.Command("sudo", args...)
|
|
|
|
|
log.Printf("cmd=%s", cmd.String())
|
|
|
|
|
|
|
|
|
|
stdin, err := cmd.StdinPipe()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("stdin pipe: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("open path %q: %w", path, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return stdin, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-20 16:44:40 +00:00
|
|
|
func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
|
2025-06-27 13:41:13 +00:00
|
|
|
user, err := userpkg.GetLoginUser()
|
2022-05-04 14:14:12 +00:00
|
|
|
if err != nil {
|
2024-11-20 16:44:40 +00:00
|
|
|
return nil, fmt.Errorf("get user: %w", err)
|
2022-05-04 14:14:12 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-27 13:41:13 +00:00
|
|
|
log.Info().Str("user", user.Name).Int64("id", user.ID).Msg("attempting to get user session type and display")
|
2024-05-16 11:39:30 +00:00
|
|
|
|
2025-02-05 17:00:13 +00:00
|
|
|
// Get user's display session type (x11 vs. wayland).
|
2025-06-27 13:41:13 +00:00
|
|
|
uid := strconv.FormatInt(user.ID, 10)
|
|
|
|
|
userDisplaySessionType, err := userpkg.GetUserDisplaySessionType(uid)
|
|
|
|
|
if userDisplaySessionType == userpkg.GuiSessionTypeTty {
|
|
|
|
|
return nil, fmt.Errorf("user %q (%d) is not running a GUI session", user.Name, user.ID)
|
|
|
|
|
}
|
2024-05-16 11:39:30 +00:00
|
|
|
if err != nil {
|
2025-02-05 17:00:13 +00:00
|
|
|
// Wayland is the default for most distributions, thus we assume
|
|
|
|
|
// wayland if we couldn't determine the session type.
|
|
|
|
|
log.Error().Err(err).Msg("assuming wayland session")
|
2025-06-27 13:41:13 +00:00
|
|
|
userDisplaySessionType = userpkg.GuiSessionTypeWayland
|
2025-02-05 17:00:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var display string
|
2025-06-27 13:41:13 +00:00
|
|
|
if userDisplaySessionType == userpkg.GuiSessionTypeX11 {
|
|
|
|
|
x11Display, err := getUserX11Display(user.Name)
|
2025-02-05 17:00:13 +00:00
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Err(err).Msg("failed to get X11 display, using default :0")
|
|
|
|
|
// TODO(lucas): Revisit when working on multi-user/multi-session support.
|
|
|
|
|
// Default to display ':0' if user display could not be found.
|
|
|
|
|
// This assumes there's only one desktop session and belongs to the
|
|
|
|
|
// user returned in `getLoginUID'.
|
|
|
|
|
display = ":0"
|
|
|
|
|
} else {
|
|
|
|
|
display = x11Display
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
waylandDisplay, err := getUserWaylandDisplay(uid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Err(err).Msg("failed to get wayland display, using default wayland-0")
|
|
|
|
|
// TODO(lucas): Revisit when working on multi-user/multi-session support.
|
|
|
|
|
// Default to display 'wayland-0' if user display could not be found.
|
|
|
|
|
// This assumes there's only one desktop session and belongs to the
|
|
|
|
|
// user returned in `getLoginUID'.
|
|
|
|
|
display = "wayland-0"
|
|
|
|
|
} else {
|
|
|
|
|
display = waylandDisplay
|
|
|
|
|
}
|
2022-05-04 14:14:12 +00:00
|
|
|
}
|
2022-06-21 19:26:14 +00:00
|
|
|
|
2024-05-16 11:39:30 +00:00
|
|
|
log.Info().
|
|
|
|
|
Str("path", path).
|
2025-06-27 13:41:13 +00:00
|
|
|
Str("user", user.Name).
|
|
|
|
|
Int64("id", user.ID).
|
2024-05-16 11:39:30 +00:00
|
|
|
Str("display", display).
|
2025-02-05 17:00:13 +00:00
|
|
|
Str("session_type", userDisplaySessionType.String()).
|
2024-05-16 11:39:30 +00:00
|
|
|
Msg("running sudo")
|
|
|
|
|
|
|
|
|
|
args := argsForSudo(user, opts)
|
|
|
|
|
|
2025-06-27 13:41:13 +00:00
|
|
|
if userDisplaySessionType == userpkg.GuiSessionTypeWayland {
|
2025-02-05 17:00:13 +00:00
|
|
|
args = append(args, "WAYLAND_DISPLAY="+display)
|
|
|
|
|
// For xdg-open to work on a Wayland session we still need to set the DISPLAY variable.
|
|
|
|
|
x11Display := ":" + strings.TrimPrefix(display, "wayland-")
|
|
|
|
|
args = append(args, "DISPLAY="+x11Display)
|
|
|
|
|
} else {
|
|
|
|
|
args = append(args, "DISPLAY="+display)
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-16 11:39:30 +00:00
|
|
|
args = append(args,
|
2022-05-04 14:14:12 +00:00
|
|
|
// DBUS_SESSION_BUS_ADDRESS sets the location of the user login session bus.
|
|
|
|
|
// Required by the libayatana-appindicator3 library to display a tray icon
|
|
|
|
|
// on the desktop session.
|
2022-06-21 19:26:14 +00:00
|
|
|
//
|
|
|
|
|
// This is required for Ubuntu 18, and not required for Ubuntu 21/22
|
|
|
|
|
// (because it's already part of the user).
|
2025-06-27 13:41:13 +00:00
|
|
|
fmt.Sprintf("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%d/bus", user.ID),
|
2022-05-04 14:14:12 +00:00
|
|
|
)
|
|
|
|
|
|
2024-11-20 16:44:40 +00:00
|
|
|
return args, nil
|
2022-05-04 14:14:12 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-27 13:41:13 +00:00
|
|
|
func argsForSudo(u *userpkg.User, opts eopts) []string {
|
2024-05-16 11:39:30 +00:00
|
|
|
// -H: "[...] to set HOME environment to what's specified in the target's user password database entry."
|
|
|
|
|
// -i: needed to run the command with the user's context, from `man sudo`:
|
|
|
|
|
// "The command is run with an environment similar to the one a user would receive at log in"
|
|
|
|
|
// -u: "[..]Run the command as a user other than the default target user (usually root)."
|
2025-06-27 13:41:13 +00:00
|
|
|
args := []string{"-i", "-u", u.Name, "-H"}
|
2024-05-16 11:39:30 +00:00
|
|
|
for _, nv := range opts.env {
|
|
|
|
|
args = append(args, fmt.Sprintf("%s=%s", nv[0], nv[1]))
|
|
|
|
|
}
|
|
|
|
|
return args
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var whoLineRegexp = regexp.MustCompile(`(\w+)\s+(:\d+)\s+`)
|
|
|
|
|
|
2025-02-05 17:00:13 +00:00
|
|
|
// getUserWaylandDisplay returns the value to set on WAYLAND_DISPLAY for the given user.
|
|
|
|
|
func getUserWaylandDisplay(uid string) (string, error) {
|
|
|
|
|
matches, err := filepath.Glob("/run/user/" + uid + "/wayland-*")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("list wayland socket files: %w", err)
|
|
|
|
|
}
|
|
|
|
|
sort.Slice(matches, func(i, j int) bool {
|
|
|
|
|
return matches[i] < matches[j]
|
|
|
|
|
})
|
|
|
|
|
for _, match := range matches {
|
|
|
|
|
if strings.HasSuffix(match, ".lock") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return filepath.Base(match), nil
|
|
|
|
|
}
|
|
|
|
|
return "", errors.New("wayland socket not found")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getUserX11Display returns the value to set on DISPLAY for the given user.
|
|
|
|
|
func getUserX11Display(user string) (string, error) {
|
2024-05-16 11:39:30 +00:00
|
|
|
cmd := exec.Command("who")
|
|
|
|
|
var stdout bytes.Buffer
|
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return "", fmt.Errorf("run 'who' to get user display: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return parseWhoOutputForDisplay(&stdout, user)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseWhoOutputForDisplay(output io.Reader, user string) (string, error) {
|
|
|
|
|
scanner := bufio.NewScanner(output)
|
|
|
|
|
for scanner.Scan() {
|
|
|
|
|
line := scanner.Text()
|
|
|
|
|
matches := whoLineRegexp.FindStringSubmatch(line)
|
|
|
|
|
if len(matches) > 1 && matches[1] == user {
|
|
|
|
|
return matches[2], nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
|
return "", fmt.Errorf("scanner error: %w", err)
|
|
|
|
|
}
|
2025-02-05 17:00:13 +00:00
|
|
|
return "", errors.New("display not found on who output")
|
2024-05-16 11:39:30 +00:00
|
|
|
}
|