aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app/share.pulse.go
blob: 14315342e71f44a8bbb161686bd1d7af503ace5b (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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package app

import (
	"errors"
	"fmt"
	"io/fs"
	"path"

	"git.ophivana.moe/security/fortify/internal/fmsg"
	"git.ophivana.moe/security/fortify/internal/linux"
	"git.ophivana.moe/security/fortify/internal/system"
)

const (
	pulseServer = "PULSE_SERVER"
	pulseCookie = "PULSE_COOKIE"

	home          = "HOME"
	xdgConfigHome = "XDG_CONFIG_HOME"
)

var (
	ErrPulseCookie = errors.New("pulse cookie not present")
	ErrPulseSocket = errors.New("pulse socket not present")
	ErrPulseMode   = errors.New("unexpected pulse socket mode")
)

func (seal *appSeal) sharePulse(os linux.System) error {
	if !seal.et.Has(system.EPulse) {
		return nil
	}

	// check PulseAudio directory presence (e.g. `/run/user/%d/pulse`)
	pd := path.Join(seal.RuntimePath, "pulse")
	ps := path.Join(pd, "native")
	if _, err := os.Stat(pd); err != nil {
		if !errors.Is(err, fs.ErrNotExist) {
			return fmsg.WrapErrorSuffix(err,
				fmt.Sprintf("cannot access PulseAudio directory %q:", pd))
		}
		return fmsg.WrapError(ErrPulseSocket,
			fmt.Sprintf("PulseAudio directory %q not found", pd))
	}

	// check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
	if s, err := os.Stat(ps); err != nil {
		if !errors.Is(err, fs.ErrNotExist) {
			return fmsg.WrapErrorSuffix(err,
				fmt.Sprintf("cannot access PulseAudio socket %q:", ps))
		}
		return fmsg.WrapError(ErrPulseSocket,
			fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pd))
	} else {
		if m := s.Mode(); m&0o006 != 0o006 {
			return fmsg.WrapError(ErrPulseMode,
				fmt.Sprintf("unexpected permissions on %q:", ps), m)
		}
	}

	// hard link pulse socket into target-executable share
	psi := path.Join(seal.shareLocal, "pulse")
	p := path.Join(seal.sys.runtime, "pulse", "native")
	seal.sys.Link(ps, psi)
	seal.sys.bwrap.Bind(psi, p)
	seal.sys.bwrap.SetEnv[pulseServer] = "unix:" + p

	// publish current user's pulse cookie for target user
	if src, err := discoverPulseCookie(os); err != nil {
		return err
	} else {
		dst := path.Join(seal.share, "pulse-cookie")
		seal.sys.bwrap.SetEnv[pulseCookie] = dst
		seal.sys.CopyFile(dst, src)
		seal.sys.bwrap.Bind(dst, dst)
	}

	return nil
}

// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie(os linux.System) (string, error) {
	if p, ok := os.LookupEnv(pulseCookie); ok {
		return p, nil
	}

	// dotfile $HOME/.pulse-cookie
	if p, ok := os.LookupEnv(home); ok {
		p = path.Join(p, ".pulse-cookie")
		if s, err := os.Stat(p); err != nil {
			if !errors.Is(err, fs.ErrNotExist) {
				return p, fmsg.WrapErrorSuffix(err,
					fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
			}
			// not found, try next method
		} else if !s.IsDir() {
			return p, nil
		}
	}

	// $XDG_CONFIG_HOME/pulse/cookie
	if p, ok := os.LookupEnv(xdgConfigHome); ok {
		p = path.Join(p, "pulse", "cookie")
		if s, err := os.Stat(p); err != nil {
			if !errors.Is(err, fs.ErrNotExist) {
				return p, fmsg.WrapErrorSuffix(err,
					fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
			}
			// not found, try next method
		} else if !s.IsDir() {
			return p, nil
		}
	}

	return "", fmsg.WrapError(ErrPulseCookie,
		fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
			pulseCookie, xdgConfigHome, home))
}