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