1package screencast
2
3import (
4 "errors"
5 "strconv"
6 "strings"
7
8 "github.com/bnema/zerobus"
9)
10
11// Object paths, interfaces and errors of the served contract.
12const (
13 pathScreenCast = "/org/gnome/Mutter/ScreenCast"
14 prefixSession = pathScreenCast + "/Session/u"
15 prefixStream = pathScreenCast + "/Stream/u"
16 pathDisplayConfig = "/org/gnome/Mutter/DisplayConfig"
17 pathIntrospect = "/org/gnome/Shell/Introspect"
18 pathScreenshot = "/org/gnome/Shell/Screenshot"
19
20 ifaceScreenCast = "org.gnome.Mutter.ScreenCast"
21 ifaceSession = ifaceScreenCast + ".Session"
22 ifaceStream = ifaceScreenCast + ".Stream"
23 ifaceDisplayConfig = "org.gnome.Mutter.DisplayConfig"
24 ifaceIntrospect = "org.gnome.Shell.Introspect"
25 ifaceScreenshot = "org.gnome.Shell.Screenshot"
26 ifaceProps = "org.freedesktop.DBus.Properties"
27 ifacePeer = "org.freedesktop.DBus.Peer"
28 ifaceIntrospectable = "org.freedesktop.DBus.Introspectable"
29
30 errAccessDenied = "org.freedesktop.DBus.Error.AccessDenied"
31 errLimits = "org.freedesktop.DBus.Error.LimitsExceeded"
32 errNotSupported = "org.freedesktop.DBus.Error.NotSupported"
33 errInvalidArgs = "org.freedesktop.DBus.Error.InvalidArgs"
34 errUnknownObject = "org.freedesktop.DBus.Error.UnknownObject"
35 errUnknownProperty = "org.freedesktop.DBus.Error.UnknownProperty"
36 errUnknownInterface = "org.freedesktop.DBus.Error.UnknownInterface"
37 errPropertyReadOnly = "org.freedesktop.DBus.Error.PropertyReadOnly"
38 errFailed = "org.freedesktop.DBus.Error.Failed"
39)
40
41// objKind is the object a call is addressed to.
42type objKind uint8
43
44const (
45 objNone objKind = iota
46 objScreenCast
47 objSession
48 objStream
49 objDisplayConfig
50 objIntrospect
51 objScreenshot
52)
53
54// method is a call the service understands, after its (path, interface,
55// member) were resolved.
56type method uint8
57
58const (
59 mNotSupported method = iota
60 mCreateSession
61 mStart
62 mStop
63 mRecordMonitor
64 mRecordWindow
65 mGet
66 mGetAll
67 mSet
68 mPing
69 mIntrospect
70 mGetState
71 mGetWindows
72 mShot
73 mShotArea
74 mShotInteractive
75 mUnknownObject
76)
77
78// request is a decoded method call. Every value is copied out of the receive
79// buffer with a length cap, so the request can wait for the caller check and
80// the reply can be built after other messages were read.
81type request struct {
82 sender string
83 serial uint32
84 noReply bool
85 obj objKind
86 id uint32 // session or stream number from the path
87 method method
88 bad bool // the body does not match the method
89
90 text, text2 string // connector; Properties interface and name
91 window uint64
92 cursor uint32
93 remote bool // CreateSession: remote-desktop-session-id given
94 x, y, w, h int32
95 flag bool
96}
97
98// privileged reports the calls that read the caller's executable again.
99func (r *request) privileged() bool {
100 switch r.method {
101 case mCreateSession, mStart, mShot, mShotArea, mShotInteractive:
102 return true
103 }
104 return false
105}
106
107// capStr copies s, cut to maxArg bytes.
108func capStr(s string) string {
109 if len(s) > maxArg {
110 s = s[:maxArg]
111 }
112 return strings.Clone(s)
113}
114
115// objectOf finds the object of a path. A served object needs its bus name.
116func (s *Service) objectOf(path string) (objKind, uint32) {
117 switch path {
118 case pathScreenCast:
119 return objScreenCast, 0
120 case pathDisplayConfig:
121 return objDisplayConfig, 0
122 case pathIntrospect:
123 return objIntrospect, 0
124 case pathScreenshot:
125 return objScreenshot, 0
126 }
127 if n, ok := strings.CutPrefix(path, prefixSession); ok {
128 id, err := strconv.ParseUint(n, 10, 32)
129 return objSession, uint32(id) | boolBit(err != nil)
130 }
131 if n, ok := strings.CutPrefix(path, prefixStream); ok {
132 id, err := strconv.ParseUint(n, 10, 32)
133 return objStream, uint32(id) | boolBit(err != nil)
134 }
135 return objNone, 0
136}
137
138// boolBit marks an unparsable number with an id no object has.
139func boolBit(bad bool) uint32 {
140 if bad {
141 return 0xffffffff
142 }
143 return 0
144}
145
146// served reports whether the bus name of the object is ours.
147func (s *Service) served(k objKind) bool {
148 switch k {
149 case objScreenCast, objSession, objStream:
150 return s.owns[0]
151 case objDisplayConfig:
152 return s.owns[1]
153 case objIntrospect:
154 return s.owns[2]
155 case objScreenshot:
156 return s.owns[3]
157 }
158 return false
159}
160
161// decode reads a call completely: nothing of m is used after it returns.
162func (s *Service) decode(m *zerobus.Message) request {
163 req := request{sender: strings.Clone(m.Sender), serial: m.Serial, noReply: m.Flags&zerobus.FlagNoReplyExpected != 0}
164 req.obj, req.id = s.objectOf(m.Path)
165 if !s.served(req.obj) {
166 req.method = mUnknownObject
167 return req
168 }
169 r := m.Body()
170 sig := m.Signature
171 want := func(w string) bool {
172 if sig != w {
173 req.bad = true
174 return false
175 }
176 return true
177 }
178 switch m.Interface {
179 case ifaceProps:
180 switch m.Member {
181 case "Get":
182 req.method = mGet
183 if want("ss") {
184 req.text, req.text2 = capStr(r.Str()), capStr(r.Str())
185 }
186 case "GetAll":
187 req.method = mGetAll
188 if want("s") {
189 req.text = capStr(r.Str())
190 }
191 case "Set":
192 req.method = mSet
193 }
194 case ifacePeer:
195 if m.Member == "Ping" {
196 req.method = mPing
197 want("")
198 }
199 case ifaceIntrospectable:
200 if m.Member == "Introspect" {
201 req.method = mIntrospect
202 want("")
203 }
204 case ifaceScreenCast:
205 if req.obj == objScreenCast && m.Member == "CreateSession" {
206 req.method = mCreateSession
207 if want("a{sv}") {
208 s.readProps(r, &req)
209 }
210 }
211 case ifaceSession:
212 if req.obj == objSession {
213 switch m.Member {
214 case "Start":
215 req.method = mStart
216 want("")
217 case "Stop":
218 req.method = mStop
219 want("")
220 case "RecordMonitor":
221 req.method = mRecordMonitor
222 if want("sa{sv}") {
223 req.text = capStr(r.Str())
224 s.readProps(r, &req)
225 }
226 case "RecordWindow":
227 req.method = mRecordWindow
228 if want("a{sv}") {
229 s.readProps(r, &req)
230 }
231 }
232 }
233 case ifaceDisplayConfig:
234 if req.obj == objDisplayConfig && m.Member == "GetCurrentState" {
235 req.method = mGetState
236 want("")
237 }
238 case ifaceIntrospect:
239 if req.obj == objIntrospect && m.Member == "GetWindows" {
240 req.method = mGetWindows
241 want("")
242 }
243 case ifaceScreenshot:
244 if req.obj == objScreenshot {
245 switch m.Member {
246 case "Screenshot":
247 req.method = mShot
248 if want("bbs") {
249 _ = r.Bool()
250 req.flag = r.Bool()
251 _ = r.Str()
252 }
253 case "ScreenshotArea":
254 req.method = mShotArea
255 if want("iiiibs") {
256 req.x, req.y, req.w, req.h = r.Int32(), r.Int32(), r.Int32(), r.Int32()
257 _ = r.Bool()
258 _ = r.Str()
259 }
260 case "InteractiveScreenshot":
261 req.method = mShotInteractive
262 want("")
263 }
264 }
265 }
266 if r.Err() != nil {
267 req.bad = true
268 }
269 return req
270}
271
272// readProps reads an a{sv} and keeps the keys the service knows.
273func (s *Service) readProps(r *zerobus.Reader, req *request) {
274 end := r.Array('{')
275 for r.More(end) {
276 r.Struct()
277 key := r.Str()
278 sig := r.Variant()
279 switch {
280 case key == "cursor-mode" && sig == "u":
281 req.cursor = r.Uint32()
282 case key == "window-id" && sig == "t":
283 req.window = r.Uint64()
284 case key == "remote-desktop-session-id":
285 req.remote = true
286 r.Skip(sig)
287 default:
288 r.Skip(sig)
289 }
290 }
291}
292
293// onMessage handles one message from the bus.
294func (s *Service) onMessage(m *zerobus.Message) {
295 switch m.Type {
296 case zerobus.TypeMethodCall:
297 if m.Sender != "" {
298 req := s.decode(m)
299 s.admit(&req)
300 }
301 case zerobus.TypeMethodReturn, zerobus.TypeError:
302 if m.Sender == busDaemon {
303 s.onBusReply(m)
304 }
305 case zerobus.TypeSignal:
306 if m.Sender == busDaemon && m.Interface == busDaemon && m.Member == "NameOwnerChanged" {
307 r := m.Body()
308 name := r.Str()
309 _ = r.Str()
310 owner := r.Str()
311 if r.Err() == nil && owner == "" && strings.HasPrefix(name, ":") {
312 s.onGone(name)
313 }
314 }
315 }
316}
317
318// send sends the message that was built. A broken connection ends the
319// attempt; any other failure drops that one message.
320func (s *Service) send() {
321 if _, err := s.conn.Send(); err != nil {
322 if errors.Is(err, zerobus.ErrClosed) {
323 s.fatal = err
324 return
325 }
326 s.log.Warn().Err(err).Msg("message not sent")
327 }
328}
329
330// replyTo builds the reply of req, or nil when the caller wants none.
331func (s *Service) replyTo(req *request, sig string) *zerobus.Encoder {
332 if req.noReply {
333 return nil
334 }
335 return s.conn.NewReply(&zerobus.Message{Sender: req.sender, Serial: req.serial}, sig)
336}
337
338// reply sends an empty reply.
339func (s *Service) reply(req *request) {
340 if s.replyTo(req, "") != nil {
341 s.send()
342 }
343}
344
345// replyErr sends an error reply.
346func (s *Service) replyErr(req *request, name, text string) {
347 if req.noReply {
348 return
349 }
350 s.conn.NewError(&zerobus.Message{Sender: req.sender, Serial: req.serial}, name, "s").Str(text)
351 s.send()
352}
353
354// emit sends a signal to one peer.
355func (s *Service) emitTo(dest, path, iface, member string) *zerobus.Encoder {
356 return s.conn.NewSignalTo(dest, path, iface, member, "")
357}
358
359// dispatch runs a call of a checked caller.
360func (s *Service) dispatch(c *caller, req *request) {
361 if !s.allowed(c.exe) {
362 s.deny(req)
363 return
364 }
365 if req.privileged() && !s.recheck(c) {
366 s.deny(req)
367 return
368 }
369 switch {
370 case req.method == mUnknownObject:
371 s.replyErr(req, errUnknownObject, "no such object")
372 return
373 case req.bad:
374 s.replyErr(req, errInvalidArgs, "unexpected arguments")
375 return
376 }
377 var sess *session
378 var st *stream
379 switch req.obj {
380 case objSession:
381 if sess = s.sessionByID(req.id); sess == nil {
382 s.replyErr(req, errUnknownObject, "no such session")
383 return
384 }
385 case objStream:
386 if st = s.streamByID(req.id); st == nil {
387 s.replyErr(req, errUnknownObject, "no such stream")
388 return
389 }
390 sess = st.sess
391 }
392 if sess != nil && sess.owner != req.sender {
393 s.deny(req)
394 return
395 }
396 switch req.method {
397 case mPing:
398 s.reply(req)
399 case mIntrospect:
400 if e := s.replyTo(req, "s"); e != nil {
401 e.Str("<node/>")
402 s.send()
403 }
404 case mGet, mGetAll, mSet:
405 s.properties(req, sess, st)
406 case mCreateSession:
407 s.createSession(req)
408 case mStart:
409 s.start(req, sess)
410 case mStop:
411 s.reply(req)
412 s.closeSession(sess, true)
413 case mRecordMonitor, mRecordWindow:
414 s.record(req, sess)
415 case mGetState:
416 s.replyState(req)
417 case mGetWindows:
418 s.replyWindows(req)
419 case mShot, mShotArea, mShotInteractive:
420 s.screenshot(req)
421 default:
422 s.replyErr(req, errNotSupported, "not supported")
423 }
424}
425
426// deny refuses a call.
427func (s *Service) deny(req *request) {
428 s.replyErr(req, errAccessDenied, "caller not allowed to capture the screen")
429}