diff options
Diffstat (limited to 'internal/app/share.pulse.go')
| -rw-r--r-- | internal/app/share.pulse.go | 116 |
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))) +} |
