aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app/share.pulse.go
blob: 633b859dd69411cbde9fdd020b23d910d6e2eca0 (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
package app

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

	"git.ophivana.moe/cat/fortify/acl"
	"git.ophivana.moe/cat/fortify/internal/state"
)

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")
)

type (
	PulseCookieAccessError BaseError
	PulseSocketAccessError BaseError
)

func (seal *appSeal) sharePulse() error {
	if !seal.et.Has(state.EnablePulse) {
		return nil
	}

	// ensure PulseAudio directory ACL (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 (*PulseSocketAccessError)(wrapError(err,
				fmt.Sprintf("cannot access PulseAudio directory '%s':", pd), err))
		}
		return (*PulseSocketAccessError)(wrapError(ErrPulseSocket,
			fmt.Sprintf("PulseAudio directory '%s' not found", pd)))
	}

	seal.appendEnv(pulseServer, "unix:"+ps)
	seal.sys.updatePerm(pd, acl.Execute)

	// ensure 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 (*PulseSocketAccessError)(wrapError(err,
				fmt.Sprintf("cannot access PulseAudio socket '%s':", ps), err))
		}
		return (*PulseSocketAccessError)(wrapError(ErrPulseSocket,
			fmt.Sprintf("PulseAudio directory '%s' found but socket does not exist", pd)))
	} else {
		if m := s.Mode(); m&0o006 != 0o006 {
			return (*PulseSocketAccessError)(wrapError(ErrPulseMode,
				fmt.Sprintf("unexpected permissions on '%s':", ps), m))
		}
	}

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

	return nil
}

// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie() (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, (*PulseCookieAccessError)(wrapError(err,
					fmt.Sprintf("cannot access PulseAudio cookie '%s':", p), err))
			}
			// 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, (*PulseCookieAccessError)(wrapError(err, "cannot access PulseAudio cookie", p+":", err))
			}
			// not found, try next method
		} else if !s.IsDir() {
			return p, nil
		}
	}

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