// Copyright 2015 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Integration with the systemd D-Bus API. See http://www.freedesktop.org/wiki/Software/systemd/dbus/ package dbus import ( "encoding/hex" "fmt" "os" "strconv" "strings" "sync" "github.com/godbus/dbus" ) const ( alpha = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` num = `0123456789` alphanum = alpha + num signalBuffer = 100 ) // needsEscape checks whether a byte in a potential dbus ObjectPath needs to be escaped func needsEscape(i int, b byte) bool { // Escape everything that is not a-z-A-Z-0-9 // Also escape 0-9 if it's the first character return strings.IndexByte(alphanum, b) == -1 || (i == 0 && strings.IndexByte(num, b) != -1) } // PathBusEscape sanitizes a constituent string of a dbus ObjectPath using the // rules that systemd uses for serializing special characters. func PathBusEscape(path string) string { // Special case the empty string if len(path) == 0 { return "_" } n := []byte{} for i := 0; i < len(path); i++ { c := path[i] if needsEscape(i, c) { e := fmt.Sprintf("_%x", c) n = append(n, []byte(e)...) } else { n = append(n, c) } } return string(n) } // pathBusUnescape is the inverse of PathBusEscape. func pathBusUnescape(path string) string { if path == "_" { return "" } n := []byte{} for i := 0; i < len(path); i++ { c := path[i] if c == '_' && i+2 < len(path) { res, err := hex.DecodeString(path[i+1 : i+3]) if err == nil { n = append(n, res...) } i += 2 } else { n = append(n, c) } } return string(n) } // Conn is a connection to systemd's dbus endpoint. type Conn struct { // sysconn/sysobj are only used to call dbus methods sysconn *dbus.Conn sysobj dbus.BusObject // sigconn/sigobj are only used to receive dbus signals sigconn *dbus.Conn sigobj dbus.BusObject jobListener struct { jobs map[dbus.ObjectPath]chan<- string sync.Mutex } subStateSubscriber struct { updateCh chan<- *SubStateUpdate errCh chan<- error sync.Mutex ignore map[dbus.ObjectPath]int64 cleanIgnore int64 } propertiesSubscriber struct { updateCh chan<- *PropertiesUpdate errCh chan<- error sync.Mutex } } // New establishes a connection to any available bus and authenticates. // Callers should call Close() when done with the connection. func New() (*Conn, error) { conn, err := NewSystemConnection() if err != nil && os.Geteuid() == 0 { return NewSystemdConnection() } return conn, err } // NewSystemConnection establishes a connection to the system bus and authenticates. // Callers should call Close() when done with the connection func NewSystemConnection() (*Conn, error) { return NewConnection(func() (*dbus.Conn, error) { return dbusAuthHelloConnection(dbus.SystemBusPrivate) }) } // NewUserConnection establishes a connection to the session bus and // authenticates. This can be used to connect to systemd user instances. // Callers should call Close() when done with the connection. func NewUserConnection() (*Conn, error) { return NewConnection(func() (*dbus.Conn, error) { return dbusAuthHelloConnection(dbus.SessionBusPrivate) }) } // NewSystemdConnection establishes a private, direct connection to systemd. // This can be used for communicating with systemd without a dbus daemon. // Callers should call Close() when done with the connection. func NewSystemdConnection() (*Conn, error) { return NewConnection(func() (*dbus.Conn, error) { // We skip Hello when talking directly to systemd. return dbusAuthConnection(func(opts ...dbus.ConnOption) (*dbus.Conn, error) { return dbus.Dial("unix:path=/run/systemd/private") }) }) } // Close closes an established connection func (c *Conn) Close() { c.sysconn.Close() c.sigconn.Close() } // NewConnection establishes a connection to a bus using a caller-supplied function. // This allows connecting to remote buses through a user-supplied mechanism. // The supplied function may be called multiple times, and should return independent connections. // The returned connection must be fully initialised: the org.freedesktop.DBus.Hello call must have succeeded, // and any authentication should be handled by the function. func NewConnection(dialBus func() (*dbus.Conn, error)) (*Conn, error) { sysconn, err := dialBus() if err != nil { return nil, err } sigconn, err := dialBus() if err != nil { sysconn.Close() return nil, err } c := &Conn{ sysconn: sysconn, sysobj: systemdObject(sysconn), sigconn: sigconn, sigobj: systemdObject(sigconn), } c.subStateSubscriber.ignore = make(map[dbus.ObjectPath]int64) c.jobListener.jobs = make(map[dbus.ObjectPath]chan<- string) // Setup the listeners on jobs so that we can get completions c.sigconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, "type='signal', interface='org.freedesktop.systemd1.Manager', member='JobRemoved'") c.dispatch() return c, nil } // GetManagerProperty returns the value of a property on the org.freedesktop.systemd1.Manager // interface. The value is returned in its string representation, as defined at // https://developer.gnome.org/glib/unstable/gvariant-text.html func (c *Conn) GetManagerProperty(prop string) (string, error) { variant, err := c.sysobj.GetProperty("org.freedesktop.systemd1.Manager." + prop) if err != nil { return "", err } return variant.String(), nil } func dbusAuthConnection(createBus func(opts ...dbus.ConnOption) (*dbus.Conn, error)) (*dbus.Conn, error) { conn, err := createBus() if err != nil { return nil, err } // Only use EXTERNAL method, and hardcode the uid (not username) // to avoid a username lookup (which requires a dynamically linked // libc) methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} err = conn.Auth(methods) if err != nil { conn.Close() return nil, err } return conn, nil } func dbusAuthHelloConnection(createBus func(opts ...dbus.ConnOption) (*dbus.Conn, error)) (*dbus.Conn, error) { conn, err := dbusAuthConnection(createBus) if err != nil { return nil, err } if err = conn.Hello(); err != nil { conn.Close() return nil, err } return conn, nil } func systemdObject(conn *dbus.Conn) dbus.BusObject { return conn.Object("org.freedesktop.systemd1", dbus.ObjectPath("/org/freedesktop/systemd1")) }