2020-02-15 10:21:12 +00:00

670 lines
14 KiB
C++

/*
* Copyright (C) 2003-2007 Justin Karneges <justin@affinix.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*
*/
#include "gpgproc_p.h"
#ifdef Q_OS_MAC
#define QT_PIPE_HACK
#endif
using namespace QCA;
namespace gpgQCAPlugin {
void releaseAndDeleteLater(QObject *owner, QObject *obj)
{
obj->disconnect(owner);
obj->setParent(nullptr);
obj->deleteLater();
}
GPGProc::Private::Private(GPGProc *_q)
: QObject(_q)
, q(_q)
, pipeAux(this)
, pipeCommand(this)
, pipeStatus(this)
, startTrigger(this)
, doneTrigger(this)
{
qRegisterMetaType<gpgQCAPlugin::GPGProc::Error>("gpgQCAPlugin::GPGProc::Error");
proc = nullptr;
proc_relay = nullptr;
startTrigger.setSingleShot(true);
doneTrigger.setSingleShot(true);
connect(&pipeAux.writeEnd(), &QCA::QPipeEnd::bytesWritten, this, &GPGProc::Private::aux_written);
connect(&pipeAux.writeEnd(), &QCA::QPipeEnd::error, this, &GPGProc::Private::aux_error);
connect(&pipeCommand.writeEnd(), &QCA::QPipeEnd::bytesWritten, this, &GPGProc::Private::command_written);
connect(&pipeCommand.writeEnd(), &QCA::QPipeEnd::error, this, &GPGProc::Private::command_error);
connect(&pipeStatus.readEnd(), &QCA::QPipeEnd::readyRead, this, &GPGProc::Private::status_read);
connect(&pipeStatus.readEnd(), &QCA::QPipeEnd::error, this, &GPGProc::Private::status_error);
connect(&startTrigger, &QCA::SafeTimer::timeout, this, &GPGProc::Private::doStart);
connect(&doneTrigger, &QCA::SafeTimer::timeout, this, &GPGProc::Private::doTryDone);
reset(ResetSessionAndData);
}
GPGProc::Private::~Private()
{
reset(ResetSession);
}
void GPGProc::Private::closePipes()
{
#ifdef QT_PIPE_HACK
pipeAux.readEnd().reset();
pipeCommand.readEnd().reset();
pipeStatus.writeEnd().reset();
#endif
pipeAux.reset();
pipeCommand.reset();
pipeStatus.reset();
}
void GPGProc::Private::reset(ResetMode mode)
{
#ifndef QT_PIPE_HACK
closePipes();
#endif
if(proc)
{
proc->disconnect(this);
if(proc->state() != QProcess::NotRunning)
{
// Before try to correct end proccess
// Terminate if failed
proc->close();
bool finished = proc->waitForFinished(5000);
if (!finished)
proc->terminate();
}
proc->setParent(nullptr);
releaseAndDeleteLater(this, proc_relay);
proc_relay = nullptr;
delete proc; // should be safe to do thanks to relay
proc = nullptr;
}
#ifdef QT_PIPE_HACK
closePipes();
#endif
startTrigger.stop();
doneTrigger.stop();
pre_stdin.clear();
pre_aux.clear();
pre_command.clear();
pre_stdin_close = false;
pre_aux_close = false;
pre_command_close = false;
need_status = false;
fin_process = false;
fin_status = false;
if(mode >= ResetSessionAndData)
{
statusBuf.clear();
statusLines.clear();
leftover_stdout.clear();
leftover_stderr.clear();
error = GPGProc::FailedToStart;
exitCode = -1;
}
}
bool GPGProc::Private::setupPipes(bool makeAux)
{
if(makeAux && !pipeAux.create())
{
closePipes();
emit q->debug(QStringLiteral("Error creating pipeAux"));
return false;
}
#ifdef QPIPE_SECURE
if(!pipeCommand.create(true)) // secure
#else
if(!pipeCommand.create())
#endif
{
closePipes();
emit q->debug(QStringLiteral("Error creating pipeCommand"));
return false;
}
if(!pipeStatus.create())
{
closePipes();
emit q->debug(QStringLiteral("Error creating pipeStatus"));
return false;
}
return true;
}
void GPGProc::Private::setupArguments()
{
QStringList fullargs;
fullargs += QStringLiteral("--no-tty");
fullargs += QStringLiteral("--pinentry-mode");
fullargs += QStringLiteral("loopback");
if(mode == ExtendedMode)
{
fullargs += QStringLiteral("--enable-special-filenames");
fullargs += QStringLiteral("--status-fd");
fullargs += QString::number(pipeStatus.writeEnd().idAsInt());
fullargs += QStringLiteral("--command-fd");
fullargs += QString::number(pipeCommand.readEnd().idAsInt());
}
for(int n = 0; n < args.count(); ++n)
{
QString a = args[n];
if(mode == ExtendedMode && a == QLatin1String("-&?"))
fullargs += QStringLiteral("-&") + QString::number(pipeAux.readEnd().idAsInt());
else
fullargs += a;
}
QString fullcmd = fullargs.join(QStringLiteral(" "));
emit q->debug(QStringLiteral("Running: [") + bin + QLatin1Char(' ') + fullcmd + QLatin1Char(']'));
args = fullargs;
}
void GPGProc::Private::doStart()
{
#ifdef Q_OS_WIN
// Note: for unix, inheritability is set in SProcess
if(pipeAux.readEnd().isValid())
pipeAux.readEnd().setInheritable(true);
if(pipeCommand.readEnd().isValid())
pipeCommand.readEnd().setInheritable(true);
if(pipeStatus.writeEnd().isValid())
pipeStatus.writeEnd().setInheritable(true);
#endif
setupArguments();
proc->start(bin, args);
proc->waitForStarted();
pipeAux.readEnd().close();
pipeCommand.readEnd().close();
pipeStatus.writeEnd().close();
}
void GPGProc::Private::aux_written(int x)
{
emit q->bytesWrittenAux(x);
}
void GPGProc::Private::aux_error(QCA::QPipeEnd::Error)
{
emit q->debug(QStringLiteral("Aux: Pipe error"));
reset(ResetSession);
emit q->error(GPGProc::ErrorWrite);
}
void GPGProc::Private::command_written(int x)
{
emit q->bytesWrittenCommand(x);
}
void GPGProc::Private::command_error(QCA::QPipeEnd::Error)
{
emit q->debug(QStringLiteral("Command: Pipe error"));
reset(ResetSession);
emit q->error(GPGProc::ErrorWrite);
}
void GPGProc::Private::status_read()
{
if(readAndProcessStatusData())
emit q->readyReadStatusLines();
}
void GPGProc::Private::status_error(QCA::QPipeEnd::Error e)
{
if(e == QPipeEnd::ErrorEOF)
emit q->debug(QStringLiteral("Status: Closed (EOF)"));
else
emit q->debug(QStringLiteral("Status: Closed (gone)"));
fin_status = true;
doTryDone();
}
void GPGProc::Private::proc_started()
{
emit q->debug(QStringLiteral("Process started"));
// Note: we don't close these here anymore. instead we
// do it just after calling proc->start().
// close these, we don't need them
/*pipeAux.readEnd().close();
pipeCommand.readEnd().close();
pipeStatus.writeEnd().close();*/
// do the pre* stuff
if(!pre_stdin.isEmpty())
{
proc->write(pre_stdin);
pre_stdin.clear();
}
if(!pre_aux.isEmpty())
{
pipeAux.writeEnd().write(pre_aux);
pre_aux.clear();
}
if(!pre_command.isEmpty())
{
#ifdef QPIPE_SECURE
pipeCommand.writeEnd().writeSecure(pre_command);
#else
pipeCommand.writeEnd().write(pre_command);
#endif
pre_command.clear();
}
if(pre_stdin_close)
{
proc->waitForBytesWritten();
proc->closeWriteChannel();
}
if(pre_aux_close)
pipeAux.writeEnd().close();
if(pre_command_close)
pipeCommand.writeEnd().close();
}
void GPGProc::Private::proc_readyReadStandardOutput()
{
emit q->readyReadStdout();
}
void GPGProc::Private::proc_readyReadStandardError()
{
emit q->readyReadStderr();
}
void GPGProc::Private::proc_bytesWritten(qint64 lx)
{
int x = (int)lx;
emit q->bytesWrittenStdin(x);
}
void GPGProc::Private::proc_finished(int x)
{
emit q->debug(QStringLiteral("Process finished: %1").arg(x));
exitCode = x;
fin_process = true;
fin_process_success = true;
if(need_status && !fin_status)
{
pipeStatus.readEnd().finalize();
fin_status = true;
if(readAndProcessStatusData())
{
doneTrigger.start();
emit q->readyReadStatusLines();
return;
}
}
doTryDone();
}
void GPGProc::Private::proc_error(QProcess::ProcessError x)
{
QMap<int, QString> errmap;
errmap[QProcess::FailedToStart] = QStringLiteral("FailedToStart");
errmap[QProcess::Crashed] = QStringLiteral("Crashed");
errmap[QProcess::Timedout] = QStringLiteral("Timedout");
errmap[QProcess::WriteError] = QStringLiteral("WriteError");
errmap[QProcess::ReadError] = QStringLiteral("ReadError");
errmap[QProcess::UnknownError] = QStringLiteral("UnknownError");
emit q->debug(QStringLiteral("Process error: %1").arg(errmap[x]));
if(x == QProcess::FailedToStart)
error = GPGProc::FailedToStart;
else if(x == QProcess::WriteError)
error = GPGProc::ErrorWrite;
else
error = GPGProc::UnexpectedExit;
fin_process = true;
fin_process_success = false;
#ifdef QT_PIPE_HACK
// If the process fails to start, then the ends of the pipes
// intended for the child process are still open. Some Mac
// users experience a lockup if we close our ends of the pipes
// when the child's ends are still open. If we ensure the
// child's ends are closed, we prevent this lockup. I have no
// idea why the problem even happens or why this fix should
// work.
pipeAux.readEnd().reset();
pipeCommand.readEnd().reset();
pipeStatus.writeEnd().reset();
#endif
if(need_status && !fin_status)
{
pipeStatus.readEnd().finalize();
fin_status = true;
if(readAndProcessStatusData())
{
doneTrigger.start();
emit q->readyReadStatusLines();
return;
}
}
doTryDone();
}
void GPGProc::Private::doTryDone()
{
if(!fin_process)
return;
if(need_status && !fin_status)
return;
emit q->debug(QStringLiteral("Done"));
// get leftover data
proc->setReadChannel(QProcess::StandardOutput);
leftover_stdout = proc->readAll();
proc->setReadChannel(QProcess::StandardError);
leftover_stderr = proc->readAll();
reset(ResetSession);
if(fin_process_success)
emit q->finished(exitCode);
else
emit q->error(error);
}
bool GPGProc::Private::readAndProcessStatusData()
{
QByteArray buf = pipeStatus.readEnd().read();
if(buf.isEmpty())
return false;
return processStatusData(buf);
}
// return true if there are newly parsed lines available
bool GPGProc::Private::processStatusData(const QByteArray &buf)
{
statusBuf.append(buf);
// extract all lines
QStringList list;
while(true)
{
int n = statusBuf.indexOf('\n');
if(n == -1)
break;
// extract the string from statusbuf
++n;
char *p = (char *)statusBuf.data();
QByteArray cs(p, n);
int newsize = statusBuf.size() - n;
memmove(p, p + n, newsize);
statusBuf.resize(newsize);
// convert to string without newline
QString str = QString::fromUtf8(cs);
str.truncate(str.length() - 1);
// ensure it has a proper header
if(str.left(9) != QLatin1String("[GNUPG:] "))
continue;
// take it off
str = str.mid(9);
// add to the list
list += str;
}
if(list.isEmpty())
return false;
statusLines += list;
return true;
}
GPGProc::GPGProc(QObject *parent)
:QObject(parent)
{
d = new Private(this);
}
GPGProc::~GPGProc()
{
delete d;
}
void GPGProc::reset()
{
d->reset(ResetAll);
}
bool GPGProc::isActive() const
{
return (d->proc ? true : false);
}
void GPGProc::start(const QString &bin, const QStringList &args, Mode mode)
{
if(isActive())
d->reset(ResetSessionAndData);
if(mode == ExtendedMode)
{
if(!d->setupPipes(args.contains(QStringLiteral("-&?"))))
{
d->error = FailedToStart;
// emit later
QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, Q_ARG(gpgQCAPlugin::GPGProc::Error, d->error));
return;
}
d->need_status = true;
emit debug(QStringLiteral("Pipe setup complete"));
}
d->proc = new SProcess(d);
#ifdef Q_OS_UNIX
QList<int> plist;
if(d->pipeAux.readEnd().isValid())
plist += d->pipeAux.readEnd().id();
if(d->pipeCommand.readEnd().isValid())
plist += d->pipeCommand.readEnd().id();
if(d->pipeStatus.writeEnd().isValid())
plist += d->pipeStatus.writeEnd().id();
d->proc->setInheritPipeList(plist);
#endif
// enable the pipes we want
if(d->pipeAux.writeEnd().isValid())
d->pipeAux.writeEnd().enable();
if(d->pipeCommand.writeEnd().isValid())
d->pipeCommand.writeEnd().enable();
if(d->pipeStatus.readEnd().isValid())
d->pipeStatus.readEnd().enable();
d->proc_relay = new QProcessSignalRelay(d->proc, d);
connect(d->proc_relay, &QProcessSignalRelay::started, d, &GPGProc::Private::proc_started);
connect(d->proc_relay, &QProcessSignalRelay::readyReadStandardOutput, d, &GPGProc::Private::proc_readyReadStandardOutput);
connect(d->proc_relay, &QProcessSignalRelay::readyReadStandardError, d, &GPGProc::Private::proc_readyReadStandardError);
connect(d->proc_relay, &QProcessSignalRelay::bytesWritten, d, &GPGProc::Private::proc_bytesWritten);
connect(d->proc_relay, &QProcessSignalRelay::finished, d, &GPGProc::Private::proc_finished);
connect(d->proc_relay, &QProcessSignalRelay::error, d, &GPGProc::Private::proc_error);
d->bin = bin;
d->args = args;
d->mode = mode;
d->startTrigger.start();
}
QByteArray GPGProc::readStdout()
{
if(d->proc)
{
d->proc->setReadChannel(QProcess::StandardOutput);
return d->proc->readAll();
}
else
{
QByteArray a = d->leftover_stdout;
d->leftover_stdout.clear();
return a;
}
}
QByteArray GPGProc::readStderr()
{
if(d->proc)
{
d->proc->setReadChannel(QProcess::StandardError);
return d->proc->readAll();
}
else
{
QByteArray a = d->leftover_stderr;
d->leftover_stderr.clear();
return a;
}
}
QStringList GPGProc::readStatusLines()
{
QStringList out = d->statusLines;
d->statusLines.clear();
return out;
}
void GPGProc::writeStdin(const QByteArray &a)
{
if(!d->proc || a.isEmpty())
return;
if(d->proc->state() == QProcess::Running)
d->proc->write(a);
else
d->pre_stdin += a;
}
void GPGProc::writeAux(const QByteArray &a)
{
if(!d->proc || a.isEmpty())
return;
if(d->proc->state() == QProcess::Running)
d->pipeAux.writeEnd().write(a);
else
d->pre_aux += a;
}
#ifdef QPIPE_SECURE
void GPGProc::writeCommand(const SecureArray &a)
#else
void GPGProc::writeCommand(const QByteArray &a)
#endif
{
if(!d->proc || a.isEmpty())
return;
if(d->proc->state() == QProcess::Running)
#ifdef QPIPE_SECURE
d->pipeCommand.writeEnd().writeSecure(a);
#else
d->pipeCommand.writeEnd().write(a);
#endif
else
d->pre_command += a;
}
void GPGProc::closeStdin()
{
if(!d->proc)
return;
if(d->proc->state() == QProcess::Running)
{
d->proc->waitForBytesWritten();
d->proc->closeWriteChannel();
}
else
{
d->pre_stdin_close = true;
}
}
void GPGProc::closeAux()
{
if(!d->proc)
return;
if(d->proc->state() == QProcess::Running)
d->pipeAux.writeEnd().close();
else
d->pre_aux_close = true;
}
void GPGProc::closeCommand()
{
if(!d->proc)
return;
if(d->proc->state() == QProcess::Running)
d->pipeCommand.writeEnd().close();
else
d->pre_command_close = true;
}
}