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