rewrite for eet 2.0 api version v4

This commit is contained in:
2026-10-13 23:28:55 +02:00
parent c926a173ba
commit 3f8e4796b3
55 changed files with 3698 additions and 1611 deletions
+17
View File
@@ -0,0 +1,17 @@
# Copy this file to a location outside version control and fill in test values.
# Never commit the copy or a PKCS#12 file.
EETCPP_PLAYGROUND=./build/playground/EetCppPlayground
EET_ENDPOINT=https://replace-with-official-eet-test-endpoint
EET_PKCS12_PATH=/secure/path/eet-test-certificate.p12
EET_PKCS12_PASSWORD=replace-with-secret
EET_EIC=CZ123456789
EET_UNIT_ID=1
EET_REGISTER_ID=PLAYGROUND
EET_RECEIPT_NUMBER=PLAYGROUND-001
EET_AMOUNT=100.00
EET_TIMEOUT_MS=30000
CA_API_URL=https://ca.test.caeet.gov.cz/api
CA_SIGNING_PKCS12_PATH=/secure/path/eet-old-certificate.p12
CA_SIGNING_PASSWORD=replace-with-secret
+72
View File
@@ -0,0 +1,72 @@
name: EetCpp CI
on:
push:
pull_request:
permissions:
contents: read
jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- name: Linux amd64
runner: ubuntu-24.04
package-manager: apt
- name: Linux arm64
runner: ubuntu-24.04-arm
package-manager: apt
- name: macOS amd64
runner: macos-13
package-manager: brew
- name: macOS arm64
runner: macos-14
package-manager: brew
- name: Windows amd64
runner: windows-2025
package-manager: vcpkg
steps:
- uses: actions/checkout@v4
- name: Install Linux dependencies
if: matrix.package-manager == 'apt'
run: |
sudo apt-get update
sudo apt-get install -y cmake ninja-build pkg-config \
qt6-base-dev libxml2-dev libxmlsec1-dev libssl-dev
- name: Install macOS dependencies
if: matrix.package-manager == 'brew'
run: |
brew install cmake ninja pkg-config qt libxml2 libxmlsec1 openssl@3
- name: Install Windows dependencies
if: matrix.package-manager == 'vcpkg'
shell: pwsh
run: |
vcpkg install `
qtbase:x64-windows `
libxml2:x64-windows `
xmlsec:x64-windows
- name: Configure
shell: bash
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DEETCPP_BUILD_PLAYGROUND=ON
- name: Build
run: cmake --build build --parallel
- name: Test
run: ctest --test-dir build --output-on-failure
- name: Install package
run: cmake --install build --prefix "$PWD/install"
+124
View File
@@ -0,0 +1,124 @@
cmake_minimum_required(VERSION 3.24)
project(EetCpp
VERSION 2.0.0
LANGUAGES CXX
)
option(EETCPP_BUILD_PLAYGROUND "Build the manual EET v4 Playground CLI" ON)
include(CTest)
include(CMakePackageConfigHelpers)
find_package(Qt6 6.5 REQUIRED COMPONENTS Core Network)
find_package(PkgConfig REQUIRED)
pkg_check_modules(XMLSEC1 REQUIRED IMPORTED_TARGET xmlsec1)
find_path(LIBXML2_INCLUDE_DIR libxml/parser.h
HINTS
${CMAKE_PREFIX_PATH}/opt/libxml2/include/libxml2
/opt/homebrew/opt/libxml2/include/libxml2
/usr/local/opt/libxml2/include/libxml2
)
find_library(LIBXML2_LIBRARY NAMES xml2
HINTS
${CMAKE_PREFIX_PATH}/opt/libxml2/lib
/opt/homebrew/opt/libxml2/lib
/usr/local/opt/libxml2/lib
)
if(NOT LIBXML2_INCLUDE_DIR OR NOT LIBXML2_LIBRARY)
message(FATAL_ERROR "A libxml2 installation compatible with xmlsec1 is required.")
endif()
add_library(EetCpp STATIC
libEet/eetcpp_global.h
libEet/eetv4request.cpp
libEet/eetv4request.h
libEet/eetv4xml.cpp
libEet/eetv4xml.h
libEet/eetv4soap.cpp
libEet/eetv4soap.h
libEet/eetv4signer.cpp
libEet/eetv4signer.h
libEet/eetv4response.cpp
libEet/eetv4response.h
libEet/eetv4client.cpp
libEet/eetv4client.h
libEet/eetca.cpp
libEet/eetca.h
)
add_library(EetCpp::EetCpp ALIAS EetCpp)
target_compile_features(EetCpp PUBLIC cxx_std_17)
target_compile_definitions(EetCpp PRIVATE EETCPP_LIBRARY)
target_include_directories(EetCpp
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/libEet>
$<INSTALL_INTERFACE:include/EetCpp>
PRIVATE
${LIBXML2_INCLUDE_DIR}
)
target_link_libraries(EetCpp
PUBLIC
Qt6::Core
Qt6::Network
${LIBXML2_LIBRARY}
PkgConfig::XMLSEC1
)
set_target_properties(EetCpp PROPERTIES
EXPORT_NAME EetCpp
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
include(GNUInstallDirs)
install(TARGETS EetCpp
EXPORT EetCppTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(FILES
libEet/eetcpp_global.h
libEet/eetcpp.h
libEet/eetv4request.h
libEet/eetv4xml.h
libEet/eetv4soap.h
libEet/eetv4signer.h
libEet/eetv4response.h
libEet/eetv4client.h
libEet/eetca.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/EetCpp
)
configure_package_config_file(
eetcpp-config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/EetCppConfig.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/EetCpp
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/EetCppConfigVersion.cmake
COMPATIBILITY SameMajorVersion
)
install(EXPORT EetCppTargets
FILE EetCppTargets.cmake
NAMESPACE EetCpp::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/EetCpp
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/EetCppConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/EetCppConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/EetCpp
)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
if(EETCPP_BUILD_PLAYGROUND)
add_subdirectory(playground)
endif()
+278
View File
@@ -0,0 +1,278 @@
# EetCpp
EetCpp is a Qt 6/CMake implementation of the Czech EET v4 protocol. The
library is intended to run on Windows, macOS and Linux on amd64 and arm64.
The current implementation is a new v4 API. EET v3 is not supported.
## Requirements
- CMake 3.24 or newer
- C++17 compiler
- Qt 6.5 or newer (`Core`, `Network`, `Test`)
- xmlsec1 with an OpenSSL backend
- libxml2
- OpenSSL 3 or newer is recommended
## Build and test
Build outside the source tree:
```sh
cmake -S . -B /tmp/EetCpp-build -DCMAKE_PREFIX_PATH=/path/to/qt
cmake --build /tmp/EetCpp-build
ctest --test-dir /tmp/EetCpp-build --output-on-failure
```
The Playground CLI is enabled by default. Disable it with
`-DEETCPP_BUILD_PLAYGROUND=OFF`.
The test suite covers:
- fixed-point amounts and v4 request validation,
- v4 XML and SOAP generation,
- XML signature creation and independent verification,
- tamper detection,
- EET response and SOAP Fault parsing,
- HTTPS client input validation,
- CA EET client input validation.
- CA EET workflow against a local TLS mock server, including JWT-bearing
requests, polling, PKCS#12 download and acknowledgement.
- EET v4 HTTPS end-to-end flow against a local TLS mock server, including
request signature verification and signed POK response parsing.
## EET v4 API
The public headers are:
- `eetv4request.h` - typed sale request and exact fixed-point amounts,
- `eetv4xml.h` - v4 `Trzba` XML,
- `eetv4soap.h` - unsigned SOAP 1.1 envelope,
- `eetv4signer.h` - PKCS#12 signing and response signature verification,
- `eetv4response.h` - `Odpoved`, `Potvrzeni`, `Chyba` and SOAP Fault parsing,
- `eetv4client.h` - HTTPS sale client,
- `eetca.h` - CA EET certificate-renewal workflow.
The client deliberately does not log or return complete SOAP messages on
errors. Callers must protect PKCS#12 files and passwords.
## Playground
The CLI is built as `EetCppPlayground`. The wrapper scripts
[scripts/eet-playground.sh](./scripts/eet-playground.sh) and
[scripts/eet-playground.ps1](./scripts/eet-playground.ps1) can be used from
Unix shells and PowerShell. Set `EETCPP_PLAYGROUND` if the executable is not
on `PATH`.
### Preflight
The preflight command validates the HTTPS endpoint, reads the PKCS#12 file and
actually opens it with the supplied password by creating a probe signature.
The password is read only from an environment variable:
```sh
export EET_PKCS12_PASSWORD='do-not-commit-this'
./EetCppPlayground preflight \
--endpoint 'https://example.test/eet' \
--certificate ./certificate.p12 \
--password-env EET_PKCS12_PASSWORD
```
### Send a verification-mode sale
```sh
./EetCppPlayground send-sale \
--endpoint 'https://example.test/eet' \
--certificate ./certificate.p12 \
--password-env EET_PKCS12_PASSWORD \
--eic CZ123456789 \
--unit 1 \
--register KASA1 \
--receipt PLAYGROUND-001 \
--amount 100.00 \
--verification
```
Use `--sale-at` and `--sent-at` for explicit ISO 8601 timestamps. The CLI
prints only a success/POK, EET error, SOAP Fault and warning codes. It does
not print passwords, JWTs, private keys or SOAP XML.
## CA EET certificate workflow
`EetCaClient` implements the JWT-authenticated CA API:
1. `requestRenewal()` submits a renewal request.
2. `requestStatus()` polls `INPROCESS`, `ISSUED`, `DELIVERING`,
`FINISHED` or `REJECTED`.
3. `listUnfinished()` finds unfinished requests.
4. `claimDownload()` retrieves the base64 PKCS#12 and its password.
5. `acknowledgeDownload()` permanently confirms and closes the download.
The JWT uses RS256 and the `x5t#S256` thumbprint of the signing certificate.
The API endpoints are selected through `EetCaClientConfig::apiBaseUrl`:
- test: `https://ca.test.caeet.gov.cz/api`
- trial: `https://ca.zkus.caeet.gov.cz/api`
- production: `https://ca.caeet.gov.cz/api`
The result of `claimDownload()` is sensitive. Do not log it, cache it, or
store it in source control. The current CA client does not persist the
download; the application must decide how to store it securely.
The Playground exposes the same workflow without printing sensitive values:
```sh
export EET_CA_PASSWORD='...'
export EET_CA_URL='https://ca.test.caeet.gov.cz/api'
./EetCppPlayground ca-renew \
--api-url "$EET_CA_URL" \
--signing-certificate ./old-certificate.p12 \
--signing-password-env EET_CA_PASSWORD
./EetCppPlayground ca-list \
--api-url "$EET_CA_URL" \
--signing-certificate ./old-certificate.p12 \
--signing-password-env EET_CA_PASSWORD
```
Use the UUID returned by `ca-renew` or `ca-list` with `ca-status`. Once the
status is `ISSUED` or `DELIVERING`, download into explicitly chosen files:
```sh
./EetCppPlayground ca-claim \
--api-url "$EET_CA_URL" \
--signing-certificate ./old-certificate.p12 \
--signing-password-env EET_CA_PASSWORD \
--request-id '00000000-0000-4000-8000-000000000000' \
--output-p12 ./new-certificate.p12 \
--output-password ./new-certificate.password
./EetCppPlayground ca-ack \
--api-url "$EET_CA_URL" \
--signing-certificate ./old-certificate.p12 \
--signing-password-env EET_CA_PASSWORD \
--request-id '00000000-0000-4000-8000-000000000000'
```
The CA commands require the signing password only through an environment
variable. `ca-claim` writes the PKCS#12 and password with owner-only file
permissions where supported; review the output directory and move the files
to the application's secure certificate store before acknowledging download.
## Testing against the official test environment
Follow this procedure only after obtaining the official test values:
- EET v4 test endpoint,
- test PKCS#12 certificate and its password,
- EIC, unit ID and cash-register ID authorized for that certificate.
The repository includes [.env.example](./.env.example) as a variable template.
Copy it to a secure location outside the repository, fill in those official
values and never commit the copy, PKCS#12 files, passwords or generated
output.
### 1. Build the Playground and run local tests
```sh
cmake -S . -B /tmp/EetCpp-build -DCMAKE_PREFIX_PATH=/path/to/qt
cmake --build /tmp/EetCpp-build
ctest --test-dir /tmp/EetCpp-build --output-on-failure
```
### 2. Export the test configuration
On Unix, set `EETCPP_PLAYGROUND` to the built executable, then load the
secure configuration:
```sh
export EETCPP_PLAYGROUND=/tmp/EetCpp-build/playground/EetCppPlayground
set -a
. /secure/path/eetcpp-test.env
set +a
```
On PowerShell:
```powershell
$env:EETCPP_PLAYGROUND = "C:\path\to\EetCppPlayground.exe"
$env:EET_ENDPOINT = "https://replace-with-official-eet-test-endpoint"
$env:EET_PKCS12_PATH = "C:\secure\eet-test-certificate.p12"
# Set EET_PKCS12_PASSWORD through the CI secret store or the local
# PowerShell secret-management policy; do not put it in this file.
```
### 3. Run preflight
This verifies the HTTPS endpoint and confirms that the PKCS#12 certificate can
be opened and used to create a probe signature. It does not submit a sale.
```sh
./scripts/eet-preflight.sh
```
```powershell
./scripts/eet-preflight.ps1
```
### 4. Submit one verification-mode sale
Set a new `EET_RECEIPT_NUMBER` for every retry, then send exactly one
verification sale:
```sh
./scripts/eet-send-verification.sh
```
```powershell
./scripts/eet-send-verification.ps1
```
The scripts always pass `--verification`; they must not be used for a
production sale. A successful result includes a POK. An EET application
error, SOAP Fault or TLS failure is printed as a sanitized error and should be
resolved before retrying.
### 5. Handle output and credentials safely
If a command fails, preserve only the exit code and the sanitized error
message. Do not attach full HTTP/SOAP captures to issues. Redact certificate
serials, request identifiers and endpoint query parameters if the environment
contains identifying information.
After testing, remove the PKCS#12 and configuration from temporary machines
or move them to the application's approved secure certificate store.
## Tools and safe usage
The repository uses the following tools:
- **CMake** configures the library and the Playground.
- **CTest** runs the complete automated suite.
- **EetCppPlayground** performs explicit manual preflight and test-sale
operations.
- **Bash/PowerShell wrappers** make the Playground usable in scripts.
- **Qt6 Network** performs HTTPS requests and applies the platform TLS trust
store.
- **xmlsec1/OpenSSL** create and verify XML signatures and JWT signatures.
- **local TLS mock server** exercises the CA workflow without contacting a
government endpoint.
- **EET TLS mock server** exercises the complete sale request/response path
without contacting a government endpoint.
Do not pass passwords as command-line arguments. Prefer environment variables
provided by a secret manager or an ephemeral shell session. Do not commit
PKCS#12 files, private keys, JWTs, certificates containing private material,
or full SOAP request/response captures.
## Current implementation boundaries
- The CA API client is implemented and covered by local validation tests, but
its complete workflow is covered by a local TLS mock server and it has not
been run against a live CA environment in this repository.
- Live EET testing requires an official test endpoint and valid test
credentials/certificate.
- Cross-platform CI and packaging for all six OS/architecture combinations
are planned next.
+10
View File
@@ -0,0 +1,10 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(Qt6 6.5 COMPONENTS Core Network)
find_dependency(PkgConfig)
if(NOT TARGET PkgConfig::XMLSEC1)
pkg_check_modules(XMLSEC1 REQUIRED IMPORTED_TARGET xmlsec1)
endif()
include("${CMAKE_CURRENT_LIST_DIR}/EetCppTargets.cmake")
+427
View File
@@ -0,0 +1,427 @@
#include "eetca.h"
#include <QCryptographicHash>
#include <QEventLoop>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTimer>
#include <openssl/evp.h>
#include <openssl/pkcs12.h>
#include <openssl/x509.h>
#include <openssl/crypto.h>
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
#include <openssl/provider.h>
#endif
namespace
{
struct SigningMaterial
{
EVP_PKEY *key{nullptr};
QByteArray certificateDer;
};
void setError(QString *error, const QString &message)
{
if (error != nullptr)
{
*error = message;
}
}
QByteArray base64Url(const QByteArray &data)
{
return data.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
}
bool loadSigningMaterial(const QString &path, const QByteArray &password,
SigningMaterial *material, QString *error)
{
OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CONFIG, nullptr);
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
OSSL_PROVIDER_load(nullptr, "default");
OSSL_PROVIDER_load(nullptr, "legacy");
#endif
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
{
setError(error, QStringLiteral("Cannot read the CA signing PKCS#12 file."));
return false;
}
const QByteArray data = file.readAll();
BIO *bio = BIO_new_mem_buf(data.constData(), data.size());
PKCS12 *container = bio == nullptr ? nullptr : d2i_PKCS12_bio(bio, nullptr);
BIO_free(bio);
if (container == nullptr)
{
setError(error, QStringLiteral("The CA signing PKCS#12 file is invalid."));
return false;
}
EVP_PKEY *key = nullptr;
X509 *certificate = nullptr;
STACK_OF(X509) *chain = nullptr;
const bool parsed = PKCS12_parse(container, password.constData(), &key,
&certificate, &chain) == 1;
PKCS12_free(container);
sk_X509_pop_free(chain, X509_free);
if (!parsed || key == nullptr || certificate == nullptr)
{
EVP_PKEY_free(key);
X509_free(certificate);
setError(error, QStringLiteral("The CA signing PKCS#12 password is invalid."));
return false;
}
const int length = i2d_X509(certificate, nullptr);
if (length <= 0)
{
EVP_PKEY_free(key);
X509_free(certificate);
setError(error, QStringLiteral("The CA signing certificate is invalid."));
return false;
}
material->certificateDer.resize(length);
unsigned char *destination =
reinterpret_cast<unsigned char *>(material->certificateDer.data());
if (i2d_X509(certificate, &destination) != length)
{
EVP_PKEY_free(key);
X509_free(certificate);
material->certificateDer.clear();
setError(error, QStringLiteral("The CA signing certificate cannot be encoded."));
return false;
}
material->key = key;
X509_free(certificate);
return true;
}
QByteArray createJwt(const SigningMaterial &material, QString *error)
{
const QByteArray thumbprint = base64Url(
QCryptographicHash::hash(material.certificateDer, QCryptographicHash::Sha256));
const QByteArray header = QJsonDocument(QJsonObject{
{QStringLiteral("alg"), QStringLiteral("RS256")},
{QStringLiteral("typ"), QStringLiteral("JWT")},
{QStringLiteral("x5t#S256"), QString::fromLatin1(thumbprint)}
}).toJson(QJsonDocument::Compact);
const QByteArray payload = QByteArrayLiteral("{}");
const QByteArray signingInput = base64Url(header) + '.' + base64Url(payload);
EVP_MD_CTX *context = EVP_MD_CTX_new();
if (context == nullptr ||
EVP_DigestSignInit(context, nullptr, EVP_sha256(), nullptr, material.key) != 1 ||
EVP_DigestSignUpdate(context, signingInput.constData(), signingInput.size()) != 1)
{
EVP_MD_CTX_free(context);
setError(error, QStringLiteral("Cannot initialize CA JWT signing."));
return {};
}
size_t signatureSize = 0;
if (EVP_DigestSignFinal(context, nullptr, &signatureSize) != 1)
{
EVP_MD_CTX_free(context);
setError(error, QStringLiteral("Cannot determine CA JWT signature size."));
return {};
}
QByteArray signature(static_cast<qsizetype>(signatureSize), Qt::Uninitialized);
if (EVP_DigestSignFinal(context, reinterpret_cast<unsigned char *>(signature.data()),
&signatureSize) != 1)
{
EVP_MD_CTX_free(context);
setError(error, QStringLiteral("Cannot create CA JWT signature."));
return {};
}
EVP_MD_CTX_free(context);
signature.resize(static_cast<qsizetype>(signatureSize));
return signingInput + '.' + base64Url(signature);
}
bool parseJsonObject(const QByteArray &data, QJsonObject *object, QString *error)
{
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(data, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject())
{
setError(error, QStringLiteral("CA API returned invalid JSON."));
return false;
}
*object = document.object();
return true;
}
bool parseApiError(const QByteArray &data, int statusCode, QString *error)
{
QJsonObject object;
if (parseJsonObject(data, &object, nullptr))
{
const QString message = object.value(QStringLiteral("message")).toString();
if (!message.isEmpty())
{
setError(error, QStringLiteral("CA API HTTP %1: %2").arg(statusCode).arg(message));
return false;
}
}
setError(error, QStringLiteral("CA API returned HTTP status %1.").arg(statusCode));
return false;
}
bool parseCertificate(const QJsonObject &object, EetCaCertificate *certificate)
{
certificate->serialNumber = object.value(QStringLiteral("sn")).toString();
certificate->validFrom = QDateTime::fromString(
object.value(QStringLiteral("validFrom")).toString(), Qt::ISODate);
certificate->validTo = QDateTime::fromString(
object.value(QStringLiteral("validTo")).toString(), Qt::ISODate);
certificate->state = object.value(QStringLiteral("state")).toString();
certificate->description = object.value(QStringLiteral("description")).toString();
certificate->email = object.value(QStringLiteral("email")).toString();
return !certificate->serialNumber.isEmpty() && certificate->validFrom.isValid() &&
certificate->validTo.isValid() && !certificate->state.isEmpty();
}
bool performRequest(const EetCaClientConfig &config, const QString &path,
const QByteArray &method, QByteArray *data, int *statusCode,
QString *error)
{
if (!config.apiBaseUrl.isValid() || config.apiBaseUrl.scheme() != QLatin1String("https") ||
config.apiBaseUrl.host().isEmpty() || config.timeoutMilliseconds <= 0)
{
setError(error, QStringLiteral("CA API requires a valid HTTPS base URL and positive timeout."));
return false;
}
SigningMaterial material;
if (!loadSigningMaterial(config.signingPkcs12Path, config.signingPkcs12Password,
&material, error))
{
return false;
}
const QByteArray jwt = createJwt(material, error);
EVP_PKEY_free(material.key);
if (jwt.isEmpty())
{
return false;
}
QNetworkAccessManager manager;
QUrl endpoint(config.apiBaseUrl);
QString basePath = endpoint.path();
if (!basePath.endsWith(QLatin1Char('/')))
{
basePath.append(QLatin1Char('/'));
}
endpoint.setPath(basePath + path.mid(1));
QNetworkRequest request(endpoint);
if (!config.sslConfiguration.isNull())
{
request.setSslConfiguration(config.sslConfiguration);
}
request.setRawHeader("Authorization", "Bearer " + jwt);
QNetworkReply *reply = method == QByteArrayLiteral("POST")
? manager.post(request, QByteArray())
: manager.get(request);
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
bool timedOut = false;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QObject::connect(&timer, &QTimer::timeout, reply, [&] { timedOut = true; reply->abort(); });
timer.start(config.timeoutMilliseconds);
loop.exec();
if (timedOut || reply->error() != QNetworkReply::NoError)
{
setError(error, timedOut ? QStringLiteral("CA API request timed out.")
: reply->errorString());
reply->deleteLater();
return false;
}
*statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
*data = reply->readAll();
reply->deleteLater();
return true;
}
}
EetCaClient::EetCaClient(EetCaClientConfig config)
: m_config(std::move(config))
{
}
bool EetCaClient::requestRenewal(QUuid *requestId, QString *error) const
{
if (requestId == nullptr)
{
setError(error, QStringLiteral("A request ID output object is required."));
return false;
}
*requestId = {};
QByteArray data;
int statusCode = 0;
if (!performRequest(m_config, QStringLiteral("/request/renew"), QByteArrayLiteral("POST"),
&data, &statusCode, error))
{
return false;
}
if (statusCode != 201)
{
return parseApiError(data, statusCode, error);
}
QJsonObject object;
if (!parseJsonObject(data, &object, error))
{
return false;
}
const QUuid id(object.value(QStringLiteral("reqId")).toString());
if (id.isNull())
{
setError(error, QStringLiteral("CA API returned an invalid request ID."));
return false;
}
*requestId = id;
return true;
}
bool EetCaClient::requestStatus(const QUuid &requestId,
EetCaRequestStatus *status,
QString *error) const
{
if (status == nullptr || requestId.isNull())
{
setError(error, QStringLiteral("A valid request ID and output object are required."));
return false;
}
QByteArray data;
int statusCode = 0;
if (!performRequest(m_config, QStringLiteral("/request/%1/status").arg(
requestId.toString(QUuid::WithoutBraces)),
QByteArrayLiteral("GET"), &data, &statusCode, error))
{
return false;
}
if (statusCode != 200)
{
return parseApiError(data, statusCode, error);
}
QJsonObject object;
if (!parseJsonObject(data, &object, error))
{
return false;
}
const QUuid returnedId(object.value(QStringLiteral("reqId")).toString());
const QString state = object.value(QStringLiteral("state")).toString();
if (returnedId.isNull() || state.isEmpty())
{
setError(error, QStringLiteral("CA API returned an invalid request status."));
return false;
}
status->requestId = returnedId;
status->state = state;
status->pollAfterSeconds = object.value(QStringLiteral("pollAfterSeconds")).toInt();
status->rejectedReason = object.value(QStringLiteral("rejectedReason")).toString();
return true;
}
bool EetCaClient::listUnfinished(QList<QUuid> *requestIds, QString *error) const
{
if (requestIds == nullptr)
{
setError(error, QStringLiteral("A request ID list output object is required."));
return false;
}
requestIds->clear();
QByteArray data;
int statusCode = 0;
if (!performRequest(m_config, QStringLiteral("/request/not-finished"),
QByteArrayLiteral("GET"), &data, &statusCode, error))
{
return false;
}
if (statusCode != 200)
{
return parseApiError(data, statusCode, error);
}
QJsonObject object;
if (!parseJsonObject(data, &object, error))
{
return false;
}
for (const QJsonValue &value : object.value(QStringLiteral("data")).toArray())
{
const QUuid id(value.toString());
if (id.isNull())
{
setError(error, QStringLiteral("CA API returned an invalid request ID."));
requestIds->clear();
return false;
}
requestIds->append(id);
}
return true;
}
bool EetCaClient::claimDownload(const QUuid &requestId,
EetCaPkcs12 *result,
QString *error) const
{
if (result == nullptr || requestId.isNull())
{
setError(error, QStringLiteral("A valid request ID and output object are required."));
return false;
}
*result = {};
QByteArray data;
int statusCode = 0;
if (!performRequest(m_config, QStringLiteral("/request/%1/claim-download").arg(
requestId.toString(QUuid::WithoutBraces)),
QByteArrayLiteral("POST"), &data, &statusCode, error))
{
return false;
}
if (statusCode != 200)
{
return parseApiError(data, statusCode, error);
}
QJsonObject object;
if (!parseJsonObject(data, &object, error))
{
return false;
}
result->pkcs12 = QByteArray::fromBase64(
object.value(QStringLiteral("pkcs12")).toString().toLatin1());
result->password = object.value(QStringLiteral("password")).toString().toUtf8();
if (result->pkcs12.isEmpty() || result->password.isEmpty() ||
!parseCertificate(object.value(QStringLiteral("certificate")).toObject(),
&result->certificate))
{
*result = {};
setError(error, QStringLiteral("CA API returned an invalid certificate package."));
return false;
}
return true;
}
bool EetCaClient::acknowledgeDownload(const QUuid &requestId, QString *error) const
{
if (requestId.isNull())
{
setError(error, QStringLiteral("A valid request ID is required."));
return false;
}
QByteArray data;
int statusCode = 0;
if (!performRequest(m_config, QStringLiteral("/request/%1/ack-download").arg(
requestId.toString(QUuid::WithoutBraces)),
QByteArrayLiteral("POST"), &data, &statusCode, error))
{
return false;
}
if (statusCode != 204)
{
return parseApiError(data, statusCode, error);
}
return true;
}
+66
View File
@@ -0,0 +1,66 @@
#ifndef EETCA_H
#define EETCA_H
#include <QByteArray>
#include <QDateTime>
#include <QList>
#include <QUuid>
#include <QUrl>
#include <QSslConfiguration>
#include "eetcpp_global.h"
struct EETCPPSHARED_EXPORT EetCaClientConfig
{
QUrl apiBaseUrl;
QString signingPkcs12Path;
QByteArray signingPkcs12Password;
int timeoutMilliseconds{30000};
QSslConfiguration sslConfiguration;
};
struct EETCPPSHARED_EXPORT EetCaRequestStatus
{
QUuid requestId;
QString state;
int pollAfterSeconds{0};
QString rejectedReason;
};
struct EETCPPSHARED_EXPORT EetCaCertificate
{
QString serialNumber;
QDateTime validFrom;
QDateTime validTo;
QString state;
QString description;
QString email;
};
struct EETCPPSHARED_EXPORT EetCaPkcs12
{
QByteArray pkcs12;
QByteArray password;
EetCaCertificate certificate;
};
class EETCPPSHARED_EXPORT EetCaClient
{
public:
explicit EetCaClient(EetCaClientConfig config);
bool requestRenewal(QUuid *requestId, QString *error = nullptr) const;
bool requestStatus(const QUuid &requestId,
EetCaRequestStatus *status,
QString *error = nullptr) const;
bool listUnfinished(QList<QUuid> *requestIds, QString *error = nullptr) const;
bool claimDownload(const QUuid &requestId,
EetCaPkcs12 *result,
QString *error = nullptr) const;
bool acknowledgeDownload(const QUuid &requestId, QString *error = nullptr) const;
private:
EetCaClientConfig m_config;
};
#endif // EETCA_H
-6
View File
@@ -1,6 +0,0 @@
#include "eetcpp.h"
EetCpp::EetCpp()
{
}
+7 -9
View File
@@ -3,14 +3,12 @@
#include "eetcpp_global.h" #include "eetcpp_global.h"
#include "eetrequest.h" #include "eetv4request.h"
#include "eetsender.h" #include "eetv4xml.h"
#include "eetv4soap.h"
class EETCPPSHARED_EXPORT EetCpp #include "eetv4signer.h"
{ #include "eetv4response.h"
#include "eetv4client.h"
public: #include "eetca.h"
EetCpp();
};
#endif // EETCPP_H #endif // EETCPP_H
-403
View File
@@ -1,403 +0,0 @@
#include "eetrequest.h"
EetRequest::EetRequest(QObject *parent) : QObject(parent)
{
m_prvniZaslani = true;
m_rezim = STANDARD;
}
QDateTime EetRequest::datOdesl() const
{
return m_datOdesl;
}
void EetRequest::setDatOdesl(const QDateTime &datOdesl)
{
m_datOdesl = datOdesl;
}
QString EetRequest::datOdeslStr() const
{
return m_datOdesl.toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate);
}
QTimeZone EetRequest::datOdeslZone() const
{
return m_datOdeslZone;
}
void EetRequest::setDatOdeslZone(const QTimeZone &datOdeslZone)
{
m_datOdeslZone = datOdeslZone;
}
bool EetRequest::prvniZaslani() const
{
return m_prvniZaslani;
}
void EetRequest::setPrvniZaslani(bool prvniZaslani)
{
m_prvniZaslani = prvniZaslani;
}
QString EetRequest::prvniZaslaniStr() const
{
return m_prvniZaslani ? "true" : "false";
}
QUuid EetRequest::uuidZpravy() const
{
return m_uuidZpravy;
}
void EetRequest::setUuidZpravy(const QUuid &uuidZpravy)
{
m_uuidZpravy = uuidZpravy;
}
QString EetRequest::uuidZpravyStr() const
{
QString ret = m_uuidZpravy.toString();
ret = ret.replace(0, 1, "");
ret = ret.replace(ret.size() - 1, 1, "");
return ret;
}
bool EetRequest::overeni() const
{
return m_overeni.isNull() ? false : *m_overeni.data();
}
void EetRequest::setOvereni(bool overeni)
{
m_overeni = QSharedPointer<bool>(new bool);
*m_overeni.data() = overeni;
}
QString EetRequest::overeniStr() const
{
return m_overeni.isNull() ? NULL_VAL : (*m_overeni.data() ? "true" : "false");
}
QString EetRequest::dicPopl() const
{
return m_dicPopl.isNull() ? NULL_VAL : QString(*m_dicPopl.data());
}
void EetRequest::setDicPopl(const QString &dicPopl)
{
m_dicPopl = QSharedPointer<QString>(new QString(dicPopl));
}
QString EetRequest::dicPoverujiciho() const
{
return m_dicPoverujiciho.isNull() ? NULL_VAL : QString(*m_dicPoverujiciho.data());
}
void EetRequest::setDicPoverujiciho(const QString &dicPoverujiciho)
{
m_dicPoverujiciho = QSharedPointer<QString>(new QString(dicPoverujiciho));
}
QString EetRequest::idProvoz() const
{
return m_idProvoz;
}
void EetRequest::setIdProvoz(const QString &idProvoz)
{
m_idProvoz = idProvoz;
}
QString EetRequest::idPokl() const
{
return m_idPokl;
}
void EetRequest::setIdPokl(const QString &idPokl)
{
m_idPokl = idPokl;
}
QString EetRequest::poradCis() const
{
return m_poradCis;
}
void EetRequest::setPoradCis(const QString &poradCis)
{
m_poradCis = poradCis;
}
QDateTime EetRequest::datTrzby() const
{
return m_datTrzby;
}
void EetRequest::setDatTrzby(const QDateTime &datTrzby)
{
m_datTrzby = datTrzby;
}
QString EetRequest::datTrzbyStr() const
{
return m_datTrzby.toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate);
}
QTimeZone EetRequest::datTrzbyZone() const
{
return m_datTrzbyZone;
}
void EetRequest::setDatTrzbyZone(const QTimeZone &datTrzbyZone)
{
m_datTrzbyZone = datTrzbyZone;
}
double EetRequest::celkTrzba() const
{
return m_celkTrzba.isNull() ? 0 : *m_celkTrzba.data();
}
void EetRequest::setCelkTrzba(double celkTrzba)
{
m_celkTrzba = QSharedPointer<double>(new double);
*m_celkTrzba.data() = celkTrzba;
}
QString EetRequest::celkTrzbaStr() const
{
return m_celkTrzba.isNull() ? NULL_VAL : QString::number(*m_celkTrzba.data(), 'f', 2);
}
double EetRequest::zaklNepodlDph() const
{
return m_zaklNepodlDph.isNull() ? 0 : *m_zaklNepodlDph.data();
}
void EetRequest::setZaklNepodlDph(double zaklNepodlDph)
{
m_zaklNepodlDph = QSharedPointer<double>(new double);
*m_zaklNepodlDph.data() = zaklNepodlDph;
}
QString EetRequest::zaklNepodlDphStr() const
{
return m_zaklNepodlDph.isNull() ? NULL_VAL : QString::number(*m_zaklNepodlDph.data(), 'f', 2);
}
double EetRequest::zaklDan1() const
{
return m_zaklDan1.isNull() ? 0 : *m_zaklDan1.data();
}
void EetRequest::setZaklDan1(double zaklDan1)
{
m_zaklDan1 = QSharedPointer<double>(new double);
*m_zaklDan1.data() = zaklDan1;
}
QString EetRequest::zaklDan1Str() const
{
return m_zaklDan1.isNull() ? NULL_VAL : QString::number(*m_zaklDan1.data(), 'f', 2);
}
double EetRequest::dan1() const
{
return m_dan1.isNull() ? 0 : *m_dan1.data();
}
void EetRequest::setDan1(double dan1)
{
m_dan1 = QSharedPointer<double>(new double);
*m_dan1.data() = dan1;
}
QString EetRequest::dan1Str() const
{
return m_dan1.isNull() ? NULL_VAL : QString::number(*m_dan1.data(), 'f', 2);
}
double EetRequest::zaklDan2() const
{
return m_zaklDan2.isNull() ? 0 : *m_zaklDan2.data();
}
void EetRequest::setZaklDan2(double zaklDan2)
{
m_zaklDan2 = QSharedPointer<double>(new double);
*m_zaklDan2.data() = zaklDan2;
}
QString EetRequest::zaklDan2Str() const
{
return m_zaklDan2.isNull() ? NULL_VAL : QString::number(*m_zaklDan2.data(), 'f', 2);
}
double EetRequest::dan2() const
{
return m_dan2.isNull() ? 0 : *m_dan2.data();
}
void EetRequest::setDan2(double dan2)
{
m_dan2 = QSharedPointer<double>(new double);
*m_dan2.data() = dan2;
}
QString EetRequest::dan2Str() const
{
return m_dan2.isNull() ? NULL_VAL : QString::number(*m_dan2.data(), 'f', 2);
}
double EetRequest::zaklDan3() const
{
return m_zaklDan3.isNull() ? 0 : *m_zaklDan3.data();
}
void EetRequest::setZaklDan3(double zaklDan3)
{
m_zaklDan3 = QSharedPointer<double>(new double);
*m_zaklDan3.data() = zaklDan3;
}
QString EetRequest::zaklDan3Str() const
{
return m_zaklDan3.isNull() ? NULL_VAL : QString::number(*m_zaklDan3.data(), 'f', 2);
}
QString EetRequest::dan3Str() const
{
return m_dan3.isNull() ? NULL_VAL : QString::number(*m_dan3.data(), 'f', 2);
}
double EetRequest::dan3() const
{
return m_dan3.isNull() ? 0 : *m_dan3.data();
}
void EetRequest::setDan3(double dan3)
{
m_dan3 = QSharedPointer<double>(new double);
*m_dan3.data() = dan3;
}
double EetRequest::cestSluz() const
{
return m_cestSluz.isNull() ? 0 : *m_cestSluz.data();
}
void EetRequest::setCestSluz(double cestSluz)
{
m_cestSluz = QSharedPointer<double>(new double);
*m_cestSluz.data() = cestSluz;
}
QString EetRequest::cestSluzStr() const
{
return m_cestSluz.isNull() ? NULL_VAL : QString::number(*m_cestSluz.data(), 'f', 2);
}
double EetRequest::pouzitZboz1() const
{
return m_pouzitZboz1.isNull() ? 0 : *m_pouzitZboz1.data();
}
void EetRequest::setPouzitZboz1(double pouzitZboz1)
{
m_pouzitZboz1 = QSharedPointer<double>(new double);
*m_pouzitZboz1.data() = pouzitZboz1;
}
QString EetRequest::pouzitZboz1Str() const
{
return m_pouzitZboz1.isNull() ? NULL_VAL : QString::number(*m_pouzitZboz1.data(), 'f', 2);
}
double EetRequest::pouzitZboz2() const
{
return m_pouzitZboz2.isNull() ? 0 : *m_pouzitZboz2.data();
}
void EetRequest::setPouzitZboz2(double pouzitZboz2)
{
m_pouzitZboz2 = QSharedPointer<double>(new double);
*m_pouzitZboz2.data() = pouzitZboz2;
}
QString EetRequest::pouzitZboz2Str() const
{
return m_pouzitZboz2.isNull() ? NULL_VAL : QString::number(*m_pouzitZboz2.data(), 'f', 2);
}
double EetRequest::pouzitZboz3() const
{
return m_pouzitZboz3.isNull() ? 0 : *m_pouzitZboz3.data();
}
void EetRequest::setPouzitZboz3(double pouzitZboz3)
{
m_pouzitZboz3 = QSharedPointer<double>(new double);
*m_pouzitZboz3.data() = pouzitZboz3;
}
QString EetRequest::pouzitZboz3Str() const
{
return m_pouzitZboz3.isNull() ? NULL_VAL : QString::number(*m_pouzitZboz3.data(), 'f', 2);
}
double EetRequest::urcenoCerpZuct() const
{
return m_urcenoCerpZuct.isNull() ? 0 : *m_urcenoCerpZuct.data();
}
void EetRequest::setUrcenoCerpZuct(double urcenoCerpZuct)
{
m_urcenoCerpZuct = QSharedPointer<double>(new double);
*m_urcenoCerpZuct.data() = urcenoCerpZuct;
}
QString EetRequest::urcenoCerpZuctStr() const
{
return m_urcenoCerpZuct.isNull() ? NULL_VAL : QString::number(*m_urcenoCerpZuct.data(), 'f', 2);
}
double EetRequest::cerpZuct() const
{
return m_cerpZuct.isNull() ? 0 : *m_cerpZuct.data();
}
void EetRequest::setCerpZuct(double cerpZuct)
{
m_cerpZuct = QSharedPointer<double>(new double);
*m_cerpZuct.data() = cerpZuct;
}
QString EetRequest::cerpZuctStr() const
{
return m_cerpZuct.isNull() ? NULL_VAL : QString::number(*m_cerpZuct.data(), 'f', 2);
}
EetRequest::EetRezim EetRequest::rezim() const
{
return m_rezim;
}
void EetRequest::setRezim(const EetRequest::EetRezim &rezim)
{
m_rezim = rezim;
}
QString EetRequest::rezimStr() const
{
switch (m_rezim) {
case ZJEDNODUSENY:
return "1";
case STANDARD:
return "0";
default:
return "0";
}
}
-186
View File
@@ -1,186 +0,0 @@
#ifndef EETREQUEST_H
#define EETREQUEST_H
#include <QString>
#include <QDate>
#include <QUuid>
#include <QTimeZone>
#include <QSharedPointer>
#include "eetcpp_global.h"
#define NULL_VAL "__NULL__"
class EETCPPSHARED_EXPORT EetRequest : public QObject
{
Q_OBJECT
Q_PROPERTY(QString dat_odesl READ datOdeslStr)
Q_PROPERTY(QString prvni_zaslani READ prvniZaslaniStr)
Q_PROPERTY(QString uuid_zpravy READ uuidZpravyStr)
Q_PROPERTY(QString overeni READ overeniStr)
Q_PROPERTY(QString dic_popl READ dicPopl)
Q_PROPERTY(QString dic_poverujiciho READ dicPoverujiciho)
Q_PROPERTY(QString id_provoz READ idProvoz)
Q_PROPERTY(QString id_pokl READ idPokl)
Q_PROPERTY(QString porad_cis READ poradCis)
Q_PROPERTY(QString dat_trzby READ datTrzbyStr)
Q_PROPERTY(QString celk_trzba READ celkTrzbaStr)
Q_PROPERTY(QString zakl_nepodl_dph READ zaklNepodlDphStr)
Q_PROPERTY(QString zakl_dan1 READ zaklDan1Str)
Q_PROPERTY(QString zakl_dan2 READ zaklDan2Str)
Q_PROPERTY(QString zakl_dan3 READ zaklDan3Str)
Q_PROPERTY(QString dan1 READ dan1Str)
Q_PROPERTY(QString dan2 READ dan2Str)
Q_PROPERTY(QString dan3 READ dan3Str)
Q_PROPERTY(QString cest_sluz READ cestSluzStr)
Q_PROPERTY(QString pouzit_zboz1 READ pouzitZboz1Str)
Q_PROPERTY(QString pouzit_zboz2 READ pouzitZboz2Str)
Q_PROPERTY(QString pouzit_zboz3 READ pouzitZboz3Str)
Q_PROPERTY(QString urceno_cerp_zuct READ urcenoCerpZuctStr)
Q_PROPERTY(QString cerp_zuct READ cerpZuctStr)
Q_PROPERTY(QString rezim READ rezimStr)
public:
explicit EetRequest(QObject *parent = 0);
enum EetRezim {
ZJEDNODUSENY = 0,
STANDARD
};
QDateTime datOdesl() const;
void setDatOdesl(const QDateTime &datOdesl);
QString datOdeslStr() const;
QTimeZone datOdeslZone() const;
void setDatOdeslZone(const QTimeZone &datOdeslZone);
QString datOdeslZoneStr() const;
bool prvniZaslani() const;
void setPrvniZaslani(bool prvniZaslani);
QString prvniZaslaniStr() const;
QUuid uuidZpravy() const;
void setUuidZpravy(const QUuid &uuidZpravy);
QString uuidZpravyStr() const;
bool overeni() const;
void setOvereni(bool overeni);
QString overeniStr() const;
QString dicPopl() const;
void setDicPopl(const QString &dicPopl);
QString dicPoverujiciho() const;
void setDicPoverujiciho(const QString &dicPoverujiciho);
QString idProvoz() const;
void setIdProvoz(const QString &idProvoz);
QString idPokl() const;
void setIdPokl(const QString &idPokl);
QString poradCis() const;
void setPoradCis(const QString &poradCis);
QDateTime datTrzby() const;
void setDatTrzby(const QDateTime &datTrzby);
QString datTrzbyStr() const;
QTimeZone datTrzbyZone() const;
void setDatTrzbyZone(const QTimeZone &datTrzbyZone);
double celkTrzba() const;
void setCelkTrzba(double celkTrzba);
QString celkTrzbaStr() const;
double zaklNepodlDph() const;
void setZaklNepodlDph(double zaklNepodlDph);
QString zaklNepodlDphStr() const;
double zaklDan1() const;
void setZaklDan1(double zaklDan1);
QString zaklDan1Str() const;
double dan1() const;
void setDan1(double dan1);
QString dan1Str() const;
double zaklDan2() const;
void setZaklDan2(double zaklDan2);
QString zaklDan2Str() const;
double dan2() const;
void setDan2(double dan2);
QString dan2Str() const;
double zaklDan3() const;
void setZaklDan3(double zaklDan3);
QString zaklDan3Str() const;
double dan3() const;
void setDan3(double dan3);
QString dan3Str() const;
double cestSluz() const;
void setCestSluz(double cestSluz);
QString cestSluzStr() const;
double pouzitZboz1() const;
void setPouzitZboz1(double pouzitZboz1);
QString pouzitZboz1Str() const;
double pouzitZboz2() const;
void setPouzitZboz2(double pouzitZboz2);
QString pouzitZboz2Str() const;
double pouzitZboz3() const;
void setPouzitZboz3(double pouzitZboz3);
QString pouzitZboz3Str() const;
double urcenoCerpZuct() const;
void setUrcenoCerpZuct(double urcenoCerpZuct);
QString urcenoCerpZuctStr() const;
double cerpZuct() const;
void setCerpZuct(double cerpZuct);
QString cerpZuctStr() const;
EetRezim rezim() const;
void setRezim(const EetRezim &rezim);
QString rezimStr() const;
private:
QDateTime m_datOdesl;
QTimeZone m_datOdeslZone;
bool m_prvniZaslani;
QUuid m_uuidZpravy;
QSharedPointer<bool> m_overeni;
QSharedPointer<QString> m_dicPopl;
QSharedPointer<QString> m_dicPoverujiciho;
QString m_idProvoz;
QString m_idPokl;
QString m_poradCis;
QDateTime m_datTrzby;
QTimeZone m_datTrzbyZone;
QSharedPointer<double> m_celkTrzba;
QSharedPointer<double> m_zaklNepodlDph;
QSharedPointer<double> m_zaklDan1;
QSharedPointer<double> m_dan1;
QSharedPointer<double> m_zaklDan2;
QSharedPointer<double> m_dan2;
QSharedPointer<double> m_zaklDan3;
QSharedPointer<double> m_dan3;
QSharedPointer<double> m_cestSluz;
QSharedPointer<double> m_pouzitZboz1;
QSharedPointer<double> m_pouzitZboz2;
QSharedPointer<double> m_pouzitZboz3;
QSharedPointer<double> m_urcenoCerpZuct;
QSharedPointer<double> m_cerpZuct;
EetRezim m_rezim;
signals:
public slots:
};
#endif // EETREQUEST_H
-119
View File
@@ -1,119 +0,0 @@
#include "eetresult.h"
EetResult::EetResult(QObject *parent) : QObject(parent)
{
m_status = RESPONSE_OK;
}
EetResult::ResponseStatus EetResult::status() const
{
return m_status;
}
void EetResult::setStatus(const EetResult::ResponseStatus &status)
{
m_status = status;
}
QString EetResult::fik() const
{
return m_fik;
}
void EetResult::setFik(const QString &fik)
{
m_fik = fik;
}
EetMessageList EetResult::warnings() const
{
return m_warnings;
}
void EetResult::setWarnings(const EetMessageList &warnings)
{
m_warnings = warnings;
}
EetMessageList EetResult::errors() const
{
return m_errors;
}
void EetResult::setErrors(const EetMessageList &errors)
{
m_errors = errors;
}
QUuid EetResult::uuid() const
{
return m_uuid;
}
void EetResult::setUuid(const QUuid &uuid)
{
m_uuid = uuid;
}
QDateTime EetResult::reciveDate() const
{
return m_reciveDate;
}
void EetResult::setReciveDate(const QDateTime &reciveDate)
{
m_reciveDate = reciveDate;
}
QString EetResult::pkp() const
{
return m_pkp;
}
void EetResult::setPkp(const QString &pkp)
{
m_pkp = pkp;
}
QString EetResult::bkp() const
{
return m_bkp;
}
void EetResult::setBkp(const QString &bkp)
{
m_bkp = bkp;
}
EetMessage::EetMessage(QObject *parent)
:QObject(parent)
{
m_code = 0;
}
EetMessage::EetMessage(int code, const QString &message, QObject *parent)
:QObject(parent)
{
m_code = code;
m_message = message;
}
int EetMessage::code() const
{
return m_code;
}
void EetMessage::setCode(int code)
{
m_code = code;
}
QString EetMessage::message() const
{
return m_message;
}
void EetMessage::setMessage(const QString &message)
{
m_message = message;
}
-85
View File
@@ -1,85 +0,0 @@
#ifndef EETRESULT_H
#define EETRESULT_H
#include <QObject>
#include <QUuid>
#include <QString>
#include <QList>
#include <QDateTime>
#include "eetcpp_global.h"
class EETCPPSHARED_EXPORT EetMessage : public QObject
{
Q_OBJECT
public:
explicit EetMessage(QObject *parent = 0);
EetMessage(int code, const QString &message, QObject *parent = 0);
int code() const;
void setCode(int code);
QString message() const;
void setMessage(const QString &message);
private:
int m_code;
QString m_message;
};
typedef QList<EetMessage*> EetMessageList;
class EETCPPSHARED_EXPORT EetResult : public QObject
{
Q_OBJECT
public:
enum ResponseStatus
{
RESPONSE_OK = 0,
DATA_ERROR,
USER_CERT_ERROR,
SERVER_ERROR,
SERVICE_CERT_ERROR,
INVALID_SIGNATURE,
SSL_ERROR,
TIMEDOUT,
OFFLINE
};
explicit EetResult(QObject *parent = 0);
ResponseStatus status() const;
void setStatus(const ResponseStatus &status);
QString fik() const;
void setFik(const QString &fik);
EetMessageList warnings() const;
void setWarnings(const EetMessageList &warnings);
EetMessageList errors() const;
void setErrors(const EetMessageList &errors);
QUuid uuid() const;
void setUuid(const QUuid &uuid);
QDateTime reciveDate() const;
void setReciveDate(const QDateTime &reciveDate);
QString pkp() const;
void setPkp(const QString &pkp);
QString bkp() const;
void setBkp(const QString &bkp);
private:
ResponseStatus m_status;
QUuid m_uuid;
QDateTime m_reciveDate;
QString m_fik;
QString m_pkp;
QString m_bkp;
EetMessageList m_warnings;
EetMessageList m_errors;
};
#endif // EETRESULT_H
-309
View File
@@ -1,309 +0,0 @@
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include "eetsender.h"
#include "eetsigner.h"
#include "eettemplate.h"
#include <QDebug>
#include <QXmlQuery>
#include <QXmlResultItems>
#include <QTimer>
const QString EetSender::ms_nsDef = "declare namespace eet = \"http://fs.mfcr.cz/eet/schema/v3\";\n"
"declare namespace ds = \"http://www.w3.org/2000/09/xmldsig#\";\n"
"declare namespace wsu = \"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\";\n"
"declare namespace wsse = \"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\";\n"
"declare namespace senc = \"http://schemas.xmlsoap.org/soap/encoding/\";\n"
"declare namespace senv = \"http://schemas.xmlsoap.org/soap/envelope/\";\n";
bool EetSender::m_online = true;
EetSender::EetSender(QObject *parent) : QObject(parent)
{
m_signer = nullptr;
m_resut = nullptr;
m_checkSignature = true;
m_resut = new EetResult(this);
m_manager = new QNetworkAccessManager(this);
connect(m_manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*)));
connect(m_manager, &QNetworkAccessManager::sslErrors, [this](QNetworkReply *rep, QList<QSslError>){
m_resut->setStatus(EetResult::SSL_ERROR);
emit sendFinished();
rep->deleteLater();
});
m_serviceUrl = PRODUCTION_URL;
m_timer = new QTimer(this);
m_timer->setInterval(1000);
m_timer->setSingleShot(true);
}
void EetSender::sendRequest(EetRequest *request)
{
if (m_signer == nullptr)
{
m_resut->setStatus(EetResult::USER_CERT_ERROR);
emit sendFinished();
return;
}
request->setUuidZpravy(QUuid::createUuid());
EetTemplate tempBody(BODY_TEMPLATE);
tempBody.setSigner(m_signer);
tempBody.setResult(m_resut);
QString strBody = tempBody.fillTemplate(request);
QByteArray digest = m_signer->sha256HashData(strBody.toUtf8());
QMap<QString, QString> val;
val["digest"] = QString(digest.toBase64());
EetTemplate tempSignature(SIGNATURE_TEMPLATE);
QString strSignature = tempSignature.fillTemplate(val);
QByteArray sign = m_signer->signData(strSignature.toUtf8());
val["signature"] = QString(sign.toBase64());
val["soap:Body"] = strBody;
val["certb64"] = m_signer->getCertificate();
EetTemplate tempRequest(REQUEST_TEMPLATE);
QString strRequest = tempRequest.fillTemplate(val);
QNetworkRequest req(QUrl(m_serviceUrl.toStdString().c_str()));
qDebug() << strRequest;
if (m_online)
{
QNetworkReply *rep = m_manager->post(req, strRequest.toUtf8());
m_timer->start();
connect(m_timer, &QTimer::timeout, [this, rep](){
rep->abort();
m_resut->setStatus(EetResult::TIMEDOUT);
emit sendFinished();
});
}
else
{
m_resut->setStatus(EetResult::OFFLINE);
m_timer->stop();
emit sendFinished();
}
}
void EetSender::setupSigner(const QString &certPath, const QString &passwd)
{
if (m_signer != nullptr)
{
delete m_signer;
}
m_signer = new EetSigner(this);
m_signer->setup(certPath, QCA::SecureArray(passwd.toUtf8()));
}
void EetSender::setCheckSignature(bool checkSignature)
{
m_checkSignature = checkSignature;
}
bool EetSender::checkSignature() const
{
return m_checkSignature;
}
void EetSender::setPlayground(bool pg)
{
m_serviceUrl = (pg ? PLAYGROUND_URL : PRODUCTION_URL);
}
EetResult *EetSender::resut() const
{
return m_resut;
}
void EetSender::setTimeout(int timeout)
{
m_timer->setInterval(timeout);
}
bool EetSender::verifySignature(const QByteArray &repData)
{
QString queryString("//senv:Envelope/senv:Header/wsse:Security/ds:Signature/ds:SignedInfo");
QString signedInfo, certB64, signatureB64;
QXmlQuery q;
q.setFocus(QString(repData));
q.setQuery(ms_nsDef + queryString);
q.evaluateTo(&signedInfo);
QStringList list = signedInfo.split("\n");
signedInfo = "<SignedInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n";
for (int i = 1; i < list.length(); i++)
{
QString line = list[i];
line = line.replace(QRegExp("^(\\s+)<([A-Za-z]+)([A-Za-z0-9-\"=/#:\\.\\ ]+)/>"), "\\1<\\2\\3></\\2>");
signedInfo += line + "\n";
}
signedInfo = signedInfo.trimmed();
signedInfo = signedInfo.replace(" ", " ");
queryString = "//senv:Envelope/senv:Header/wsse:Security/wsse:BinarySecurityToken/text()";
q.setQuery(ms_nsDef + queryString);
q.evaluateTo(&certB64);
queryString = "//senv:Envelope/senv:Header/wsse:Security/ds:Signature/ds:SignatureValue/text()";
q.setQuery(ms_nsDef + queryString);
q.evaluateTo(&signatureB64);
QCA::ConvertResult res;
QCA::Certificate cert = QCA::Certificate::fromDER(QByteArray::fromBase64(certB64.toUtf8()), &res);
if (res != QCA::ConvertGood)
{
m_resut->setStatus(EetResult::SERVICE_CERT_ERROR);
return false;
}
QCA::PublicKey pubKey = cert.subjectPublicKey();
if (!pubKey.canVerify())
{
m_resut->setStatus(EetResult::SERVICE_CERT_ERROR);
return false;
}
bool signValid = pubKey.verifyMessage(QCA::MemoryRegion(signedInfo.toUtf8()), QByteArray::fromBase64(signatureB64.toUtf8()), QCA::EMSA3_SHA256);
if (!signValid)
{
m_resut->setStatus(EetResult::INVALID_SIGNATURE);
return false;
}
return true;
}
void EetSender::replyFinished(QNetworkReply *reply)
{
m_timer->stop();
if (reply->error() != QNetworkReply::NoError)
{
m_resut->setStatus(EetResult::SERVER_ERROR);
emit sendFinished();
reply->deleteLater();
return;
}
QByteArray repData = reply->readAll();
qDebug() << QString(repData);
QXmlQuery q;
q.setFocus(QString(repData));
QXmlResultItems items;
QString result;
QString queryString = "//senv:Envelope/senv:Body/eet:Odpoved/eet:Chyba";
q.setQuery(ms_nsDef + queryString);
q.evaluateTo(&items);
EetMessageList errors;
QXmlItem item = items.next();
while (!item.isNull())
{
EetMessage *mesg = new EetMessage(m_resut);
queryString = "./@kod/data(.)";
q.setQuery(ms_nsDef + queryString);
q.setFocus(item);
q.evaluateTo(&result);
result = result.trimmed();
mesg->setCode(result.toInt());
queryString = "./string()";
q.setQuery(ms_nsDef + queryString);
q.setFocus(item);
q.evaluateTo(&result);
result = result.trimmed();
mesg->setMessage(result);
errors.append(mesg);
item = items.next();
}
m_resut->setErrors(errors);
if (m_checkSignature && errors.isEmpty() && !verifySignature(repData))
{
emit sendFinished();
return;
}
queryString = "//senv:Envelope/senv:Body/eet:Odpoved/eet:Hlavicka/@uuid_zpravy/data(.)";
q.setQuery(ms_nsDef + queryString);
q.evaluateTo(&result);
result = result.trimmed();
m_resut->setUuid(QUuid(result));
queryString = "//senv:Envelope/senv:Body/eet:Odpoved/eet:Hlavicka/@dat_prij/data(.)";
q.setQuery(ms_nsDef + queryString);
q.evaluateTo(&result);
result = result.trimmed();
m_resut->setReciveDate(QDateTime::fromString(result));
queryString = "//senv:Envelope/senv:Body/eet:Odpoved/eet:Potvrzeni/@fik/data(.)";
q.setQuery(ms_nsDef + queryString);
q.evaluateTo(&result);
result = result.trimmed();
m_resut->setFik(result);
queryString = "//senv:Envelope/senv:Body/eet:Odpoved/eet:Varovani";
q.setQuery(ms_nsDef + queryString);
q.evaluateTo(&items);
EetMessageList warnings;
item = items.next();
while (!item.isNull())
{
EetMessage *mesg = new EetMessage(m_resut);
queryString = "./@kod_varov/data(.)";
q.setQuery(ms_nsDef + queryString);
q.setFocus(item);
q.evaluateTo(&result);
result = result.trimmed();
mesg->setCode(result.toInt());
queryString = "./string()";
q.setQuery(ms_nsDef + queryString);
q.setFocus(item);
q.evaluateTo(&result);
result = result.trimmed();
mesg->setMessage(result);
warnings.append(mesg);
item = items.next();
}
m_resut->setWarnings(warnings);
if (errors.isEmpty())
{
m_resut->setReciveDate(QDateTime::currentDateTime());
}
else
{
m_resut->setStatus(EetResult::DATA_ERROR);
}
emit sendFinished();
reply->deleteLater();
}
-86
View File
@@ -1,86 +0,0 @@
#ifndef EETSENDER_H
#define EETSENDER_H
#include <QObject>
#include <QTimer>
#include "eetcpp_global.h"
#include "eetrequest.h"
#include "eetresult.h"
#define PRODUCTION_URL "https://prod.eet.cz/eet/services/EETServiceSOAP/v3"
#define PLAYGROUND_URL "https://pg.eet.cz/eet/services/EETServiceSOAP/v3"
class QNetworkAccessManager;
class QNetworkReply;
class EetSigner;
/**
* @brief The EetSender class Performs send actions.
*/
class EETCPPSHARED_EXPORT EetSender : public QObject
{
Q_OBJECT
public:
explicit EetSender(QObject *parent = 0);
/**
* @brief Sends request to EET web service
* @param request EET data
*/
void sendRequest(EetRequest *request);
/**
* @brief Setup certificate for signing EET request data.
* @param certPath Path to certificate p12 file. File must contain private key.
* @param passwd Password for accesing p12 file.
*/
void setupSigner(const QString &certPath, const QString &passwd);
/**
* @brief Sets verifing signature of response data.
* @param checkSignature true if you wish to verify.
*/
void setCheckSignature(bool checkSignature);
/**
* @brief Returns signature verifing state.
* @return true if verify.
*/
bool checkSignature() const;
/**
* @brief Sets comunication with playground EET portal.
* @param pg true if you wish to communicate with playground portal.
*/
void setPlayground(bool pg);
/**
* @brief Returns comunication result.
* @return result
*/
EetResult *resut() const;
void setTimeout(int timeout);
static bool m_online;
private:
static const QString ms_nsDef;
EetSigner *m_signer;
QNetworkAccessManager *m_manager;
QTimer *m_timer;
bool m_checkSignature;
EetResult *m_resut;
QString m_serviceUrl;
bool verifySignature(const QByteArray &repData);
private slots:
void replyFinished(QNetworkReply *reply);
signals:
void sendFinished();
public slots:
};
#endif // EETSENDER_H
-70
View File
@@ -1,70 +0,0 @@
#include "eetsigner.h"
#include <QDebug>
#include <QStringList>
EetSigner::EetSigner(QObject *parent) : QObject(parent)
{
}
QByteArray EetSigner::signData(const QByteArray &data)
{
if (!QCA::isSupported("sha256"))
{
return QByteArray();
}
if (m_result != QCA::ConvertGood || m_bundle.isNull())
{
return QByteArray();
}
QCA::PrivateKey privKey = m_bundle.privateKey();
return privKey.signMessage(QCA::MemoryRegion(data), QCA::EMSA3_SHA256);
}
QByteArray EetSigner::sha1HashData(const QByteArray &data)
{
if (!QCA::isSupported("sha1"))
{
return QByteArray();
}
return QCA::Hash("sha1").hash(QCA::MemoryRegion(data)).toByteArray();
}
QByteArray EetSigner::sha256HashData(const QByteArray &data)
{
if (!QCA::isSupported("sha256"))
{
return QByteArray();
}
return QCA::Hash("sha256").hash(QCA::MemoryRegion(data)).toByteArray();
}
QString EetSigner::getCertificate()
{
if (!QCA::isSupported("cert"))
{
return "";
}
if (m_result != QCA::ConvertGood || m_bundle.isNull())
{
return "";
}
if (!m_bundle.certificateChain().isEmpty())
{
return QString(m_bundle.certificateChain().primary().toDER().toBase64());
}
return "";
}
void EetSigner::setup(const QString &certPath, const QCA::SecureArray &certPasswd)
{
m_bundle = QCA::KeyBundle::fromFile(certPath, certPasswd, &m_result);
}
-30
View File
@@ -1,30 +0,0 @@
#ifndef EETSIGNER_H
#define EETSIGNER_H
#include <QObject>
#include <QByteArray>
#include <Qca-qt5/QtCrypto/QtCrypto>
class EetSigner : public QObject
{
Q_OBJECT
public:
explicit EetSigner(QObject *parent = 0);
QByteArray signData(const QByteArray &data);
QByteArray sha1HashData(const QByteArray &data);
QByteArray sha256HashData(const QByteArray &data);
QString getCertificate();
void setup(const QString &certPath, const QCA::SecureArray &certPasswd);
private:
QCA::Initializer m_qcaInit;
QCA::KeyBundle m_bundle;
QCA::ConvertResult m_result;
signals:
public slots:
};
#endif // EETSIGNER_H
-147
View File
@@ -1,147 +0,0 @@
#include "eetresult.h"
#include "eettemplate.h"
#include <QFile>
#include <QTextStream>
EetTemplate::EetTemplate(QObject *parent) : QObject(parent)
{
m_result = nullptr;
}
EetTemplate::EetTemplate(const QString &tmp, QObject *parent) : QObject(parent)
{
QFile tmpFile(tmp);
tmpFile.open(QFile::ReadOnly | QFile::Text);
QTextStream stream(&tmpFile);
m_template = stream.readAll();
m_result = nullptr;
}
QString EetTemplate::fillTemplate(EetRequest *request)
{
m_template = fillTemplateInternal(request, nullptr);
QString pkp = getPkpString(request);
QByteArray signedPkp = m_signer->signData(pkp.toUtf8());
QByteArray sha1Bkp = m_signer->sha1HashData(signedPkp);
QString base64Sign(signedPkp.toBase64());
QString base16bkp(sha1Bkp.toHex());
QString splitedBkp;
for (int i = 0; i < base16bkp.length(); i++)
{
if (i > 0 && i % 8 == 0)
{
splitedBkp += "-";
}
splitedBkp += base16bkp[i];
}
if (m_result != nullptr)
{
m_result->setPkp(base64Sign);
m_result->setBkp(splitedBkp.toUpper());
}
QMap<QString, QString> varMap;
varMap["pkp"] = base64Sign;
varMap["bkp"] = splitedBkp.toUpper();
return fillTemplate(varMap);
}
QString EetTemplate::fillTemplate(QMap<QString, QString> map)
{
return fillTemplateInternal(nullptr, &map);
}
QString EetTemplate::fillTemplateInternal(EetRequest *request, QMap<QString, QString> *map)
{
QString ret;
QString phStart;
if (request != nullptr)
{
phStart = "@{";
}
else
{
phStart = "${";
}
for (int i = 0; i < m_template.size(); i++)
{
QChar c = m_template[i];
if (!m_template.mid(i).startsWith(phStart))
{
if ((i > 0 && c == ' ' && m_template[i - 1] == ' '))
{
continue;
}
else
{
ret += c;
continue;
}
}
int p = m_template.indexOf("}", i + 2);
if (p < 0)
{
return ret;
}
QString var = m_template.mid(i + 2, p - i - 2);
QString value;
if (request != nullptr)
{
value = request->property(var.toStdString().c_str()).toString();
}
else
{
value = map->value(var, NULL_VAL);
}
if (value != NULL_VAL)
{
QString attrib;
if (request != NULL)
{
attrib = QString("%1=\"%2\"").arg(var, value);
}
else
{
attrib = value;
}
ret += attrib;
}
i = p;
}
return ret.replace(QRegExp("\" >"), "\">");
}
QString EetTemplate::getPkpString(EetRequest *request)
{
QString ret = QString("%1|%2|%3|%4|%5|%6").arg(request->property("dic_popl").toString(),
request->property("id_provoz").toString(),
request->property("id_pokl").toString(),
request->property("porad_cis").toString(),
request->property("dat_trzby").toString(),
request->property("celk_trzba").toString());
return ret;
}
void EetTemplate::setResult(EetResult *result)
{
m_result = result;
}
-41
View File
@@ -1,41 +0,0 @@
#ifndef EETTEMPLATE_H
#define EETTEMPLATE_H
#include <QObject>
#include <QString>
#include <QMap>
#include "eetrequest.h"
#include "eetsigner.h"
#define BODY_TEMPLATE ":/res/template_body.txt"
#define REQUEST_TEMPLATE ":/res/template_request.txt"
#define SIGNATURE_TEMPLATE ":/res/template_signature.txt"
class EetResult;
class EetTemplate : public QObject
{
Q_OBJECT
public:
explicit EetTemplate(QObject *parent = 0);
EetTemplate(const QString &tmp, QObject *parent = 0);
QString fillTemplate(EetRequest *request);
QString fillTemplate(QMap<QString, QString> map);
void setSigner(EetSigner *signer) { m_signer = signer; }
void setResult(EetResult *result);
signals:
public slots:
private:
QString m_template;
QString fillTemplateInternal(EetRequest *request, QMap<QString, QString> *map);
QString getPkpString(EetRequest *request);
EetSigner *m_signer;
EetResult *m_result;
};
#endif // EETTEMPLATE_H
+133
View File
@@ -0,0 +1,133 @@
#include "eetv4client.h"
#include "eetv4signer.h"
#include "eetv4soap.h"
#include <QEventLoop>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTimer>
#include <QUuid>
namespace
{
void setError(QString *error, const QString &message)
{
if (error != nullptr)
{
*error = message;
}
}
}
EetV4Client::EetV4Client(EetV4ClientConfig config)
: m_config(std::move(config))
{
}
bool EetV4Client::sendSale(const EetV4SaleRequest &request,
const QString &pkcs12Path,
const QByteArray &pkcs12Password,
EetV4Response *response,
QString *error) const
{
if (response == nullptr)
{
setError(error, QStringLiteral("A response output object is required."));
return false;
}
*response = {};
if (!m_config.endpoint.isValid() || m_config.endpoint.scheme() != QLatin1String("https") ||
m_config.endpoint.host().isEmpty())
{
setError(error, QStringLiteral("EET endpoint must be a valid HTTPS URL."));
return false;
}
if (m_config.timeoutMilliseconds <= 0)
{
setError(error, QStringLiteral("EET timeout must be positive."));
return false;
}
const QByteArray bodyId = QByteArrayLiteral("id-") +
request.messageUuid.toString(QUuid::WithoutBraces).toLatin1();
QString soapError;
const QByteArray unsignedSoap = EetV4Soap::serializeUnsigned(request, bodyId, &soapError);
if (unsignedSoap.isEmpty())
{
setError(error, soapError);
return false;
}
QString signingError;
const QByteArray signedSoap = EetV4Signer::signSoap(
unsignedSoap, pkcs12Path, pkcs12Password, &signingError);
if (signedSoap.isEmpty())
{
setError(error, signingError);
return false;
}
QNetworkAccessManager networkAccessManager;
QNetworkRequest networkRequest(m_config.endpoint);
if (!m_config.sslConfiguration.isNull())
{
networkRequest.setSslConfiguration(m_config.sslConfiguration);
}
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader,
QStringLiteral("text/xml; charset=utf-8"));
networkRequest.setRawHeader("Accept", "text/xml");
QNetworkReply *reply = networkAccessManager.post(networkRequest, signedSoap);
QEventLoop eventLoop;
QTimer timeout;
timeout.setSingleShot(true);
bool timedOut = false;
QObject::connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit);
QObject::connect(&timeout, &QTimer::timeout, reply, [&] {
timedOut = true;
reply->abort();
});
timeout.start(m_config.timeoutMilliseconds);
eventLoop.exec();
timeout.stop();
if (timedOut)
{
setError(error, QStringLiteral("EET request timed out."));
reply->deleteLater();
return false;
}
if (reply->error() != QNetworkReply::NoError)
{
setError(error, reply->errorString());
reply->deleteLater();
return false;
}
const QByteArray responseSoap = reply->readAll();
const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
QString verificationError;
if (!EetV4Signer::verifySoap(responseSoap, &verificationError))
{
setError(error, statusCode >= 300
? QStringLiteral("EET endpoint returned HTTP status %1 and an invalid "
"SOAP signature: %2.")
.arg(statusCode)
.arg(verificationError)
: verificationError);
return false;
}
QString responseError;
if (!EetV4ResponseParser::parseSoap(responseSoap, response, &responseError))
{
setError(error, statusCode >= 300
? QStringLiteral("EET endpoint returned HTTP status %1: %2.")
.arg(statusCode)
.arg(responseError)
: responseError);
return false;
}
return true;
}
+33
View File
@@ -0,0 +1,33 @@
#ifndef EETV4CLIENT_H
#define EETV4CLIENT_H
#include <QUrl>
#include <QSslConfiguration>
#include "eetcpp_global.h"
#include "eetv4request.h"
#include "eetv4response.h"
struct EETCPPSHARED_EXPORT EetV4ClientConfig
{
QUrl endpoint;
int timeoutMilliseconds{30000};
QSslConfiguration sslConfiguration;
};
class EETCPPSHARED_EXPORT EetV4Client
{
public:
explicit EetV4Client(EetV4ClientConfig config);
bool sendSale(const EetV4SaleRequest &request,
const QString &pkcs12Path,
const QByteArray &pkcs12Password,
EetV4Response *response,
QString *error = nullptr) const;
private:
EetV4ClientConfig m_config;
};
#endif // EETV4CLIENT_H
+118
View File
@@ -0,0 +1,118 @@
#include "eetv4request.h"
#include <QRegularExpression>
namespace
{
void addError(QStringList *errors, const QString &message)
{
if (errors != nullptr)
{
errors->append(message);
}
}
bool isValidEic(const QString &value)
{
static const QRegularExpression expression(QStringLiteral("^CZ[0-9]{8,10}$"));
return expression.match(value).hasMatch();
}
bool isValidRestrictedString(const QString &value, int maxLength)
{
static const QRegularExpression expression(QStringLiteral("^[0-9A-Za-z.,:;/#_\\- ]+$"));
return value.size() >= 1 && value.size() <= maxLength && expression.match(value).hasMatch();
}
}
EetV4Amount::EetV4Amount(qint64 cents)
: m_cents(cents)
{
}
bool EetV4Amount::fromString(const QString &value, EetV4Amount *amount, QString *error)
{
static const QRegularExpression expression(
QStringLiteral("^(?:(?:0|-?[1-9][0-9]{0,7})\\.[0-9]{2}|-0\\.(?:0[1-9]|[1-9][0-9]))$"));
if (!expression.match(value).hasMatch())
{
if (error != nullptr)
{
*error = QStringLiteral("Amount must contain two decimal places and fit the EET range.");
}
return false;
}
const bool negative = value.startsWith('-');
const QString absolute = negative ? value.mid(1) : value;
const QStringList parts = absolute.split('.');
const qint64 whole = parts.at(0).toLongLong();
const qint64 cents = whole * 100 + parts.at(1).toLongLong();
if (amount != nullptr)
{
*amount = EetV4Amount(negative ? -cents : cents);
}
return true;
}
qint64 EetV4Amount::cents() const
{
return m_cents;
}
QString EetV4Amount::toString() const
{
const bool negative = m_cents < 0;
const qint64 absolute = negative ? -m_cents : m_cents;
return QStringLiteral("%1%2.%3")
.arg(negative ? QStringLiteral("-") : QString())
.arg(absolute / 100)
.arg(absolute % 100, 2, 10, QLatin1Char('0'));
}
bool EetV4SaleRequest::validate(QStringList *errors) const
{
bool valid = true;
const auto require = [&valid, errors](bool condition, const QString &message) {
if (!condition)
{
valid = false;
addError(errors, message);
}
};
require(!messageUuid.isNull(), QStringLiteral("messageUuid must not be null."));
require(sentAt.isValid() && sentAt.timeSpec() != Qt::LocalTime,
QStringLiteral("sentAt must be a valid timestamp with an explicit offset."));
require(saleAt.isValid() && saleAt.timeSpec() != Qt::LocalTime,
QStringLiteral("saleAt must be a valid timestamp with an explicit offset."));
require(isValidEic(taxpayerEic),
QStringLiteral("taxpayerEic must match CZ followed by 8 to 10 digits."));
require(authorizingTaxpayerEic.isEmpty() || isValidEic(authorizingTaxpayerEic),
QStringLiteral("authorizingTaxpayerEic has an invalid format."));
require(!multipleTaxpayerAuthorization || !authorizingTaxpayerEic.isEmpty(),
QStringLiteral("authorizingTaxpayerEic is required for multiple taxpayer authorization."));
require(unitId >= 1 && unitId <= 999999999,
QStringLiteral("unitId must be between 1 and 999999999."));
require(isValidRestrictedString(cashRegisterId, 20),
QStringLiteral("cashRegisterId has an invalid format or length."));
require(isValidRestrictedString(receiptNumber, 25),
QStringLiteral("receiptNumber has an invalid format or length."));
return valid;
}
QString EetV4SaleRequest::formatDateTime(const QDateTime &dateTime)
{
const int offset = dateTime.offsetFromUtc();
const QChar sign = offset < 0 ? QLatin1Char('-') : QLatin1Char('+');
const int absoluteOffset = qAbs(offset);
return dateTime.toString(QStringLiteral("yyyy-MM-dd'T'HH:mm:ss"))
+ sign
+ QStringLiteral("%1:%2")
.arg(absoluteOffset / 3600, 2, 10, QLatin1Char('0'))
.arg((absoluteOffset % 3600) / 60, 2, 10, QLatin1Char('0'));
}
+52
View File
@@ -0,0 +1,52 @@
#ifndef EETV4REQUEST_H
#define EETV4REQUEST_H
#include <QDateTime>
#include <QString>
#include <QStringList>
#include <QUuid>
#include "eetcpp_global.h"
class EETCPPSHARED_EXPORT EetV4Amount
{
public:
EetV4Amount() = default;
static bool fromString(const QString &value, EetV4Amount *amount, QString *error = nullptr);
qint64 cents() const;
QString toString() const;
private:
explicit EetV4Amount(qint64 cents);
qint64 m_cents{0};
};
class EETCPPSHARED_EXPORT EetV4SaleRequest
{
public:
QUuid messageUuid{QUuid::createUuid()};
QDateTime sentAt;
QDateTime saleAt;
bool firstSubmission{true};
bool verificationMode{false};
QString taxpayerEic;
QString authorizingTaxpayerEic;
bool multipleTaxpayerAuthorization{false};
qint32 unitId{0};
QString cashRegisterId;
QString receiptNumber;
EetV4Amount totalSale;
EetV4Amount intendedSettlement;
EetV4Amount settlement;
bool hasIntendedSettlement{false};
bool hasSettlement{false};
bool validate(QStringList *errors = nullptr) const;
static QString formatDateTime(const QDateTime &dateTime);
};
#endif // EETV4REQUEST_H
+310
View File
@@ -0,0 +1,310 @@
#include "eetv4response.h"
#include <QRegularExpression>
#include <QXmlStreamReader>
namespace
{
constexpr auto SoapNamespace = "http://schemas.xmlsoap.org/soap/envelope/";
constexpr auto EetNamespace = "http://fs.gov.cz/eet/schema/v4";
void setError(QString *error, const QString &message)
{
if (error != nullptr)
{
*error = message;
}
}
bool isElement(const QXmlStreamReader &reader, const char *name, const char *namespaceUri)
{
return reader.isStartElement() &&
reader.name() == QLatin1String(name) &&
reader.namespaceUri() == QLatin1String(namespaceUri);
}
bool parseBoolean(const QString &value, bool *result)
{
if (value == QLatin1String("true") || value == QLatin1String("1"))
{
*result = true;
return true;
}
if (value == QLatin1String("false") || value == QLatin1String("0"))
{
*result = false;
return true;
}
return false;
}
bool parseDateTime(const QString &value, QDateTime *result)
{
const QDateTime parsed = QDateTime::fromString(value, Qt::ISODate);
if (!parsed.isValid() || value.size() < 20 ||
!(value.endsWith(QLatin1Char('Z')) || value.at(value.size() - 6) == QLatin1Char('+') ||
value.at(value.size() - 6) == QLatin1Char('-')))
{
return false;
}
*result = parsed;
return true;
}
bool parseUuid(const QString &value)
{
static const QRegularExpression expression(
QStringLiteral("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-"
"[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"));
return expression.match(value).hasMatch();
}
bool parsePok(const QString &value)
{
static const QRegularExpression expression(
QStringLiteral("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-"
"[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}-[0-9a-fA-F]{2}$"));
return expression.match(value).hasMatch();
}
bool parseResponseBody(QXmlStreamReader &reader, EetV4Response *response, QString *error)
{
bool hasHeader = false;
bool hasResult = false;
while (reader.readNextStartElement())
{
if (isElement(reader, "Hlavicka", EetNamespace))
{
if (hasHeader)
{
setError(error, QStringLiteral("EET response contains duplicate Hlavicka."));
return false;
}
hasHeader = true;
const auto attributes = reader.attributes();
const QString uuid = attributes.value(QStringLiteral("uuid_zpravy")).toString();
if (!uuid.isEmpty() && !parseUuid(uuid))
{
setError(error, QStringLiteral("EET response contains an invalid message UUID."));
return false;
}
response->messageUuid = uuid;
const QString receivedAt = attributes.value(QStringLiteral("dat_prij")).toString();
const QString rejectedAt = attributes.value(QStringLiteral("dat_odmit")).toString();
if (!receivedAt.isEmpty() && !parseDateTime(receivedAt, &response->receivedAt))
{
setError(error, QStringLiteral("EET response contains an invalid receipt timestamp."));
return false;
}
if (!rejectedAt.isEmpty() && !parseDateTime(rejectedAt, &response->rejectedAt))
{
setError(error, QStringLiteral("EET response contains an invalid rejection timestamp."));
return false;
}
reader.skipCurrentElement();
}
else if (isElement(reader, "Potvrzeni", EetNamespace))
{
if (hasResult)
{
setError(error, QStringLiteral("EET response contains multiple results."));
return false;
}
hasResult = true;
const auto attributes = reader.attributes();
const QString pok = attributes.value(QStringLiteral("pok")).toString();
if (!parsePok(pok))
{
setError(error, QStringLiteral("EET response contains an invalid POK."));
return false;
}
response->pok = pok;
response->status = EetV4ResponseStatus::Success;
const QString test = attributes.value(QStringLiteral("test")).toString();
if (!test.isEmpty() && !parseBoolean(test, &response->test))
{
setError(error, QStringLiteral("EET response contains an invalid test flag."));
return false;
}
reader.skipCurrentElement();
}
else if (isElement(reader, "Chyba", EetNamespace))
{
if (hasResult)
{
setError(error, QStringLiteral("EET response contains multiple results."));
return false;
}
hasResult = true;
const auto attributes = reader.attributes();
bool ok = false;
const int code = attributes.value(QStringLiteral("kod")).toInt(&ok);
if (!ok || code < -999 || code > 999)
{
setError(error, QStringLiteral("EET response contains an invalid error code."));
return false;
}
response->errorCode = code;
response->message = reader.readElementText(QXmlStreamReader::IncludeChildElements).trimmed();
response->status = EetV4ResponseStatus::Error;
const QString test = attributes.value(QStringLiteral("test")).toString();
if (!test.isEmpty() && !parseBoolean(test, &response->test))
{
setError(error, QStringLiteral("EET response contains an invalid test flag."));
return false;
}
}
else if (isElement(reader, "Varovani", EetNamespace))
{
bool ok = false;
const int code = reader.attributes().value(QStringLiteral("kod_varov")).toInt(&ok);
if (!ok || code < 1 || code > 999)
{
setError(error, QStringLiteral("EET response contains an invalid warning code."));
return false;
}
response->warningCodes.append(code);
reader.skipCurrentElement();
}
else
{
reader.skipCurrentElement();
}
}
if (!hasHeader || !hasResult)
{
setError(error, QStringLiteral("EET response is missing Hlavicka or a result."));
return false;
}
return true;
}
bool parseFault(QXmlStreamReader &reader, EetV4Response *response, QString *error)
{
QString code;
QString reason;
QString detail;
while (reader.readNextStartElement())
{
if (reader.name() == QLatin1String("faultcode") ||
reader.name() == QLatin1String("Value"))
{
code = reader.readElementText(QXmlStreamReader::IncludeChildElements).trimmed();
}
else if (reader.name() == QLatin1String("faultstring") ||
reader.name() == QLatin1String("Text"))
{
reason = reader.readElementText(QXmlStreamReader::IncludeChildElements).trimmed();
}
else if (reader.name() == QLatin1String("Detail"))
{
detail = reader.readElementText(QXmlStreamReader::IncludeChildElements).trimmed();
}
else
{
reader.skipCurrentElement();
}
}
response->status = EetV4ResponseStatus::SoapFault;
response->message = reason.isEmpty() ? detail : reason;
if (!code.isEmpty())
{
response->message.prepend(code + QStringLiteral(": "));
}
if (response->message.isEmpty())
{
setError(error, QStringLiteral("SOAP Fault does not contain a reason."));
return false;
}
return true;
}
}
bool EetV4ResponseParser::parseSoap(const QByteArray &soap,
EetV4Response *response,
QString *error)
{
if (response == nullptr)
{
setError(error, QStringLiteral("A response output object is required."));
return false;
}
*response = {};
QXmlStreamReader reader(soap);
bool inBody = false;
bool hasPayload = false;
while (!reader.atEnd())
{
reader.readNext();
if (!reader.isStartElement())
{
continue;
}
if (reader.name() == QLatin1String("Envelope"))
{
if (reader.namespaceUri() != QLatin1String(SoapNamespace))
{
setError(error, QStringLiteral("SOAP Envelope has an invalid namespace."));
return false;
}
}
else if (isElement(reader, "Body", SoapNamespace))
{
if (inBody)
{
setError(error, QStringLiteral("SOAP response contains duplicate Body."));
return false;
}
inBody = true;
while (reader.readNextStartElement())
{
if (hasPayload)
{
setError(error, QStringLiteral("SOAP Body contains multiple payloads."));
return false;
}
hasPayload = true;
if (isElement(reader, "Odpoved", EetNamespace))
{
if (!parseResponseBody(reader, response, error))
{
return false;
}
}
else if (reader.name() == QLatin1String("Fault") &&
reader.namespaceUri() == QLatin1String(SoapNamespace))
{
if (!parseFault(reader, response, error))
{
return false;
}
}
else
{
setError(error, QStringLiteral("SOAP Body contains an unknown payload."));
return false;
}
}
}
else if (reader.name() == QLatin1String("Header") &&
reader.namespaceUri() == QLatin1String(SoapNamespace))
{
reader.skipCurrentElement();
}
else
{
reader.skipCurrentElement();
}
}
if (reader.hasError())
{
setError(error, reader.errorString());
return false;
}
if (!inBody || !hasPayload)
{
setError(error, QStringLiteral("SOAP response does not contain a payload."));
return false;
}
return true;
}
+43
View File
@@ -0,0 +1,43 @@
#ifndef EETV4RESPONSE_H
#define EETV4RESPONSE_H
#include <QDateTime>
#include <QList>
#include <QString>
#include "eetcpp_global.h"
enum class EetV4ResponseStatus
{
Success,
Error,
SoapFault
};
struct EETCPPSHARED_EXPORT EetV4Response
{
EetV4ResponseStatus status = EetV4ResponseStatus::Error;
QString message;
QString messageUuid;
QDateTime receivedAt;
QDateTime rejectedAt;
QString pok;
int errorCode = 0;
bool test = false;
QList<int> warningCodes;
bool isSuccess() const
{
return status == EetV4ResponseStatus::Success;
}
};
class EETCPPSHARED_EXPORT EetV4ResponseParser
{
public:
static bool parseSoap(const QByteArray &soap,
EetV4Response *response,
QString *error = nullptr);
};
#endif // EETV4RESPONSE_H
+413
View File
@@ -0,0 +1,413 @@
#include "eetv4signer.h"
#include "eetv4soap.h"
#include <QFile>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <openssl/pkcs12.h>
#include <openssl/err.h>
#include <openssl/crypto.h>
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
#include <openssl/provider.h>
#endif
#include <openssl/x509.h>
#include <xmlsec/app.h>
#include <xmlsec/crypto.h>
#include <xmlsec/templates.h>
#include <xmlsec/xmlsec.h>
#include <xmlsec/xmltree.h>
#include <xmlsec/xmldsig.h>
#include <xmlsec/openssl/crypto.h>
#include <mutex>
namespace
{
constexpr auto SoapNamespace = "http://schemas.xmlsoap.org/soap/envelope/";
constexpr auto XmlDsigNamespace = "http://www.w3.org/2000/09/xmldsig#";
constexpr auto WsseNamespace = "http://docs.oasis-open.org/wss/2004/01/"
"oasis-200401-wss-wssecurity-secext-1.0.xsd";
constexpr auto WsuNamespace = "http://docs.oasis-open.org/wss/2004/01/"
"oasis-200401-wss-wssecurity-utility-1.0.xsd";
constexpr auto X509ValueType = "http://docs.oasis-open.org/wss/2004/01/"
"oasis-200401-wss-x509-token-profile-1.0#X509v3";
constexpr auto Base64Encoding = "http://docs.oasis-open.org/wss/2004/01/"
"oasis-200401-wss-soap-message-security-1.0#Base64Binary";
constexpr auto BodyId = "Id";
constexpr auto CertificateId = "eet-certificate";
bool initializeXmlSec()
{
OSSL_PROVIDER_load(nullptr, "default");
return xmlSecCheckVersion() == 1 &&
xmlSecInit() >= 0 &&
xmlSecOpenSSLAppInit(nullptr) >= 0 &&
xmlSecCryptoInit() >= 0;
}
bool ensureXmlSecInitialized()
{
static std::once_flag flag;
static bool initialized = false;
std::call_once(flag, [] { initialized = initializeXmlSec(); });
return initialized;
}
void ensureOpenSslProvider()
{
static std::once_flag flag;
std::call_once(flag, [] {
OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CONFIG, nullptr);
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
OSSL_PROVIDER_load(nullptr, "default");
OSSL_PROVIDER_load(nullptr, "legacy");
#endif
});
}
void setError(QString *error, const QString &message)
{
if (error != nullptr)
{
*error = message;
}
}
QByteArray certificateDer(const QByteArray &pkcs12, const QByteArray &password, QString *error)
{
BIO *bio = BIO_new_mem_buf(pkcs12.constData(), pkcs12.size());
if (bio == nullptr)
{
setError(error, QStringLiteral("Cannot create a PKCS#12 memory buffer."));
return {};
}
PKCS12 *container = d2i_PKCS12_bio(bio, nullptr);
BIO_free(bio);
if (container == nullptr)
{
setError(error, QStringLiteral("The PKCS#12 file is invalid."));
return {};
}
EVP_PKEY *privateKey = nullptr;
X509 *certificate = nullptr;
STACK_OF(X509) *caCertificates = nullptr;
const bool parsed = PKCS12_parse(container, password.constData(), &privateKey,
&certificate, &caCertificates) == 1;
PKCS12_free(container);
EVP_PKEY_free(privateKey);
sk_X509_pop_free(caCertificates, X509_free);
if (!parsed || certificate == nullptr)
{
X509_free(certificate);
char buffer[256] = {};
const unsigned long reason = ERR_get_error();
if (reason != 0)
{
ERR_error_string_n(reason, buffer, sizeof(buffer));
}
setError(error, QStringLiteral("The PKCS#12 file cannot be opened with the supplied password (%1).")
.arg(QString::fromLatin1(buffer)));
return {};
}
const int length = i2d_X509(certificate, nullptr);
QByteArray der;
if (length > 0)
{
der.resize(length);
unsigned char *destination = reinterpret_cast<unsigned char *>(der.data());
if (i2d_X509(certificate, &destination) != length)
{
der.clear();
}
}
X509_free(certificate);
if (der.isEmpty())
{
setError(error, QStringLiteral("The certificate could not be encoded."));
}
return der;
}
xmlNodePtr findChild(xmlNodePtr parent, const char *name, const char *namespaceUri)
{
for (xmlNodePtr child = parent == nullptr ? nullptr : parent->children;
child != nullptr; child = child->next)
{
if (child->type == XML_ELEMENT_NODE &&
xmlStrEqual(child->name, BAD_CAST name) &&
child->ns != nullptr &&
xmlStrEqual(child->ns->href, BAD_CAST namespaceUri))
{
return child;
}
}
return nullptr;
}
xmlNodePtr findDescendant(xmlNodePtr parent, const char *name, const char *namespaceUri)
{
if (parent == nullptr)
{
return nullptr;
}
for (xmlNodePtr child = parent->children; child != nullptr; child = child->next)
{
if (child->type == XML_ELEMENT_NODE &&
xmlStrEqual(child->name, BAD_CAST name) &&
child->ns != nullptr &&
xmlStrEqual(child->ns->href, BAD_CAST namespaceUri))
{
return child;
}
if (xmlNodePtr descendant = findDescendant(child, name, namespaceUri);
descendant != nullptr)
{
return descendant;
}
}
return nullptr;
}
}
QByteArray EetV4Signer::signSoap(const QByteArray &unsignedSoap,
const QString &pkcs12Path,
const QByteArray &pkcs12Password,
QString *error)
{
if (unsignedSoap.isEmpty() || pkcs12Path.isEmpty())
{
setError(error, QStringLiteral("SOAP data and PKCS#12 path are required."));
return {};
}
ensureOpenSslProvider();
QFile pkcs12File(pkcs12Path);
if (!pkcs12File.open(QIODevice::ReadOnly))
{
setError(error, QStringLiteral("Cannot read the PKCS#12 file."));
return {};
}
const QByteArray pkcs12 = pkcs12File.readAll();
const QByteArray certificate = certificateDer(pkcs12, pkcs12Password, error);
if (certificate.isEmpty())
{
return {};
}
if (!ensureXmlSecInitialized())
{
setError(error, QStringLiteral("XML security initialization failed."));
return {};
}
xmlDocPtr document = xmlReadMemory(unsignedSoap.constData(), unsignedSoap.size(),
nullptr, "UTF-8", XML_PARSE_NONET);
if (document == nullptr)
{
setError(error, QStringLiteral("The unsigned SOAP document is not valid XML."));
return {};
}
QByteArray result;
xmlNodePtr envelope = xmlDocGetRootElement(document);
xmlNodePtr header = findChild(envelope, "Header", SoapNamespace);
xmlNodePtr body = findChild(envelope, "Body", SoapNamespace);
if (header == nullptr || body == nullptr)
{
setError(error, QStringLiteral("SOAP Envelope must contain Header and Body."));
xmlFreeDoc(document);
return {};
}
xmlAttrPtr bodyIdAttribute = xmlHasNsProp(body, BAD_CAST BodyId, BAD_CAST WsuNamespace);
if (bodyIdAttribute == nullptr || bodyIdAttribute->children == nullptr)
{
setError(error, QStringLiteral("SOAP Body does not contain a wsu:Id."));
xmlFreeDoc(document);
return {};
}
QByteArray bodyReference = QByteArray("#") +
QByteArray(reinterpret_cast<const char *>(
bodyIdAttribute->children->content));
const xmlChar *ids[] = { BAD_CAST BodyId, nullptr };
xmlSecAddIDs(document, nullptr, ids);
xmlNsPtr wsse = xmlSearchNsByHref(document, envelope, BAD_CAST WsseNamespace);
xmlNsPtr wsu = xmlSearchNsByHref(document, envelope, BAD_CAST WsuNamespace);
if (wsse == nullptr || wsu == nullptr)
{
setError(error, QStringLiteral("SOAP security namespaces are missing."));
xmlFreeDoc(document);
return {};
}
xmlNodePtr security = xmlNewNode(wsse, BAD_CAST "Security");
xmlNodePtr token = xmlNewChild(security, wsse, BAD_CAST "BinarySecurityToken",
BAD_CAST certificate.toBase64().constData());
xmlSetNsProp(token, wsu, BAD_CAST "Id", BAD_CAST CertificateId);
xmlNewProp(token, BAD_CAST "EncodingType", BAD_CAST Base64Encoding);
xmlNewProp(token, BAD_CAST "ValueType", BAD_CAST X509ValueType);
xmlAddChild(header, security);
xmlNodePtr signature = xmlSecTmplSignatureCreateNsPref(
document, xmlSecTransformExclC14NId, xmlSecTransformRsaSha256Id,
nullptr, BAD_CAST "ds");
if (signature == nullptr)
{
setError(error, QStringLiteral("Cannot create the XML signature template."));
xmlFreeDoc(document);
return {};
}
xmlNodePtr reference = xmlSecTmplSignatureAddReference(
signature, xmlSecTransformSha256Id, nullptr,
BAD_CAST bodyReference.constData(), nullptr);
if (reference == nullptr ||
xmlSecTmplReferenceAddTransform(reference, xmlSecTransformExclC14NId) == nullptr)
{
setError(error, QStringLiteral("Cannot create the SOAP Body signature reference."));
xmlFreeDoc(document);
return {};
}
xmlNodePtr keyInfo = xmlSecTmplSignatureEnsureKeyInfo(signature, nullptr);
xmlNodePtr tokenReference = xmlNewChild(keyInfo, wsse, BAD_CAST "SecurityTokenReference", nullptr);
xmlNodePtr tokenReferenceValue = xmlNewChild(tokenReference, wsse, BAD_CAST "Reference", nullptr);
xmlNewProp(tokenReferenceValue, BAD_CAST "URI", BAD_CAST "#eet-certificate");
xmlNewProp(tokenReferenceValue, BAD_CAST "ValueType", BAD_CAST X509ValueType);
xmlAddChild(security, signature);
xmlSecKeyPtr key = xmlSecCryptoAppPkcs12LoadMemory(
reinterpret_cast<const xmlSecByte *>(pkcs12.constData()), pkcs12.size(),
pkcs12Password.constData(), nullptr, nullptr);
if (key == nullptr)
{
setError(error, QStringLiteral("XML security could not load the PKCS#12 key."));
xmlFreeDoc(document);
return {};
}
xmlSecDSigCtxPtr context = xmlSecDSigCtxCreate(nullptr);
if (context == nullptr)
{
setError(error, QStringLiteral("Cannot create the XML signature context."));
xmlSecKeyDestroy(key);
xmlFreeDoc(document);
return {};
}
context->signKey = key;
const bool signedSuccessfully = xmlSecDSigCtxSign(context, signature) == 0;
xmlSecDSigCtxDestroy(context);
if (!signedSuccessfully)
{
setError(error, QStringLiteral("XML security could not sign the SOAP Body."));
xmlFreeDoc(document);
return {};
}
xmlChar *serialized = nullptr;
int serializedSize = 0;
xmlDocDumpMemoryEnc(document, &serialized, &serializedSize, "UTF-8");
if (serialized != nullptr && serializedSize > 0)
{
result = QByteArray(reinterpret_cast<const char *>(serialized), serializedSize);
}
xmlFree(serialized);
xmlFreeDoc(document);
if (result.isEmpty())
{
setError(error, QStringLiteral("Cannot serialize the signed SOAP document."));
}
else if (result.size() > EetV4Soap::MaximumMessageSize)
{
result.clear();
setError(error, QStringLiteral("Signed SOAP message exceeds the 12 KiB EET limit."));
}
return result;
}
bool EetV4Signer::verifySoap(const QByteArray &signedSoap, QString *error)
{
if (signedSoap.isEmpty())
{
setError(error, QStringLiteral("Signed SOAP data is required."));
return false;
}
ensureOpenSslProvider();
if (!ensureXmlSecInitialized())
{
setError(error, QStringLiteral("XML security initialization failed."));
return false;
}
xmlDocPtr document = xmlReadMemory(signedSoap.constData(), signedSoap.size(),
nullptr, "UTF-8", XML_PARSE_NONET);
if (document == nullptr)
{
setError(error, QStringLiteral("The signed SOAP document is not valid XML."));
return false;
}
xmlNodePtr envelope = xmlDocGetRootElement(document);
xmlNodePtr body = findChild(envelope, "Body", SoapNamespace);
xmlNodePtr signature = findDescendant(envelope, "Signature", XmlDsigNamespace);
xmlNodePtr token = findDescendant(envelope, "BinarySecurityToken", WsseNamespace);
if (body == nullptr || signature == nullptr || token == nullptr)
{
setError(error, QStringLiteral("Signed SOAP is missing Body, Signature, or certificate."));
xmlFreeDoc(document);
return false;
}
xmlAttrPtr bodyId = xmlHasNsProp(body, BAD_CAST BodyId, BAD_CAST WsuNamespace);
if (bodyId == nullptr || bodyId->children == nullptr)
{
setError(error, QStringLiteral("SOAP Body does not contain a wsu:Id."));
xmlFreeDoc(document);
return false;
}
const xmlChar *ids[] = { BAD_CAST BodyId, nullptr };
xmlSecAddIDs(document, nullptr, ids);
xmlChar *tokenContent = xmlNodeGetContent(token);
const QByteArray certificate = tokenContent == nullptr
? QByteArray()
: QByteArray::fromBase64(
QByteArray(reinterpret_cast<const char *>(tokenContent)));
xmlFree(tokenContent);
if (certificate.isEmpty())
{
setError(error, QStringLiteral("The embedded certificate is invalid."));
xmlFreeDoc(document);
return false;
}
xmlSecKeyPtr key = xmlSecCryptoAppKeyLoadMemory(
reinterpret_cast<const xmlSecByte *>(certificate.constData()), certificate.size(),
xmlSecKeyDataFormatCertDer, nullptr, nullptr, nullptr);
if (key == nullptr)
{
setError(error, QStringLiteral("XML security could not load the embedded certificate."));
xmlFreeDoc(document);
return false;
}
xmlSecDSigCtxPtr context = xmlSecDSigCtxCreate(nullptr);
if (context == nullptr)
{
setError(error, QStringLiteral("Cannot create the XML verification context."));
xmlSecKeyDestroy(key);
xmlFreeDoc(document);
return false;
}
context->signKey = key;
const bool verified = xmlSecDSigCtxVerify(context, signature) == 0 &&
context->status == xmlSecDSigStatusSucceeded;
xmlSecDSigCtxDestroy(context);
xmlFreeDoc(document);
if (!verified)
{
setError(error, QStringLiteral("SOAP XML signature verification failed."));
}
return verified;
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef EETV4SIGNER_H
#define EETV4SIGNER_H
#include <QByteArray>
#include <QString>
#include "eetcpp_global.h"
class EETCPPSHARED_EXPORT EetV4Signer
{
public:
static QByteArray signSoap(const QByteArray &unsignedSoap,
const QString &pkcs12Path,
const QByteArray &pkcs12Password,
QString *error = nullptr);
static bool verifySoap(const QByteArray &signedSoap,
QString *error = nullptr);
};
#endif // EETV4SIGNER_H
+68
View File
@@ -0,0 +1,68 @@
#include "eetv4soap.h"
#include "eetv4xml.h"
#include <QRegularExpression>
namespace
{
QByteArray withoutXmlDeclaration(const QByteArray &xml)
{
const int declarationEnd = xml.indexOf("?>");
return declarationEnd >= 0 ? xml.mid(declarationEnd + 2).trimmed() : xml.trimmed();
}
bool isValidBodyId(const QByteArray &bodyId)
{
static const QRegularExpression expression(QStringLiteral("^[A-Za-z_][A-Za-z0-9_.-]{0,63}$"));
return expression.match(QString::fromLatin1(bodyId)).hasMatch();
}
}
QByteArray EetV4Soap::serializeUnsigned(const EetV4SaleRequest &request,
const QByteArray &bodyId,
QString *error)
{
if (!isValidBodyId(bodyId))
{
if (error != nullptr)
{
*error = QStringLiteral("SOAP Body ID has an invalid format.");
}
return {};
}
QString saleError;
const QByteArray saleXml = EetV4Xml::serializeSale(request, &saleError);
if (saleXml.isEmpty())
{
if (error != nullptr)
{
*error = saleError;
}
return {};
}
QByteArray output;
output.reserve(saleXml.size() + 512);
output.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
output.append("<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\">");
output.append("<soap:Header/>");
output.append("<soap:Body wsu:Id=\"");
output.append(bodyId);
output.append("\">");
output.append(withoutXmlDeclaration(saleXml));
output.append("</soap:Body></soap:Envelope>");
if (output.size() > MaximumMessageSize)
{
if (error != nullptr)
{
*error = QStringLiteral("SOAP message exceeds the 12 KiB EET limit.");
}
return {};
}
return output;
}
+20
View File
@@ -0,0 +1,20 @@
#ifndef EETV4SOAP_H
#define EETV4SOAP_H
#include <QByteArray>
#include <QString>
#include "eetcpp_global.h"
#include "eetv4request.h"
class EETCPPSHARED_EXPORT EetV4Soap
{
public:
static constexpr int MaximumMessageSize = 12 * 1024;
static QByteArray serializeUnsigned(const EetV4SaleRequest &request,
const QByteArray &bodyId = QByteArrayLiteral("id-body"),
QString *error = nullptr);
};
#endif // EETV4SOAP_H
+79
View File
@@ -0,0 +1,79 @@
#include "eetv4xml.h"
#include <QXmlStreamWriter>
namespace
{
QString booleanValue(bool value)
{
return value ? QStringLiteral("true") : QStringLiteral("false");
}
}
QByteArray EetV4Xml::serializeSale(const EetV4SaleRequest &request, QString *error)
{
QStringList validationErrors;
if (!request.validate(&validationErrors))
{
if (error != nullptr)
{
*error = validationErrors.join(QStringLiteral(" "));
}
return {};
}
QByteArray output;
QXmlStreamWriter writer(&output);
writer.setAutoFormatting(false);
writer.writeStartDocument(QStringLiteral("1.0"));
writer.writeStartElement(QStringLiteral("Trzba"));
writer.writeDefaultNamespace(QStringLiteral("http://fs.gov.cz/eet/schema/v4"));
writer.writeStartElement(QStringLiteral("Hlavicka"));
writer.writeAttribute(QStringLiteral("uuid_zpravy"), request.messageUuid.toString(QUuid::WithoutBraces));
writer.writeAttribute(QStringLiteral("dat_odesl"), EetV4SaleRequest::formatDateTime(request.sentAt));
writer.writeAttribute(QStringLiteral("prvni_zaslani"), booleanValue(request.firstSubmission));
if (request.verificationMode)
{
writer.writeAttribute(QStringLiteral("overeni"), QStringLiteral("true"));
}
writer.writeEndElement();
writer.writeStartElement(QStringLiteral("Data"));
writer.writeAttribute(QStringLiteral("eic_popl"), request.taxpayerEic);
if (!request.authorizingTaxpayerEic.isEmpty())
{
writer.writeAttribute(QStringLiteral("eic_poverujiciho"), request.authorizingTaxpayerEic);
}
if (request.multipleTaxpayerAuthorization)
{
writer.writeAttribute(QStringLiteral("povereni_vice_popl"), QStringLiteral("true"));
}
writer.writeAttribute(QStringLiteral("id_jednotky"), QString::number(request.unitId));
writer.writeAttribute(QStringLiteral("id_pokl"), request.cashRegisterId);
writer.writeAttribute(QStringLiteral("porad_cis"), request.receiptNumber);
writer.writeAttribute(QStringLiteral("dat_trzby"), EetV4SaleRequest::formatDateTime(request.saleAt));
writer.writeAttribute(QStringLiteral("celk_trzba"), request.totalSale.toString());
if (request.hasIntendedSettlement)
{
writer.writeAttribute(QStringLiteral("urceno_cerp_zuct"), request.intendedSettlement.toString());
}
if (request.hasSettlement)
{
writer.writeAttribute(QStringLiteral("cerp_zuct"), request.settlement.toString());
}
writer.writeEndElement();
writer.writeEndElement();
writer.writeEndDocument();
if (writer.hasError())
{
if (error != nullptr)
{
*error = QStringLiteral("Failed to serialize the EET v4 XML document.");
}
return {};
}
return output;
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef EETV4XML_H
#define EETV4XML_H
#include <QByteArray>
#include <QString>
#include "eetcpp_global.h"
#include "eetv4request.h"
class EETCPPSHARED_EXPORT EetV4Xml
{
public:
static QByteArray serializeSale(const EetV4SaleRequest &request, QString *error = nullptr);
};
#endif // EETV4XML_H
-54
View File
@@ -1,54 +0,0 @@
#-------------------------------------------------
#
# Project created by QtCreator 2017-01-07T15:25:00
#
#-------------------------------------------------
win32 {
QCA_PATH = d:/prac/qt/lib/qca
}
QT += network xmlpatterns
QT -= gui
CONFIG += c++11
TARGET = EetCpp
TEMPLATE = lib
DEFINES += EETCPP_LIBRARY
release:DESTDIR = $$PWD/../bin/release
debug:DESTDIR = $$PWD/../bin/debug
SOURCES += eetcpp.cpp \
eetrequest.cpp \
eettemplate.cpp \
eetsender.cpp \
eetsigner.cpp \
eetresult.cpp
HEADERS += eetcpp.h\
eetcpp_global.h \
eetrequest.h \
eettemplate.h \
eetsender.h \
eetsigner.h \
eetresult.h
unix {
target.path = /usr/lib
INSTALLS += target
}
win32 {
INCLUDEPATH += $$QCA_PATH/include
}
win32:CONFIG(release, debug|release): LIBS += -L$$QCA_PATH -lqca-qt5
else:win32:CONFIG(debug, debug|release): LIBS += -L$$QCA_PATH -lqca-qt5d
else:unix: LIBS += -lqca-qt5
RESOURCES += \
resource.qrc
-10
View File
@@ -1,10 +0,0 @@
<soap:Body xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="TheBody" xml:id="TheBody">
<eet:Trzba xmlns:eet="http://fs.mfcr.cz/eet/schema/v3">
<eet:Hlavicka @{dat_odesl} @{overeni} @{prvni_zaslani} @{uuid_zpravy}></eet:Hlavicka>
<eet:Data @{celk_trzba} @{cerp_zuct} @{cest_sluz} @{dan1} @{dan2} @{dan3} @{dat_trzby} @{dic_popl} @{dic_poverujiciho} @{id_pokl} @{id_provoz} @{porad_cis} @{pouzit_zboz1} @{pouzit_zboz2} @{pouzit_zboz3} @{rezim} @{urceno_cerp_zuct} @{zakl_dan1} @{zakl_dan2} @{zakl_dan3} @{zakl_nepodl_dph}></eet:Data>
<eet:KontrolniKody>
<eet:pkp cipher="RSA2048" digest="SHA256" encoding="base64">${pkp}</eet:pkp>
<eet:bkp digest="SHA1" encoding="base16">${bkp}</eet:bkp>
</eet:KontrolniKody>
</eet:Trzba>
</soap:Body>
-36
View File
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<wsse:Security
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:mustUnderstand="1">
<wsse:BinarySecurityToken
EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"
wsu:Id="TheCert">${certb64}</wsse:BinarySecurityToken>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="TheSignature">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<ec:InclusiveNamespaces xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="soap"/>
</ds:CanonicalizationMethod>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#TheBody">
<ds:Transforms>
<ds:Transform Algorithm='http://www.w3.org/2001/10/xml-exc-c14n#'/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>${digest}</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>${signature}</ds:SignatureValue>
<ds:KeyInfo Id="TheKeyInfo">
<wsse:SecurityTokenReference wsu:Id="TheSecurityTokenReference">
<wsse:Reference URI="#TheCert" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>
</wsse:SecurityTokenReference>
</ds:KeyInfo>
</ds:Signature>
</wsse:Security>
</SOAP-ENV:Header>
${soap:Body}
</soap:Envelope>
-13
View File
@@ -1,13 +0,0 @@
<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<ec:InclusiveNamespaces xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="soap"></ec:InclusiveNamespaces>
</ds:CanonicalizationMethod>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></ds:SignatureMethod>
<ds:Reference URI="#TheBody">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod>
<ds:DigestValue>${digest}</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
-7
View File
@@ -1,7 +0,0 @@
<RCC>
<qresource prefix="/">
<file>res/template_body.txt</file>
<file>res/template_request.txt</file>
<file>res/template_signature.txt</file>
</qresource>
</RCC>
+9
View File
@@ -0,0 +1,9 @@
add_executable(EetCppPlayground
main.cpp
)
target_link_libraries(EetCppPlayground
PRIVATE
EetCpp::EetCpp
Qt6::Core
)
+409
View File
@@ -0,0 +1,409 @@
#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;
}
+10
View File
@@ -0,0 +1,10 @@
param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$Command,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$Arguments
)
$playground = if ($env:EETCPP_PLAYGROUND) { $env:EETCPP_PLAYGROUND } else { "EetCppPlayground" }
& $playground $Command @Arguments
exit $LASTEXITCODE
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -eu
playground="${EETCPP_PLAYGROUND:-EetCppPlayground}"
exec "$playground" "$@"
+19
View File
@@ -0,0 +1,19 @@
$ErrorActionPreference = "Stop"
$required = @(
"EETCPP_PLAYGROUND",
"EET_ENDPOINT",
"EET_PKCS12_PATH",
"EET_PKCS12_PASSWORD"
)
foreach ($name in $required) {
if (-not (Get-Item "Env:$name" -ErrorAction SilentlyContinue)) {
throw "Set $name before running preflight."
}
}
& $env:EETCPP_PLAYGROUND preflight `
--endpoint $env:EET_ENDPOINT `
--certificate $env:EET_PKCS12_PATH `
--password-env EET_PKCS12_PASSWORD
exit $LASTEXITCODE
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -eu
: "${EETCPP_PLAYGROUND:?Set EETCPP_PLAYGROUND}"
: "${EET_ENDPOINT:?Set EET_ENDPOINT}"
: "${EET_PKCS12_PATH:?Set EET_PKCS12_PATH}"
: "${EET_PKCS12_PASSWORD:?Set EET_PKCS12_PASSWORD}"
exec "$EETCPP_PLAYGROUND" preflight \
--endpoint "$EET_ENDPOINT" \
--certificate "$EET_PKCS12_PATH" \
--password-env EET_PKCS12_PASSWORD
+32
View File
@@ -0,0 +1,32 @@
$ErrorActionPreference = "Stop"
$required = @(
"EETCPP_PLAYGROUND",
"EET_ENDPOINT",
"EET_PKCS12_PATH",
"EET_PKCS12_PASSWORD",
"EET_EIC",
"EET_UNIT_ID",
"EET_REGISTER_ID",
"EET_RECEIPT_NUMBER",
"EET_AMOUNT"
)
foreach ($name in $required) {
if (-not (Get-Item "Env:$name" -ErrorAction SilentlyContinue)) {
throw "Set $name before running verification sale."
}
}
$timeout = if ($env:EET_TIMEOUT_MS) { $env:EET_TIMEOUT_MS } else { "30000" }
& $env:EETCPP_PLAYGROUND send-sale `
--endpoint $env:EET_ENDPOINT `
--certificate $env:EET_PKCS12_PATH `
--password-env EET_PKCS12_PASSWORD `
--eic $env:EET_EIC `
--unit $env:EET_UNIT_ID `
--register $env:EET_REGISTER_ID `
--receipt $env:EET_RECEIPT_NUMBER `
--amount $env:EET_AMOUNT `
--timeout $timeout `
--verification
exit $LASTEXITCODE
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -eu
: "${EETCPP_PLAYGROUND:?Set EETCPP_PLAYGROUND}"
: "${EET_ENDPOINT:?Set EET_ENDPOINT}"
: "${EET_PKCS12_PATH:?Set EET_PKCS12_PATH}"
: "${EET_PKCS12_PASSWORD:?Set EET_PKCS12_PASSWORD}"
: "${EET_EIC:?Set EET_EIC}"
: "${EET_UNIT_ID:?Set EET_UNIT_ID}"
: "${EET_REGISTER_ID:?Set EET_REGISTER_ID}"
: "${EET_RECEIPT_NUMBER:?Set EET_RECEIPT_NUMBER}"
: "${EET_AMOUNT:?Set EET_AMOUNT}"
exec "$EETCPP_PLAYGROUND" send-sale \
--endpoint "$EET_ENDPOINT" \
--certificate "$EET_PKCS12_PATH" \
--password-env EET_PKCS12_PASSWORD \
--eic "$EET_EIC" \
--unit "$EET_UNIT_ID" \
--register "$EET_REGISTER_ID" \
--receipt "$EET_RECEIPT_NUMBER" \
--amount "$EET_AMOUNT" \
--timeout "${EET_TIMEOUT_MS:-30000}" \
--verification
+145
View File
@@ -0,0 +1,145 @@
find_package(Qt6 6.5 REQUIRED COMPONENTS Test)
add_executable(EetCppV4RequestTest
eetv4request_test.cpp
)
set_target_properties(EetCppV4RequestTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4RequestTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.request COMMAND EetCppV4RequestTest)
add_executable(EetCppV4XmlTest
eetv4xml_test.cpp
)
set_target_properties(EetCppV4XmlTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4XmlTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.xml COMMAND EetCppV4XmlTest)
add_executable(EetCppV4SoapTest
eetv4soap_test.cpp
)
set_target_properties(EetCppV4SoapTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4SoapTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.soap COMMAND EetCppV4SoapTest)
add_executable(EetCppV4SignerTest
eetv4signer_test.cpp
)
set_target_properties(EetCppV4SignerTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4SignerTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.signer COMMAND EetCppV4SignerTest)
add_executable(EetCppV4ResponseTest
eetv4response_test.cpp
)
set_target_properties(EetCppV4ResponseTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4ResponseTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.response COMMAND EetCppV4ResponseTest)
add_executable(EetCppV4ClientTest
eetv4client_test.cpp
)
set_target_properties(EetCppV4ClientTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4ClientTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.client COMMAND EetCppV4ClientTest)
add_executable(EetCppV4ClientIntegrationTest
eetv4client_integration_test.cpp
)
set_target_properties(EetCppV4ClientIntegrationTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4ClientIntegrationTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.client.integration COMMAND EetCppV4ClientIntegrationTest)
add_executable(EetCaTest
eetca_test.cpp
)
set_target_properties(EetCaTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCaTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.ca COMMAND EetCaTest)
add_executable(EetCaIntegrationTest
eetca_integration_test.cpp
)
set_target_properties(EetCaIntegrationTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCaIntegrationTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.ca.integration COMMAND EetCaIntegrationTest)
+150
View File
@@ -0,0 +1,150 @@
#include "eetca.h"
#include <QFile>
#include <QProcess>
#include <QRegularExpression>
#include <QSslCertificate>
#include <QSslKey>
#include <QSslServer>
#include <QTemporaryDir>
#include <QtTest>
namespace
{
class MockCaServer : public QSslServer
{
public:
explicit MockCaServer(const QByteArray &pkcs12)
: m_pkcs12(pkcs12)
{
connect(this, &QSslServer::pendingConnectionAvailable, this, [this] {
QSslSocket *socket = qobject_cast<QSslSocket *>(nextPendingConnection());
if (socket == nullptr)
{
return;
}
connect(socket, &QSslSocket::readyRead, this, [this, socket] {
const QByteArray request = socket->readAll();
const QByteArray firstLine = request.left(request.indexOf("\r\n"));
const QList<QByteArray> parts = firstLine.split(' ');
if (parts.size() < 2 || !request.contains("Authorization: Bearer "))
{
socket->disconnectFromHost();
return;
}
QByteArray body;
int status = 200;
if (parts.at(1) == "/api/request/renew")
{
body = "{\"reqId\":\"123e4567-e89b-42d3-a456-426614174000\"}";
status = 201;
}
else if (parts.at(1) == "/api/request/123e4567-e89b-42d3-a456-426614174000/status")
{
body = "{\"reqId\":\"123e4567-e89b-42d3-a456-426614174000\","
"\"state\":\"ISSUED\"}";
}
else if (parts.at(1) == "/api/request/not-finished")
{
body = "{\"count\":1,\"data\":[\"123e4567-e89b-42d3-a456-426614174000\"]}";
}
else if (parts.at(1) == "/api/request/123e4567-e89b-42d3-a456-426614174000/claim-download")
{
body = "{\"pkcs12\":\"" + m_pkcs12.toBase64() +
"\",\"password\":\"ABcd1234\",\"certificate\":{"
"\"sn\":\"12345\",\"validFrom\":\"2026-01-01T00:00:00+00:00\","
"\"validTo\":\"2027-01-01T00:00:00+00:00\",\"state\":\"VALID\"}}";
}
else if (parts.at(1) == "/api/request/123e4567-e89b-42d3-a456-426614174000/ack-download")
{
status = 204;
}
else
{
status = 404;
body = "{\"code\":4,\"message\":\"Unknown object.\"}";
}
const QByteArray response =
"HTTP/1.1 " + QByteArray::number(status) + " OK\r\n"
"Content-Type: application/json\r\nContent-Length: " +
QByteArray::number(body.size()) + "\r\nConnection: close\r\n\r\n" + body;
socket->write(response);
socket->disconnectFromHost();
});
});
}
private:
QByteArray m_pkcs12;
};
}
class EetCaIntegrationTest : public QObject
{
Q_OBJECT
private slots:
void runsCertificateWorkflow();
};
void EetCaIntegrationTest::runsCertificateWorkflow()
{
QTemporaryDir directory;
QVERIFY(directory.isValid());
const QString keyPath = directory.filePath(QStringLiteral("key.pem"));
const QString certPath = directory.filePath(QStringLiteral("certificate.pem"));
const QString p12Path = directory.filePath(QStringLiteral("certificate.p12"));
auto runOpenSsl = [](const QStringList &arguments) {
QProcess process;
process.start(QStringLiteral("openssl"), arguments);
QVERIFY2(process.waitForFinished(30000), qPrintable(process.errorString()));
QVERIFY2(process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0,
process.readAllStandardError().constData());
};
runOpenSsl({QStringLiteral("req"), QStringLiteral("-x509"), QStringLiteral("-newkey"),
QStringLiteral("rsa:2048"), QStringLiteral("-nodes"), QStringLiteral("-subj"),
QStringLiteral("/CN=localhost"), QStringLiteral("-keyout"), keyPath,
QStringLiteral("-out"), certPath, QStringLiteral("-days"), QStringLiteral("1")});
runOpenSsl({QStringLiteral("pkcs12"), QStringLiteral("-export"), QStringLiteral("-out"),
p12Path, QStringLiteral("-inkey"), keyPath, QStringLiteral("-in"), certPath,
QStringLiteral("-passout"), QStringLiteral("pass:test")});
QFile p12File(p12Path);
QVERIFY(p12File.open(QIODevice::ReadOnly));
const QByteArray p12 = p12File.readAll();
MockCaServer server(p12);
QSslConfiguration ssl = QSslConfiguration::defaultConfiguration();
QFile keyFile(keyPath);
QFile certificateFile(certPath);
QVERIFY(keyFile.open(QIODevice::ReadOnly));
QVERIFY(certificateFile.open(QIODevice::ReadOnly));
ssl.setPrivateKey(QSslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem));
ssl.setLocalCertificate(QSslCertificate(certificateFile.readAll(), QSsl::Pem));
server.setSslConfiguration(ssl);
QVERIFY(server.listen(QHostAddress::LocalHost));
QSslConfiguration clientSsl = QSslConfiguration::defaultConfiguration();
clientSsl.setCaCertificates({ssl.localCertificate()});
EetCaClient client({
QUrl(QStringLiteral("https://localhost:%1/api").arg(server.serverPort())),
p12Path, QByteArrayLiteral("test"), 5000, clientSsl
});
QString error;
QUuid requestId;
QVERIFY2(client.requestRenewal(&requestId, &error), qPrintable(error));
QCOMPARE(requestId, QUuid(QStringLiteral("123e4567-e89b-42d3-a456-426614174000")));
EetCaRequestStatus status;
QVERIFY2(client.requestStatus(requestId, &status, &error), qPrintable(error));
QCOMPARE(status.state, QStringLiteral("ISSUED"));
QList<QUuid> unfinished;
QVERIFY2(client.listUnfinished(&unfinished, &error), qPrintable(error));
QCOMPARE(unfinished, QList<QUuid>{requestId});
EetCaPkcs12 package;
QVERIFY2(client.claimDownload(requestId, &package, &error), qPrintable(error));
QCOMPARE(package.pkcs12, p12);
QCOMPARE(package.password, QByteArrayLiteral("ABcd1234"));
QVERIFY2(client.acknowledgeDownload(requestId, &error), qPrintable(error));
}
QTEST_MAIN(EetCaIntegrationTest)
#include "eetca_integration_test.moc"
+35
View File
@@ -0,0 +1,35 @@
#include "eetca.h"
#include <QtTest>
class EetCaTest : public QObject
{
Q_OBJECT
private slots:
void rejectsInvalidConfiguration();
void rejectsMissingSigningCertificate();
};
void EetCaTest::rejectsInvalidConfiguration()
{
EetCaClient client({QUrl(QStringLiteral("http://localhost/api")),
QStringLiteral("missing.p12"), QByteArrayLiteral("password"), 1000});
QUuid requestId;
QString error;
QVERIFY(!client.requestRenewal(&requestId, &error));
QCOMPARE(error, QStringLiteral("CA API requires a valid HTTPS base URL and positive timeout."));
}
void EetCaTest::rejectsMissingSigningCertificate()
{
EetCaClient client({QUrl(QStringLiteral("https://localhost/api")),
QStringLiteral("missing.p12"), QByteArrayLiteral("password"), 1000});
QUuid requestId;
QString error;
QVERIFY(!client.requestRenewal(&requestId, &error));
QCOMPARE(error, QStringLiteral("Cannot read the CA signing PKCS#12 file."));
}
QTEST_MAIN(EetCaTest)
#include "eetca_test.moc"
+139
View File
@@ -0,0 +1,139 @@
#include "eetv4client.h"
#include "eetv4signer.h"
#include <QFile>
#include <QProcess>
#include <QSslCertificate>
#include <QSslKey>
#include <QSslServer>
#include <QTemporaryDir>
#include <QtTest>
namespace
{
class MockEetServer : public QSslServer
{
public:
explicit MockEetServer(const QByteArray &response)
: m_response(response)
{
connect(this, &QSslServer::pendingConnectionAvailable, this, [this] {
QSslSocket *socket = qobject_cast<QSslSocket *>(nextPendingConnection());
if (socket == nullptr)
{
return;
}
connect(socket, &QSslSocket::readyRead, this, [this, socket] {
const QByteArray request = socket->readAll();
const qsizetype bodyStart = request.indexOf("\r\n\r\n");
const QByteArray soap = bodyStart < 0 ? QByteArray()
: request.mid(bodyStart + 4);
QString verificationError;
m_requestVerified = !soap.isEmpty() &&
EetV4Signer::verifySoap(soap, &verificationError);
const QByteArray http =
"HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-8\r\n"
"Content-Length: " + QByteArray::number(m_response.size()) +
"\r\nConnection: close\r\n\r\n" + m_response;
socket->write(http);
socket->disconnectFromHost();
});
});
}
bool requestVerified() const
{
return m_requestVerified;
}
private:
QByteArray m_response;
bool m_requestVerified{false};
};
}
class EetV4ClientIntegrationTest : public QObject
{
Q_OBJECT
private slots:
void sendsSignedSaleAndParsesPok();
};
void EetV4ClientIntegrationTest::sendsSignedSaleAndParsesPok()
{
QTemporaryDir directory;
QVERIFY(directory.isValid());
const QString keyPath = directory.filePath(QStringLiteral("key.pem"));
const QString certPath = directory.filePath(QStringLiteral("certificate.pem"));
const QString p12Path = directory.filePath(QStringLiteral("certificate.p12"));
auto runOpenSsl = [](const QStringList &arguments) {
QProcess process;
process.start(QStringLiteral("openssl"), arguments);
QVERIFY2(process.waitForFinished(30000), qPrintable(process.errorString()));
QVERIFY2(process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0,
process.readAllStandardError().constData());
};
runOpenSsl({QStringLiteral("req"), QStringLiteral("-x509"), QStringLiteral("-newkey"),
QStringLiteral("rsa:2048"), QStringLiteral("-nodes"), QStringLiteral("-subj"),
QStringLiteral("/CN=localhost"), QStringLiteral("-keyout"), keyPath,
QStringLiteral("-out"), certPath, QStringLiteral("-days"), QStringLiteral("1")});
runOpenSsl({QStringLiteral("pkcs12"), QStringLiteral("-export"), QStringLiteral("-out"),
p12Path, QStringLiteral("-inkey"), keyPath, QStringLiteral("-in"), certPath,
QStringLiteral("-passout"), QStringLiteral("pass:test")});
const QByteArray unsignedResponse =
"<?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=\"response-body\">"
"<Odpoved xmlns=\"http://fs.gov.cz/eet/schema/v4\">"
"<Hlavicka uuid_zpravy=\"123e4567-e89b-42d3-a456-426614174000\" "
"dat_prij=\"2026-10-13T20:00:00+02:00\"/>"
"<Potvrzeni pok=\"123e4567-e89b-42d3-a456-426614174000-01\"/>"
"</Odpoved></soap:Body></soap:Envelope>";
QString signingError;
const QByteArray signedResponse = EetV4Signer::signSoap(
unsignedResponse, p12Path, QByteArrayLiteral("test"), &signingError);
QVERIFY2(!signedResponse.isEmpty(), qPrintable(signingError));
MockEetServer server(signedResponse);
QSslConfiguration serverSsl = QSslConfiguration::defaultConfiguration();
QFile keyFile(keyPath);
QFile certificateFile(certPath);
QVERIFY(keyFile.open(QIODevice::ReadOnly));
QVERIFY(certificateFile.open(QIODevice::ReadOnly));
serverSsl.setPrivateKey(QSslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem));
serverSsl.setLocalCertificate(QSslCertificate(certificateFile.readAll(), QSsl::Pem));
server.setSslConfiguration(serverSsl);
QVERIFY(server.listen(QHostAddress::LocalHost));
QSslConfiguration clientSsl = QSslConfiguration::defaultConfiguration();
clientSsl.setCaCertificates({serverSsl.localCertificate()});
EetV4Client client({
QUrl(QStringLiteral("https://localhost:%1/eet").arg(server.serverPort())),
5000, clientSsl
});
EetV4SaleRequest request;
request.sentAt = QDateTime::currentDateTime().toOffsetFromUtc(0);
request.saleAt = request.sentAt;
request.taxpayerEic = QStringLiteral("CZ123456789");
request.unitId = 1;
request.cashRegisterId = QStringLiteral("KASA1");
request.receiptNumber = QStringLiteral("TEST-1");
QVERIFY(EetV4Amount::fromString(QStringLiteral("100.00"), &request.totalSale));
EetV4Response response;
QString error;
QVERIFY2(client.sendSale(request, p12Path, QByteArrayLiteral("test"), &response, &error),
qPrintable(error));
QVERIFY(server.requestVerified());
QVERIFY(response.isSuccess());
QCOMPARE(response.pok, QStringLiteral("123e4567-e89b-42d3-a456-426614174000-01"));
}
QTEST_MAIN(EetV4ClientIntegrationTest)
#include "eetv4client_integration_test.moc"
+33
View File
@@ -0,0 +1,33 @@
#include "eetv4client.h"
#include <QtTest>
class EetV4ClientTest : public QObject
{
Q_OBJECT
private slots:
void rejectsNonHttpsEndpoint();
void rejectsNonPositiveTimeout();
};
void EetV4ClientTest::rejectsNonHttpsEndpoint()
{
EetV4Client client({QUrl(QStringLiteral("http://localhost/eet")), 1000});
EetV4Response response;
QString error;
QVERIFY(!client.sendSale({}, QStringLiteral("missing.p12"), {}, &response, &error));
QCOMPARE(error, QStringLiteral("EET endpoint must be a valid HTTPS URL."));
}
void EetV4ClientTest::rejectsNonPositiveTimeout()
{
EetV4Client client({QUrl(QStringLiteral("https://localhost/eet")), 0});
EetV4Response response;
QString error;
QVERIFY(!client.sendSale({}, QStringLiteral("missing.p12"), {}, &response, &error));
QCOMPARE(error, QStringLiteral("EET timeout must be positive."));
}
QTEST_MAIN(EetV4ClientTest)
#include "eetv4client_test.moc"
+64
View File
@@ -0,0 +1,64 @@
#include <QTest>
#include "../libEet/eetv4request.h"
class EetV4RequestTest : public QObject
{
Q_OBJECT
private slots:
void parsesFixedPointAmounts()
{
EetV4Amount amount;
QString error;
QVERIFY(EetV4Amount::fromString(QStringLiteral("123.45"), &amount, &error));
QCOMPARE(amount.cents(), qint64(12345));
QCOMPARE(amount.toString(), QStringLiteral("123.45"));
QVERIFY(EetV4Amount::fromString(QStringLiteral("-0.01"), &amount, &error));
QCOMPARE(amount.cents(), qint64(-1));
QCOMPARE(amount.toString(), QStringLiteral("-0.01"));
}
void rejectsInvalidFixedPointAmounts()
{
EetV4Amount amount;
QString error;
QVERIFY(!EetV4Amount::fromString(QStringLiteral("1"), &amount, &error));
QVERIFY(!EetV4Amount::fromString(QStringLiteral("100000000.00"), &amount, &error));
QVERIFY(!EetV4Amount::fromString(QStringLiteral("-0.00"), &amount, &error));
}
void validatesSaleRequest()
{
EetV4SaleRequest request;
request.sentAt = QDateTime(QDate(2027, 1, 1), QTime(12, 0),
QTimeZone::fromSecondsAheadOfUtc(3600));
request.saleAt = request.sentAt;
request.taxpayerEic = QStringLiteral("CZ00000019");
request.unitId = 11;
request.cashRegisterId = QStringLiteral("1");
request.receiptNumber = QStringLiteral("123");
QStringList errors;
QVERIFY(request.validate(&errors));
QVERIFY(errors.isEmpty());
request.taxpayerEic = QStringLiteral("CZ12");
QVERIFY(!request.validate(&errors));
QVERIFY(!errors.isEmpty());
}
void formatsOffsetDateTime()
{
const QDateTime dateTime(QDate(2027, 1, 1), QTime(12, 34, 56),
QTimeZone::fromSecondsAheadOfUtc(-18000));
QCOMPARE(EetV4SaleRequest::formatDateTime(dateTime),
QStringLiteral("2027-01-01T12:34:56-05:00"));
}
};
QTEST_MAIN(EetV4RequestTest)
#include "eetv4request_test.moc"
+9
View File
@@ -0,0 +1,9 @@
QT += core testlib
CONFIG += c++11 console testcase
TEMPLATE = app
INCLUDEPATH += ../libEet
SOURCES += \
eetv4request_test.cpp \
../libEet/eetv4request.cpp
+80
View File
@@ -0,0 +1,80 @@
#include "eetv4response.h"
#include <QtTest>
class EetV4ResponseTest : public QObject
{
Q_OBJECT
private slots:
void parsesConfirmation();
void parsesError();
void parsesSoapFault();
void rejectsInvalidResponse();
};
void EetV4ResponseTest::parsesConfirmation()
{
const QByteArray soap =
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
"<soap:Body><Odpoved xmlns=\"http://fs.gov.cz/eet/schema/v4\">"
"<Hlavicka uuid_zpravy=\"123e4567-e89b-42d3-a456-426614174000\" "
"dat_prij=\"2026-10-13T20:00:00+02:00\"/>"
"<Potvrzeni pok=\"123e4567-e89b-42d3-a456-426614174000-01\" test=\"true\"/>"
"<Varovani kod_varov=\"12\"/>"
"</Odpoved></soap:Body></soap:Envelope>";
EetV4Response response;
QString error;
QVERIFY2(EetV4ResponseParser::parseSoap(soap, &response, &error), qPrintable(error));
QCOMPARE(response.status, EetV4ResponseStatus::Success);
QCOMPARE(response.pok, QStringLiteral("123e4567-e89b-42d3-a456-426614174000-01"));
QCOMPARE(response.messageUuid, QStringLiteral("123e4567-e89b-42d3-a456-426614174000"));
QVERIFY(response.test);
QCOMPARE(response.warningCodes, QList<int>{12});
QVERIFY(response.receivedAt.isValid());
}
void EetV4ResponseTest::parsesError()
{
const QByteArray soap =
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
"<soap:Body><Odpoved xmlns=\"http://fs.gov.cz/eet/schema/v4\">"
"<Hlavicka dat_odmit=\"2026-10-13T20:00:00Z\"/>"
"<Chyba kod=\"-25\">Invalid certificate</Chyba>"
"</Odpoved></soap:Body></soap:Envelope>";
EetV4Response response;
QVERIFY(EetV4ResponseParser::parseSoap(soap, &response));
QCOMPARE(response.status, EetV4ResponseStatus::Error);
QCOMPARE(response.errorCode, -25);
QCOMPARE(response.message, QStringLiteral("Invalid certificate"));
QVERIFY(response.rejectedAt.isValid());
}
void EetV4ResponseTest::parsesSoapFault()
{
const QByteArray soap =
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
"<soap:Body><soap:Fault><faultcode>soap:Server</faultcode>"
"<faultstring>Service unavailable</faultstring></soap:Fault>"
"</soap:Body></soap:Envelope>";
EetV4Response response;
QVERIFY(EetV4ResponseParser::parseSoap(soap, &response));
QCOMPARE(response.status, EetV4ResponseStatus::SoapFault);
QCOMPARE(response.message, QStringLiteral("soap:Server: Service unavailable"));
}
void EetV4ResponseTest::rejectsInvalidResponse()
{
const QByteArray soap =
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
"<soap:Body><Odpoved xmlns=\"http://fs.gov.cz/eet/schema/v4\">"
"<Hlavicka/><Potvrzeni pok=\"not-a-pok\"/>"
"</Odpoved></soap:Body></soap:Envelope>";
EetV4Response response;
QString error;
QVERIFY(!EetV4ResponseParser::parseSoap(soap, &response, &error));
QVERIFY(!error.isEmpty());
}
QTEST_MAIN(EetV4ResponseTest)
#include "eetv4response_test.moc"
+62
View File
@@ -0,0 +1,62 @@
#include "eetv4signer.h"
#include <QProcess>
#include <QTemporaryDir>
#include <QtTest>
class EetV4SignerTest : public QObject
{
Q_OBJECT
private slots:
void signsSoapWithPkcs12();
};
void EetV4SignerTest::signsSoapWithPkcs12()
{
QTemporaryDir directory;
QVERIFY(directory.isValid());
const QString keyPath = directory.filePath(QStringLiteral("key.pem"));
const QString certificatePath = directory.filePath(QStringLiteral("certificate.pem"));
const QString bundlePath = directory.filePath(QStringLiteral("certificate.p12"));
auto runOpenSsl = [](const QStringList &arguments) {
QProcess process;
process.start(QStringLiteral("openssl"), arguments);
QVERIFY2(process.waitForFinished(30000), qPrintable(process.errorString()));
QVERIFY2(process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0,
process.readAllStandardError().constData());
};
runOpenSsl({QStringLiteral("req"), QStringLiteral("-x509"), QStringLiteral("-newkey"),
QStringLiteral("rsa:2048"), QStringLiteral("-nodes"), QStringLiteral("-subj"),
QStringLiteral("/CN=EetCppTest"), QStringLiteral("-keyout"), keyPath,
QStringLiteral("-out"), certificatePath, QStringLiteral("-days"), QStringLiteral("1")});
runOpenSsl({QStringLiteral("pkcs12"), QStringLiteral("-export"), QStringLiteral("-out"),
bundlePath, QStringLiteral("-inkey"), keyPath, QStringLiteral("-in"),
certificatePath, QStringLiteral("-passout"), QStringLiteral("pass:test")});
const QByteArray unsignedSoap =
"<?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=\"body-id\"><test/></soap:Body></soap:Envelope>";
QString error;
const QByteArray signedSoap = EetV4Signer::signSoap(
unsignedSoap, bundlePath, QByteArrayLiteral("test"), &error);
QVERIFY2(!signedSoap.isEmpty(), qPrintable(error));
QVERIFY(signedSoap.contains("<ds:Signature"));
QVERIFY(signedSoap.contains("URI=\"#body-id\""));
QVERIFY(signedSoap.contains("BinarySecurityToken"));
QString verificationError;
QVERIFY2(EetV4Signer::verifySoap(signedSoap, &verificationError),
qPrintable(verificationError));
QByteArray tamperedSoap = signedSoap;
tamperedSoap.replace("<test/>", "<test>changed</test>");
QVERIFY(!EetV4Signer::verifySoap(tamperedSoap, &verificationError));
}
QTEST_MAIN(EetV4SignerTest)
#include "eetv4signer_test.moc"
+67
View File
@@ -0,0 +1,67 @@
#include <QTest>
#include <QXmlStreamReader>
#include "../libEet/eetv4soap.h"
class EetV4SoapTest : public QObject
{
Q_OBJECT
private slots:
void serializesSoapEnvelope()
{
EetV4SaleRequest request;
request.messageUuid = QUuid(QStringLiteral("550e8400-e29b-41d4-a716-446655440000"));
request.sentAt = QDateTime(QDate(2027, 1, 1), QTime(12, 0),
QTimeZone::fromSecondsAheadOfUtc(3600));
request.saleAt = request.sentAt;
request.taxpayerEic = QStringLiteral("CZ00000019");
request.unitId = 11;
request.cashRegisterId = QStringLiteral("1");
request.receiptNumber = QStringLiteral("123");
EetV4Amount::fromString(QStringLiteral("123.45"), &request.totalSale);
QString error;
const QByteArray soap = EetV4Soap::serializeUnsigned(request, QByteArrayLiteral("body-1"), &error);
QVERIFY2(!soap.isEmpty(), qPrintable(error));
QVERIFY(soap.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
QVERIFY(soap.contains("<soap:Envelope"));
QVERIFY(soap.contains("<soap:Header/>"));
QVERIFY(soap.contains("<soap:Body wsu:Id=\"body-1\">"));
QVERIFY(soap.contains("<Trzba xmlns=\"http://fs.gov.cz/eet/schema/v4\">"));
QVERIFY(soap.endsWith("</soap:Body></soap:Envelope>"));
}
void rejectsInvalidBodyId()
{
EetV4SaleRequest request;
QString error;
QVERIFY(EetV4Soap::serializeUnsigned(request, QByteArrayLiteral("invalid id"), &error).isEmpty());
QVERIFY(!error.isEmpty());
}
void producesParseableSoap()
{
EetV4SaleRequest request;
request.sentAt = QDateTime(QDate(2027, 1, 1), QTime(12, 0),
QTimeZone::fromSecondsAheadOfUtc(3600));
request.saleAt = request.sentAt;
request.taxpayerEic = QStringLiteral("CZ00000019");
request.unitId = 11;
request.cashRegisterId = QStringLiteral("1");
request.receiptNumber = QStringLiteral("123");
EetV4Amount::fromString(QStringLiteral("1.00"), &request.totalSale);
QXmlStreamReader reader(EetV4Soap::serializeUnsigned(request));
while (!reader.atEnd())
{
reader.readNext();
}
QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
}
};
QTEST_MAIN(EetV4SoapTest)
#include "eetv4soap_test.moc"
+87
View File
@@ -0,0 +1,87 @@
#include <QTest>
#include <QXmlStreamReader>
#include "../libEet/eetv4xml.h"
class EetV4XmlTest : public QObject
{
Q_OBJECT
private:
static EetV4SaleRequest validRequest()
{
EetV4SaleRequest request;
request.messageUuid = QUuid(QStringLiteral("550e8400-e29b-41d4-a716-446655440000"));
request.sentAt = QDateTime(QDate(2027, 1, 1), QTime(12, 0),
QTimeZone::fromSecondsAheadOfUtc(3600));
request.saleAt = request.sentAt;
request.taxpayerEic = QStringLiteral("CZ00000019");
request.unitId = 11;
request.cashRegisterId = QStringLiteral("1");
request.receiptNumber = QStringLiteral("123");
EetV4Amount::fromString(QStringLiteral("123.45"), &request.totalSale);
return request;
}
private slots:
void serializesRequiredAttributes()
{
QString error;
const QByteArray xml = EetV4Xml::serializeSale(validRequest(), &error);
QVERIFY2(!xml.isEmpty(), qPrintable(error));
QVERIFY(xml.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
QVERIFY(xml.contains("xmlns=\"http://fs.gov.cz/eet/schema/v4\""));
QVERIFY(xml.contains("uuid_zpravy=\"550e8400-e29b-41d4-a716-446655440000\""));
QVERIFY(xml.contains("eic_popl=\"CZ00000019\""));
QVERIFY(xml.contains("id_jednotky=\"11\""));
QVERIFY(xml.contains("celk_trzba=\"123.45\""));
QVERIFY(!xml.contains("fik"));
QVERIFY(!xml.contains("pkp"));
QVERIFY(!xml.contains("bkp"));
}
void serializesOptionalAttributes()
{
EetV4SaleRequest request = validRequest();
request.verificationMode = true;
request.authorizingTaxpayerEic = QStringLiteral("CZ00000020");
request.multipleTaxpayerAuthorization = true;
request.hasIntendedSettlement = true;
request.hasSettlement = true;
EetV4Amount::fromString(QStringLiteral("100.00"), &request.intendedSettlement);
EetV4Amount::fromString(QStringLiteral("25.00"), &request.settlement);
const QByteArray xml = EetV4Xml::serializeSale(request);
QVERIFY(xml.contains("overeni=\"true\""));
QVERIFY(xml.contains("eic_poverujiciho=\"CZ00000020\""));
QVERIFY(xml.contains("povereni_vice_popl=\"true\""));
QVERIFY(xml.contains("urceno_cerp_zuct=\"100.00\""));
QVERIFY(xml.contains("cerp_zuct=\"25.00\""));
}
void rejectsInvalidRequest()
{
EetV4SaleRequest request = validRequest();
request.taxpayerEic = QStringLiteral("invalid");
QString error;
QVERIFY(EetV4Xml::serializeSale(request, &error).isEmpty());
QVERIFY(!error.isEmpty());
}
void producesParseableXml()
{
QXmlStreamReader reader(EetV4Xml::serializeSale(validRequest()));
while (!reader.atEnd())
{
reader.readNext();
}
QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
}
};
QTEST_MAIN(EetV4XmlTest)
#include "eetv4xml_test.moc"