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 | |
25 | extern int kdesuDebugArea(); |
26 | |
27 | namespace KDESu |
28 | { |
29 | class ClientPrivate |
30 | { |
31 | public: |
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 | |
45 | Client::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 | |
70 | Client::~Client() |
71 | { |
72 | if (d->sockfd >= 0) { |
73 | close(fd: d->sockfd); |
74 | } |
75 | } |
76 | |
77 | int 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 | |
160 | QByteArray 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 | |
181 | int 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 | |
211 | int 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 | |
221 | int 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 | |
240 | int Client::setHost(const QByteArray &host) |
241 | { |
242 | QByteArray cmd = "HOST " ; |
243 | cmd += escape(str: host); |
244 | cmd += '\n'; |
245 | return command(cmd); |
246 | } |
247 | |
248 | int 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 | |
257 | int 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 | |
266 | int 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 | } |
275 | int 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 | |
289 | QByteArray 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 | |
299 | QList<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 | |
328 | bool 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 | |
339 | int Client::delVar(const QByteArray &key) |
340 | { |
341 | QByteArray cmd = "DELV " ; |
342 | cmd += escape(str: key); |
343 | cmd += '\n'; |
344 | return command(cmd); |
345 | } |
346 | |
347 | int Client::delGroup(const QByteArray &group) |
348 | { |
349 | QByteArray cmd = "DELG " ; |
350 | cmd += escape(str: group); |
351 | cmd += '\n'; |
352 | return command(cmd); |
353 | } |
354 | |
355 | int 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 | |
363 | int Client::ping() |
364 | { |
365 | return command(cmd: "PING\n" ); |
366 | } |
367 | |
368 | int 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 | |
378 | int Client::stopServer() |
379 | { |
380 | return command(cmd: "STOP\n" ); |
381 | } |
382 | |
383 | static 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 | |
395 | int 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 | |