1package wayland
2
3import (
4 "crypto/rand"
5 "crypto/subtle"
6 "encoding/hex"
7 "os"
8 "slices"
9
10 ext "github.com/bnema/go-wayland-bindings/server/extimagecopycapture"
11 "github.com/bnema/go-wayland-bindings/server/wayland"
12 "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
13 "github.com/bnema/neferwl/internal/ports"
14 "github.com/bnema/purego-libwayland/server"
15)
16
17// Capture exclusion (neferwl_capture_exclusion_manager_v1). A recorder asks
18// for the exclusion of its ext-image-copy-capture session and gets a token;
19// its HUD, usually on another connection of the same user, attaches layer
20// surfaces with it. Core owns the decisions (ports/capture.go): this file
21// relays requests and tells clients what core decided. Only one exclusion is
22// live in the compositor, and it ends with its session.
23//
24// Privacy: the token never reaches a log, and neither does any peer
25// identity; only lifecycle events are logged.
26
27// exclusion is the state behind one neferwl_capture_exclusion_v1.
28type exclusion struct {
29 s *Server
30 res *imagecapture.NeferwlCaptureExclusionV1
31 session *captureSession
32 token string
33 // layers are the attached layer surfaces; attached their attachment
34 // objects, pending until a core state lists the layer.
35 layers []*layerSurface
36 attached []*layerAttachment
37 ended bool
38}
39
40// layerAttachment is one neferwl_capture_layer_v1: it reports the result of
41// an attach_surface and the layer's end.
42type layerAttachment struct {
43 res *imagecapture.NeferwlCaptureLayerV1
44 layer *layerSurface
45 confirmed bool // attached sent
46}
47
48func (a *layerAttachment) fail(reason imagecapture.NeferwlCaptureLayerV1Failure) {
49 if a.res.Alive() {
50 a.res.SendFailed(uint32(reason))
51 }
52}
53
54// finish ends an attachment: detached after attached, failed before it.
55func (a *layerAttachment) finish(failure imagecapture.NeferwlCaptureLayerV1Failure, detach imagecapture.NeferwlCaptureLayerV1DetachReason) {
56 if !a.res.Alive() {
57 return
58 }
59 if a.confirmed {
60 a.res.SendDetached(uint32(detach))
61 } else {
62 a.res.SendFailed(uint32(failure))
63 }
64}
65
66// trustedPeer reports whether a peer with these credentials may attach
67// surfaces: its credentials are readable and it runs as the compositor's own
68// user.
69func trustedPeer(own uint32, peer server.Credentials, err error) bool {
70 return err == nil && peer.UID == own
71}
72
73func (s *Server) trusted(c server.Client) bool {
74 cr, err := c.Credentials()
75 return trustedPeer(s.ownUID, cr, err)
76}
77
78func registerExclusion(d *server.Display, s *Server) error {
79 s.ownUID = uint32(os.Geteuid())
80 return imagecapture.NewNeferwlCaptureExclusionManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
81 m := &exclusionManager{s: s, trusted: s.trusted(c)}
82 _, _ = imagecapture.NewNeferwlCaptureExclusionManagerV1(c, int32(v), id, m)
83 })
84}
85
86type exclusionManager struct {
87 s *Server
88 trusted bool
89}
90
91func (*exclusionManager) Destroy(*imagecapture.NeferwlCaptureExclusionManagerV1) {}
92
93// sessionOf finds the registered session behind an ext session object.
94func (s *Server) sessionOf(res *ext.ExtImageCopyCaptureSessionV1) *captureSession {
95 if res == nil {
96 return nil
97 }
98 for c := range s.captureSessions {
99 if c.res != nil && c.res.Resource == res.Resource {
100 return c
101 }
102 }
103 return nil
104}
105
106func (m *exclusionManager) GetExclusion(r *imagecapture.NeferwlCaptureExclusionManagerV1, id uint32, session *ext.ExtImageCopyCaptureSessionV1) {
107 s := m.s
108 c := s.sessionOf(session)
109 ex := &exclusion{s: s, session: c}
110 res, err := imagecapture.NewNeferwlCaptureExclusionV1(r.Client(), r.Version(), id, ex)
111 if err != nil {
112 return
113 }
114 ex.res = res
115 if c != nil && c.excl != nil {
116 r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorAlreadyExcluded), "the capture session already has an exclusion")
117 return
118 }
119 switch {
120 case c == nil || c.stopped || c.id == 0:
121 ex.session = nil
122 ex.ended = true
123 res.SendFailed(uint32(imagecapture.NeferwlCaptureExclusionV1FailureSessionStopped))
124 return
125 case s.excl != nil:
126 ex.session = nil
127 ex.ended = true
128 s.log.Info().Str("reason", "busy").Msg("capture exclusion refused")
129 res.SendFailed(uint32(imagecapture.NeferwlCaptureExclusionV1FailureBusy))
130 return
131 }
132 var raw [32]byte
133 if _, err := rand.Read(raw[:]); err != nil {
134 s.log.Warn().Msg("capture exclusion refused: no randomness")
135 ex.session, ex.ended = nil, true
136 res.SendFailed(uint32(imagecapture.NeferwlCaptureExclusionV1FailureSessionStopped))
137 return
138 }
139 ex.token = hex.EncodeToString(raw[:])
140 s.excl, c.excl = ex, ex
141 s.log.Info().Uint64("session", c.id).Msg("capture exclusion begin")
142 s.emit(ports.CaptureExclusionBegin{Session: c.id})
143 res.SendToken(ex.token)
144}
145
146// Destroy: the owner let go of the exclusion; the session lives on.
147func (ex *exclusion) Destroy(*imagecapture.NeferwlCaptureExclusionV1) {
148 if !ex.ended {
149 ex.end(true)
150 }
151}
152
153func (m *exclusionManager) AttachSurface(r *imagecapture.NeferwlCaptureExclusionManagerV1, id uint32, token string, surface *wayland.Surface) {
154 s := m.s
155 if surface == nil || s.surfaces[surface.Resource] == nil || surface.Resource.Client() != r.Client() {
156 r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorInvalidSurface), "surface is not yours")
157 return
158 }
159 state := s.surfaces[surface.Resource]
160 l := state.layer
161 att := &layerAttachment{layer: l}
162 res, err := imagecapture.NewNeferwlCaptureLayerV1(r.Client(), r.Version(), id, att)
163 if err != nil {
164 return
165 }
166 att.res = res
167 // Same user as the compositor, and the owner of the exclusion passed the
168 // same check by holding the token.
169 ex := s.excl
170 switch {
171 case s.protected() || !m.trusted || !s.mayCapture(r.Client()): // DISPLAY protection; the UID of this connection, checked once at bind; the capture allowlist
172 att.fail(imagecapture.NeferwlCaptureLayerV1FailureUnauthorized)
173 return
174 case ex == nil || subtle.ConstantTimeCompare([]byte(token), []byte(ex.token)) != 1:
175 att.fail(imagecapture.NeferwlCaptureLayerV1FailureUnknownToken)
176 return
177 case state.kind != roleLayer || l == nil:
178 r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorSurfaceRole), "not a layer surface")
179 return
180 case l.pending.layer != ports.LayerTop && l.pending.layer != ports.LayerOverlay:
181 r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorInvalidLayer), "layer must be top or overlay")
182 return
183 case l.pending.keyboard == 1:
184 r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorExclusiveKeyboard), "exclusive keyboard")
185 return
186 case l.mapped || l.closed:
187 r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorInvalidSurface), "already mapped")
188 return
189 case l.capture != nil:
190 att.fail(imagecapture.NeferwlCaptureLayerV1FailureAlreadyAttached)
191 return
192 case len(ex.layers) >= ports.MaxExclusionLayers:
193 att.fail(imagecapture.NeferwlCaptureLayerV1FailureTooManyLayers)
194 return
195 }
196 l.capture, l.attachment = ex, att
197 l.pending.keyboard, l.current.keyboard = 0, 0
198 ex.layers = append(ex.layers, l)
199 ex.attached = append(ex.attached, att)
200 // Queued before any LayerChanged of this layer: core excludes it from
201 // the first scene that shows it.
202 s.emit(ports.CaptureExclusionLayer{Session: ex.session.id, Layer: l.id, Attached: true})
203}
204
205func (*layerAttachment) Destroy(*imagecapture.NeferwlCaptureLayerV1) {}
206
207// end ends the exclusion: every attachment is told (detached, or failed
208// before it was confirmed) and its layer surface is closed so none lingers
209// into the next exclusion. notify tells core; it is false when the session
210// goes (core forgets the exclusion with it, or decided the end itself).
211func (ex *exclusion) end(notify bool) {
212 if ex.ended {
213 return
214 }
215 s := ex.s
216 ex.ended = true
217 if s.excl == ex {
218 s.excl = nil
219 }
220 sess := ex.session
221 if sess != nil {
222 // exclSeen stays as core's last state says: core keeps the ended
223 // exclusion's layers excluded while a screen lists them, and frames
224 // keep asking for that exclusion (fenced by its revision) until a
225 // state says it is gone.
226 sess.excl = nil
227 }
228 layers, attached := ex.layers, ex.attached
229 ex.layers, ex.attached = nil, nil
230 for _, a := range attached {
231 a.finish(imagecapture.NeferwlCaptureLayerV1FailureExclusionEnded, imagecapture.NeferwlCaptureLayerV1DetachReasonExclusionEnded)
232 }
233 for _, l := range layers {
234 l.capture, l.attachment = nil, nil
235 // Closing unmaps the surface: the LayerChanged that removes it is
236 // queued before the CaptureExclusionEnd below.
237 l.close()
238 }
239 if notify && sess != nil {
240 s.log.Info().Uint64("session", sess.id).Msg("capture exclusion end")
241 s.emit(ports.CaptureExclusionEnd{Session: sess.id})
242 }
243}
244
245// confirm sends attached to the attachments core now lists: only then is it
246// safe for the client to show the layer.
247func (ex *exclusion) confirm(listed []ports.WindowID) {
248 for _, a := range ex.attached {
249 if !a.confirmed && slices.Contains(listed, a.layer.id) {
250 a.confirmed = true
251 if a.res.Alive() {
252 a.res.SendAttached()
253 }
254 }
255 }
256}
257
258// captureDetach drops a layer surface whose role is gone from its exclusion.
259// The caller unmapped it first, so the LayerChanged that removes the layer is
260// already queued: core keeps excluding the layer until it reads that.
261func (l *layerSurface) captureDetach() {
262 ex, att := l.capture, l.attachment
263 if ex == nil {
264 return
265 }
266 l.capture, l.attachment = nil, nil
267 ex.layers = removeItem(ex.layers, l)
268 ex.attached = removeItem(ex.attached, att)
269 att.finish(imagecapture.NeferwlCaptureLayerV1FailureLayerDestroyed, imagecapture.NeferwlCaptureLayerV1DetachReasonLayerDestroyed)
270 if !ex.ended && ex.session != nil {
271 ex.s.emit(ports.CaptureExclusionLayer{Session: ex.session.id, Layer: l.id})
272 }
273}