1010#include " KDSoapServerObjectInterface.h"
1111#include " KDSoapClient/KDSoapValue.h"
1212#include " KDSoapServerSocket_p.h"
13+ #include < QBuffer>
1314#include < QDebug>
15+ #include < QFileInfo>
16+ #include < QMetaEnum>
1417#include < QPointer>
18+ #include < QResource>
19+ #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
20+ #include " ByteArrayViewSplitter.h"
21+ #endif
1522
1623class KDSoapServerObjectInterface ::Private
1724{
@@ -33,6 +40,7 @@ class KDSoapServerObjectInterface::Private
3340 KDSoap::SoapVersion m_requestVersion = KDSoap::SoapVersion::SOAP1_1 ;
3441 // QPointer in case the client disconnects during a delayed response
3542 QPointer<KDSoapServerSocket> m_serverSocket;
43+ QMap<QByteArray, QByteArray> m_httpHeaders;
3644};
3745
3846KDSoapServerObjectInterface::HttpResponseHeaderItem::HttpResponseHeaderItem (const QByteArray &name, const QByteArray &value)
@@ -183,6 +191,7 @@ KDSoapDelayedResponseHandle KDSoapServerObjectInterface::prepareDelayedResponse(
183191void KDSoapServerObjectInterface::setServerSocket (KDSoapServerSocket *serverSocket)
184192{
185193 d->m_serverSocket = serverSocket;
194+ d->m_httpHeaders = d->m_serverSocket ->m_httpHeaders ;
186195}
187196
188197void KDSoapServerObjectInterface::sendDelayedResponse (const KDSoapDelayedResponseHandle &responseHandle, const KDSoapMessage &response)
@@ -193,6 +202,161 @@ void KDSoapServerObjectInterface::sendDelayedResponse(const KDSoapDelayedRespons
193202 }
194203}
195204
205+ namespace {
206+ // Parses an Accept-Encoding header into a bitmask of EncodingFormat values.
207+ //
208+ // See RFC 9110 §12.5.3: Accept-Encoding
209+ // Recognizes q-values (q=0 means "not acceptable") and wildcard "*" entries.
210+ //
211+ // Accept-Encoding: gzip, deflate, br, identity;q=0
212+ // Accept-Encoding: *, identity;q=0 → means "anything except identity"
213+ // Accept-Encoding: *;q=0, gzip → means "anything except gzip"
214+ KDSoapServerSocket::EncodingFormats parseAcceptEncoding (const QByteArray &headerValue)
215+ {
216+ KDSoapServerSocket::EncodingFormats formats; // Accepted formats
217+ KDSoapServerSocket::EncodingFormats rejectedFormats; // Explicitly rejected formats (q=0)
218+
219+ const QMetaEnum metaEnum = QMetaEnum::fromType<KDSoapServerSocket::EncodingFormats>();
220+
221+ #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
222+ using PartType = QByteArrayView;
223+ const auto parts = ByteArrayViewSplitter (headerValue, ' ,' , Qt::SkipEmptyParts);
224+ #else
225+ using PartType = QByteArray;
226+ const auto parts = headerValue.split (' ,' );
227+ #endif
228+
229+ for (const auto &rawPart : parts) {
230+ const auto part = rawPart.trimmed ();
231+ if (part.isEmpty ())
232+ continue ;
233+
234+ // Per RFC 9110 §12.5.3, we parse each `token[;q=value]`
235+ const int semiPos = part.indexOf (' ;' );
236+ const auto encoding = semiPos >= 0 ? part.left (semiPos).trimmed () : part;
237+ const auto params = semiPos >= 0 ? part.mid (semiPos + 1 ).trimmed () : PartType ();
238+
239+ // Parse q= parameter (if present) looking for q=0 (rejection)
240+ bool rejected = false ;
241+ if (params.startsWith (" q=" )) {
242+ bool ok = false ;
243+ const float qVal = params.mid (2 ).trimmed ().toFloat (&ok);
244+ if (ok && qVal == 0 .0f ) {
245+ rejected = true ;
246+ }
247+ }
248+
249+ // Handle wildcard "*" — add all known formats not explicitly rejected
250+ if (encoding == " *" ) {
251+ if (!rejected) {
252+ // Accept all known formats (per our enum) except already rejected ones
253+ for (int i = 0 ; i < metaEnum.keyCount (); ++i) {
254+ const auto val = static_cast <KDSoapServerSocket::EncodingFormat>(metaEnum.value (i));
255+ if (!rejectedFormats.testFlag (val))
256+ formats |= val;
257+ }
258+ } else if (!formats.testFlag (KDSoapServerSocket::EncodingFormat::identity)) {
259+ // Reject all formats not specifically mentioned, including identity
260+ rejectedFormats |= KDSoapServerSocket::EncodingFormat::identity;
261+ qInfo () << __LINE__ << formats << rejectedFormats;
262+ }
263+ continue ;
264+ }
265+
266+ // Normalize encoding to lowercase and look up enum value
267+ #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
268+ const int value = metaEnum.keyToValue (QByteArray (encoding.constData (), encoding.size ()).toLower ().constData ());
269+ #else
270+ const int value = metaEnum.keyToValue (encoding.toLower ().constData ());
271+ #endif
272+ if (value >= 0 ) {
273+ const auto format = static_cast <KDSoapServerSocket::EncodingFormats>(value);
274+
275+ if (rejected) {
276+ // Remove from accepted in case it was added earlier (e.g. by *) and add to rejected
277+ formats &= ~format;
278+ rejectedFormats |= format;
279+ } else {
280+ // Accept format and remove from rejected
281+ formats |= format;
282+ rejectedFormats &= ~format;
283+ }
284+ }
285+ // Unknown encodings are ignored per RFC 9110 §12.5.3
286+ }
287+
288+ // Ensure identity is accepted unless explicitly rejected
289+ if (!rejectedFormats.testFlag (KDSoapServerSocket::EncodingFormat::identity)) {
290+ formats |= KDSoapServerSocket::EncodingFormat::identity;
291+ }
292+
293+ return formats;
294+ }
295+
296+ // Convert QResource::Compression enum to our HTTP encoding enum
297+ KDSoapServerSocket::EncodingFormat compressionToEncodingFormat (QResource::Compression c)
298+ {
299+ switch (c) {
300+ case QResource::NoCompression:
301+ return KDSoapServerSocket::EncodingFormat::identity;
302+ case QResource::ZlibCompression:
303+ // Qt uses zlib internally, HTTP calls this "deflate" (RFC 7231 §4.2.2)
304+ return KDSoapServerSocket::EncodingFormat::deflate;
305+ case QResource::ZstdCompression:
306+ // zstd
307+ return KDSoapServerSocket::EncodingFormat::zstd;
308+ }
309+ return KDSoapServerSocket::EncodingFormat::identity;
310+ }
311+ }
312+
313+ std::pair<QIODevice *, QByteArray> KDSoapServerObjectInterface::fileForEncoding (const QString &logicalPath)
314+ {
315+ QFileInfo fileInfo (logicalPath);
316+ if (!fileInfo.exists ()) {
317+ qWarning () << " File does not exist:" << logicalPath;
318+ return {nullptr , QByteArrayLiteral (" HTTP/1.1 404 Not Found\r\n Content-Length: 0\r\n\r\n " )};
319+ }
320+
321+ const QString resolvedPath = fileInfo.absoluteFilePath ();
322+ const auto acceptedEncodings = parseAcceptEncoding (d->m_httpHeaders .value (" accept-encoding" ));
323+
324+ #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
325+ // Attempt to interpret as a Qt resource
326+ if (QResource res (resolvedPath); res.isValid ()) {
327+ KDSoapServerSocket::EncodingFormat resourceCompression = compressionToEncodingFormat (res.compressionAlgorithm ());
328+
329+ // If resource is compressed and the format is accepted
330+ if (resourceCompression != KDSoapServerSocket::EncodingFormat::identity && acceptedEncodings.testFlag (resourceCompression)) {
331+ QBuffer *buffer = new QBuffer;
332+ // Per https://doc.qt.io/archives/qt-5.13/qresource.html and
333+ // https://doc.qt.io/archives/qt-5.13/qbytearray.html#qUncompress
334+ // the resource has a 4 byte header which contains the big endian uncompressed size.
335+ buffer->setData (reinterpret_cast <const char *>(res.data () + 4 ), res.size () - 4 );
336+
337+ // Return compressed content with Content-Encoding
338+ const auto metaEnum = QMetaEnum::fromType<KDSoapServerSocket::EncodingFormats>();
339+ return {buffer, QByteArray::fromRawData (metaEnum.valueToKey (resourceCompression), ::qstrlen (metaEnum.valueToKey (resourceCompression)))};
340+ }
341+ }
342+ #endif
343+
344+ // At this point:
345+ // - Either it's not a Qt resource, or
346+ // - It's a resource but compression was not accepted
347+
348+ // RFC 9110 §8.4.4: "If the identity encoding is not acceptable, the origin server SHOULD send a 406 (Not Acceptable) response."
349+ if (!acceptedEncodings.testFlag (KDSoapServerSocket::EncodingFormat::identity)) {
350+ qWarning () << " Uncompressed file rejected by Accept-Encoding:" << resolvedPath;
351+ return {nullptr , QByteArrayLiteral (" HTTP/1.1 406 Not Acceptable\r\n Content-Length: 0\r\n\r\n " )};
352+ }
353+
354+ // Fall back to serving uncompressed version via QFile
355+ QFile *file = new QFile (resolvedPath);
356+
357+ return {file, QByteArray ()};
358+ }
359+
196360void KDSoapServerObjectInterface::writeHTTP (const QByteArray &httpReply)
197361{
198362 const qint64 written = d->m_serverSocket ->write (httpReply);
@@ -210,6 +374,7 @@ void KDSoapServerObjectInterface::copyFrom(KDSoapServerObjectInterface *other)
210374 d->m_requestHeaders = other->d ->m_requestHeaders ;
211375 d->m_soapAction = other->d ->m_soapAction ;
212376 d->m_serverSocket = other->d ->m_serverSocket ;
377+ d->m_httpHeaders = other->d ->m_httpHeaders ;
213378 d->m_requestVersion = other->d ->m_requestVersion ;
214379}
215380
0 commit comments