rewrite for eet 2.0 api version v4
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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")
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
#include "eetcpp.h"
|
||||
|
||||
|
||||
EetCpp::EetCpp()
|
||||
{
|
||||
}
|
||||
+7
-9
@@ -3,14 +3,12 @@
|
||||
|
||||
#include "eetcpp_global.h"
|
||||
|
||||
#include "eetrequest.h"
|
||||
#include "eetsender.h"
|
||||
|
||||
class EETCPPSHARED_EXPORT EetCpp
|
||||
{
|
||||
|
||||
public:
|
||||
EetCpp();
|
||||
};
|
||||
#include "eetv4request.h"
|
||||
#include "eetv4xml.h"
|
||||
#include "eetv4soap.h"
|
||||
#include "eetv4signer.h"
|
||||
#include "eetv4response.h"
|
||||
#include "eetv4client.h"
|
||||
#include "eetca.h"
|
||||
|
||||
#endif // EETCPP_H
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,9 @@
|
||||
add_executable(EetCppPlayground
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(EetCppPlayground
|
||||
PRIVATE
|
||||
EetCpp::EetCpp
|
||||
Qt6::Core
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
playground="${EETCPP_PLAYGROUND:-EetCppPlayground}"
|
||||
exec "$playground" "$@"
|
||||
@@ -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
|
||||
Executable
+12
@@ -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
|
||||
@@ -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
|
||||
Executable
+24
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,9 @@
|
||||
QT += core testlib
|
||||
CONFIG += c++11 console testcase
|
||||
TEMPLATE = app
|
||||
|
||||
INCLUDEPATH += ../libEet
|
||||
|
||||
SOURCES += \
|
||||
eetv4request_test.cpp \
|
||||
../libEet/eetv4request.cpp
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user