aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app/share.pulse.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/app/share.pulse.go')
-rw-r--r--internal/app/share.pulse.go116
1 files changed, 116 insertions, 0 deletions
diff --git a/internal/app/share.pulse.go b/internal/app/share.pulse.go
new file mode 100644
index 00000000..633b859d
--- /dev/null
+++ b/internal/app/share.pulse.go
@@ -0,0 +1,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)))
+}