Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ SPDX-PackageDownloadLocation = "https://www.github.com/KDAB/KDSoap"

#misc source code
[[annotations]]
path = ["unittests/**/**.xsd", "unittests/**/**.wsdl", "unittests/**/**.XSD", "unittests/**/**.xml", "testtools/**.qrc", "examples/**/**.qrc", "examples/**/**.wsdl", "**.html", "**.css", "docs/manual/**.pdf", "**.bat", "**.pem"]
path = ["unittests/**/**.xsd", "unittests/**/**.wsdl", "unittests/**/**.XSD", "unittests/**/**.xml", "unittests/**/**.qrc", "testtools/**.qrc", "examples/**/**.qrc", "examples/**/**.wsdl", "**.html", "**.css", "docs/manual/**.pdf", "**.bat", "**.pem"]
precedence = "aggregate"
SPDX-FileCopyrightText = "Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>"
SPDX-License-Identifier = "MIT"
Expand Down
177 changes: 177 additions & 0 deletions src/KDSoapServer/ByteArrayViewSplitter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/****************************************************************************
**
** This file is part of the KD Soap project.
**
** SPDX-FileCopyrightText: 2025 Jonathan Brady <jtjbrady@users.noreply.github.com>
**
** SPDX-License-Identifier: MIT
**
****************************************************************************/

#ifndef BYTEARRAYVIEWSPLITTER_H
#define BYTEARRAYVIEWSPLITTER_H

#include <QByteArrayView>
#include <QDebug>
#include <iterator>
#include <type_traits>
#if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L
#include <ranges>
#endif

// A zero-copy byte-splitting view over a QByteArrayView.
// Splits by a character or QByteArrayView delimiter, with optional skipping of empty parts.
template<typename Delim>
class ByteArrayViewSplitter
#if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L
: public std::ranges::view_interface<ByteArrayViewSplitter<Delim>>
#endif
{
using DelimType = std::decay_t<Delim>;

// Enforce supported delimiter types: byte values or QByteArrayView
static_assert(
std::is_same_v<DelimType, char> || std::is_same_v<DelimType, signed char> || std::is_same_v<DelimType, unsigned char> || std::is_same_v<DelimType, std::byte> || std::is_same_v<DelimType, QByteArrayView>,
"Delimiter must be a valid QByteArrayView byte type or QByteArrayView itself");

public:
ByteArrayViewSplitter(QByteArrayView origin, Delim delim, Qt::SplitBehavior skipEmpty = Qt::SplitBehaviorFlags::KeepEmptyParts)
: m_origin(origin)
, m_delim(delim)
, m_skipEmpty(skipEmpty == Qt::SplitBehaviorFlags::SkipEmptyParts)
{
}

class iterator
{
// Returns 1 for char-like delimiters, or size() for QByteArrayView delimiters
static constexpr qsizetype computeDelimSize(const DelimType &delim)
{
if constexpr (std::is_same_v<DelimType, QByteArrayView>)
return delim.size();
else
return 1;
}

public:
using value_type = QByteArrayView;
using difference_type = std::ptrdiff_t;
using iterator_category = std::forward_iterator_tag;

iterator() = default;

iterator(QByteArrayView origin, Delim delim, qsizetype offset, bool skipEmpty)
: m_origin(origin)
, m_delim(delim)
, m_delimSize(computeDelimSize(delim))
, m_offset(offset)
, m_skipEmpty(skipEmpty)
{
if (m_offset < 0) {
// Marks end iterator
m_offset = m_nextOffset = -1;
} else {
// Start at offset, advance once to initialize m_nextOffset
m_nextOffset = m_offset;
do {
advance();
} while (m_skipEmpty && m_offset >= 0 && m_offset + m_delimSize == m_nextOffset); // Skip empty parts
}
}

// Returns the current part
QByteArrayView operator*() const
{
if (m_offset < 0)
return {};

const char *start = m_origin.data() + m_offset;
const char *end = m_origin.data() + m_nextOffset - m_delimSize;

return QByteArrayView(start, end - start);
}

// Advance to the next part
iterator &operator++()
{
do {
advance();
} while (m_skipEmpty && m_offset >= 0 && m_offset + m_delimSize == m_nextOffset); // Skip empty parts

return *this;
}

#if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L
// C++20 sentinel-based end comparison
bool operator==(std::default_sentinel_t) const
{
return m_offset < 0;
}
#endif

// Equality comparison for pre-C++20 use
bool operator==(const iterator &other) const
{
return m_offset == other.m_offset && m_origin.data() == other.m_origin.data() && m_origin.size() == other.m_origin.size();
}

bool operator!=(const iterator &other) const
{
return !(*this == other);
}

private:
// Advances m_offset and m_nextOffset to the next segment
void advance()
{
m_offset = m_nextOffset;

if (m_offset > m_origin.size()) {
m_offset = -1; // Marks end
return;
}

// Find next delimiter
const qsizetype delimPos = m_origin.indexOf(m_delim, m_offset);
if (delimPos != -1) {
m_nextOffset = delimPos + m_delimSize;
} else {
// No more delimiters: consume rest of input
m_nextOffset = m_origin.size() + m_delimSize;
}
}

const QByteArrayView m_origin;
const DelimType m_delim;
const qsizetype m_delimSize;
qsizetype m_offset = 0; // Start of current part
qsizetype m_nextOffset = 0; // End of current part (includes delimiter)
const bool m_skipEmpty = false;
};

iterator begin() const
{
return iterator {m_origin, m_delim, qsizetype(0), m_skipEmpty};
}

#if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L
// C++20 sentinel-based end
std::default_sentinel_t end() const noexcept
{
return {};
}
#else
// Classic iterator-based end
iterator end() const noexcept
{
return iterator {m_origin, m_delim, qsizetype(-1), m_skipEmpty};
}
#endif

private:
const QByteArrayView m_origin;
const DelimType m_delim;
const bool m_skipEmpty;
};

