KD SOAP API Documentation 2.2
Loading...
Searching...
No Matches
KDSoapServerSocket.cpp
Go to the documentation of this file.
1/****************************************************************************
2**
3** This file is part of the KD Soap project.
4**
5** SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
6**
7** SPDX-License-Identifier: MIT
8**
9****************************************************************************/
10#include "KDSoapServer.h"
16#include "KDSoapSocketList_p.h"
21#include <QBuffer>
22#include <QDir>
23#include <QFile>
24#include <QFileInfo>
25#include <QMetaMethod>
26#include <QThread>
27#include <QVarLengthArray>
28
29static const char s_forbidden[] = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n";
30
32#ifndef QT_NO_SSL
33 : QSslSocket()
34 ,
35#else
36 : QTcpSocket()
37 ,
38#endif
39 m_owner(owner)
40 , m_serverObject(serverObject)
41 , m_delayedResponse(false)
42 , m_socketEnabled(true)
43 , m_receivedData(false)
44 , m_useRawXML(false)
45 , m_bytesReceived(0)
46 , m_chunkStart(0)
47{
48 connect(this, &QIODevice::readyRead, this, &KDSoapServerSocket::slotReadyRead);
49 m_doDebug = qEnvironmentVariableIsSet("KDSOAP_DEBUG");
50}
51
52// The socket is deleted when it emits disconnected() (see KDSoapSocketList::handleIncomingConnection).
54{
55 // same as m_owner->socketDeleted, but safe in case m_owner is deleted first
56 emit socketDeleted(this);
57}
58
60static HeadersMap parseHeaders(const QByteArray &headerData)
61{
62 HeadersMap headersMap;
63 QBuffer sourceBuffer;
64 sourceBuffer.setData(headerData);
65 sourceBuffer.open(QIODevice::ReadOnly);
66 // The first line is special, it's the GET or POST line
67 const QList<QByteArray> firstLine = sourceBuffer.readLine().split(' ');
68 if (firstLine.count() < 3) {
69 qDebug() << "Malformed HTTP request:" << firstLine;
70 return headersMap;
71 }
72 const QByteArray &requestType = firstLine.at(0);
73 headersMap.insert("_requestType", requestType);
74
75 // Grammar from https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1
76 // origin-form = absolute-path [ "?" query ]
77 // and https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
78 // says the path ends at the first '?' or '#' character
79 const QByteArray arg1 = firstLine.at(1);
80 const int queryPos = arg1.indexOf('?');
81 const QByteArray path = queryPos >= 0 ? arg1.left(queryPos) : arg1;
82 const QByteArray query = queryPos >= 0 ? arg1.mid(queryPos) : QByteArray();
83 // Unfortunately QDir::cleanPath works with QString
84 const QByteArray cleanedPath = QDir::cleanPath(QString::fromUtf8(path)).toUtf8();
85 headersMap.insert("_path", cleanedPath + query);
86
87 const QByteArray &httpVersion = firstLine.at(2);
88 headersMap.insert("_httpVersion", httpVersion);
89
90 while (!sourceBuffer.atEnd()) {
91 const QByteArray line = sourceBuffer.readLine();
92 const int pos = line.indexOf(':');
93 if (pos == -1) {
94 qDebug() << "Malformed HTTP header:" << line;
95 }
96 const QByteArray header = line.left(pos).toLower(); // RFC2616 section 4.2 "Field names are case-insensitive"
97 const QByteArray value = line.mid(pos + 1).trimmed(); // remove space before and \r\n after
98 // qDebug() << "HEADER" << header << "VALUE" << value;
99 headersMap.insert(header, value);
100 }
101 return headersMap;
102}
103
104// We could parse headers as we go along looking for \r\n, and stop at empty header line, to avoid all this memory copying
105// But in practice XML parsing (and writing) is far, far slower anyway.
106static bool splitHeadersAndData(const QByteArray &request, QByteArray &header, QByteArray &data)
107{
108 const int sep = request.indexOf("\r\n\r\n");
109 if (sep <= 0) {
110 return false;
111 }
112 header = request.left(sep);
113 data = request.mid(sep + 4);
114 return true;
115}
116
118{
119 if (bar.startsWith('\"') && bar.endsWith('\"')) {
120 return bar.mid(1, bar.length() - 2);
121 }
122
123 return bar;
124}
125
126static QByteArray httpResponseHeaders(bool fault, const QByteArray &contentType, int responseDataSize, QObject *serverObject)
127{
128 QByteArray httpResponse;
129 httpResponse.reserve(50);
130 if (fault) {
131 // https://www.w3.org/TR/2007/REC-soap12-part0-20070427 and look for 500
132 httpResponse += "HTTP/1.1 500 Internal Server Error\r\n";
133 } else if (responseDataSize == 0) {
134 httpResponse += "HTTP/1.1 204 No Content\r\n";
135 } else {
136 httpResponse += "HTTP/1.1 200 OK\r\n";
137 }
138
139 httpResponse += "Content-Type: ";
140 httpResponse += contentType;
141 httpResponse += "\r\nContent-Length: ";
142 httpResponse += QByteArray::number(responseDataSize);
143 httpResponse += "\r\n";
144
145 KDSoapServerObjectInterface *serverObjectInterface = qobject_cast<KDSoapServerObjectInterface *>(serverObject);
146 if (serverObjectInterface) {
147 const KDSoapServerObjectInterface::HttpResponseHeaderItems &additionalItems = serverObjectInterface->additionalHttpResponseHeaderItems();
148 for (const KDSoapServerObjectInterface::HttpResponseHeaderItem &headerItem : qAsConst(additionalItems)) {
149 httpResponse += headerItem.m_name;
150 httpResponse += ": ";
151 httpResponse += headerItem.m_value;
152 httpResponse += "\r\n";
153 }
154 }
155
156 httpResponse += "\r\n"; // end of headers
157 return httpResponse;
158}
159
160void KDSoapServerSocket::slotReadyRead()
161{
162 if (!m_socketEnabled) {
163 return;
164 }
165
166 // QNAM in Qt 5.x tends to connect additional sockets in advance and not use them
167 // So only count the sockets which actually sent us data (for the servertest unittest).
168 if (!m_receivedData) {
169 m_receivedData = true;
170 m_owner->increaseConnectionCount();
171 }
172
173 // qDebug() << this << QThread::currentThread() << "slotReadyRead!";
174
175 QByteArray buf(2048, ' ');
176 qint64 nread = -1;
177 while (nread != 0) {
178 nread = read(buf.data(), buf.size());
179 if (nread < 0) {
180 qDebug() << "Error reading from server socket:" << errorString();
181 return;
182 }
183 m_requestBuffer += buf.left(nread);
184 m_bytesReceived += nread;
185 }
186
188
189 if (m_httpHeaders.isEmpty()) {
190 // New request: see if we can parse headers
192 const bool splitOK = splitHeadersAndData(m_requestBuffer, receivedHttpHeaders, receivedData);
193 if (!splitOK) {
194 // qDebug() << "Incomplete SOAP request, wait for more data";
195 // incomplete request, wait for more data
196 return;
197 }
198 m_httpHeaders = parseHeaders(receivedHttpHeaders);
199 // Leave only the actual data in the buffer
200 m_requestBuffer = receivedData;
201 m_bytesReceived = receivedData.size();
202 m_useRawXML = false;
203 if (rawXmlInterface) {
205 serverObjectInterface->setServerSocket(this);
206 m_useRawXML = rawXmlInterface->newRequest(m_httpHeaders.value("_requestType"), m_httpHeaders);
207 }
208 }
209
210 if (m_doDebug) {
211 qDebug() << "headers:" << m_httpHeaders;
212 qDebug() << "data received:" << m_requestBuffer;
213 }
214
215 if (m_httpHeaders.value("transfer-encoding") != "chunked") {
216 if (m_useRawXML) {
217 rawXmlInterface->processXML(m_requestBuffer);
218 m_requestBuffer.clear();
219 }
220
221 const QByteArray contentLength = m_httpHeaders.value("content-length");
222 if (m_bytesReceived < contentLength.toInt()) {
223 return; // incomplete request, wait for more data
224 }
225
226 if (m_useRawXML) {
227 rawXmlInterface->endRequest();
228 } else {
229 handleRequest(m_httpHeaders, m_requestBuffer);
230 }
231 } else {
232 // qDebug() << "requestBuffer has " << m_requestBuffer.size() << "bytes, starting at" << m_chunkStart;
233 while (m_chunkStart >= 0) {
234 const int nextEOL = m_requestBuffer.indexOf("\r\n", m_chunkStart);
235 if (nextEOL == -1) {
236 return;
237 }
238 const QByteArray chunkSizeStr = m_requestBuffer.mid(m_chunkStart, nextEOL - m_chunkStart);
239 // qDebug() << m_chunkStart << nextEOL << "chunkSizeStr=" << chunkSizeStr;
240 bool ok;
241 int chunkSize = chunkSizeStr.toInt(&ok, 16);
242 if (!ok) {
243 const QByteArray badRequest = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n";
245 return;
246 }
247 if (chunkSize == 0) { // done!
248 m_requestBuffer = m_requestBuffer.mid(nextEOL);
249 m_chunkStart = -1;
250 break;
251 }
252 if (nextEOL + 2 + chunkSize + 2 >= m_requestBuffer.size()) {
253 return; // not enough data, chunk is incomplete
254 }
255 const QByteArray chunk = m_requestBuffer.mid(nextEOL + 2, chunkSize);
256 if (m_useRawXML) {
257 rawXmlInterface->processXML(chunk);
258 } else {
259 m_decodedRequestBuffer += chunk;
260 }
261 m_chunkStart = nextEOL + 2 + chunkSize + 2;
262 }
263 // We have the full data, now ensure we read trailers
264 if (!m_requestBuffer.contains("\r\n\r\n")) {
265 return;
266 }
267 if (m_useRawXML) {
268 rawXmlInterface->endRequest();
269 } else {
270 handleRequest(m_httpHeaders, m_decodedRequestBuffer);
271 }
272 m_decodedRequestBuffer.clear();
273 m_chunkStart = 0;
274 }
275 m_requestBuffer.clear();
276 m_httpHeaders.clear();
277 m_receivedData = false;
278}
279
280void KDSoapServerSocket::handleRequest(const QMap<QByteArray, QByteArray> &httpHeaders, const QByteArray &receivedData)
281{
282 const QByteArray requestType = httpHeaders.value("_requestType");
283 const QString path = QString::fromLatin1(httpHeaders.value("_path").constData());
284
285 if (!path.startsWith(QLatin1String("/"))) {
286 // denied for security reasons (ex: path starting with "..")
288 return;
289 }
290
293 const QByteArray authValue = httpHeaders.value("authorization");
294 if (!serverAuthInterface->handleHttpAuth(authValue, path)) {
295 // send auth request (Qt supports basic, ntlm and digest)
297 "HTTP/1.1 401 Authorization Required\r\nWWW-Authenticate: Basic realm=\"example\"\r\nContent-Length: 0\r\n\r\n";
299 return;
300 }
301 }
302
303 if (requestType != "GET" && requestType != "POST") {
308 return;
309 } else {
310 qWarning() << "Unknown HTTP request:" << requestType;
311 // handleError(replyMsg, "Client.Data", QString::fromLatin1("Invalid request type '%1', should be GET or
312 // POST").arg(QString::fromLatin1(requestType.constData()))); sendReply(0, replyMsg);
313 const QByteArray methodNotAllowed = "HTTP/1.1 405 Method Not Allowed\r\nAllow: GET POST\r\nContent-Length: 0\r\n\r\n";
315 return;
316 }
317 }
318
319 KDSoapServer *server = m_owner->server();
321 replyMsg.setUse(server->use());
322
325 const QString error = QString::fromLatin1("Server object %1 does not implement KDSoapServerObjectInterface!")
326 .arg(QString::fromLatin1(m_serverObject->metaObject()->className()));
327 handleError(replyMsg, "Server.ImplementationError", error);
328 sendReply(nullptr, replyMsg);
329 return;
330 } else {
331 serverObjectInterface->setServerSocket(this);
332 }
333
334 if (requestType == "GET") {
335 if (path == server->wsdlPathInUrl() && handleWsdlDownload()) {
336 return;
337 } else if (handleFileDownload(serverObjectInterface, path)) {
338 return;
339 }
340
341 // See https://www.ibm.com/developerworks/xml/library/x-tipgetr/
342 // We could implement it, but there's no SOAP request, just a query in the URL,
343 // which we'd have to pass to a different virtual than processRequest.
344 handleError(replyMsg, "Client.Data", QString::fromLatin1("Support for GET requests not implemented yet."));
345 sendReply(nullptr, replyMsg);
346 return;
347 }
348
349 // parse message
351 KDSoapHeaders requestHeaders;
355 // qDebug() << "Incomplete SOAP message, wait for more data";
356 // This should never happen, since we check for content-size above.
357 return;
358 } // TODO handle parse errors?
359
360 // check soap version and extract soapAction header
361 QByteArray soapAction;
362 const QByteArray contentType = httpHeaders.value("content-type");
363 if (contentType.startsWith("text/xml")) { // krazy:exclude=strings
364 // SOAP 1.1
365 soapAction = httpHeaders.value("soapaction");
366 // The SOAP standard allows quotation marks around the SoapAction, so we have to get rid of these.
367 soapAction = stripQuotes(soapAction);
368
369 } else if (contentType.startsWith("application/soap+xml")) { // krazy:exclude=strings
370 // SOAP 1.2
371 // Example: application/soap+xml;charset=utf-8;action=ActionHex
372 const QList<QByteArray> parts = contentType.split(';');
373 for (const QByteArray &part : qAsConst(parts)) {
374 if (part.trimmed().startsWith("action=")) { // krazy:exclude=strings
375 soapAction = stripQuotes(part.mid(part.indexOf('=') + 1));
376 }
377 }
378 }
379
380 m_method = requestMsg.name();
381
382 if (!replyMsg.isFault()) {
383 makeCall(serverObjectInterface, requestMsg, replyMsg, requestHeaders, soapAction, path);
384 }
385
386 if (serverObjectInterface && m_delayedResponse) {
387 // Delayed response. Disable the socket to make sure we don't handle another call at the same time.
388 setSocketEnabled(false);
389 } else {
391 }
392}
393
394bool KDSoapServerSocket::handleWsdlDownload()
395{
396 KDSoapServer *server = m_owner->server();
397 const QString wsdlFile = server->wsdlFile();
398 QFile wf(wsdlFile);
399 if (wf.open(QIODevice::ReadOnly)) {
400 // qDebug() << "Returning wsdl file contents";
401 const QByteArray responseText = wf.readAll();
402 const QByteArray response = httpResponseHeaders(false, "application/xml", responseText.size(), m_serverObject);
403 write(response);
405 return true;
406 }
407 return false;
408}
409
410bool KDSoapServerSocket::handleFileDownload(KDSoapServerObjectInterface *serverObjectInterface, const QString &path)
411{
413 QIODevice *device = serverObjectInterface->processFileRequest(path, contentType);
414 if (!device) {
415 const QByteArray notFound = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n";
417 return true;
418 }
419 if (!device->open(QIODevice::ReadOnly)) {
421 delete device;
422 return true; // handled!
423 }
424 const QByteArray response = httpResponseHeaders(false, contentType, device->size(), m_serverObject);
425 if (m_doDebug) {
426 qDebug() << "KDSoapServerSocket: file download response" << response;
427 }
428 qint64 written = write(response);
429 Q_ASSERT(written == response.size()); // Please report a bug if you hit this.
431
432 char block[4096] = {0};
433 // qint64 totalRead = 0;
434 while (!device->atEnd()) {
435 const qint64 in = device->read(block, sizeof(block));
436 if (in <= 0) {
437 break;
438 }
439 // totalRead += in;
440 if (in != write(block, in)) {
441 // error = true;
442 break;
443 }
444 }
445 // if (totalRead != device->size()) {
446 // // Unable to read from the source.
447 // error = true;
448 //}
449
450 delete device;
451 // TODO log the file request, if logging is enabled?
452 return true;
453}
454
455void KDSoapServerSocket::writeXML(const QByteArray &xmlResponse, bool isFault)
456{
457 const QByteArray httpHeaders = httpResponseHeaders(isFault, "text/xml", xmlResponse.size(),
458 m_serverObject); // TODO return application/soap+xml;charset=utf-8 instead for SOAP 1.2
459 if (m_doDebug) {
460 qDebug() << "KDSoapServerSocket: writing" << httpHeaders << xmlResponse;
461 }
463 if (written != httpHeaders.size()) {
464 qWarning() << "Only wrote" << written << "out of" << httpHeaders.size() << "bytes of HTTP headers. Error:" << errorString();
465 }
467 if (written != xmlResponse.size()) {
468 qWarning() << "Only wrote" << written << "out of" << xmlResponse.size() << "bytes of response. Error:" << errorString();
469 }
470}
471
473{
474 const bool isFault = replyMsg.isFault();
475
477 if (!replyMsg.isNil()) {
479 // Note that the kdsoap client parsing code doesn't care for the name (except if it's fault), even in
480 // Document mode. Other implementations do, though.
481 QString responseName = isFault ? QString::fromLatin1("Fault") : replyMsg.name();
482 if (responseName.isEmpty()) {
483 responseName = m_method;
484 }
485 QString responseNamespace = m_messageNamespace;
486 KDSoapHeaders responseHeaders;
488 responseHeaders = serverObjectInterface->responseHeaders();
489 if (!serverObjectInterface->responseNamespace().isEmpty()) {
490 responseNamespace = serverObjectInterface->responseNamespace();
491 }
492 }
493 msgWriter.setMessageNamespace(responseNamespace);
494 xmlResponse = msgWriter.messageToXml(replyMsg, responseName, responseHeaders, QMap<QString, KDSoapMessage>());
495 }
496
497 writeXML(xmlResponse, isFault);
498
499 // All done, check if we should log this
500 KDSoapServer *server = m_owner->server();
501 const KDSoapServer::LogLevel logLevel =
502 server->logLevel(); // we do this here in order to support dynamic settings changes (at the price of a mutex)
503 if (logLevel != KDSoapServer::LogNothing) {
504 if (logLevel == KDSoapServer::LogEveryCall || (logLevel == KDSoapServer::LogFaults && isFault)) {
505
506 if (isFault) {
507 server->log("FAULT " + m_method.toLatin1() + " -- " + replyMsg.faultAsString().toUtf8() + '\n');
508 } else {
509 server->log("CALL " + m_method.toLatin1() + '\n');
510 }
511 }
512 }
513}
514
516{
518 m_delayedResponse = false;
519 setSocketEnabled(true);
520}
521
523{
524 m_delayedResponse = true;
525}
526
527void KDSoapServerSocket::handleError(KDSoapMessage &replyMsg, const char *errorCode, const QString &error)
528{
529 qWarning("%s", qPrintable(error));
530 const KDSoap::SoapVersion soapVersion = KDSoap::SOAP1_1; // TODO version selection on the server side
531 replyMsg.createFaultMessage(QString::fromLatin1(errorCode), error, soapVersion);
532}
533
534void KDSoapServerSocket::makeCall(KDSoapServerObjectInterface *serverObjectInterface, const KDSoapMessage &requestMsg, KDSoapMessage &replyMsg,
535 const KDSoapHeaders &requestHeaders, const QByteArray &soapAction, const QString &path)
536{
538
539 if (requestMsg.isFault()) {
540 // Can this happen? Getting a fault as a request !? Doesn't make sense...
541 // reply with a fault, but we don't even know what main element name to use
542 // Oh well, just use the incoming fault :-)
544 handleError(replyMsg, "Client.Data", QString::fromLatin1("Request was a fault"));
545 } else {
546
547 // Call method on m_serverObject
548 serverObjectInterface->setRequestHeaders(requestHeaders, soapAction);
549
550 KDSoapServer *server = m_owner->server();
551 if (path != server->path()) {
552 serverObjectInterface->processRequestWithPath(requestMsg, replyMsg, soapAction, path);
553 } else {
554 serverObjectInterface->processRequest(requestMsg, replyMsg, soapAction);
555 }
556 if (serverObjectInterface->hasFault()) {
557 // qDebug() << "Got fault!";
558 replyMsg.setFault(true);
559 serverObjectInterface->storeFaultAttributes(replyMsg);
560 }
561 }
562}
563
564// Prevention against concurrent requests without waiting for a (delayed) reply,
565// but untestable with QNAM on the client side, since it doesn't do that.
566void KDSoapServerSocket::setSocketEnabled(bool enabled)
567{
568 if (m_socketEnabled == enabled) {
569 return;
570 }
571
572 m_socketEnabled = enabled;
573 if (enabled) {
574 slotReadyRead();
575 }
576}
577
578#include "moc_KDSoapServerSocket_p.cpp"
static const char s_forbidden[]
static QByteArray httpResponseHeaders(bool fault, const QByteArray &contentType, int responseDataSize, QObject *serverObject)
static HeadersMap parseHeaders(const QByteArray &headerData)
QMap< QByteArray, QByteArray > HeadersMap
static QByteArray stripQuotes(const QByteArray &bar)
static bool splitHeadersAndData(const QByteArray &request, QByteArray &header, QByteArray &data)
XmlError xmlToMessage(const QByteArray &data, KDSoapMessage *pParsedMessage, QString *pMessageNamespace, KDSoapHeaders *pRequestHeaders, KDSoap::SoapVersion soapVersion) const
void setUse(Use use)
virtual HttpResponseHeaderItems additionalHttpResponseHeaderItems() const
void sendDelayedReply(KDSoapServerObjectInterface *serverObjectInterface, const KDSoapMessage &replyMsg)
void socketDeleted(KDSoapServerSocket *)
KDSoapServerSocket(KDSoapSocketList *owner, QObject *serverObject)
void sendReply(KDSoapServerObjectInterface *serverObjectInterface, const KDSoapMessage &replyMsg)
KDSoapMessage::Use use() const
QString wsdlFile() const
QString wsdlPathInUrl() const
LogLevel logLevel() const
QString path() const
KDSoapServer * server() const
QAbstractSocket::SocketError error() const const
virtual bool atEnd() const const override
virtual bool open(QIODevice::OpenMode flags) override
void setData(const QByteArray &data)
void clear()
bool contains(char ch) const const
bool endsWith(const QByteArray &ba) const const
int indexOf(char ch, int from) const const
QByteArray left(int len) const const
int length() const const
QByteArray mid(int pos, int len) const const
QByteArray number(int n, int base)
void reserve(int size)
int size() const const
bool startsWith(const QByteArray &ba) const const
int toInt(bool *ok, int base) const const
QByteArray toLower() const const
QByteArray trimmed() const const
QString cleanPath(const QString &path)
virtual bool atEnd() const const
QString errorString() const const
virtual bool open(QIODevice::OpenMode mode)
qint64 read(char *data, qint64 maxSize)
qint64 readLine(char *data, qint64 maxSize)
void readyRead()
virtual qint64 size() const const
qint64 write(const char *data, qint64 maxSize)
const T & at(int i) const const
int count(const T &value) const const
void clear()
QMap::iterator insert(const Key &key, const T &value)
bool isEmpty() const const
const T value(const Key &key, const T &defaultValue) const const
const char * className() const const
virtual const QMetaObject * metaObject() const const
T qobject_cast(QObject *object)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString fromLatin1(const char *str, int size)
QString fromUtf8(const char *str, int size)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
QByteArray toUtf8() const const

© Klarälvdalens Datakonsult AB (KDAB)
"The Qt, C++ and OpenGL Experts"
https://www.kdab.com/
https://www.kdab.com/development-resources/qt-tools/kd-soap/
Generated on Sat Apr 20 2024 00:04:25 for KD SOAP API Documentation by doxygen 1.9.8