410 lines
14 KiB
C++
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;
|
|
}
|