1/*
2 This file is part of the KDE project, module kdesu.
3 SPDX-FileCopyrightText: 1999, 2000 Geert Jansen <jansen@kde.org>
4
5 SPDX-License-Identifier: GPL-2.0-only
6
7 client.cpp: A client for kdesud.
8*/
9
10#include "client.h"
11
12#include <config-kdesu.h>
13#include <ksu_debug.h>
14
15#include <cerrno>
16#include <sys/socket.h>
17#include <sys/un.h>
18
19#include <QFile>
20#include <QProcess>
21#include <QRegularExpression>
22#include <QStandardPaths>
23#include <qplatformdefs.h>
24
25extern int kdesuDebugArea();
26
27namespace KDESu
28{
29class ClientPrivate
30{
31public:
32 ClientPrivate()
33 : sockfd(-1)
34 {
35 }
36 QString daemon;
37 int sockfd;
38 QByteArray sock;
39};
40
41#ifndef SUN_LEN
42#define SUN_LEN(ptr) ((QT_SOCKLEN_T)(((struct sockaddr_un *)0)->sun_path) + strlen((ptr)->sun_path))
43#endif
44
45Client::Client()
46 : d(new ClientPrivate)
47{
48#if HAVE_X11
49 QString display = QString::fromLocal8Bit(ba: qgetenv(varName: "DISPLAY"));
50 if (display.isEmpty()) {
51 // we might be on Wayland
52 display = QString::fromLocal8Bit(ba: qgetenv(varName: "WAYLAND_DISPLAY"));
53 }
54 if (display.isEmpty()) {
55 qCWarning(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
56 << "$DISPLAY is not set.";
57 return;
58 }
59
60 // strip the screen number from the display
61 display.remove(re: QRegularExpression(QStringLiteral("\\.[0-9]+$")));
62#else
63 QString display = QStringLiteral("NODISPLAY");
64#endif
65
66 d->sock = QFile::encodeName(fileName: QStandardPaths::writableLocation(type: QStandardPaths::RuntimeLocation) + QStringLiteral("/kdesud_") + display);
67 connect();
68}
69
70Client::~Client()
71{
72 if (d->sockfd >= 0) {
73 close(fd: d->sockfd);
74 }
75}
76
77int Client::connect()
78{
79 if (d->sockfd >= 0) {
80 close(fd: d->sockfd);
81 }
82 if (access(name: d->sock.constData(), R_OK | W_OK)) {
83 d->sockfd = -1;
84 return -1;
85 }
86
87 d->sockfd = socket(PF_UNIX, SOCK_STREAM, protocol: 0);
88 if (d->sockfd < 0) {
89 qCWarning(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
90 << "socket():" << strerror(errno);
91 return -1;
92 }
93 struct sockaddr_un addr;
94 addr.sun_family = AF_UNIX;
95 strcpy(dest: addr.sun_path, src: d->sock.constData());
96
97 if (QT_SOCKET_CONNECT(fd: d->sockfd, addr: (struct sockaddr *)&addr, SUN_LEN(&addr)) < 0) {
98 qCWarning(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
99 << "connect():" << strerror(errno);
100 close(fd: d->sockfd);
101 d->sockfd = -1;
102 return -1;
103 }
104
105#if !defined(SO_PEERCRED) || !HAVE_STRUCT_UCRED
106#if HAVE_GETPEEREID
107 uid_t euid;
108 gid_t egid;
109 // Security: if socket exists, we must own it
110 if (getpeereid(d->sockfd, &euid, &egid) == 0 && euid != getuid()) {
111 qCWarning(KSU_LOG) << "socket not owned by me! socket uid =" << euid;
112 close(d->sockfd);
113 d->sockfd = -1;
114 return -1;
115 }
116#else
117#ifdef __GNUC__
118#warning "Using sloppy security checks"
119#endif
120 // We check the owner of the socket after we have connected.
121 // If the socket was somehow not ours an attacker will be able
122 // to delete it after we connect but shouldn't be able to
123 // create a socket that is owned by us.
124 QT_STATBUF s;
125 if (QT_LSTAT(d->sock.constData(), &s) != 0) {
126 qCWarning(KSU_LOG) << "stat failed (" << d->sock << ")";
127 close(d->sockfd);
128 d->sockfd = -1;
129 return -1;
130 }
131 if (s.st_uid != getuid()) {
132 qCWarning(KSU_LOG) << "socket not owned by me! socket uid =" << s.st_uid;
133 close(d->sockfd);
134 d->sockfd = -1;
135 return -1;
136 }
137 if (!S_ISSOCK(s.st_mode)) {
138 qCWarning(KSU_LOG) << "socket is not a socket (" << d->sock << ")";
139 close(d->sockfd);
140 d->sockfd = -1;
141 return -1;
142 }
143#endif
144#else
145 struct ucred cred;
146 QT_SOCKLEN_T siz = sizeof(cred);
147
148 // Security: if socket exists, we must own it
149 if (getsockopt(fd: d->sockfd, SOL_SOCKET, SO_PEERCRED, optval: &cred, optlen: &siz) == 0 && cred.uid != getuid()) {
150 qCWarning(KSU_LOG) << "socket not owned by me! socket uid =" << cred.uid;
151 close(fd: d->sockfd);
152 d->sockfd = -1;
153 return -1;
154 }
155#endif
156
157 return 0;
158}
159
160QByteArray Client::escape(const QByteArray &str)
161{
162 QByteArray copy;
163 copy.reserve(asize: str.size() + 4);
164 copy.append(c: '"');
165 for (const uchar c : str) {
166 if (c < 32) {
167 copy.append(c: '\\');
168 copy.append(c: '^');
169 copy.append(c: c + '@');
170 } else {
171 if (c == '\\' || c == '"') {
172 copy.append(c: '\\');
173 }
174 copy.append(c);
175 }
176 }
177 copy.append(c: '"');
178 return copy;
179}
180
181int Client::command(const QByteArray &cmd, QByteArray *result)
182{
183 if (d->sockfd < 0) {
184 return -1;
185 }
186
187 if (send(fd: d->sockfd, buf: cmd.constData(), n: cmd.length(), flags: 0) != (int)cmd.length()) {
188 return -1;
189 }
190
191 char buf[1024];
192 int nbytes = recv(fd: d->sockfd, buf: buf, n: 1023, flags: 0);
193 if (nbytes <= 0) {
194 qCWarning(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
195 << "no reply from daemon.";
196 return -1;
197 }
198 buf[nbytes] = '\000';
199
200 QByteArray reply = buf;
201 if (reply.left(len: 2) != "OK") {
202 return -1;
203 }
204
205 if (result) {
206 *result = reply.mid(index: 3, len: reply.length() - 4);
207 }
208 return 0;
209}
210
211int Client::setPass(const char *pass, int timeout)
212{
213 QByteArray cmd = "PASS ";
214 cmd += escape(str: pass);
215 cmd += ' ';
216 cmd += QByteArray().setNum(n: timeout);
217 cmd += '\n';
218 return command(cmd);
219}
220
221int Client::exec(const QByteArray &prog, const QByteArray &user, const QByteArray &options, const QList<QByteArray> &env)
222{
223 QByteArray cmd;
224 cmd = "EXEC ";
225 cmd += escape(str: prog);
226 cmd += ' ';
227 cmd += escape(str: user);
228 if (!options.isEmpty() || !env.isEmpty()) {
229 cmd += ' ';
230 cmd += escape(str: options);
231 for (const auto &var : env) {
232 cmd += ' ';
233 cmd += escape(str: var);
234 }
235 }
236 cmd += '\n';
237 return command(cmd);
238}
239
240int Client::setHost(const QByteArray &host)
241{
242 QByteArray cmd = "HOST ";
243 cmd += escape(str: host);
244 cmd += '\n';
245 return command(cmd);
246}
247
248int Client::setPriority(int prio)
249{
250 QByteArray cmd;
251 cmd += "PRIO ";
252 cmd += QByteArray::number(prio);
253 cmd += '\n';
254 return command(cmd);
255}
256
257int Client::setScheduler(int sched)
258{
259 QByteArray cmd;
260 cmd += "SCHD ";
261 cmd += QByteArray::number(sched);
262 cmd += '\n';
263 return command(cmd);
264}
265
266int Client::delCommand(const QByteArray &key, const QByteArray &user)
267{
268 QByteArray cmd = "DEL ";
269 cmd += escape(str: key);
270 cmd += ' ';
271 cmd += escape(str: user);
272 cmd += '\n';
273 return command(cmd);
274}
275int Client::setVar(const QByteArray &key, const QByteArray &value, int timeout, const QByteArray &group)
276{
277 QByteArray cmd = "SET ";
278 cmd += escape(str: key);
279 cmd += ' ';
280 cmd += escape(str: value);
281 cmd += ' ';
282 cmd += escape(str: group);
283 cmd += ' ';
284 cmd += QByteArray().setNum(n: timeout);
285 cmd += '\n';
286 return command(cmd);
287}
288
289QByteArray Client::getVar(const QByteArray &key)
290{
291 QByteArray cmd = "GET ";
292 cmd += escape(str: key);
293 cmd += '\n';
294 QByteArray reply;
295 command(cmd, result: &reply);
296 return reply;
297}
298
299QList<QByteArray> Client::getKeys(const QByteArray &group)
300{
301 QByteArray cmd = "GETK ";
302 cmd += escape(str: group);
303 cmd += '\n';
304 QByteArray reply;
305 command(cmd, result: &reply);
306 int index = 0;
307 int pos;
308 QList<QByteArray> list;
309 if (!reply.isEmpty()) {
310 while (1) {
311 pos = reply.indexOf(c: '\007', from: index);
312 if (pos == -1) {
313 if (index == 0) {
314 list.append(t: reply);
315 } else {
316 list.append(t: reply.mid(index));
317 }
318 break;
319 } else {
320 list.append(t: reply.mid(index, len: pos - index));
321 }
322 index = pos + 1;
323 }
324 }
325 return list;
326}
327
328bool Client::findGroup(const QByteArray &group)
329{
330 QByteArray cmd = "CHKG ";
331 cmd += escape(str: group);
332 cmd += '\n';
333 if (command(cmd) == -1) {
334 return false;
335 }
336 return true;
337}
338
339int Client::delVar(const QByteArray &key)
340{
341 QByteArray cmd = "DELV ";
342 cmd += escape(str: key);
343 cmd += '\n';
344 return command(cmd);
345}
346
347int Client::delGroup(const QByteArray &group)
348{
349 QByteArray cmd = "DELG ";
350 cmd += escape(str: group);
351 cmd += '\n';
352 return command(cmd);
353}
354
355int Client::delVars(const QByteArray &special_key)
356{
357 QByteArray cmd = "DELS ";
358 cmd += escape(str: special_key);
359 cmd += '\n';
360 return command(cmd);
361}
362
363int Client::ping()
364{
365 return command(cmd: "PING\n");
366}
367
368int Client::exitCode()
369{
370 QByteArray result;
371 if (command(cmd: "EXIT\n", result: &result) != 0) {
372 return -1;
373 }
374
375 return result.toInt();
376}
377
378int Client::stopServer()
379{
380 return command(cmd: "STOP\n");
381}
382
383static QString findDaemon()
384{
385 QString daemon = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kdesud");
386 if (!QFile::exists(fileName: daemon)) { // if not in libexec, find it in PATH
387 daemon = QStandardPaths::findExecutable(QStringLiteral("kdesud"));
388 if (daemon.isEmpty()) {
389 qCWarning(KSU_LOG) << "kdesud daemon not found.";
390 }
391 }
392 return daemon;
393}
394
395int Client::startServer()
396{
397 if (d->daemon.isEmpty()) {
398 d->daemon = findDaemon();
399 }
400 if (d->daemon.isEmpty()) {
401 return -1;
402 }
403
404 QProcess proc;
405 proc.start(program: d->daemon, arguments: QStringList{});
406 if (!proc.waitForFinished()) {
407 qCCritical(KSU_LOG) << "Couldn't start kdesud!";
408 return -1;
409 }
410
411 connect();
412 return proc.exitCode();
413}
414
415} // namespace KDESu
416

source code of kdesu/src/client.cpp