1package zerobus
2
3import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "io"
9 "net"
10 "os"
11 "strconv"
12 "strings"
13 "syscall"
14)
15
16const (
17 busName = "org.freedesktop.DBus"
18 busPath = "/org/freedesktop/DBus"
19 maxAuthLine = 512
20 defaultBufSz = 16 << 10
21)
22
23var (
24 // ErrClosed reports a read or send on a closed or broken connection.
25 ErrClosed = errors.New("zerobus: connection closed")
26 // ErrNested reports a Call or ReadMessage made from inside Conn.Handle.
27 ErrNested = errors.New("zerobus: Call or ReadMessage inside Handle")
28)
29
30// Error is a D-Bus error reply.
31type Error struct {
32 Name string // such as org.freedesktop.DBus.Error.ServiceUnknown
33 Message string // the first string of the body, if any
34}
35
36func (e *Error) Error() string {
37 if e.Message == "" {
38 return e.Name
39 }
40 return e.Name + ": " + e.Message
41}
42
43// Conn is a connection to a message bus.
44//
45// A Conn is not safe for concurrent use: read and send from one goroutine,
46// usually an event loop around ReadMessage. Close may be called from any
47// goroutine to stop a blocked read.
48type Conn struct {
49 c *net.UnixConn
50 serial uint32
51 name string
52
53 rbuf []byte // received bytes; rbuf[start:end] are not read yet
54 start, end int
55 msg Message
56 enc Encoder
57 err error // sticky: the connection is unusable after it
58 handling bool // inside Handle
59
60 // Handle receives the messages that arrive while Call waits for its
61 // reply. It may be nil; those messages are then dropped.
62 //
63 // Handle may answer or emit messages with NewReply, NewError,
64 // NewSignal and Send. It may not wait: Call and ReadMessage return
65 // ErrNested inside it.
66 Handle func(*Message)
67}
68
69// SessionBus connects to the session bus named by
70// DBUS_SESSION_BUS_ADDRESS, or $XDG_RUNTIME_DIR/bus when it is not set.
71func SessionBus() (*Conn, error) {
72 addr := os.Getenv("DBUS_SESSION_BUS_ADDRESS")
73 if addr == "" {
74 dir := os.Getenv("XDG_RUNTIME_DIR")
75 if dir == "" {
76 return nil, errors.New("zerobus: no session bus: DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are not set")
77 }
78 return dialSocket(dir + "/bus")
79 }
80 return Dial(addr)
81}
82
83// SystemBus connects to the system bus named by DBUS_SYSTEM_BUS_ADDRESS, or
84// the standard socket when it is not set.
85func SystemBus() (*Conn, error) {
86 addr := os.Getenv("DBUS_SYSTEM_BUS_ADDRESS")
87 if addr == "" {
88 addr = "unix:path=/run/dbus/system_bus_socket"
89 }
90 return Dial(addr)
91}
92
93// Dial connects to a bus address such as "unix:path=/run/user/1000/bus",
94// authenticates, and registers with the bus. Only unix:path and
95// unix:abstract addresses are supported; with several addresses separated by
96// ';', the first that connects is used.
97func Dial(address string) (*Conn, error) {
98 var errs []error
99 for a := range strings.SplitSeq(address, ";") {
100 if a == "" {
101 continue
102 }
103 c, err := dialOne(a)
104 if err == nil {
105 return c, nil
106 }
107 errs = append(errs, err)
108 }
109 if len(errs) == 0 {
110 return nil, fmt.Errorf("zerobus: empty bus address")
111 }
112 return nil, errors.Join(errs...)
113}
114
115func dialOne(addr string) (*Conn, error) {
116 transport, params, ok := strings.Cut(addr, ":")
117 if !ok || transport != "unix" {
118 return nil, fmt.Errorf("zerobus: unsupported address %q", addr)
119 }
120 var sock string
121 for kv := range strings.SplitSeq(params, ",") {
122 k, v, _ := strings.Cut(kv, "=")
123 v, err := unescape(v)
124 if err != nil {
125 return nil, fmt.Errorf("zerobus: address %q: %w", addr, err)
126 }
127 switch k {
128 case "path":
129 sock = v
130 case "abstract":
131 sock = "@" + v
132 }
133 }
134 if sock == "" {
135 return nil, fmt.Errorf("zerobus: address %q has no path or abstract socket", addr)
136 }
137 return dialSocket(sock)
138}
139
140// dialSocket connects to the Unix socket sock ("@name" for an abstract
141// one), authenticates, and registers with the bus.
142func dialSocket(sock string) (*Conn, error) {
143 uc, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: sock, Net: "unix"})
144 if err != nil {
145 return nil, fmt.Errorf("zerobus: %w", err)
146 }
147 c := &Conn{c: uc, rbuf: make([]byte, defaultBufSz)}
148 c.enc.buf = make([]byte, 0, defaultBufSz)
149 if err := c.auth(); err != nil {
150 return nil, errors.Join(err, uc.Close())
151 }
152 if err := c.hello(); err != nil {
153 return nil, errors.Join(err, uc.Close())
154 }
155 return c, nil
156}
157
158// unescape decodes the %xx escapes of a D-Bus address value.
159func unescape(s string) (string, error) {
160 if !strings.Contains(s, "%") {
161 return s, nil
162 }
163 var b strings.Builder
164 for i := 0; i < len(s); i++ {
165 if s[i] != '%' {
166 b.WriteByte(s[i])
167 continue
168 }
169 if i+2 >= len(s) {
170 return "", errors.New("bad escape")
171 }
172 v, err := strconv.ParseUint(s[i+1:i+3], 16, 8)
173 if err != nil {
174 return "", errors.New("bad escape")
175 }
176 b.WriteByte(byte(v))
177 i += 2
178 }
179 return b.String(), nil
180}
181
182// auth runs the SASL EXTERNAL handshake. The bus checks the peer
183// credentials of the socket against the user id sent here.
184func (c *Conn) auth() error {
185 uid := strconv.Itoa(os.Getuid())
186 line := fmt.Appendf(nil, "\x00AUTH EXTERNAL %x\r\n", uid)
187 if _, err := c.c.Write(line); err != nil {
188 return fmt.Errorf("zerobus: auth: %w", err)
189 }
190 reply, err := c.authLine()
191 if err != nil {
192 return err
193 }
194 if !bytes.HasPrefix(reply, []byte("OK ")) {
195 return fmt.Errorf("zerobus: auth rejected: %q", reply)
196 }
197 if _, err := c.c.Write([]byte("BEGIN\r\n")); err != nil {
198 return fmt.Errorf("zerobus: auth: %w", err)
199 }
200 return nil
201}
202
203// authLine reads one line of the handshake. Bytes after it stay in rbuf for
204// the message reader.
205func (c *Conn) authLine() ([]byte, error) {
206 for {
207 if i := bytes.Index(c.rbuf[c.start:c.end], []byte("\r\n")); i >= 0 {
208 line := c.rbuf[c.start : c.start+i]
209 c.start += i + 2
210 return line, nil
211 }
212 if c.end-c.start >= maxAuthLine {
213 return nil, errors.New("zerobus: auth: line too long")
214 }
215 n, err := c.c.Read(c.rbuf[c.end:])
216 if err != nil {
217 return nil, fmt.Errorf("zerobus: auth: %w", err)
218 }
219 c.end += n
220 }
221}
222
223func (c *Conn) hello() error {
224 c.NewCall(busName, busPath, busName, "Hello", "")
225 m, err := c.Call()
226 if err != nil {
227 return fmt.Errorf("zerobus: hello: %w", err)
228 }
229 body := m.Body()
230 name := body.Str()
231 if body.Err() != nil {
232 return fmt.Errorf("zerobus: hello: %w", body.Err())
233 }
234 c.name = strings.Clone(name)
235 return nil
236}
237
238// UniqueName returns the name the bus gave this connection, such as ":1.42".
239func (c *Conn) UniqueName() string { return c.name }
240
241// Close closes the connection. A blocked ReadMessage returns ErrClosed.
242func (c *Conn) Close() error { return c.c.Close() }
243
244func (c *Conn) nextSerial() uint32 {
245 c.serial++
246 if c.serial == 0 {
247 c.serial = 1
248 }
249 return c.serial
250}
251
252// NewCall starts a method call and returns the encoder for its body, which
253// must match sig. Send it with Send or Call. Empty dest and iface are left
254// out of the header.
255//
256// The connection has one encoder: send a message before starting another.
257func (c *Conn) NewCall(dest, path, iface, member, sig string) *Encoder {
258 c.enc.begin(TypeMethodCall, c.nextSerial(), 0, dest, path, iface, member, "", sig)
259 return &c.enc
260}
261
262// NewSignal starts a signal and returns the encoder for its body.
263func (c *Conn) NewSignal(path, iface, member, sig string) *Encoder {
264 c.enc.begin(TypeSignal, c.nextSerial(), 0, "", path, iface, member, "", sig)
265 return &c.enc
266}
267
268// NewSignalTo starts a signal addressed to the single peer dest, a unique
269// name such as ":1.42" or a well-known name, and returns the encoder for its
270// body. Only dest receives it, whatever match rules other peers have. An
271// empty dest makes Send fail: use NewSignal to broadcast.
272func (c *Conn) NewSignalTo(dest, path, iface, member, sig string) *Encoder {
273 c.enc.begin(TypeSignal, c.nextSerial(), 0, dest, path, iface, member, "", sig)
274 if dest == "" {
275 c.enc.invalid()
276 }
277 return &c.enc
278}
279
280// NewMethodCall starts a method call like NewCall and also returns its
281// serial. A loop that only uses Send and ReadMessage keeps the serial to
282// recognize the reply later, as Message.ReplySerial, without blocking in Call.
283// The serial is the one Send returns for this message.
284func (c *Conn) NewMethodCall(dest, path, iface, member, sig string) (*Encoder, uint32) {
285 e := c.NewCall(dest, path, iface, member, sig)
286 return e, c.serial
287}
288
289// NewReply starts the reply to the method call m and returns the encoder for
290// its body.
291func (c *Conn) NewReply(m *Message, sig string) *Encoder {
292 c.enc.begin(TypeMethodReturn, c.nextSerial(), m.Serial, m.Sender, "", "", "", "", sig)
293 return &c.enc
294}
295
296// NewError starts an error reply to the method call m. Write a message
297// string as the body when sig is "s".
298func (c *Conn) NewError(m *Message, name, sig string) *Encoder {
299 c.enc.begin(TypeError, c.nextSerial(), m.Serial, m.Sender, "", "", "", name, sig)
300 return &c.enc
301}
302
303// Send sends the message built since the last NewCall, NewSignal, NewReply
304// or NewError and returns its serial.
305func (c *Conn) Send() (uint32, error) {
306 if c.err != nil {
307 return 0, c.err
308 }
309 b, err := c.enc.finish()
310 c.enc.body = 0
311 if err != nil {
312 return 0, err
313 }
314 if _, err := c.c.Write(b); err != nil {
315 c.err = closedErr(err)
316 return 0, c.err
317 }
318 return binary.LittleEndian.Uint32(b[8:]), nil
319}
320
321// Call sends the method call being built and waits for its reply. Messages
322// that arrive meanwhile go to Handle. An error reply returns an *Error.
323// A call flagged FlagNoReplyExpected gets no reply: send it with Send.
324//
325// The reply is valid until the next read, like ReadMessage's.
326func (c *Conn) Call() (*Message, error) {
327 if c.handling {
328 return nil, ErrNested
329 }
330 if c.enc.body != 0 && (Type(c.enc.buf[1]) != TypeMethodCall || Flags(c.enc.buf[2])&FlagNoReplyExpected != 0) {
331 c.enc.body = 0
332 return nil, ErrInvalid
333 }
334 serial, err := c.Send()
335 if err != nil {
336 return nil, err
337 }
338 for {
339 m, err := c.ReadMessage()
340 if err != nil {
341 return nil, err
342 }
343 if m.ReplySerial == serial && (m.Type == TypeMethodReturn || m.Type == TypeError) {
344 if m.Type == TypeError {
345 return m, replyError(m)
346 }
347 return m, nil
348 }
349 if c.Handle != nil {
350 c.handle(m)
351 }
352 }
353}
354
355func (c *Conn) handle(m *Message) {
356 c.handling = true
357 defer func() { c.handling = false }()
358 c.Handle(m)
359}
360
361// replyError builds an *Error from an error reply. It copies the strings so
362// the error stays valid; this is the only allocation of an error path.
363func replyError(m *Message) *Error {
364 e := &Error{Name: strings.Clone(m.ErrorName)}
365 if len(m.Signature) > 0 && m.Signature[0] == 's' {
366 if s := m.Body().Str(); s != "" {
367 e.Message = strings.Clone(s)
368 }
369 }
370 return e
371}
372
373// SyscallConn returns the raw socket of the connection, for a poll loop that
374// waits for it to become readable together with other file descriptors.
375// Only wait on the descriptor: read, write and close through the Conn.
376//
377// Bytes already read from the socket are not visible to poll: check Buffered
378// before waiting. After the descriptor turns readable, ReadMessage may still
379// block until the rest of a partially received message arrives.
380func (c *Conn) SyscallConn() (syscall.RawConn, error) { return c.c.SyscallConn() }
381
382// Buffered reports whether ReadMessage can return without waiting for the
383// socket: a complete message, or a sticky error, is already in the receive
384// buffer. A poll loop must call ReadMessage until Buffered is false before it
385// waits on the descriptor again, because ReadMessage reads everything the
386// socket has and a later poll would not see those bytes.
387func (c *Conn) Buffered() bool {
388 if c.err != nil {
389 return true
390 }
391 if c.end-c.start < headerLen {
392 return false
393 }
394 n, err := frameLen(c.rbuf[c.start:c.end])
395 return err != nil || c.end-c.start >= n
396}
397
398// ReadMessage blocks until the next message arrives. The message and its
399// strings are valid until the next call to ReadMessage or Call.
400func (c *Conn) ReadMessage() (*Message, error) {
401 if c.handling {
402 return nil, ErrNested
403 }
404 if c.err != nil {
405 return nil, c.err
406 }
407 if err := c.fill(headerLen); err != nil {
408 return nil, err
409 }
410 n, err := frameLen(c.rbuf[c.start:])
411 if err != nil {
412 c.err = err
413 return nil, err
414 }
415 if err := c.fill(n); err != nil {
416 return nil, err
417 }
418 b := c.rbuf[c.start : c.start+n : c.start+n]
419 c.start += n
420 if err := parse(b, &c.msg); err != nil {
421 c.err = err
422 return nil, err
423 }
424 return &c.msg, nil
425}
426
427// fill reads until rbuf[start:] holds at least n bytes. It moves unread
428// bytes to the front, which is safe because the previous message is no
429// longer valid once a new read starts.
430func (c *Conn) fill(n int) error {
431 if c.end-c.start >= n {
432 return nil
433 }
434 if c.start > 0 {
435 c.end = copy(c.rbuf, c.rbuf[c.start:c.end])
436 c.start = 0
437 }
438 if n > len(c.rbuf) {
439 grown := make([]byte, max(n, 2*len(c.rbuf)))
440 copy(grown, c.rbuf[:c.end])
441 c.rbuf = grown
442 }
443 for c.end < n {
444 k, err := c.c.Read(c.rbuf[c.end:])
445 c.end += k
446 if err != nil && c.end < n {
447 c.err = closedErr(err)
448 return c.err
449 }
450 }
451 return nil
452}
453
454func closedErr(err error) error {
455 if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
456 return ErrClosed
457 }
458 return fmt.Errorf("zerobus: %w", err)
459}
460
461// AddMatch asks the bus to send this connection the messages that match
462// rule, such as "type='signal',interface='org.kde.StatusNotifierItem'".
463func (c *Conn) AddMatch(rule string) error {
464 c.NewCall(busName, busPath, busName, "AddMatch", "s").Str(rule)
465 _, err := c.Call()
466 return err
467}
468
469// RemoveMatch removes a rule added by AddMatch.
470func (c *Conn) RemoveMatch(rule string) error {
471 c.NewCall(busName, busPath, busName, "RemoveMatch", "s").Str(rule)
472 _, err := c.Call()
473 return err
474}
475
476// Reply codes of RequestName.
477const (
478 NamePrimaryOwner = 1
479 NameInQueue = 2
480 NameExists = 3
481 NameAlreadyOwner = 4
482)
483
484// Flags of RequestName.
485const (
486 NameFlagAllowReplacement = 0x1
487 NameFlagReplaceExisting = 0x2
488 NameFlagDoNotQueue = 0x4
489)
490
491// RequestName asks the bus for a well-known name and returns its reply code,
492// such as NamePrimaryOwner.
493func (c *Conn) RequestName(name string, flags uint32) (uint32, error) {
494 e := c.NewCall(busName, busPath, busName, "RequestName", "su")
495 e.Str(name)
496 e.Uint32(flags)
497 m, err := c.Call()
498 if err != nil {
499 return 0, err
500 }
501 body := m.Body()
502 code := body.Uint32()
503 return code, body.Err()
504}