1// Copyright 2015 CoreOS, Inc. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Integration with the systemd D-Bus API. See http://www.freedesktop.org/wiki/Software/systemd/dbus/ 16package dbus 17 18import ( 19 "encoding/hex" 20 "fmt" 21 "os" 22 "strconv" 23 "strings" 24 "sync" 25 26 "github.com/godbus/dbus" 27) 28 29const ( 30 alpha = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` 31 num = `0123456789` 32 alphanum = alpha + num 33 signalBuffer = 100 34) 35 36// needsEscape checks whether a byte in a potential dbus ObjectPath needs to be escaped 37func needsEscape(i int, b byte) bool { 38 // Escape everything that is not a-z-A-Z-0-9 39 // Also escape 0-9 if it's the first character 40 return strings.IndexByte(alphanum, b) == -1 || 41 (i == 0 && strings.IndexByte(num, b) != -1) 42} 43 44// PathBusEscape sanitizes a constituent string of a dbus ObjectPath using the 45// rules that systemd uses for serializing special characters. 46func PathBusEscape(path string) string { 47 // Special case the empty string 48 if len(path) == 0 { 49 return "_" 50 } 51 n := []byte{} 52 for i := 0; i < len(path); i++ { 53 c := path[i] 54 if needsEscape(i, c) { 55 e := fmt.Sprintf("_%x", c) 56 n = append(n, []byte(e)...) 57 } else { 58 n = append(n, c) 59 } 60 } 61 return string(n) 62} 63 64// pathBusUnescape is the inverse of PathBusEscape. 65func pathBusUnescape(path string) string { 66 if path == "_" { 67 return "" 68 } 69 n := []byte{} 70 for i := 0; i < len(path); i++ { 71 c := path[i] 72 if c == '_' && i+2 < len(path) { 73 res, err := hex.DecodeString(path[i+1 : i+3]) 74 if err == nil { 75 n = append(n, res...) 76 } 77 i += 2 78 } else { 79 n = append(n, c) 80 } 81 } 82 return string(n) 83} 84 85// Conn is a connection to systemd's dbus endpoint. 86type Conn struct { 87 // sysconn/sysobj are only used to call dbus methods 88 sysconn *dbus.Conn 89 sysobj dbus.BusObject 90 91 // sigconn/sigobj are only used to receive dbus signals 92 sigconn *dbus.Conn 93 sigobj dbus.BusObject 94 95 jobListener struct { 96 jobs map[dbus.ObjectPath]chan<- string 97 sync.Mutex 98 } 99 subStateSubscriber struct { 100 updateCh chan<- *SubStateUpdate 101 errCh chan<- error 102 sync.Mutex 103 ignore map[dbus.ObjectPath]int64 104 cleanIgnore int64 105 } 106 propertiesSubscriber struct { 107 updateCh chan<- *PropertiesUpdate 108 errCh chan<- error 109 sync.Mutex 110 } 111} 112 113// New establishes a connection to any available bus and authenticates. 114// Callers should call Close() when done with the connection. 115func New() (*Conn, error) { 116 conn, err := NewSystemConnection() 117 if err != nil && os.Geteuid() == 0 { 118 return NewSystemdConnection() 119 } 120 return conn, err 121} 122 123// NewSystemConnection establishes a connection to the system bus and authenticates. 124// Callers should call Close() when done with the connection 125func NewSystemConnection() (*Conn, error) { 126 return NewConnection(func() (*dbus.Conn, error) { 127 return dbusAuthHelloConnection(dbus.SystemBusPrivate) 128 }) 129} 130 131// NewUserConnection establishes a connection to the session bus and 132// authenticates. This can be used to connect to systemd user instances. 133// Callers should call Close() when done with the connection. 134func NewUserConnection() (*Conn, error) { 135 return NewConnection(func() (*dbus.Conn, error) { 136 return dbusAuthHelloConnection(dbus.SessionBusPrivate) 137 }) 138} 139 140// NewSystemdConnection establishes a private, direct connection to systemd. 141// This can be used for communicating with systemd without a dbus daemon. 142// Callers should call Close() when done with the connection. 143func NewSystemdConnection() (*Conn, error) { 144 return NewConnection(func() (*dbus.Conn, error) { 145 // We skip Hello when talking directly to systemd. 146 return dbusAuthConnection(func() (*dbus.Conn, error) { 147 return dbus.Dial("unix:path=/run/systemd/private") 148 }) 149 }) 150} 151 152// Close closes an established connection 153func (c *Conn) Close() { 154 c.sysconn.Close() 155 c.sigconn.Close() 156} 157 158// NewConnection establishes a connection to a bus using a caller-supplied function. 159// This allows connecting to remote buses through a user-supplied mechanism. 160// The supplied function may be called multiple times, and should return independent connections. 161// The returned connection must be fully initialised: the org.freedesktop.DBus.Hello call must have succeeded, 162// and any authentication should be handled by the function. 163func NewConnection(dialBus func() (*dbus.Conn, error)) (*Conn, error) { 164 sysconn, err := dialBus() 165 if err != nil { 166 return nil, err 167 } 168 169 sigconn, err := dialBus() 170 if err != nil { 171 sysconn.Close() 172 return nil, err 173 } 174 175 c := &Conn{ 176 sysconn: sysconn, 177 sysobj: systemdObject(sysconn), 178 sigconn: sigconn, 179 sigobj: systemdObject(sigconn), 180 } 181 182 c.subStateSubscriber.ignore = make(map[dbus.ObjectPath]int64) 183 c.jobListener.jobs = make(map[dbus.ObjectPath]chan<- string) 184 185 // Setup the listeners on jobs so that we can get completions 186 c.sigconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, 187 "type='signal', interface='org.freedesktop.systemd1.Manager', member='JobRemoved'") 188 189 c.dispatch() 190 return c, nil 191} 192 193// GetManagerProperty returns the value of a property on the org.freedesktop.systemd1.Manager 194// interface. The value is returned in its string representation, as defined at 195// https://developer.gnome.org/glib/unstable/gvariant-text.html 196func (c *Conn) GetManagerProperty(prop string) (string, error) { 197 variant, err := c.sysobj.GetProperty("org.freedesktop.systemd1.Manager." + prop) 198 if err != nil { 199 return "", err 200 } 201 return variant.String(), nil 202} 203 204func dbusAuthConnection(createBus func() (*dbus.Conn, error)) (*dbus.Conn, error) { 205 conn, err := createBus() 206 if err != nil { 207 return nil, err 208 } 209 210 // Only use EXTERNAL method, and hardcode the uid (not username) 211 // to avoid a username lookup (which requires a dynamically linked 212 // libc) 213 methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} 214 215 err = conn.Auth(methods) 216 if err != nil { 217 conn.Close() 218 return nil, err 219 } 220 221 return conn, nil 222} 223 224func dbusAuthHelloConnection(createBus func() (*dbus.Conn, error)) (*dbus.Conn, error) { 225 conn, err := dbusAuthConnection(createBus) 226 if err != nil { 227 return nil, err 228 } 229 230 if err = conn.Hello(); err != nil { 231 conn.Close() 232 return nil, err 233 } 234 235 return conn, nil 236} 237 238func systemdObject(conn *dbus.Conn) dbus.BusObject { 239 return conn.Object("org.freedesktop.systemd1", dbus.ObjectPath("/org/freedesktop/systemd1")) 240} 241