aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/outcome/hsu.go
blob: c1be676181256f0fce20a6bcea337a848e25f22e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package outcome

import (
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strconv"
	"sync"

	"hakurei.app/container/fhs"
	"hakurei.app/hst"
	"hakurei.app/message"
)

// Hsu caches responses from cmd/hsu.
type Hsu struct {
	idOnce sync.Once
	idErr  error
	id     int

	kOnce sync.Once

	// msg is not populated
	k syscallDispatcher
}

var ErrHsuAccess = errors.New("current user is not in the hsurc file")

// ensureDispatcher ensures Hsu.k is not nil.
func (h *Hsu) ensureDispatcher() {
	h.kOnce.Do(func() {
		if h.k == nil {
			h.k = direct{}
		}
	})
}

// ID returns the current user hsurc identifier.
// [ErrHsuAccess] is returned if the current user is not in hsurc.
func (h *Hsu) ID() (int, error) {
	h.ensureDispatcher()
	h.idOnce.Do(func() {
		h.id = -1
		hsuPath := h.k.mustHsuPath().String()

		cmd := exec.Command(hsuPath)
		cmd.Path = hsuPath
		cmd.Stderr = os.Stderr // pass through fatal messages
		cmd.Env = make([]string, 0)
		cmd.Dir = fhs.Root
		var (
			p         []byte
			exitError *exec.ExitError
		)

		const step = "obtain uid from hsu"
		if p, h.idErr = h.k.cmdOutput(cmd); h.idErr == nil {
			h.id, h.idErr = strconv.Atoi(string(p))
			if h.idErr != nil {
				h.idErr = &hst.AppError{Step: step, Err: h.idErr, Msg: "invalid uid string from hsu"}
			}
		} else if errors.As(h.idErr, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
			// hsu prints an error message in this case
			h.idErr = &hst.AppError{Step: step, Err: ErrHsuAccess}
		} else if errors.Is(h.idErr, os.ErrNotExist) {
			h.idErr = &hst.AppError{Step: step, Err: h.idErr,
				Msg: fmt.Sprintf("the setuid helper is missing: %s", hsuPath)}
		}
	})

	return h.id, h.idErr
}

// MustID calls [Hsu.ID] and terminates on error.
func (h *Hsu) MustID(msg message.Msg) int {
	id, err := h.ID()
	if err == nil {
		return id
	}

	const fallback = "cannot retrieve user id from setuid wrapper:"
	if errors.Is(err, ErrHsuAccess) {
		if msg != nil {
			msg.Verbose("*"+fallback, err)
		}
		os.Exit(1)
		return -0xbad // not reached
	} else if m, ok := message.GetMessage(err); ok {
		log.Fatal(m)
		return -0xbad // not reached
	} else {
		log.Fatalln(fallback, err)
		return -0xbad // not reached
	}
}