#endif
165 changes: 165 additions & 0 deletions src/KDSoapServer/KDSoapServerObjectInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@
#include "KDSoapServerObjectInterface.h"
#include "KDSoapClient/KDSoapValue.h"
#include "KDSoapServerSocket_p.h"
#include <QBuffer>
#include <QDebug>
#include <QFileInfo>
#include <QMetaEnum>
#include <QPointer>
#include <QResource>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include "ByteArrayViewSplitter.h"
#endif

class KDSoapServerObjectInterface::Private
{
Expand All @@ -33,6 +40,7 @@ class KDSoapServerObjectInterface::Private
KDSoap::SoapVersion m_requestVersion = KDSoap::SoapVersion::SOAP1_1;
// QPointer in case the client disconnects during a delayed response
QPointer<KDSoapServerSocket> m_serverSocket;
QMap<QByteArray, QByteArray> m_httpHeaders;
};

KDSoapServerObjectInterface::HttpResponseHeaderItem::HttpResponseHeaderItem(const QByteArray &name, const QByteArray &value)
Expand Down Expand Up @@ -183,6 +191,7 @@ KDSoapDelayedResponseHandle KDSoapServerObjectInterface::prepareDelayedResponse(
void KDSoapServerObjectInterface::setServerSocket(KDSoapServerSocket *serverSocket)
{
d->m_serverSocket = serverSocket;
d->m_httpHeaders = d->m_serverSocket->m_httpHeaders;
}

void KDSoapServerObjectInterface::sendDelayedResponse(const KDSoapDelayedResponseHandle &responseHandle, const KDSoapMessage &response)
Expand All @@ -193,6 +202,161 @@ void KDSoapServerObjectInterface::sendDelayedResponse(const KDSoapDelayedRespons
}
}

namespace {
// Parses an Accept-Encoding header into a bitmask of EncodingFormat values.
//
// See RFC 9110 §12.5.3: Accept-Encoding
// Recognizes q-values (q=0 means "not acceptable") and wildcard "*" entries.
//
// Accept-Encoding: gzip, deflate, br, identity;q=0
// Accept-Encoding: *, identity;q=0 → means "anything except identity"
// Accept-Encoding: *;q=0, gzip → means "anything except gzip"
KDSoapServerSocket::EncodingFormats parseAcceptEncoding(const QByteArray &headerValue)
{
KDSoapServerSocket::EncodingFormats formats; // Accepted formats
KDSoapServerSocket::EncodingFormats rejectedFormats; // Explicitly rejected formats (q=0)

const QMetaEnum metaEnum = QMetaEnum::fromType<KDSoapServerSocket::EncodingFormats>();

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
using PartType = QByteArrayView;
const auto parts = ByteArrayViewSplitter(headerValue, ',', Qt::SkipEmptyParts);
#else
using PartType = QByteArray;
const auto parts = headerValue.split(',');
#endif

for (const auto &rawPart : parts) {
const auto part = rawPart.trimmed();
if (part.isEmpty())
continue;

// Per RFC 9110 §12.5.3, we parse each `token[;q=value]`
const int semiPos = part.indexOf(';');
const auto encoding = semiPos >= 0 ? part.left(semiPos).trimmed() : part;
const auto params = semiPos >= 0 ? part.mid(semiPos + 1).trimmed() : PartType();

// Parse q= parameter (if present) looking for q=0 (rejection)
bool rejected = false;
if (params.startsWith("q=")) {
bool ok = false;
const float qVal = params.mid(2).trimmed().toFloat(&ok);
if (ok && qVal == 0.0f) {
rejected = true;
}
}

// Handle wildcard "*" — add all known formats not explicitly rejected
if (encoding == "*") {
if (!rejected) {
// Accept all known formats (per our enum) except already rejected ones
for (int i = 0; i < metaEnum.keyCount(); ++i) {
const auto val = static_cast<KDSoapServerSocket::EncodingFormat>(metaEnum.value(i));
if (!rejectedFormats.testFlag(val))
formats |= val;
}
} else if (!formats.testFlag(KDSoapServerSocket::EncodingFormat::identity)) {
// Reject all formats not specifically mentioned, including identity
rejectedFormats |= KDSoapServerSocket::EncodingFormat::identity;
qInfo() << __LINE__ << formats << rejectedFormats;
}
continue;
}

// Normalize encoding to lowercase and look up enum value
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const int value = metaEnum.keyToValue(QByteArray(encoding.constData(), encoding.size()).toLower().constData());
#else
const int value = metaEnum.keyToValue(encoding.toLower().constData());
#endif
if (value >= 0) {
const auto format = static_cast<KDSoapServerSocket::EncodingFormats>(value);

if (rejected) {
// Remove from accepted in case it was added earlier (e.g. by *) and add to rejected
formats &= ~format;
rejectedFormats |= format;
} else {
// Accept format and remove from rejected
formats |= format;
rejectedFormats &= ~format;
}
}
// Unknown encodings are ignored per RFC 9110 §12.5.3
}

// Ensure identity is accepted unless explicitly rejected
if (!rejectedFormats.testFlag(KDSoapServerSocket::EncodingFormat::identity)) {
formats |= KDSoapServerSocket::EncodingFormat::identity;
}

return formats;
}

// Convert QResource::Compression enum to our HTTP encoding enum
KDSoapServerSocket::EncodingFormat compressionToEncodingFormat(QResource::Compression c)
{
switch (c) {
case QResource::NoCompression:
return KDSoapServerSocket::EncodingFormat::identity;
case QResource::ZlibCompression:
// Qt uses zlib internally, HTTP calls this "deflate" (RFC 7231 §4.2.2)
return KDSoapServerSocket::EncodingFormat::deflate;
case QResource::ZstdCompression:
// zstd
return KDSoapServerSocket::EncodingFormat::zstd;
}
return KDSoapServerSocket::EncodingFormat::identity;
}
}

