1 /*
2 * Copyright 2015-2021 The Regents of the University of California
3 * All rights reserved.
4 *
5 * This file is part of Spoofer.
6 *
7 * Spoofer is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * Spoofer is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with Spoofer. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include <time.h>
22 #include <locale.h>
23 #include <cstdio>
24 #include <errno.h>
25 #include <unistd.h> // unlink()
26 #include "spoof_qt.h"
27 #include <QCommandLineParser>
28 #include <QtGlobal>
29 #include <QDir>
30 #include <QUrl>
31 #ifdef Q_OS_WIN32
32 # include <windows.h> // RegGetValue()
33 # include <psapi.h> // GetProcessImageFileName()
34 #endif
35 #ifdef Q_OS_UNIX
36 # include <sys/types.h>
37 # include <signal.h> // kill()
38 #endif
39 #include "../../config.h"
40 #include "common.h"
41 static const char cvsid[] ATR_USED = "$Id: common.cpp,v 1.92 2021/04/28 17:39:08 kkeys Exp $";
42
43 SpooferBase::OnDemandDevice SpooferBase::outdev(stdout);
44 SpooferBase::OnDemandDevice SpooferBase::errdev(stderr);
45 QTextStream SpooferBase::spout(&SpooferBase::outdev);
46 QTextStream SpooferBase::sperr(&SpooferBase::errdev);
47 const QStringList *SpooferBase::args;
48 QString SpooferBase::optSettings;
49 SpooferBase::Config *SpooferBase::config;
50 QSettings *SpooferBase::Config::settings;
51 QList<SpooferBase::Config::MemberBase*> SpooferBase::Config::members;
52
53 // We avoid ".log" Suffix because OSX "open" would launch a log reader app
54 // we don't want.
55 const QString SpooferBase::proberLogFtime = QSL("'spoofer-prober-'yyyy~MM~dd-HH~mm~ss'.txt'");
56 const QString SpooferBase::proberLogGlob = QSL("spoofer-prober-\?\?\?\?\?\?\?\?-\?\?\?\?\?\?.txt"); // backslashes prevent trigraphs
57 const QString SpooferBase::proberLogRegex = QSL("spoofer-prober-(\\d{4})(\\d{2})(\\d{2})-(\\d{2})(\\d{2})(\\d{2}).txt$");
58
59 #ifdef Q_OS_WIN32
getLastErr()60 unsigned long getLastErr() { return GetLastError(); }
61 #endif
62
getErrmsg(error_t err)63 QString getErrmsg(error_t err)
64 {
65 #if defined(Q_OS_UNIX)
66 QString msg = QString::fromLocal8Bit(strerror(err));
67 #elif defined(Q_OS_WIN32)
68 wchar_t buf[1024];
69 FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
70 nullptr, err, 0, buf, sizeof(buf), nullptr);
71 QString msg = QString::fromWCharArray(buf);
72 #endif
73 return QString(QSL("%2 (error %1)")).arg(err).arg(msg.trimmed());
74 }
75
76 // Returns false if process does not exist. Otherwise, returns true and
77 // writes the process name (if possible) or an empty string into buf.
78 // Unix: process name may include a path, and includes arguments.
79 // Windows: process name does not included path or arguments.
getProcessName(qint64 pid,char * buf,unsigned len)80 bool getProcessName(qint64 pid, char *buf, unsigned len)
81 {
82 #if defined(Q_OS_UNIX)
83 if (kill(static_cast<pid_t>(pid), 0) < 0 && errno == ESRCH) {
84 // kill() is more reliable than the "ps" below
85 return false;
86 } else {
87 // POSIX "ps -ocomm=" prints just the name of the command, but some
88 // platforms truncate it. So we use "ps -oargs=" to print the full
89 // argv (space-delimited and unquoted, making spaces ambiguous).
90 // (Either form _might_ include a path in the command name.)
91 snprintf(buf, len, "ps -p%lu -oargs=", static_cast<unsigned long>(pid));
92 FILE *ps = popen(buf, "r");
93 if (!ps) {
94 buf[0] = '\0';
95 return true;
96 }
97 if (fgets(buf, static_cast<int>(len), ps)) {
98 char *p = buf + strlen(buf);
99 if (p > buf && *--p == '\n') *p = '\0'; // strip trailing newline
100 } else {
101 buf[0] = '\0';
102 }
103 fclose(ps);
104 return true;
105 }
106
107 #elif defined(Q_OS_WIN32)
108 HANDLE hProc;
109 // Note: QueryProcessImageName() and GetProcessImageFileName() require
110 // only PROCESS_QUERY_LIMITED_INFORMATION.
111 // GetModuleBaseName() and GetModuleFileNameEx() require
112 // PROCESS_QUERY_INFORMATION, which apparently isn't allowed for Services.
113 if (!(hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, static_cast<DWORD>(pid)))) {
114 buf[0] = '\0';
115 return GetLastError() != ERROR_INVALID_PARAMETER; // false if process did not exist
116 }
117 if (GetProcessImageFileNameA(hProc, buf, len) > 0) {
118 char *p = strrchr(buf, '\\');
119 if (p)
120 memmove(buf, p+1, strlen(p)); // strip path
121 } else {
122 buf[0] = '\0';
123 }
124 CloseHandle(hProc);
125 return true;
126 #endif
127 }
128
processErrorMessage(const QProcess & proc)129 QString processErrorMessage(const QProcess &proc)
130 {
131 QString msg;
132 switch (proc.error()) {
133 case QProcess::FailedToStart: msg = QSL("failed to start"); break;
134 case QProcess::Crashed: msg = QSL("crashed"); break;
135 case QProcess::Timedout: msg = QSL("timed out"); break;
136 case QProcess::WriteError: msg = QSL("write error"); break;
137 case QProcess::ReadError: msg = QSL("read error"); break;
138 case QProcess::UnknownError: msg = QSL("unknown error"); break;
139 default: msg = QSL("error"); break;
140 }
141 return msg % QSL(": ") % proc.errorString();
142 }
143
Config()144 SpooferBase::Config::Config() :
145 forWriting(false),
146 // Internal
147 dataDir(QSL("dataDir"), QString(),
148 QSL("Use <dir> as data directory"), true),
149 schedulerSocketName(QSL("schedulerSocketName")),
150 paused(QSL("paused"), false,
151 QSL("Start with scheduled prober runs disabled"), true),
152 #if DEBUG
153 // Debug
154 useDevServer(QSL("useDevServer"), true,
155 QSL("use development test server")),
156 spooferProtocolVersion(QSL("spooferProtocolVersion"), 0, 0, INT_MAX,
157 QSL("force spoofer protocol version number (0 for default)")),
158 pretendMode(QSL("pretendMode"), false,
159 QSL("pretend mode - don't send any probe packets")),
160 standaloneMode(QSL("standaloneMode"), false,
161 QSL("standalone debugging mode - run a test without server")),
162 installerAddTaint(QSL("installerAddTaint"), true,
163 #if defined(Q_OS_WIN32)
164 QSL("add Windows MOTW to downloaded installer")),
165 #elif defined(Q_OS_MACOS)
166 QSL("add OSX quarantine attribute to downloaded installer")),
167 #else
168 QSL("not used")),
169 #endif // Q_OS_MACOS
170 installerVerifySig(QSL("installerVerifySig"), true,
171 QSL("verify signature of downloaded installer")),
172 installerKeep(QSL("installerKeep"), false,
173 QSL("don't delete downloaded installer after use")),
174 #endif // DEBUG
175
176 // General
177 #ifdef AUTOUPGRADE_ENABLED
178 autoUpgrade(QSL("autoUpgrade"), true,
179 QSL("Enable automatic software upgrades")),
180 #endif
181 enableIPv4(QSL("enableIPv4"), true,
182 QSL("Enable testing on IPv4 interfaces (if available)")),
183 enableIPv6(QSL("enableIPv6"), true,
184 QSL("Enable testing on IPv6 interfaces (if available)")),
185 keepLogs(QSL("keepLogs"), 60, 0, INT_MAX,
186 QSL("Number of prober log files to keep (0 means unlimited)")),
187 sharePublic(QSL("sharePublic"), true,
188 QSL(DESC_SHARE_PUBLIC)),
189 shareRemedy(QSL("shareRemedy"), true,
190 QSL(DESC_SHARE_REMEDY)),
191 enableTLS(QSL("enableTLS"), true,
192 QSL("Use SSL/TLS to connect to server (recommended unless blocked by your provider)")),
193 // Probing
194 netPollInterval(QSL("netPollInterval"), 2*60, 1, 86400,
195 QSL("Wait to check for a network change (seconds)")),
196 delayInterval(QSL("delayInterval"), 60, 1, 3600,
197 QSL("Wait to run a test after detecting a network change (seconds)")),
198 // odd proberInterval helps prevent many clients from synchronizing
199 proberInterval(QSL("proberInterval"), 7*24*60*60 + 65*60, 3600, INT_MAX,
200 QSL("Wait to run a test after a successful run on the same network (seconds)")),
201 proberRetryInterval(QSL("proberRetryInterval"), 10*60, 60, INT_MAX,
202 QSL("Wait to retry after first incomplete run (seconds) (doubles each time)")),
203 maxRetries(QSL("maxRetries"), 3, 0, INT_MAX,
204 QSL("Maximum number of retries after an incomplete run")),
205 // Permissions: "Allow unprivileged users on this computer to..."
206 unprivView(QSL("unprivView"), true,
207 QSL("Observe a test in progress and view results of past tests")),
208 unprivTest(QSL("unprivTest"), false,
209 QSL("Run a test")),
210 unprivPref(QSL("unprivPref"), false,
211 QSL("Change preferences"))
212 {
213 sharePublic.required = true;
214 shareRemedy.required = true;
215 #ifdef Q_OS_UNIX
216 // We don't want these user-desktop-specific environment variables
217 // affecting our defaults for dataDir or settings file; we want the
218 // same defaults for all users, including system daemon launchers.
219 unsetenv("XDG_CONFIG_DIRS");
220 unsetenv("XDG_DATA_DIRS");
221 #endif
222 }
223
initSettings(bool _forWriting,bool debug)224 void SpooferBase::Config::initSettings(bool _forWriting, bool debug)
225 {
226 config->forWriting = _forWriting;
227 if (!optSettings.isEmpty()) {
228 settings = new QSettings(optSettings, QSettings::IniFormat);
229 } else {
230 settings = findDefaultSettings(debug);
231 settings->setFallbacksEnabled(false);
232 }
233 }
234
lockFileName()235 QString SpooferBase::Config::lockFileName()
236 {
237 // Note: QSettings may generate a temporary lock file by appending ".lock"
238 // to the file name, so we must use a different name for our lock.
239 if (isFile()) return settings->fileName() % QSL(".write-lock");
240
241 QString path;
242 #ifdef Q_OS_WIN32
243 // We want the system's %TEMP%, not the user's.
244 LONG err;
245 char buf[1024];
246 DWORD size = sizeof(buf) - 1;
247 const char *subkeyName =
248 "System\\CurrentControlSet\\Control\\Session Manager\\Environment";
249 err = RegGetValueA(HKEY_LOCAL_MACHINE, subkeyName, "TEMP", RRF_RT_REG_SZ,
250 nullptr, buf, &size);
251 if (err != ERROR_SUCCESS) {
252 qDebug() << "RegGetValue:" << getErrmsg(static_cast<DWORD>(err));
253 goto doneReg;
254 }
255 path = QString::fromLocal8Bit(buf).trimmed();
256 qDebug() << "TEMP:" << qPrintable(path);
257 doneReg:
258 #endif
259 if (path.isEmpty()) path = QDir::tempPath();
260 return (path % QSL("/") % QCoreApplication::applicationName() % QSL(".lock"));
261 }
262
error(const char * label)263 bool SpooferBase::Config::error(const char *label)
264 {
265 if (settings->status() == QSettings::NoError)
266 return false;
267
268 logError(label,
269 (settings->status() == QSettings::AccessError) ? QSL("AccessErrror") :
270 (settings->status() == QSettings::FormatError) ? QSL("FormatError") :
271 QSL("unknown error %1").arg(int(settings->status())));
272
273 return true;
274 }
275
logError(const char * label,QString msg,QString msg2)276 void SpooferBase::Config::logError(const char *label, QString msg, QString msg2)
277 {
278 msg = QSL("%1 in \"%2\"").arg(msg,
279 QDir::toNativeSeparators(settings->fileName()));
280
281 if (isFile()) {
282 // Use the standard library to get a more informative error message.
283 FILE *f = fopen(qPrintable(settings->fileName()),
284 forWriting ? "r+" : "r");
285 if (!f)
286 msg = QSL("%1 (%2)").arg(msg,
287 QString::fromLocal8Bit(strerror(errno)));
288 else
289 fclose(f);
290 }
291 qCritical().nospace().noquote() << label << ": " << msg << ". " << msg2;
292 }
293
remove()294 void SpooferBase::Config::remove()
295 {
296 if (!settings) return;
297 QString name = isFile() ? settings->fileName() : QString();
298 settings->clear();
299 delete settings;
300 settings = nullptr;
301 if (!name.isEmpty()) {
302 if (unlink(name.toStdString().c_str()) == 0)
303 sperr << name << " removed." << Qt_endl;
304 else
305 sperr << "Error removing " << name << ": " << strerror(errno) << Qt_endl;
306 }
307 }
308
SpooferBase()309 SpooferBase::SpooferBase() :
310 appDir(QCoreApplication::applicationDirPath()),
311 appFile(QCoreApplication::applicationFilePath())
312 {
313 setlocale(LC_NUMERIC, "C");
314
315 // for QStandardPaths::standardLocations() and QSettings
316 QCoreApplication::setOrganizationName(QSL(ORG_NAME));
317 QCoreApplication::setOrganizationDomain(QSL(ORG_DOMAIN));
318 QCoreApplication::setApplicationName(QSL("Spoofer")); // may change later
319
320 qInstallMessageHandler(logHandler);
321
322 config = new Config();
323 }
324
writeData(const char * data,qint64 maxSize)325 qint64 SpooferBase::OnDemandDevice::writeData(const char *data, qint64 maxSize)
326 {
327 if (newdev) {
328 QFile *file = dynamic_cast<QFile*>(dev);
329 QFile *newfile = dynamic_cast<QFile*>(newdev);
330 if (newfile) {
331 // Make sure filename is clean and absolute for comparison below.
332 QDir newpath(QDir::cleanPath(newfile->fileName()));
333 newfile->setFileName(newpath.absolutePath());
334 }
335 if (file && file->isOpen() && newfile && file->fileName() ==
336 newfile->fileName())
337 {
338 // Old dev is open, and newdev is the same file; ignore newdev.
339 } else {
340 char buf[2048];
341 if (dev && dev->isOpen() && !newname.isEmpty()) {
342 snprintf(buf, sizeof(buf), "Redirecting output to %s\n",
343 qPrintable(newname));
344 dev->write(buf);
345 }
346 if (!newdev->open(WriteOnly|Unbuffered|Append|Text)) {
347 if (dev && dev->isOpen()) {
348 snprintf(buf, sizeof(buf), "Redirection failed: %s.\n",
349 qPrintable(newdev->errorString()));
350 dev->write(buf);
351 }
352 delete newdev;
353 } else { // success
354 if (dev) delete dev;
355 dev = newdev;
356 }
357 }
358 newdev = nullptr;
359 }
360
361 if (!dev) {
362 if (!fallback) {
363 setErrorString(QSL("output device is not set"));
364 return maxSize; // *dev failed, but *this can still work
365 }
366 // E.g., dev was nulled in a previous call, and fallback was set later.
367 newdev = fallback;
368 fallback = nullptr;
369 return writeData(data, maxSize); // Try again with the fallback.
370 }
371
372 if (timestampEnabled) {
373 char tbuf[40];
374 time_t t = time(nullptr);
375 struct tm *tm = gmtime(&t);
376 strftime(tbuf, sizeof(tbuf), "[%Y-%m-%d %H:%M:%S] ", tm);
377 dev->write(tbuf, safe_int<qint64>(strlen(tbuf)));
378 }
379 qint64 retval = dev->write(data, maxSize);
380 if (retval < 0) {
381 // E.g., stderr is closed when running as a Windows service.
382 if (!fallback) { // There was no fallback.
383 delete dev; dev = nullptr;
384 return maxSize; // *dev failed, but *this can still work
385 }
386 newdev = fallback;
387 fallback = nullptr;
388 return writeData(data, maxSize); // Try again with the fallback.
389 }
390 fallback = nullptr; // Dev worked; we won't need the fallback.
391 return retval;
392 }
393
logHandler(QtMsgType type,const QMessageLogContext & ctx,const QString & msg)394 void SpooferBase::logHandler(QtMsgType type, const QMessageLogContext &ctx,
395 const QString &msg)
396 {
397 Q_UNUSED(ctx);
398
399 #if !DEBUG
400 if (type == QtDebugMsg) return;
401 #endif
402
403 const char *prefix =
404 (type == QtDebugMsg) ? "Debug" :
405 (type == QtWarningMsg) ? "Warning" :
406 (type == QtCriticalMsg) ? "Critical" :
407 (type == QtFatalMsg) ? "Fatal" :
408 nullptr;
409
410 static int depth = 0;
411 if (++depth > 1) { // should not happen
412 fprintf(stderr, "INTERNAL ERROR: logHandler recursion\n");
413 fprintf(stderr, "%s: %s\n", prefix, qPrintable(msg));
414 ::fflush(stderr);
415 } else {
416 if (prefix)
417 sperr << prefix << ": " << msg << Qt_endl;
418 else
419 sperr << msg << Qt_endl;
420 }
421 depth--;
422 }
423
424 // Caller can add additional options before calling parseCommandLine(), and
425 // inspect them after.
parseCommandLine(QCommandLineParser & clp,QString desc)426 bool SpooferBase::parseCommandLine(QCommandLineParser &clp, QString desc)
427 {
428 clp.setApplicationDescription(desc);
429
430 QSettings *ds = findDefaultSettings(false);
431 QString format = QSL("");
432 #ifdef Q_OS_WIN32
433 if (ds->format() == QSettings::NativeFormat) format = QSL("registry ");
434 #endif
435 QCommandLineOption cloSettings(QSL("settings"),
436 QSL("Use settings in <file> [%1\"%2\"].").arg(format).arg(ds->fileName()),
437 QSL("file"));
438 clp.addOption(cloSettings);
439 delete ds;
440
441 QCommandLineOption cloVersion(QStringList() << QSL("v") << QSL("version"),
442 QSL("Display version information."));
443 clp.addOption(cloVersion);
444
445 // clp.addHelpOption() wouldn't include "-?" on non-Windows.
446 QCommandLineOption cloHelp(QStringList() << QSL("?") << QSL("h") << QSL("help"),
447 QSL("Display this help."));
448 clp.addOption(cloHelp);
449
450 if (!clp.parse(SpooferBase::args ? *SpooferBase::args : QCoreApplication::arguments())) {
451 qCritical() << qPrintable(clp.errorText());
452 return false;
453 }
454
455 if (clp.isSet(cloHelp)) {
456 qInfo() << qPrintable(clp.helpText());
457 return false;
458 }
459
460 if (clp.isSet(cloVersion)) {
461 qInfo() << PACKAGE_NAME << "version" << PACKAGE_VERSION << Qt_endl <<
462 "Qt version" << qVersion();
463 return false;
464 }
465
466 if (clp.isSet(cloSettings))
467 optSettings = QDir::current().absoluteFilePath(clp.value(cloSettings));
468
469 return true;
470 }
471
472 // Format is like QDateTime::toString(), with the addition that '~' will be
473 // removed after formatting, allowing you to create adjacent non-separated
474 // fields in output by inserting '~' between them in the input format.
ftime_zone(const QString & fmt,const time_t * tp,const Qt::TimeSpec & spec)475 QString SpooferBase::ftime_zone(const QString &fmt, const time_t *tp, const Qt::TimeSpec &spec)
476 {
477 time_t t;
478 if (!tp) { time(&t); tp = &t; }
479 // QDateTime::fromTime_t() is not available in Qt >= 5.8(?);
480 // QDateTime::fromSecsSinceEpoch() is not available in Qt < 5.8.
481 return QDateTime::fromMSecsSinceEpoch(qint64(*tp) * 1000, spec)
482 .toString(!fmt.isEmpty() ? fmt : QSL("yyyy-MM-dd HH:mm:ss t"))
483 .remove(QLatin1Char('~'));
484 }
485
sc_msg_upgrade_available(bool wantAuto,bool _mandatory,int32_t _vnum,const QString & _vstr,const QString & _file)486 sc_msg_upgrade_available::sc_msg_upgrade_available(bool wantAuto,
487 bool _mandatory, int32_t _vnum, const QString &_vstr, const QString &_file) :
488 autoTime(wantAuto ? 60 : -1), mandatory(_mandatory),
489 vnum(_vnum), vstr(_vstr), file(_file), warning()
490 {
491 #if 0 // these checks aren't needed since we have TLS and signed installer
492 if (file.isEmpty()) return;
493 QStringList warnings;
494 #if defined(UPGRADE_KEY) && !defined(UPGRADE_WITHOUT_DOWNLOAD)
495 QUrl u(file);
496 if (!u.isValid()) {
497 warnings << QSL("invalid URL: ") % u.errorString();
498 } else {
499 if (!file.startsWith(QSL(UPGRADE_KEY)))
500 warnings << QSL("URL does not start with \"" UPGRADE_KEY "\"");
501 }
502 #endif
503 if (!warnings.isEmpty()) {
504 warning = QSL("WARNING: ") % warnings.join(QSL("; ")) % QSL(".");
505 #if !DEBUG
506 // Don't autoupgrade if URL looks fishy
507 autoTime = -1;
508 #endif
509 }
510 #endif // 0
511 }
512
513