1 /* BEGIN_COMMON_COPYRIGHT_HEADER
2 * (c)LGPL2+
3 *
4 * LXQt - a lightweight, Qt based, desktop toolset
5 * https://lxqt.org
6 *
7 * Copyright: 2015-2018 LXQt team
8 * Authors:
9 * Palo Kisa <palo.kisa@gmail.com>
10 *
11 * This program or library is free software; you can redistribute it
12 * and/or modify it under the terms of the GNU Lesser General Public
13 * License as published by the Free Software Foundation; either
14 * version 2.1 of the License, or (at your option) any later version.
15 *
16 * This library is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 * Lesser General Public License for more details.
20
21 * You should have received a copy of the GNU Lesser General
22 * Public License along with this library; if not, write to the
23 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24 * Boston, MA 02110-1301 USA
25 *
26 * END_COMMON_COPYRIGHT_HEADER */
27
28 #include "sudo.h"
29 #include "passworddialog.h"
30
31 #include <LXQt/Application>
32
33 #include <QTextStream>
34 #include <QMessageBox>
35 #include <QFileInfo>
36 #include <QSocketNotifier>
37 #include <QDebug>
38 #include <QThread>
39 #include <QProcessEnvironment>
40 #include <QTimer>
41 #include <QRegularExpression>
42 #if defined(Q_OS_LINUX)
43 #include <pty.h>
44 #else
45 #include <errno.h>
46 #include <termios.h>
47 #include <libutil.h>
48 #endif
49 #include <unistd.h>
50 #include <memory>
51 #include <csignal>
52 #include <sys/wait.h>
53 #include <fcntl.h>
54 #include <iostream>
55 #include <thread>
56 #include <sstream>
57
58 namespace
59 {
60 const QString app_master{QStringLiteral(LXQTSUDO)};
61 const QString app_version{QStringLiteral(LXQT_VERSION)};
62 const QString app_lxsu{QStringLiteral(LXQTSUDO_LXSU)};
63 const QString app_lxsudo{QStringLiteral(LXQTSUDO_LXSUDO)};
64
65 const QString su_prog{QStringLiteral(LXQTSUDO_SU)};
66 const QString sudo_prog{QStringLiteral(LXQTSUDO_SUDO)};
67 #ifdef __FreeBSD__
68 const QString pwd_prompt_end_c_locale{QStringLiteral(":")};
69 #endif
70 const QString pwd_prompt_end{QStringLiteral(": ")};
71
72 const QChar nl{QLatin1Char('\n')};
73
usage(QString const & err=QString ())74 void usage(QString const & err = QString())
75 {
76 if (!err.isEmpty())
77 QTextStream(stderr) << err << '\n';
78 QTextStream(stdout)
79 << QObject::tr("Usage: %1 option [command [arguments...]]\n\n"
80 "GUI frontend for %2/%3\n\n"
81 "Arguments:\n"
82 " option:\n"
83 " -h|--help Print this help.\n"
84 " -v|--version Print version information.\n"
85 " -s|--su Use %3(1) as backend.\n"
86 " -d|--sudo Use %2(8) as backend.\n"
87 " command Command to run.\n"
88 " arguments Optional arguments for command.\n\n").arg(app_master).arg(sudo_prog).arg(su_prog);
89 if (!err.isEmpty())
90 QMessageBox(QMessageBox::Critical, app_master, err, QMessageBox::Ok).exec();
91 }
92
version()93 void version()
94 {
95 QTextStream(stdout)
96 << QObject::tr("%1 version %2\n").arg(app_master).arg(app_version);
97 }
98
99 //Note: array must be sorted to allow usage of binary search
100 static constexpr char const * const ALLOWED_VARS[] = {
101 "DISPLAY"
102 , "LANG", "LANGUAGE", "LC_ADDRESS", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_IDENTIFICATION", "LC_MEASUREMENT"
103 , "LC_MESSAGES", "LC_MONETARY", "LC_NAME", "LC_NUMERIC", "LC_PAPER", "LC_TELEPHONE", "LC_TIME"
104 , "PATH", "QT_PLATFORM_PLUGIN", "QT_QPA_PLATFORMTHEME", "TERM", "WAYLAND_DISPLAY", "XAUTHLOCALHOSTNAME", "XAUTHORITY"
105 };
106 static constexpr char const * const * const ALLOWED_END = ALLOWED_VARS + sizeof (ALLOWED_VARS) / sizeof (ALLOWED_VARS[0]);
107 struct assert_helper
108 {
assert_helper__anona79a38b70111::assert_helper109 assert_helper()
110 {
111 Q_ASSERT(std::is_sorted(ALLOWED_VARS, ALLOWED_END
112 , [] (char const * const a, char const * const b) { return strcmp(a, b) < 0; }));
113 }
114 };
115 assert_helper h;
116
env_workarounds()117 inline std::string env_workarounds()
118 {
119 std::cerr << LXQTSUDO << ": Stripping child environment except for: ";
120 std::ostringstream left_env_params;
121 std::copy(ALLOWED_VARS, ALLOWED_END - 1, std::ostream_iterator<const char *>{left_env_params, ","});
122 left_env_params << *(ALLOWED_END - 1); // printing the last separately to avoid trailing comma
123 std::cerr << left_env_params.str() << '\n';
124 // cleanup environment, because e.g.:
125 // - pcmanfm-qt will not start if the DBUS_SESSION_BUS_ADDRESS is preserved
126 // - Qt apps may change user's config files permissions if the XDG_* are preserved
127 for (auto const & key : QProcessEnvironment::systemEnvironment().keys())
128 {
129 auto const & i = std::lower_bound(ALLOWED_VARS, ALLOWED_END, key, [] (char const * const a, QString const & b) {
130 return b > QLatin1String(a);
131 });
132 if (i == ALLOWED_END || key != QLatin1String(*i))
133 {
134 unsetenv(key.toLatin1().data());
135 }
136 }
137 return left_env_params.str();
138 }
139
quoteShellArg(const QString & arg,bool userFriendly)140 inline QString quoteShellArg(const QString& arg, bool userFriendly)
141 {
142 QString rv = arg;
143
144 //^ check if thre are any bash special file characters
145 if (!userFriendly || arg.contains(QRegularExpression(QStringLiteral("(\\s|[][!\"#$&'()*,;<=>?\\^`{}|~])")))) {
146 rv.replace(QStringLiteral("'"), QStringLiteral("'\\''"));
147 rv.prepend (QLatin1Char('\'')).append(QLatin1Char('\''));
148 }
149
150 return rv;
151 }
152 }
153
Sudo()154 Sudo::Sudo()
155 : mArgs{lxqtApp->arguments()}
156 , mBackend{BACK_NONE}
157 {
158 QString cmd = QFileInfo(mArgs[0]).fileName();
159 mArgs.removeAt(0);
160 if (app_lxsu == cmd)
161 mBackend = BACK_SU;
162 else if (app_lxsudo == cmd || app_master == cmd)
163 mBackend = BACK_SUDO;
164 mRet = mPwdFd = mChildPid = 0;
165 }
166
~Sudo()167 Sudo::~Sudo()
168 {
169 }
170
main()171 int Sudo::main()
172 {
173 if (0 < mArgs.size())
174 {
175 //simple option check
176 QString const & arg1 = mArgs[0];
177 if (QStringLiteral("-h") == arg1 || QStringLiteral("--help") == arg1)
178 {
179 usage();
180 return 0;
181 } else if (QStringLiteral("-v") == arg1 || QStringLiteral("--version") == arg1)
182 {
183 version();
184 return 0;
185 } else if (QStringLiteral("-s") == arg1 || QStringLiteral("--su") == arg1)
186 {
187 mBackend = BACK_SU;
188 mArgs.removeAt(0);
189 } else if (QStringLiteral("-d") == arg1 || QStringLiteral("--sudo") == arg1)
190 {
191 mBackend = BACK_SUDO;
192 mArgs.removeAt(0);
193 }
194 }
195 //any other arguments we simply forward to su/sudo
196
197 if (1 > mArgs.size())
198 {
199 usage(tr("%1: no command to run provided!").arg(app_master));
200 return 1;
201 }
202
203 if (BACK_NONE == mBackend)
204 {
205 //we were invoked through unknown link (or renamed binary)
206 usage(tr("%1: no backend chosen!").arg(app_master));
207 return 1;
208 }
209
210 mChildPid = forkpty(&mPwdFd, nullptr, nullptr, nullptr);
211 if (0 == mChildPid)
212 {
213 child(); // never returns
214 return 1; // but for sure
215 }
216
217 mDlg.reset(new PasswordDialog{squashedArgs(/*userFriendly = */ true), backendName()});
218 mDlg->setModal(true);
219 lxqtApp->setActiveWindow(mDlg.data());
220
221 if (-1 == mChildPid)
222 QMessageBox(QMessageBox::Critical, mDlg->windowTitle()
223 , tr("Syscall error, failed to fork: %1").arg(QString::fromUtf8(strerror(errno))), QMessageBox::Ok).exec();
224 else
225 return parent();
226
227 return 1;
228 }
229
squashedArgs(bool userFriendly) const230 QString Sudo::squashedArgs(bool userFriendly) const
231 {
232 QString rv;
233
234 rv = quoteShellArg (mArgs[0], userFriendly);
235 for (auto argP = ++mArgs.begin(); argP != mArgs.end(); ++argP) {
236 rv.append (QLatin1Char(' ')).append(quoteShellArg (*argP, userFriendly));
237 }
238
239 return rv;
240 }
241
backendName(backend_t backEnd)242 QString Sudo::backendName (backend_t backEnd)
243 {
244 QString rv;
245 // Remove leading paths in case variables are set with full path
246 switch (backEnd) {
247 case BACK_SU : rv = su_prog; break;
248 case BACK_SUDO : rv = sudo_prog; break;
249 //: shouldn't be actually used but keep as short as possible in translations just in case.
250 case BACK_NONE : rv = tr("unset");
251 }
252
253 return rv;
254 }
255
child()256 void Sudo::child()
257 {
258 int params_cnt = 3 //1. su/sudo & "shell command" & last nullptr
259 + (BACK_SU == mBackend ? 1 : 3); //-c for su | -E /bin/sh -c for sudo
260 std::unique_ptr<char const *[]> params{new char const *[params_cnt]};
261 const char ** param_arg = params.get() + 1;
262
263 std::string program = backendName().toLocal8Bit().data();
264
265 std::string preserve_env_param;
266 switch (mBackend)
267 {
268 case BACK_SUDO:
269 preserve_env_param = "--preserve-env=";
270
271 preserve_env_param += env_workarounds();
272
273 *(param_arg++) = preserve_env_param.c_str(); //preserve environment
274 *(param_arg++) = "/bin/sh";
275 break;
276 case BACK_SU:
277 *(param_arg++) = "-m";
278 *(param_arg++) = "root";
279 case BACK_NONE:
280 env_workarounds();
281 break;
282
283 }
284 *(param_arg++) = "-c"; //run command
285
286 params[0] = program.c_str();
287
288 // Note: we force the su/sudo to communicate with us in the simplest
289 // locale and then set the locale back for the command
290 char const * const env_lc_all = getenv("LC_ALL");
291 std::string command;
292 if (env_lc_all == nullptr)
293 {
294 command = "unset LC_ALL; ";
295 } else
296 {
297 // Note: we need to check if someone is not trying to inject commands
298 // for privileged execution via the LC_ALL
299 if (nullptr != strchr(env_lc_all, '\''))
300 {
301 QTextStream{stderr, QIODevice::WriteOnly} << tr("%1: Detected attempt to inject privileged command via LC_ALL env(%2). Exiting!\n").arg(app_master).arg(QString::fromUtf8(env_lc_all));
302 exit(1);
303 }
304 command = "LC_ALL='";
305 command += env_lc_all;
306 command += "' ";
307 }
308 command += "exec ";
309 command += squashedArgs().toLocal8Bit().data();
310 *(param_arg++) = command.c_str();
311
312 *param_arg = nullptr;
313
314 setenv("LC_ALL", "C", 1);
315
316 setsid(); //session leader
317 execvp(params[0], const_cast<char **>(params.get()));
318
319 //exec never returns in case of success
320 QTextStream{stderr, QIODevice::WriteOnly} << tr("%1: Failed to exec '%2': %3\n").arg(app_master).arg(QString::fromUtf8(params[0])).arg(QString::fromUtf8(strerror(errno)));
321 exit(1);
322 }
323
stopChild()324 void Sudo::stopChild()
325 {
326 kill(mChildPid, SIGINT);
327 int res, status;
328 for (int cnt = 10; 0 == (res = waitpid(mChildPid, &status, WNOHANG)) && 0 < cnt; --cnt)
329 QThread::msleep(100);
330
331 if (0 == res)
332 {
333 kill(mChildPid, SIGKILL);
334 }
335 }
336
parent()337 int Sudo::parent()
338 {
339 //set the FD as non-blocking
340 if (0 != fcntl(mPwdFd, F_SETFL, O_NONBLOCK))
341 {
342 QMessageBox(QMessageBox::Critical, mDlg->windowTitle()
343 , tr("Syscall error, failed to bring pty to non-block mode: %1").arg(QString::fromUtf8(strerror(errno))), QMessageBox::Ok).exec();
344 return 1;
345 }
346
347 FILE * pwd_f = fdopen(mPwdFd, "r+");
348 if (nullptr == pwd_f)
349 {
350 QMessageBox(QMessageBox::Critical, mDlg->windowTitle()
351 , tr("Syscall error, failed to fdopen pty: %1").arg(QString::fromUtf8(strerror(errno))), QMessageBox::Ok).exec();
352 return 1;
353 }
354
355 QTextStream child_str{pwd_f};
356
357 QObject::connect(mDlg.data(), &QDialog::finished, [&] (int result)
358 {
359 if (QDialog::Accepted == result)
360 {
361 child_str << mDlg->password().append(nl);
362 child_str.flush();
363 } else
364 {
365 stopChild();
366 }
367 });
368
369 QString last_line;
370 QScopedPointer<QSocketNotifier> pwd_watcher{new QSocketNotifier{mPwdFd, QSocketNotifier::Read}};
371 auto reader = [&]
372 {
373 QString line = child_str.readAll();
374 if (line.isEmpty())
375 {
376 pwd_watcher.reset(nullptr); //stop the notifications events
377
378 QString const & prog = backendName();
379 if (last_line.startsWith(QStringLiteral("%1:").arg(prog)))
380 {
381 QMessageBox(QMessageBox::Critical, mDlg->windowTitle()
382 , tr("Child '%1' process failed!\n%2").arg(prog).arg(last_line), QMessageBox::Ok).exec();
383 }
384 } else
385 {
386 #ifdef __FreeBSD__
387 if( line.endsWith(pwd_prompt_end_c_locale) || line.endsWith(pwd_prompt_end))
388 #else
389
390 if (line.endsWith(pwd_prompt_end))
391 #endif
392 {
393 //if now echo is turned off, su/sudo requests password
394 struct termios tios;
395 //loop to be sure we don't miss the flag (we can afford such small delay in "normal" output processing)
396 for (size_t cnt = 10; 0 < cnt && 0 == tcgetattr(mPwdFd, &tios) && (ECHO & tios.c_lflag); --cnt)
397 QThread::msleep(10);
398 if (!(ECHO & tios.c_lflag))
399 {
400 mDlg->show();
401 return;
402 }
403 }
404 QTextStream{stderr, QIODevice::WriteOnly} << line;
405 //assuming text oriented output
406 QStringList lines = line.split(nl, Qt::SkipEmptyParts);
407 last_line = lines.isEmpty() ? QString() : lines.back();
408 }
409
410 };
411
412 QObject::connect(pwd_watcher.data(), &QSocketNotifier::activated, reader);
413
414 std::unique_ptr<std::thread> child_waiter;
415 QTimer::singleShot(0, [&child_waiter, this] {
416 child_waiter.reset(new std::thread{[this] {
417 int res, status;
418 res = waitpid(mChildPid, &status, 0);
419 mRet = (mChildPid == res && WIFEXITED(status)) ? WEXITSTATUS(status) : 1;
420 lxqtApp->quit();
421 }
422 });
423 });
424
425 lxqtApp->exec();
426
427 child_waiter->join();
428
429 // try to read the last line(s)
430 reader();
431
432 fclose(pwd_f);
433 close(mPwdFd);
434
435 return mRet;
436 }
437
438