Files
EetCpp/playground/main.cpp
T

410 lines
14 KiB
C++

#include "eetv4client.h"
#include "eetv4signer.h"
#include "eetca.h"
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QDateTime>
#include <QFile>
#include <QSaveFile>
#include <QTextStream>
namespace
{
void printError(const QString &message)
{
QTextStream(stderr) << "error: " << message << Qt::endl;
}
bool requiredOption(const QCommandLineParser &parser, const QString &name)
{
if (!parser.isSet(name))
{
printError(QStringLiteral("missing required option --%1").arg(name));
return false;
}
return true;
}
bool parseAmount(const QString &value, EetV4Amount *amount)
{
QString error;
if (!EetV4Amount::fromString(value, amount, &error))
{
printError(error);
return false;
}
return true;
}
int preflight(const QCommandLineParser &parser)
{
if (!requiredOption(parser, QStringLiteral("endpoint")) ||
!requiredOption(parser, QStringLiteral("certificate")) ||
!requiredOption(parser, QStringLiteral("password-env")))
{
return 2;
}
const QUrl endpoint(parser.value(QStringLiteral("endpoint")));
if (!endpoint.isValid() || endpoint.scheme() != QLatin1String("https") ||
endpoint.host().isEmpty())
{
printError(QStringLiteral("endpoint must be a valid HTTPS URL"));
return 2;
}
QFile certificate(parser.value(QStringLiteral("certificate")));
if (!certificate.open(QIODevice::ReadOnly) || certificate.size() == 0)
{
printError(QStringLiteral("certificate file cannot be read"));
return 2;
}
const QByteArray password = qgetenv(parser.value(QStringLiteral("password-env")).toLatin1());
if (password.isEmpty())
{
printError(QStringLiteral("PKCS#12 password environment variable is empty or unset"));
return 2;
}
const QByteArray probeSoap =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\""
" xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/"
"oasis-200401-wss-wssecurity-secext-1.0.xsd\""
" xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/"
"oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
"<soap:Header/><soap:Body wsu:Id=\"preflight-body\"><test/></soap:Body>"
"</soap:Envelope>";
QString signingError;
if (EetV4Signer::signSoap(probeSoap, certificate.fileName(), password, &signingError).isEmpty())
{
printError(signingError);
return 2;
}
QTextStream(stdout) << "configuration looks usable" << Qt::endl;
return 0;
}
int sendSale(const QCommandLineParser &parser)
{
const QStringList required{
QStringLiteral("endpoint"), QStringLiteral("certificate"),
QStringLiteral("password-env"), QStringLiteral("eic"),
QStringLiteral("unit"), QStringLiteral("register"),
QStringLiteral("receipt"), QStringLiteral("amount")};
for (const QString &option : required)
{
if (!requiredOption(parser, option))
{
return 2;
}
}
EetV4SaleRequest request;
request.taxpayerEic = parser.value(QStringLiteral("eic"));
request.unitId = parser.value(QStringLiteral("unit")).toInt();
request.cashRegisterId = parser.value(QStringLiteral("register"));
request.receiptNumber = parser.value(QStringLiteral("receipt"));
if (!parseAmount(parser.value(QStringLiteral("amount")), &request.totalSale))
{
return 2;
}
const QDateTime localNow = QDateTime::currentDateTime();
const QDateTime now = localNow.toOffsetFromUtc(localNow.offsetFromUtc());
request.sentAt = now;
request.saleAt = now;
if (parser.isSet(QStringLiteral("sale-at")))
{
request.saleAt = QDateTime::fromString(parser.value(QStringLiteral("sale-at")),
Qt::ISODate);
}
if (parser.isSet(QStringLiteral("sent-at")))
{
request.sentAt = QDateTime::fromString(parser.value(QStringLiteral("sent-at")),
Qt::ISODate);
}
if (parser.isSet(QStringLiteral("verification")))
{
request.verificationMode = true;
}
if (parser.isSet(QStringLiteral("first-submission")))
{
request.firstSubmission = parser.value(QStringLiteral("first-submission")) !=
QLatin1String("false");
}
const QByteArray password = qgetenv(parser.value(QStringLiteral("password-env")).toLatin1());
if (password.isEmpty())
{
printError(QStringLiteral("PKCS#12 password environment variable is empty or unset"));
return 2;
}
EetV4Client client({QUrl(parser.value(QStringLiteral("endpoint"))),
parser.value(QStringLiteral("timeout")).toInt()});
EetV4Response response;
QString error;
if (!client.sendSale(request, parser.value(QStringLiteral("certificate")), password,
&response, &error))
{
printError(error);
return 1;
}
if (response.status == EetV4ResponseStatus::Success)
{
QTextStream(stdout) << "sale accepted" << Qt::endl
<< "POK: " << response.pok << Qt::endl;
}
else if (response.status == EetV4ResponseStatus::Error)
{
QTextStream(stdout) << "EET rejected the sale" << Qt::endl
<< "code: " << response.errorCode << Qt::endl
<< "message: " << response.message << Qt::endl;
return 1;
}
else
{
QTextStream(stdout) << "SOAP Fault: " << response.message << Qt::endl;
return 1;
}
if (!response.warningCodes.isEmpty())
{
QTextStream output(stdout);
output << "warnings:";
for (const int code : response.warningCodes)
{
output << ' ' << code;
}
output << Qt::endl;
}
return 0;
}
bool caOptions(const QCommandLineParser &parser, EetCaClient *client)
{
if (!requiredOption(parser, QStringLiteral("api-url")) ||
!requiredOption(parser, QStringLiteral("signing-certificate")) ||
!requiredOption(parser, QStringLiteral("signing-password-env")))
{
return false;
}
const QByteArray password =
qgetenv(parser.value(QStringLiteral("signing-password-env")).toLatin1());
if (password.isEmpty())
{
printError(QStringLiteral("CA signing password environment variable is empty or unset"));
return false;
}
*client = EetCaClient({
QUrl(parser.value(QStringLiteral("api-url"))),
parser.value(QStringLiteral("signing-certificate")),
password,
parser.value(QStringLiteral("timeout")).toInt()
});
return true;
}
bool parseRequestId(const QCommandLineParser &parser, QUuid *requestId)
{
if (!requiredOption(parser, QStringLiteral("request-id")))
{
return false;
}
*requestId = QUuid(parser.value(QStringLiteral("request-id")));
if (requestId->isNull())
{
printError(QStringLiteral("request-id must be a valid UUID"));
return false;
}
return true;
}
bool writeSecretFile(const QString &path, const QByteArray &data, QString *error)
{
if (path.isEmpty() || data.isEmpty())
{
*error = QStringLiteral("output path and secret data are required");
return false;
}
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit())
{
*error = QStringLiteral("cannot securely write %1").arg(path);
return false;
}
file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
return true;
}
int caCommand(const QString &command, const QCommandLineParser &parser)
{
EetCaClient client({QUrl(), {}, {}, 0});
if (!caOptions(parser, &client))
{
return 2;
}
QString error;
if (command == QLatin1String("ca-renew"))
{
QUuid requestId;
if (!client.requestRenewal(&requestId, &error))
{
printError(error);
return 1;
}
QTextStream(stdout) << requestId.toString(QUuid::WithoutBraces) << Qt::endl;
return 0;
}
if (command == QLatin1String("ca-list"))
{
QList<QUuid> requestIds;
if (!client.listUnfinished(&requestIds, &error))
{
printError(error);
return 1;
}
for (const QUuid &requestId : requestIds)
{
QTextStream(stdout) << requestId.toString(QUuid::WithoutBraces) << Qt::endl;
}
return 0;
}
QUuid requestId;
if (!parseRequestId(parser, &requestId))
{
return 2;
}
if (command == QLatin1String("ca-status"))
{
EetCaRequestStatus status;
if (!client.requestStatus(requestId, &status, &error))
{
printError(error);
return 1;
}
QTextStream output(stdout);
output << "state: " << status.state << Qt::endl;
if (status.pollAfterSeconds > 0)
{
output << "poll-after-seconds: " << status.pollAfterSeconds << Qt::endl;
}
if (!status.rejectedReason.isEmpty())
{
output << "rejected-reason: " << status.rejectedReason << Qt::endl;
}
return 0;
}
if (command == QLatin1String("ca-claim"))
{
if (!requiredOption(parser, QStringLiteral("output-p12")) ||
!requiredOption(parser, QStringLiteral("output-password")))
{
return 2;
}
EetCaPkcs12 package;
if (!client.claimDownload(requestId, &package, &error))
{
printError(error);
return 1;
}
if (!writeSecretFile(parser.value(QStringLiteral("output-p12")), package.pkcs12, &error) ||
!writeSecretFile(parser.value(QStringLiteral("output-password")),
package.password, &error))
{
printError(error);
return 1;
}
QTextStream(stdout) << "certificate package saved; secret values were not printed"
<< Qt::endl;
return 0;
}
if (command == QLatin1String("ca-ack"))
{
if (!client.acknowledgeDownload(requestId, &error))
{
printError(error);
return 1;
}
QTextStream(stdout) << "certificate download acknowledged" << Qt::endl;
return 0;
}
return 2;
}
}
int main(int argc, char **argv)
{
QCoreApplication application(argc, argv);
QCoreApplication::setApplicationName(QStringLiteral("EetCppPlayground"));
QCommandLineParser parser;
parser.setApplicationDescription(QStringLiteral("Manual EET v4 test client"));
parser.addHelpOption();
parser.addVersionOption();
parser.addPositionalArgument(QStringLiteral("command"),
QStringLiteral("preflight, send-sale, ca-renew, ca-status, "
"ca-list, ca-claim or ca-ack"));
parser.addOption({QStringLiteral("endpoint"), QStringLiteral("EET HTTPS endpoint."),
QStringLiteral("url")});
parser.addOption({QStringLiteral("certificate"), QStringLiteral("PKCS#12 certificate path."),
QStringLiteral("path")});
parser.addOption({QStringLiteral("password-env"),
QStringLiteral("Environment variable containing the PKCS#12 password."),
QStringLiteral("name")});
parser.addOption({QStringLiteral("eic"), QStringLiteral("Taxpayer EIC."),
QStringLiteral("value")});
parser.addOption({QStringLiteral("unit"), QStringLiteral("Evidence unit ID."),
QStringLiteral("number")});
parser.addOption({QStringLiteral("register"), QStringLiteral("Cash register ID."),
QStringLiteral("value")});
parser.addOption({QStringLiteral("receipt"), QStringLiteral("Receipt number."),
QStringLiteral("value")});
parser.addOption({QStringLiteral("amount"), QStringLiteral("Total sale amount."),
QStringLiteral("amount")});
parser.addOption({QStringLiteral("sale-at"), QStringLiteral("Sale timestamp (ISO 8601)."),
QStringLiteral("timestamp")});
parser.addOption({QStringLiteral("sent-at"), QStringLiteral("Submission timestamp (ISO 8601)."),
QStringLiteral("timestamp")});
parser.addOption({QStringLiteral("timeout"), QStringLiteral("Timeout in milliseconds."),
QStringLiteral("milliseconds"), QStringLiteral("30000")});
parser.addOption({QStringLiteral("verification"),
QStringLiteral("Use verification mode.")});
parser.addOption({QStringLiteral("first-submission"),
QStringLiteral("Whether this is the first submission (default true)."),
QStringLiteral("true|false"), QStringLiteral("true")});
parser.addOption({QStringLiteral("api-url"), QStringLiteral("CA EET API base URL."),
QStringLiteral("url")});
parser.addOption({QStringLiteral("signing-certificate"),
QStringLiteral("PKCS#12 certificate used for CA JWT signing."),
QStringLiteral("path")});
parser.addOption({QStringLiteral("signing-password-env"),
QStringLiteral("Environment variable containing the CA signing password."),
QStringLiteral("name")});
parser.addOption({QStringLiteral("request-id"), QStringLiteral("CA request UUID."),
QStringLiteral("uuid")});
parser.addOption({QStringLiteral("output-p12"),
QStringLiteral("Output path for downloaded PKCS#12 data."),
QStringLiteral("path")});
parser.addOption({QStringLiteral("output-password"),
QStringLiteral("Output path for downloaded PKCS#12 password."),
QStringLiteral("path")});
parser.process(application);
if (parser.positionalArguments().size() != 1)
{
printError(QStringLiteral("exactly one command is required"));
return 2;
}
const QString command = parser.positionalArguments().first();
if (command == QLatin1String("preflight"))
{
return preflight(parser);
}
if (command == QLatin1String("send-sale"))
{
return sendSale(parser);
}
if (command.startsWith(QLatin1String("ca-")))
{
return caCommand(command, parser);
}
printError(QStringLiteral("unknown command: %1").arg(command));
return 2;
}