File indexing completed on 2024-06-23 05:14:19

0001 /* utils/windowsprocessdevice.cpp
0002     This file is part of Kleopatra, the KDE keymanager
0003     SPDX-FileCopyrightText: 2019 g 10code GmbH
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #ifdef WIN32
0009 #include "windowsprocessdevice.h"
0010 
0011 #include "kleopatra_debug.h"
0012 
0013 #include <Libkleo/GnuPG>
0014 
0015 #include <windows.h>
0016 
0017 #include <QDir>
0018 #include <QRegularExpression>
0019 
0020 /* This is the amount of data GPGME reads at once */
0021 #define PIPEBUF_SIZE 16384
0022 
0023 using namespace Kleo;
0024 
0025 static void CloseHandleX(HANDLE &h)
0026 {
0027     if (h && h != INVALID_HANDLE_VALUE) {
0028         if (!CloseHandle(h)) {
0029             qCWarning(KLEOPATRA_LOG) << "CloseHandle failed!";
0030         }
0031         h = nullptr;
0032     }
0033 }
0034 
0035 class WindowsProcessDevice::Private
0036 {
0037 public:
0038     ~Private();
0039 
0040     Private(const QString &path, const QStringList &args, const QString &wd)
0041         : mPath(path)
0042         , mArgs(args)
0043         , mWorkingDirectory(wd)
0044         , mStdInRd(nullptr)
0045         , mStdInWr(nullptr)
0046         , mStdOutRd(nullptr)
0047         , mStdOutWr(nullptr)
0048         , mStdErrRd(nullptr)
0049         , mStdErrWr(nullptr)
0050         , mProc(nullptr)
0051         , mThread(nullptr)
0052         , mEnded(false)
0053     {
0054     }
0055 
0056     bool start(QIODevice::OpenMode mode);
0057 
0058     qint64 write(const char *data, qint64 size)
0059     {
0060         if (size < 0 || (size >> 32)) {
0061             qCDebug(KLEOPATRA_LOG) << "Invalid write";
0062             return -1;
0063         }
0064 
0065         if (!mStdInWr) {
0066             qCDebug(KLEOPATRA_LOG) << "Write to closed or read only device";
0067             return -1;
0068         }
0069 
0070         DWORD dwWritten;
0071         if (!WriteFile(mStdInWr, data, (DWORD)size, &dwWritten, nullptr)) {
0072             qCDebug(KLEOPATRA_LOG) << "Failed to write";
0073             return -1;
0074         }
0075         if (dwWritten != size) {
0076             qCDebug(KLEOPATRA_LOG) << "Failed to write everything";
0077             return -1;
0078         }
0079         return size;
0080     }
0081 
0082     qint64 read(char *data, qint64 maxSize)
0083     {
0084         if (!mStdOutRd) {
0085             qCDebug(KLEOPATRA_LOG) << "Read of closed or write only device";
0086             return -1;
0087         }
0088 
0089         if (!maxSize) {
0090             return 0;
0091         }
0092 
0093         DWORD exitCode = 0;
0094         if (GetExitCodeProcess(mProc, &exitCode)) {
0095             if (exitCode != STILL_ACTIVE) {
0096                 if (exitCode) {
0097                     qCDebug(KLEOPATRA_LOG) << "Non zero exit code";
0098                     mError = readAllStdErr();
0099                     return -1;
0100                 }
0101                 mEnded = true;
0102                 qCDebug(KLEOPATRA_LOG) << "Process finished with code " << exitCode;
0103             }
0104         } else {
0105             qCDebug(KLEOPATRA_LOG) << "GetExitCodeProcess Failed";
0106         }
0107 
0108         if (mEnded) {
0109             DWORD avail = 0;
0110             if (!PeekNamedPipe(mStdOutRd, nullptr, 0, nullptr, &avail, nullptr)) {
0111                 qCDebug(KLEOPATRA_LOG) << "Failed to peek pipe";
0112                 return -1;
0113             }
0114             if (!avail) {
0115                 qCDebug(KLEOPATRA_LOG) << "Process ended and nothing more in pipe";
0116                 return 0;
0117             }
0118         }
0119 
0120         DWORD dwRead;
0121         if (!ReadFile(mStdOutRd, data, (DWORD)maxSize, &dwRead, nullptr)) {
0122             qCDebug(KLEOPATRA_LOG) << "Failed to read";
0123             return -1;
0124         }
0125 
0126         return dwRead;
0127     }
0128 
0129     QString readAllStdErr()
0130     {
0131         QString ret;
0132         if (!mStdErrRd) {
0133             qCDebug(KLEOPATRA_LOG) << "Read of closed stderr";
0134         }
0135         DWORD dwRead = 0;
0136         do {
0137             char buf[4096];
0138             DWORD avail;
0139             if (!PeekNamedPipe(mStdErrRd, nullptr, 0, nullptr, &avail, nullptr)) {
0140                 qCDebug(KLEOPATRA_LOG) << "Failed to peek pipe";
0141                 return ret;
0142             }
0143             if (!avail) {
0144                 return ret;
0145             }
0146             ReadFile(mStdErrRd, buf, 4096, &dwRead, nullptr);
0147             if (dwRead) {
0148                 QByteArray ba(buf, dwRead);
0149                 ret += QString::fromLocal8Bit(ba);
0150             }
0151         } while (dwRead);
0152         return ret;
0153     }
0154 
0155     void close()
0156     {
0157         if (mProc && mProc != INVALID_HANDLE_VALUE) {
0158             TerminateProcess(mProc, 0xf291);
0159             CloseHandleX(mProc);
0160         }
0161     }
0162 
0163     QString errorString()
0164     {
0165         return mError;
0166     }
0167 
0168     void closeWriteChannel()
0169     {
0170         CloseHandleX(mStdInWr);
0171     }
0172 
0173 private:
0174     QString mPath;
0175     QStringList mArgs;
0176     QString mWorkingDirectory;
0177     QString mError;
0178     HANDLE mStdInRd;
0179     HANDLE mStdInWr;
0180     HANDLE mStdOutRd;
0181     HANDLE mStdOutWr;
0182     HANDLE mStdErrRd;
0183     HANDLE mStdErrWr;
0184     HANDLE mProc;
0185     HANDLE mThread;
0186     bool mEnded;
0187 };
0188 
0189 WindowsProcessDevice::WindowsProcessDevice(const QString &path, const QStringList &args, const QString &wd)
0190     : d(new Private(path, args, wd))
0191 {
0192 }
0193 
0194 bool WindowsProcessDevice::open(QIODevice::OpenMode mode)
0195 {
0196     bool ret = d->start(mode);
0197     if (ret) {
0198         setOpenMode(mode);
0199     }
0200     return ret;
0201 }
0202 
0203 qint64 WindowsProcessDevice::readData(char *data, qint64 maxSize)
0204 {
0205     return d->read(data, maxSize);
0206 }
0207 
0208 qint64 WindowsProcessDevice::writeData(const char *data, qint64 maxSize)
0209 {
0210     return d->write(data, maxSize);
0211 }
0212 
0213 bool WindowsProcessDevice::isSequential() const
0214 {
0215     return true;
0216 }
0217 
0218 void WindowsProcessDevice::closeWriteChannel()
0219 {
0220     d->closeWriteChannel();
0221 }
0222 
0223 void WindowsProcessDevice::close()
0224 {
0225     d->close();
0226     QIODevice::close();
0227 }
0228 
0229 QString getLastErrorString()
0230 {
0231     wchar_t *lpMsgBuf = nullptr;
0232     DWORD dw = GetLastError();
0233 
0234     FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
0235                    NULL,
0236                    dw,
0237                    MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
0238                    (wchar_t *)&lpMsgBuf,
0239                    0,
0240                    NULL);
0241 
0242     QString ret = QString::fromWCharArray(lpMsgBuf);
0243 
0244     LocalFree(lpMsgBuf);
0245     return ret;
0246 }
0247 
0248 WindowsProcessDevice::Private::~Private()
0249 {
0250     if (mProc && mProc != INVALID_HANDLE_VALUE) {
0251         close();
0252     }
0253     CloseHandleX(mThread);
0254     CloseHandleX(mStdInRd);
0255     CloseHandleX(mStdInWr);
0256     CloseHandleX(mStdOutRd);
0257     CloseHandleX(mStdOutWr);
0258     CloseHandleX(mStdErrRd);
0259     CloseHandleX(mStdErrWr);
0260 }
0261 
0262 static QString qt_create_commandline(const QString &program, const QStringList &arguments, const QString &nativeArguments)
0263 {
0264     QString args;
0265     if (!program.isEmpty()) {
0266         QString programName = program;
0267         if (!programName.startsWith(QLatin1Char('\"')) && !programName.endsWith(QLatin1Char('\"')) && programName.contains(QLatin1Char(' ')))
0268             programName = QLatin1Char('\"') + programName + QLatin1Char('\"');
0269         programName.replace(QLatin1Char('/'), QLatin1Char('\\'));
0270 
0271         // add the prgram as the first arg ... it works better
0272         args = programName + QLatin1Char(' ');
0273     }
0274 
0275     for (int i = 0; i < arguments.size(); ++i) {
0276         QString tmp = arguments.at(i);
0277         // Quotes are escaped and their preceding backslashes are doubled.
0278         tmp.replace(QRegularExpression(QLatin1StringView(R"--((\\*)")--")), QLatin1String(R"--(\1\1\")--"));
0279         if (tmp.isEmpty() || tmp.contains(QLatin1Char(' ')) || tmp.contains(QLatin1Char('\t'))) {
0280             // The argument must not end with a \ since this would be interpreted
0281             // as escaping the quote -- rather put the \ behind the quote: e.g.
0282             // rather use "foo"\ than "foo\"
0283             int i = tmp.length();
0284             while (i > 0 && tmp.at(i - 1) == QLatin1Char('\\'))
0285                 --i;
0286             tmp.insert(i, QLatin1Char('"'));
0287             tmp.prepend(QLatin1Char('"'));
0288         }
0289         args += QLatin1Char(' ') + tmp;
0290     }
0291 
0292     if (!nativeArguments.isEmpty()) {
0293         if (!args.isEmpty())
0294             args += QLatin1Char(' ');
0295         args += nativeArguments;
0296     }
0297 
0298     return args;
0299 }
0300 
0301 bool WindowsProcessDevice::Private::start(QIODevice::OpenMode mode)
0302 {
0303     if (mode != QIODevice::ReadOnly //
0304         && mode != QIODevice::WriteOnly //
0305         && mode != QIODevice::ReadWrite) {
0306         qCDebug(KLEOPATRA_LOG) << "Unsupported open mode " << mode;
0307         return false;
0308     }
0309 
0310     SECURITY_ATTRIBUTES saAttr;
0311     ZeroMemory(&saAttr, sizeof(SECURITY_ATTRIBUTES));
0312     saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
0313     saAttr.bInheritHandle = TRUE;
0314     saAttr.lpSecurityDescriptor = NULL;
0315 
0316     // Create the pipes
0317     if (!CreatePipe(&mStdOutRd, &mStdOutWr, &saAttr, PIPEBUF_SIZE) //
0318         || !CreatePipe(&mStdErrRd, &mStdErrWr, &saAttr, 0) //
0319         || !CreatePipe(&mStdInRd, &mStdInWr, &saAttr, PIPEBUF_SIZE)) {
0320         qCDebug(KLEOPATRA_LOG) << "Failed to create pipes";
0321         mError = getLastErrorString();
0322         return false;
0323     }
0324 
0325     // Ensure only the proper handles are inherited
0326     if (!SetHandleInformation(mStdOutRd, HANDLE_FLAG_INHERIT, 0) //
0327         || !SetHandleInformation(mStdErrRd, HANDLE_FLAG_INHERIT, 0) //
0328         || !SetHandleInformation(mStdInWr, HANDLE_FLAG_INHERIT, 0)) {
0329         qCDebug(KLEOPATRA_LOG) << "Failed to set inherit flag";
0330         mError = getLastErrorString();
0331         return false;
0332     }
0333 
0334     PROCESS_INFORMATION piProcInfo;
0335     ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION));
0336 
0337     STARTUPINFO siStartInfo;
0338     ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
0339     siStartInfo.cb = sizeof(STARTUPINFO);
0340     siStartInfo.hStdError = mStdErrWr;
0341     siStartInfo.hStdOutput = mStdOutWr;
0342     siStartInfo.hStdInput = mStdInRd;
0343     siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
0344 
0345     const auto args = qt_create_commandline(mPath, mArgs, QString());
0346     wchar_t *cmdLine = wcsdup(reinterpret_cast<const wchar_t *>(args.utf16()));
0347     const wchar_t *proc = reinterpret_cast<const wchar_t *>(mPath.utf16());
0348     const QString nativeWorkingDirectory = QDir::toNativeSeparators(mWorkingDirectory);
0349     const wchar_t *wd = reinterpret_cast<const wchar_t *>(nativeWorkingDirectory.utf16());
0350 
0351     /* Filter out the handles to inherit only the three handles which we want to
0352      * inherit. As a Qt Application we have a multitude of open handles which
0353      * may or may not be inheritable. Testing has shown that this reduced about
0354      * thirty handles. Open File handles in our child application can also cause the
0355      * read pipe not to be closed correcly on exit. */
0356     SIZE_T size;
0357     bool suc = InitializeProcThreadAttributeList(NULL, 1, 0, &size) || GetLastError() == ERROR_INSUFFICIENT_BUFFER;
0358     if (!suc) {
0359         qCDebug(KLEOPATRA_LOG) << "Failed to get Attribute List size";
0360         mError = getLastErrorString();
0361         return false;
0362     }
0363     LPPROC_THREAD_ATTRIBUTE_LIST attributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(HeapAlloc(GetProcessHeap(), 0, size));
0364     if (!attributeList) {
0365         qCDebug(KLEOPATRA_LOG) << "Failed to Allocate Attribute List";
0366         return false;
0367     }
0368     suc = InitializeProcThreadAttributeList(attributeList, 1, 0, &size);
0369     if (!suc) {
0370         qCDebug(KLEOPATRA_LOG) << "Failed to Initalize Attribute List";
0371         mError = getLastErrorString();
0372         return false;
0373     }
0374 
0375     HANDLE handles[3];
0376     handles[0] = mStdOutWr;
0377     handles[1] = mStdErrWr;
0378     handles[2] = mStdInRd;
0379     suc = UpdateProcThreadAttribute(attributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, 3 * sizeof(HANDLE), NULL, NULL);
0380     if (!suc) {
0381         qCDebug(KLEOPATRA_LOG) << "Failed to Update Attribute List";
0382         mError = getLastErrorString();
0383         return false;
0384     }
0385     STARTUPINFOEX info;
0386     ZeroMemory(&info, sizeof(info));
0387     info.StartupInfo = siStartInfo;
0388     info.StartupInfo.cb = sizeof(info); // You have to know this,..
0389     info.lpAttributeList = attributeList;
0390 
0391     // Now lets start
0392     qCDebug(KLEOPATRA_LOG) << "Spawning:" << args;
0393     suc = CreateProcessW(proc,
0394                          cmdLine, // command line
0395                          NULL, // process security attributes
0396                          NULL, // primary thread security attributes
0397                          TRUE, // handles are inherited
0398                          CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, // creation flags
0399                          NULL, // use parent's environment
0400                          wd, // use parent's current directory
0401                          &info.StartupInfo, // STARTUPINFO pointer
0402                          &piProcInfo); // receives PROCESS_INFORMATION
0403     DeleteProcThreadAttributeList(attributeList);
0404     HeapFree(GetProcessHeap(), 0, attributeList);
0405     CloseHandleX(mStdOutWr);
0406     CloseHandleX(mStdErrWr);
0407     CloseHandleX(mStdInRd);
0408 
0409     free(cmdLine);
0410     if (!suc) {
0411         qCDebug(KLEOPATRA_LOG) << "Failed to create process";
0412         mError = getLastErrorString();
0413         return false;
0414     }
0415 
0416     mProc = piProcInfo.hProcess;
0417     mThread = piProcInfo.hThread;
0418     if (mode == QIODevice::WriteOnly) {
0419         CloseHandleX(mStdOutRd);
0420     }
0421 
0422     if (mode == QIODevice::ReadOnly) {
0423         CloseHandleX(mStdInWr);
0424     }
0425 
0426     return true;
0427 }
0428 
0429 QString WindowsProcessDevice::errorString()
0430 {
0431     return d->errorString();
0432 }
0433 
0434 #endif
0435 
0436 #include "moc_windowsprocessdevice.cpp"