std::pair<QIODevice *, QByteArray> KDSoapServerObjectInterface::fileForEncoding(const QString &logicalPath)
{
QFileInfo fileInfo(logicalPath);
if (!fileInfo.exists()) {
qWarning() << "File does not exist:" << logicalPath;
return {nullptr, QByteArrayLiteral("HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n")};
}

const QString resolvedPath = fileInfo.absoluteFilePath();
const auto acceptedEncodings = parseAcceptEncoding(d->m_httpHeaders.value("accept-encoding"));

#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
// Attempt to interpret as a Qt resource
if (QResource res(resolvedPath); res.isValid()) {
KDSoapServerSocket::EncodingFormat resourceCompression = compressionToEncodingFormat(res.compressionAlgorithm());

// If resource is compressed and the format is accepted
if (resourceCompression != KDSoapServerSocket::EncodingFormat::identity && acceptedEncodings.testFlag(resourceCompression)) {
QBuffer *buffer = new QBuffer;
// Per https://doc.qt.io/archives/qt-5.13/qresource.html and
// https://doc.qt.io/archives/qt-5.13/qbytearray.html#qUncompress
// the resource has a 4 byte header which contains the big endian uncompressed size.
buffer->setData(reinterpret_cast<const char *>(res.data() + 4), res.size() - 4);

// Return compressed content with Content-Encoding
const auto metaEnum = QMetaEnum::fromType<KDSoapServerSocket::EncodingFormats>();
return {buffer, QByteArray::fromRawData(metaEnum.valueToKey(resourceCompression), ::qstrlen(metaEnum.valueToKey(resourceCompression)))};
}
}
#endif

// At this point:
// - Either it's not a Qt resource, or
// - It's a resource but compression was not accepted

// RFC 9110 §8.4.4: "If the identity encoding is not acceptable, the origin server SHOULD send a 406 (Not Acceptable) response."
if (!acceptedEncodings.testFlag(KDSoapServerSocket::EncodingFormat::identity)) {
qWarning() << "Uncompressed file rejected by Accept-Encoding:" << resolvedPath;
return {nullptr, QByteArrayLiteral("HTTP/1.1 406 Not Acceptable\r\nContent-Length: 0\r\n\r\n")};
}

// Fall back to serving uncompressed version via QFile
QFile *file = new QFile(resolvedPath);

return {file, QByteArray()};
}

void KDSoapServerObjectInterface::writeHTTP(const QByteArray &httpReply)
{
const qint64 written = d->m_serverSocket->write(httpReply);
Expand All @@ -210,6 +374,7 @@ void KDSoapServerObjectInterface::copyFrom(KDSoapServerObjectInterface *other)
d->m_requestHeaders = other->d->m_requestHeaders;
d->m_soapAction = other->d->m_soapAction;
d->m_serverSocket = other->d->m_serverSocket;
d->m_httpHeaders = other->d->m_httpHeaders;
d->m_requestVersion = other->d->m_requestVersion;
}

Expand Down
Loading
Loading