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 <iostream>
22 #include <stdexcept>
23 #include "spoof_qt.h"
24 #include <QThread>
25 #include <QDir>
26 #include <QMutex>
27 #include <QWaitCondition>
28 #include <QCommandLineParser>
29 #include <QLockFile>
30 #include <windows.h> // GetConsoleMode(), CTRL_*_EVENT, etc
31 #include <shlobj.h> // SHGetFolderPath()
32 #include "../../config.h"
33 #include "appwin.h"
34 #include "common.h"
35 #include "ServiceStarterThread.h"
36 static const char cvsid[] ATR_USED = "$Id: appwin.cpp,v 1.39 2021/04/28 17:39:09 kkeys Exp $";
37
isaconsole(HANDLE h)38 static bool isaconsole(HANDLE h)
39 {
40 DWORD mode;
41 return h != INVALID_HANDLE_VALUE && GetConsoleMode(h, &mode);
42 }
43
AppWin(int & argc,char ** argv)44 AppWin::AppWin(int &argc, char **argv) : App(argc, argv),
45 winIn(GetStdHandle(STD_INPUT_HANDLE)),
46 winOut(GetStdHandle(STD_OUTPUT_HANDLE)),
47 winErr(GetStdHandle(STD_ERROR_HANDLE)),
48 installerProcess(INVALID_HANDLE_VALUE),
49 installerJob(INVALID_HANDLE_VALUE),
50 isService(false)
51 {
52 isInteractive = isaconsole(winIn) && isaconsole(winOut);
53 }
54
dumpPaths() const55 void AppWin::dumpPaths() const
56 {
57 TCHAR szPath[MAX_PATH];
58 App::dumpPaths();
59 spout << "Windows paths:" << Qt_endl;
60
61 #define dumpWinPath(id) do { \
62 spout << #id << Qt_endl; \
63 if (SUCCEEDED(SHGetFolderPath(nullptr, CSIDL_##id, nullptr, 0, szPath))) \
64 spout << " " << QString::fromWCharArray(szPath) << Qt_endl; \
65 } while (0)
66
67 dumpWinPath(APPDATA);
68 dumpWinPath(COMMON_APPDATA);
69 dumpWinPath(LOCAL_APPDATA);
70 dumpWinPath(PERSONAL);
71 dumpWinPath(COMMON_DOCUMENTS);
72 dumpWinPath(PROGRAM_FILES);
73 dumpWinPath(PROGRAM_FILES_COMMON);
74 dumpWinPath(PROGRAM_FILES_COMMONX86);
75 }
76
chooseDataDir()77 QString AppWin::chooseDataDir()
78 {
79 // We want only system-wide dirs, but QStandardPaths::standardLocations()
80 // doesn't separate user dirs from system-wide dirs.
81 int nFolder = CSIDL_COMMON_APPDATA;
82 TCHAR szPath[MAX_PATH];
83 if (!SUCCEEDED(SHGetFolderPath(nullptr, nFolder, nullptr, 0, szPath))) {
84 sperr << "can't get data folder path" << Qt_endl;
85 return QString();
86 }
87 return QString::fromWCharArray(szPath) %
88 QSL("\\") % QCoreApplication::organizationName() %
89 QSL("\\") % QCoreApplication::applicationName();
90 }
91
92 enum AppState { APPSTATE_NOTREADY, APPSTATE_READY, APPSTATE_DONE };
93 static QMutex appStateMutex;
94 static QWaitCondition appStateCond;
95 static AppState appState = APPSTATE_NOTREADY;
96 static ServiceStarterThread *starter = nullptr;
97
98 static SERVICE_STATUS svcStatus;
99 static SERVICE_STATUS_HANDLE svcStatusHandle;
100 //static HANDLE svcStopEvent = nullptr;
101
102 static void WINAPI SvcCtrlHandler(DWORD);
103 static void WINAPI SvcMainA(DWORD, LPSTR *);
104
105 static void ReportSvcStatus(DWORD, DWORD, DWORD);
106 // static void SvcReportEvent(LPTSTR);
107
108 static const SERVICE_TABLE_ENTRYA serviceTable[] = {
109 { const_cast<char *>(APPNAME), SvcMainA },
110 { nullptr, nullptr }
111 };
112
ReportSvcStatus(DWORD state,DWORD exitCode,DWORD waitHint)113 static void ReportSvcStatus(DWORD state, DWORD exitCode, DWORD waitHint)
114 {
115 static DWORD checkpoint = 1;
116 svcStatus.dwCurrentState = state;
117 svcStatus.dwWin32ExitCode = exitCode;
118 svcStatus.dwWaitHint = waitHint;
119
120 if (state == SERVICE_START_PENDING)
121 svcStatus.dwControlsAccepted = 0;
122 else
123 svcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE;
124
125 if (state == SERVICE_START_PENDING || state == SERVICE_STOP_PENDING)
126 svcStatus.dwCheckPoint = checkpoint++;
127 else
128 svcStatus.dwCheckPoint = 0;
129
130 SetServiceStatus(svcStatusHandle, &svcStatus);
131 }
132
pause()133 void AppWin::pause()
134 {
135 ReportSvcStatus(SERVICE_PAUSE_PENDING, NO_ERROR, 0);
136 App::pause();
137 ReportSvcStatus(SERVICE_PAUSED, NO_ERROR, 0);
138 }
139
resume()140 void AppWin::resume()
141 {
142 ReportSvcStatus(SERVICE_CONTINUE_PENDING, NO_ERROR, 0);
143 App::resume();
144 ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
145 }
146
SvcCtrlHandler(DWORD ctrl)147 static void WINAPI SvcCtrlHandler(DWORD ctrl)
148 {
149 AppWin *app = static_cast<AppWin*>(QCoreApplication::instance());
150 switch (ctrl) {
151 case SERVICE_CONTROL_STOP:
152 App::sperr << "received SERVICE_CONTROL_STOP" << Qt_endl;
153 ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
154 app->exit(0);
155 break;
156 case SERVICE_CONTROL_SHUTDOWN:
157 App::sperr << "received SERVICE_CONTROL_SHUTDOWN" << Qt_endl;
158 ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
159 app->exit(0);
160 break;
161 case SERVICE_CONTROL_PAUSE:
162 App::sperr << "received SERVICE_CONTROL_PAUSE" << Qt_endl;
163 app->pause();
164 break;
165 case SERVICE_CONTROL_CONTINUE:
166 App::sperr << "received SERVICE_CONTROL_CONTINUE" << Qt_endl;
167 app->resume();
168 break;
169 case SERVICE_CONTROL_INTERROGATE:
170 qDebug() << "received SERVICE_CONTROL_INTERROGATE";
171 break;
172 default:
173 qDebug() << "received unknown SERVICE_CONTROL" << ctrl;
174 break;
175 }
176 }
177
SvcMainA(DWORD argc,LPSTR * argv)178 static void SvcMainA(DWORD argc, LPSTR *argv)
179 {
180 // pass service arguments along to parseCommandLine()
181 if (argc) {
182 QStringList *args = new QStringList();
183 for (unsigned i = 0; i < argc; i++)
184 *args << QString::fromLocal8Bit(argv[i]);
185 SpooferBase::args = args;
186 }
187
188 // tell main thread it can start app.exec()
189 // qDebug() << "SvcMain sending READY";
190 appStateMutex.lock();
191 appState = APPSTATE_READY;
192 appStateCond.wakeAll();
193
194 // wait for main thread to finish app.exec()
195 // qDebug() << "SvcMain waiting for DONE";
196 while (appState < APPSTATE_DONE) appStateCond.wait(&appStateMutex);
197 appStateMutex.unlock();
198 // qDebug() << "SvcMain got DONE";
199 }
200
run()201 void ServiceStarterThread::run() {
202 AppWin *app = (AppWin*)QCoreApplication::instance();
203 app->isService = true;
204 if (StartServiceCtrlDispatcherA(serviceTable)) {
205 // Ran as service; SvcMain has already run in another thread.
206 } else if (GetLastError() != ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) {
207 // Failed to run as service.
208 app->isService = false;
209 qDebug() << "StartServiceCtrlDispatcher: " << getLastErrmsg();
210 qDebug() << "starter sending DONE";
211 appStateMutex.lock();
212 appState = APPSTATE_DONE;
213 appStateCond.wakeAll();
214 appStateMutex.unlock();
215 } else {
216 // We are running as a console app, not a service.
217 app->isService = false;
218 SvcMainA(0, nullptr);
219 }
220 }
221
init(int & exitCode)222 bool AppWin::init(int &exitCode)
223 {
224 exitCode = 1; // default result is failure
225
226 // App::exec() must be run in the main thread. But Windows'
227 // StartServiceCtrlDispatcher() launches a new thread to run SvcMain(),
228 // and blocks its own thread until the SvcMain() thread exits.
229 // Solution:
230 // Main thread launches "Starter" thread, then waits for READY.
231 // Starter thread calls StartServiceCtrlDispatcher() and blocks.
232 // SvcMain() thread signals READY, then waits for DONE.
233 // Main thread wakes on READY, calls App::exec(), signals DONE, and exits.
234 // SvcMain() thread wakes on DONE and exits.
235 // Starter thread wakes on return of SvcMain(), and exits.
236
237 // start a thread that starts a Windows Service
238 starter = new ServiceStarterThread(this);
239 starter->start();
240
241 // wait for starter to tell us it's ready
242 qDebug() << "main waiting for READY";
243 appStateMutex.lock();
244 while (appState < APPSTATE_READY) appStateCond.wait(&appStateMutex);
245 appStateMutex.unlock();
246 if (appState == APPSTATE_DONE) { // error in starter
247 qDebug() << "main got DONE";
248 return false;
249 }
250 qDebug() << "main got READY";
251
252 if (this->isService) {
253 svcStatusHandle = RegisterServiceCtrlHandlerA(APPNAME, SvcCtrlHandler);
254 if (!svcStatusHandle) {
255 // SvcReportEvent(TEXT("RegisterServiceCtrlHandler"));
256 qDebug() << "RegisterServiceCtrlHandler failed: " << getLastErrmsg();
257 return false;
258 }
259
260 svcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
261 svcStatus.dwServiceSpecificExitCode = 0;
262
263 ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
264 // TODO: use a lower waitHint here so control panel doesn't wait so
265 // long; and, to make sure control panel doesn't give up too early, we
266 // should call ReportSvcStatus(SERVICE_START_PENDING,...) occasionally
267 // to increment checkpoint between now and readyService(). Then we
268 // can lower the sleep in readyService().
269 }
270
271 return App::init(exitCode);
272 }
273
verifyDaemon(PROCESS_INFORMATION & procinfo)274 bool AppWin::verifyDaemon(PROCESS_INFORMATION &procinfo)
275 {
276 QLockFile lockFile(config->lockFileName());
277 qDebug().noquote() << "waiting for child to lock" <<
278 QDir::toNativeSeparators(config->lockFileName());
279 qint64 qpid;
280 while (!lockFile.getLockInfo(&qpid, nullptr, nullptr) ||
281 qpid != procinfo.dwProcessId) // child is not ready
282 {
283 DWORD code;
284 if (!GetExitCodeProcess(procinfo.hProcess, &code) || code != STILL_ACTIVE) {
285 sperr << "Detached process exited prematurely" << Qt_endl;
286 return false;
287 }
288 QThread::msleep(100);
289 }
290 return true;
291 }
292
prestart(int & exitCode)293 bool AppWin::prestart(int &exitCode)
294 {
295 if (!this->isService && GetConsoleWindow() && this->optDetach) {
296 // Start a child process with same command line as this one, but
297 // detached, with stdout and stderr redirected to a log file.
298 // Exit parent without waiting for child to finish.
299 QString logname = AppLog::makeName();
300
301 STARTUPINFO startupinfo;
302 memset(&startupinfo, 0, sizeof(startupinfo));
303 startupinfo.cb = sizeof(STARTUPINFO);
304 startupinfo.dwFlags = STARTF_USESTDHANDLES;
305 startupinfo.hStdInput = INVALID_HANDLE_VALUE;
306
307 #if 1 // Windows API
308 SECURITY_ATTRIBUTES secattr;
309 memset(&secattr, 0, sizeof(secattr));
310 secattr.nLength = sizeof(secattr);
311 secattr.bInheritHandle = true;
312 startupinfo.hStdOutput = startupinfo.hStdError = CreateFileA(
313 qPrintable(logname), GENERIC_WRITE, FILE_SHARE_READ,
314 &secattr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
315 if (startupinfo.hStdError == INVALID_HANDLE_VALUE) {
316 sperr << "log file " << logname << ": " << getLastErrmsg() << Qt_endl;
317 return false;
318 }
319 #else // stdio API
320 FILE *out = fopen(qPrintable(logname), "wt");
321 if (!out) {
322 sperr << "log file " << logname << ": " << strerror(errno) << Qt_endl;
323 return false;
324 }
325 startupinfo.hStdOutput = startupinfo.hStdError =
326 (HANDLE)_get_osfhandle(_fileno(out));
327 #endif
328
329 PROCESS_INFORMATION procinfo;
330 TCHAR exename[MAX_PATH];
331 GetModuleFileName(nullptr, exename, MAX_PATH);
332 if (CreateProcess(exename, GetCommandLine(), nullptr, nullptr, TRUE,
333 DETACHED_PROCESS, nullptr, nullptr, &startupinfo, &procinfo))
334 {
335 exitCode = SP_EXIT_OK; // success
336 qDebug() << "Detached; child pid =" << procinfo.dwProcessId;
337 sperr << "Log file: " << QDir::toNativeSeparators(logname) << Qt_endl;
338 } else {
339 exitCode = static_cast<int>(getLastErr());
340 sperr << "Detach failed: " << getLastErrmsg() << Qt_endl;
341 }
342 pApplabel = QSL(" parent");
343 exitCode = verifyDaemon(procinfo) ? SP_EXIT_OK : SP_EXIT_DAEMON_FAILED;
344 return false; // skip app.exec()
345 }
346
347 return true;
348 }
349
readyService(int exitCode) const350 void AppWin::readyService(int exitCode) const
351 {
352 if (!isService) return;
353 ReportSvcStatus(SERVICE_RUNNING, (DWORD)exitCode, 0);
354 if (paused) {
355 QThread::msleep(3100); // hack: give control panel a chance to see SERVICE_RUNNING before we switch to SERVICE_PAUSED.
356 ReportSvcStatus(SERVICE_PAUSED, NO_ERROR, 0);
357 }
358 }
359
endService(int exitCode)360 void AppWin::endService(int exitCode)
361 {
362 if (isService)
363 ReportSvcStatus(SERVICE_STOPPED, (DWORD)exitCode, 0);
364 // tell starter we're done
365 qDebug() << "main sending DONE";
366 appStateMutex.lock();
367 appState = APPSTATE_DONE;
368 appStateCond.wakeAll();
369 appStateMutex.unlock();
370 starter->wait(1000);
371 delete starter;
372 App::endService(exitCode);
373 }
374
end() const375 void AppWin::end() const
376 {
377 DWORD proclist[1];
378 DWORD numProcsOnConsole = GetConsoleProcessList(proclist, 1);
379 if (this->isInteractive && numProcsOnConsole == 1) {
380 const char prompt[] = "Press enter.\n";
381 char c;
382 WriteFile(this->winErr, prompt, sizeof(prompt)-1, nullptr, nullptr);
383 ReadFile(this->winIn, &c, 1, nullptr, nullptr);
384 }
385 App::end();
386 }
387
ConsoleCtrlHandler(DWORD type)388 static BOOL ConsoleCtrlHandler(DWORD type)
389 {
390 const char *name;
391 QCoreApplication *app = QCoreApplication::instance();
392 switch (type) {
393 case CTRL_C_EVENT:
394 name = "C"; break;
395 case CTRL_CLOSE_EVENT: // closed console or "End Task"
396 name = "Close"; break;
397 case CTRL_BREAK_EVENT:
398 name = "Break"; break;
399 case CTRL_LOGOFF_EVENT:
400 return true;
401 case CTRL_SHUTDOWN_EVENT:
402 return true;
403 default:
404 return true;
405 }
406 qDebug().nospace() << "Scheduler: caught Ctrl-" << name << ".";
407 app->exit(255); // request app to end its event loop
408 QThread::msleep(10000); // if app exits cleanly now, it'll kill this thread
409 return false; // app hasn't exited; kill all threads
410 }
411
initSignals()412 bool AppWin::initSignals()
413 {
414 SetConsoleCtrlHandler((PHANDLER_ROUTINE)ConsoleCtrlHandler, TRUE);
415 return true;
416 }
417
killProber()418 void AppWin::killProber()
419 {
420 // prober->terminate() does not work with non-gui processes like prober
421 prober->kill();
422 }
423
424 #ifdef AUTOUPGRADE_ENABLED
installerIsRunning()425 bool AppWin::installerIsRunning()
426 {
427 DWORD exitCode;
428 if (!GetExitCodeProcess(installerProcess, &exitCode)) return false;
429 if (exitCode == STILL_ACTIVE) return true;
430 qWarning() << "installer exit code:" << exitCode;
431 return false;
432 }
433
killInstaller()434 void AppWin::killInstaller()
435 {
436 TerminateJobObject(installerJob, 1);
437 }
438
executeInstaller(const QString & installerName)439 void AppWin::executeInstaller(const QString &installerName)
440 {
441 LPWSTR comspec = _wgetenv(L"ComSpec");
442 std::wstring wInstaller = QDir::toNativeSeparators(installerName).toStdWString();
443 std::wstring wDataDir = QDir::toNativeSeparators(dataDir).toStdWString();
444 std::wstring wUpFin = QDir::toNativeSeparators(upFinName()).toStdWString();
445 std::wstring wVStr = upgradeInfo->vstr.toStdWString();
446 // Note: Windows cmd.exe syntax:
447 // escape character is ^
448 // like posix shell: && || > 2>&1
449 // ( ) groups commands.
450 // & separates commands.
451 // quotes and spaces are not removed (with /S).
452 // %var% substitutions are done at parse time.
453 // !var! substitutions are done at run time (with /V:ON).
454 // "if (cond) (cmd1) else (cmd2)" does not seem to allow & after it.
455 // "cmd0 && (cmd1) || (cmd2)" does allow & after it, but
456 // executes cmd2 if cmd1 fails, which is not what we want.
457 std::wstring cmdline =
458 L"cmd.exe /V:ON /S /C ( "
459 L"set installer=" + wInstaller + L"& "
460 L"echo Installing !installer!& "
461 L"set FromTo=from " PACKAGE_VERSION " to " + wVStr + L"& "
462 L"\"!installer!\" /S& "
463 L"set status=!errorlevel!& "
464 L"cmd /c exit !status! && ( " + // essentially "if status == 0"
465 #ifndef UPGRADE_WITHOUT_DOWNLOAD
466 (config->installerKeep() ? L"" : L"del \"!installer!\"& ") +
467 #endif
468 L"set msg=Successfully upgraded !FromTo!"
469 L")& "
470 L"cmd /c exit !status! || ( " + // essentially "if status != 0"
471 L"set msg=Failed to upgrade !FromTo! ^(errorlevel !status!^)"
472 L")& "
473 L"echo !msg! at !DATE! !TIME!>\"" + wUpFin + L"\"& "
474 L"echo !msg!&"
475 L"exit !status!"
476 L") >\"" + wDataDir + L"\\upgrade-log.txt\" 2>&1";
477 qDebug().noquote() << QSL("cmdline:") << QString::fromStdWString(cmdline);
478 STARTUPINFOW startupinfo;
479 memset(&startupinfo, 0, sizeof(startupinfo));
480 startupinfo.cb = sizeof(startupinfo);
481 PROCESS_INFORMATION procinfo;
482 if (CreateProcessW(comspec, const_cast<wchar_t*>(cmdline.data()), nullptr, nullptr, TRUE,
483 DETACHED_PROCESS, nullptr, nullptr, &startupinfo, &procinfo))
484 {
485 installerProcess = procinfo.hProcess;
486 installerJob = CreateJobObjectA(nullptr, "spoofer-install-script");
487 AssignProcessToJobObject(installerJob, installerProcess);
488 // All children of installerProcess will also belong to installerJob.
489 qDebug() << "executing cmd script, pid" << procinfo.dwProcessId;
490 } else {
491 abortInstallation(QSL("Failed to execute upgrade script: ") % getLastErrmsg());
492 return;
493 }
494 }
495 #endif // AUTOUPGRADE_ENABLED
496