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