1 /****************************************************************************
2 ** Copyright (C) 2010-2020 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com.
3 ** All rights reserved.
4 **
5 ** This file is part of the KD Soap library.
6 **
7 ** Licensees holding valid commercial KD Soap licenses may use this file in
8 ** accordance with the KD Soap Commercial License Agreement provided with
9 ** the Software.
10 **
11 **
12 ** This file may be distributed and/or modified under the terms of the
13 ** GNU Lesser General Public License version 2.1 and version 3 as published by the
14 ** Free Software Foundation and appearing in the file LICENSE.LGPL.txt included.
15 **
16 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
17 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
18 **
19 ** Contact info@kdab.com if any conditions of this licensing are not
20 ** clear to you.
21 **
22 **********************************************************************/
23
24 #include "httpserver_p.h"
25 #include <QNetworkRequest>
26 #include <QNetworkReply>
27 #include <QNetworkAccessManager>
28 #include <QDomDocument>
29 #include <QDateTime>
30 #include <QEventLoop>
31 #include <QFile>
32 #ifndef QT_NO_OPENSSL
33 #include <QSslConfiguration>
34 #endif
35
36 // Helper for xmlBufferCompare
textBufferCompare(const QByteArray & source,const QByteArray & dest,QIODevice & sourceFile,QIODevice & destFile)37 static bool textBufferCompare(
38 const QByteArray &source, const QByteArray &dest, // for the qDebug only
39 QIODevice &sourceFile, QIODevice &destFile)
40 {
41 int lineNumber = 1;
42 while (!sourceFile.atEnd()) {
43 if (destFile.atEnd()) {
44 return false;
45 }
46 QByteArray sourceLine = sourceFile.readLine();
47 QByteArray destLine = destFile.readLine();
48 if (sourceLine != destLine) {
49 sourceLine.chop(1); // remove '\n'
50 destLine.chop(1); // remove '\n'
51 qDebug() << source << "and" << dest << "differ at line" << lineNumber;
52 qDebug("got : %s", sourceLine.constData());
53 qDebug("expected: %s", destLine.constData());
54 return false;
55 }
56 ++lineNumber;
57 }
58 return true;
59 }
60
61 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
62 #if QT_VERSION < QT_VERSION_CHECK(5, 6, 0)
63 QT_BEGIN_NAMESPACE
64 // Avoid QHash randomization so that the order of the XML attributes is stable
65 extern Q_CORE_EXPORT QBasicAtomicInt qt_qhash_seed; // from qhash.cpp
66 QT_END_NAMESPACE
67 #endif
68
initHashSeed()69 static void initHashSeed()
70 {
71 #if QT_VERSION < QT_VERSION_CHECK(5, 6, 0)
72 // If the seed not initialized yet (-1), set it to 0;
73 // if it was 0 already, do nothing;
74 // otherwise abort, so we don't get unexplained test failures later.
75 qt_qhash_seed.testAndSetRelaxed(-1, 0);
76 Q_ASSERT(qt_qhash_seed.loadAcquire() == 0);
77 #else
78 qSetGlobalQHashSeed(0);
79 #endif
80 }
81
Q_CONSTRUCTOR_FUNCTION(initHashSeed)82 Q_CONSTRUCTOR_FUNCTION(initHashSeed)
83 #endif
84
85 // A tool for comparing XML documents and outputting something useful if they differ
86 bool KDSoapUnitTestHelpers::xmlBufferCompare(const QByteArray &source, const QByteArray &dest)
87 {
88 QBuffer sourceFile;
89 sourceFile.setData(source);
90 if (!sourceFile.open(QIODevice::ReadOnly)) {
91 qDebug() << "ERROR opening QIODevice";
92 return false;
93 }
94 QBuffer destFile;
95 destFile.setData(dest);
96 if (!destFile.open(QIODevice::ReadOnly)) {
97 qDebug() << "ERROR opening QIODevice";
98 return false;
99 }
100
101 // Use QDomDocument to reformat the XML with newlines
102 QDomDocument sourceDoc;
103 if (!sourceDoc.setContent(&sourceFile)) {
104 qDebug() << "ERROR parsing XML:" << source;
105 return false;
106 }
107 QDomDocument destDoc;
108 if (!destDoc.setContent(&destFile)) {
109 qDebug() << "ERROR parsing XML:" << dest;
110 return false;
111 }
112
113 const QByteArray sourceXml = sourceDoc.toByteArray();
114 const QByteArray destXml = destDoc.toByteArray();
115 sourceFile.close();
116 destFile.close();
117
118 QBuffer sourceBuffer;
119 sourceBuffer.setData(sourceXml);
120 sourceBuffer.open(QIODevice::ReadOnly);
121 QBuffer destBuffer;
122 destBuffer.setData(destXml);
123 destBuffer.open(QIODevice::ReadOnly);
124
125 return textBufferCompare(source, dest, sourceBuffer, destBuffer);
126 }
127
httpGet(const QUrl & url)128 void KDSoapUnitTestHelpers::httpGet(const QUrl &url)
129 {
130 QNetworkRequest request(url);
131 QNetworkAccessManager manager;
132 QNetworkReply *reply = manager.get(request);
133 //QObject::connect(reply, SIGNAL(sslErrors(QList<QSslError>)), reply, SLOT(ignoreSslErrors()));
134
135 QEventLoop ev;
136 QObject::connect(reply, SIGNAL(finished()), &ev, SLOT(quit()));
137 ev.exec();
138
139 //QObject::connect(reply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop()));
140 //QTestEventLoop::instance().enterLoop(11);
141
142 //qDebug() << "httpGet:" << reply->readAll();
143 delete reply;
144 }
145
146 #ifndef QT_NO_OPENSSL
147
148 // To debug this: openssl s_client -connect 127.0.0.1:443
149
setupSslServer(QSslSocket * serverSocket)150 static void setupSslServer(QSslSocket *serverSocket)
151 {
152 Q_INIT_RESOURCE(testtools);
153 //qDebug() << "setupSslServer";
154 serverSocket->setProtocol(QSsl::AnyProtocol);
155 serverSocket->setLocalCertificate(QString::fromLatin1(":/certs/test-127.0.0.1-cert.pem"));
156 serverSocket->setPrivateKey(QString::fromLatin1(":/certs/test-127.0.0.1-key.pem"));
157 }
158
initResource()159 static void initResource()
160 {
161 Q_INIT_RESOURCE(testtools);
162 }
163
setSslConfiguration()164 bool KDSoapUnitTestHelpers::setSslConfiguration()
165 {
166 initResource();
167
168 // To make SSL work, we need to tell Qt about our local certificate
169
170 // Both ways work:
171 #if 0
172 QSslConfiguration defaultConfig = QSslConfiguration::defaultConfiguration();
173 QFile certFile(QString::fromLatin1(":/certs/cacert.pem"));
174 if (!certFile.open(QIODevice::ReadOnly)) {
175 qDebug() << "Could not open cacert.pem";
176 return false;
177 }
178 QSslCertificate cert(&certFile);
179 if (!cert.isValid()) {
180 return false;
181 }
182 defaultConfig.setCaCertificates(QList<QSslCertificate>() << cert);
183 QSslConfiguration::setDefaultConfiguration(defaultConfig);
184 #endif
185
186 QFile certFile(QString::fromLatin1(":/certs/cacert.pem"));
187 if (!certFile.open(QIODevice::ReadOnly)) {
188 qDebug() << "Could not open cacert.pem";
189 return false;
190 }
191 QSslCertificate cert(&certFile);
192 const QDateTime currentTime = QDateTime::currentDateTime();
193 if (cert.effectiveDate() > currentTime
194 || cert.expiryDate() < currentTime) {
195 qDebug() << "Certificate" << certFile.fileName() << "is not valid";
196 qDebug() << "It is valid from" << cert.effectiveDate() << "to" << cert.expiryDate();
197 return false;
198 }
199 QSslSocket::addDefaultCaCertificate(cert);
200
201 return true;
202 }
203 #endif
204
205 // A blocking http server (must be used in a thread) which supports SSL.
206 class BlockingHttpServer : public QTcpServer
207 {
208 Q_OBJECT
209 public:
BlockingHttpServer(bool ssl)210 BlockingHttpServer(bool ssl) : doSsl(ssl), sslSocket(0) {}
~BlockingHttpServer()211 ~BlockingHttpServer() {}
212
waitForNextConnectionSocket()213 QTcpSocket *waitForNextConnectionSocket()
214 {
215 if (!waitForNewConnection(20000)) { // 2000 would be enough, except in valgrind
216 return 0;
217 }
218 if (doSsl) {
219 Q_ASSERT(sslSocket);
220 return sslSocket;
221 } else {
222 //qDebug() << "returning nextPendingConnection";
223 return nextPendingConnection();
224 }
225 }
226
227 #if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
incomingConnection(qintptr socketDescriptor)228 virtual void incomingConnection(qintptr socketDescriptor) override
229 #else
230 virtual void incomingConnection(int socketDescriptor) override
231 #endif
232 {
233 #ifndef QT_NO_OPENSSL
234 if (doSsl) {
235 QSslSocket *serverSocket = new QSslSocket;
236 serverSocket->setParent(this);
237 serverSocket->setSocketDescriptor(socketDescriptor);
238 connect(serverSocket, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(slotSslErrors(QList<QSslError>)));
239 setupSslServer(serverSocket);
240 //qDebug() << "Created QSslSocket, starting server encryption";
241 serverSocket->startServerEncryption();
242 sslSocket = serverSocket;
243 // If startServerEncryption fails internally [and waitForEncrypted hangs],
244 // then this is how to debug it.
245 // A way to catch such errors is really missing in Qt..
246 //qDebug() << "startServerEncryption said:" << sslSocket->errorString();
247 bool ok = serverSocket->waitForEncrypted();
248 Q_ASSERT(ok);
249 Q_UNUSED(ok);
250 } else
251 #endif
252 QTcpServer::incomingConnection(socketDescriptor);
253 }
disableSsl()254 void disableSsl()
255 {
256 doSsl = false;
257 }
258
259 private slots:
slotSslErrors(const QList<QSslError> & errors)260 void slotSslErrors(const QList<QSslError> &errors)
261 {
262 #ifndef QT_NO_OPENSSL
263 qDebug() << "server-side: slotSslErrors" << sslSocket->errorString() << errors;
264 #else
265 Q_UNUSED(errors);
266 #endif
267 }
268 private:
269 bool doSsl;
270 QTcpSocket *sslSocket;
271 };
272
splitHeadersAndData(const QByteArray & request,QByteArray & header,QByteArray & data)273 static bool splitHeadersAndData(const QByteArray &request, QByteArray &header, QByteArray &data)
274 {
275 const int sep = request.indexOf("\r\n\r\n");
276 if (sep <= 0) {
277 return false;
278 }
279 header = request.left(sep);
280 data = request.mid(sep + 4);
281 return true;
282 }
283
284 typedef QMap<QByteArray, QByteArray> HeadersMap;
parseHeaders(const QByteArray & headerData)285 static HeadersMap parseHeaders(const QByteArray &headerData)
286 {
287 HeadersMap headersMap;
288 QBuffer sourceBuffer;
289 sourceBuffer.setData(headerData);
290 sourceBuffer.open(QIODevice::ReadOnly);
291 // The first line is special, it's the GET or POST line
292 const QList<QByteArray> firstLine = sourceBuffer.readLine().split(' ');
293 if (firstLine.count() < 3) {
294 qDebug() << "Malformed HTTP request:" << firstLine;
295 return headersMap;
296 }
297 const QByteArray request = firstLine[0];
298 const QByteArray path = firstLine[1];
299 const QByteArray httpVersion = firstLine[2];
300 if (request != "GET" && request != "POST") {
301 qDebug() << "Unknown HTTP request:" << firstLine;
302 return headersMap;
303 }
304 headersMap.insert("_path", path);
305 headersMap.insert("_httpVersion", httpVersion);
306
307 while (!sourceBuffer.atEnd()) {
308 const QByteArray line = sourceBuffer.readLine();
309 const int pos = line.indexOf(':');
310 if (pos == -1) {
311 qDebug() << "Malformed HTTP header:" << line;
312 }
313 const QByteArray header = line.left(pos);
314 const QByteArray value = line.mid(pos + 1).trimmed(); // remove space before and \r\n after
315 //qDebug() << "HEADER" << header << "VALUE" << value;
316 headersMap.insert(header, value);
317 }
318 return headersMap;
319 }
320
disableSsl()321 void HttpServerThread::disableSsl()
322 {
323 if (m_server) {
324 m_server->disableSsl();
325 }
326 }
327
run()328 void HttpServerThread::run()
329 {
330 m_server = new BlockingHttpServer(m_features & Ssl);
331 if (!m_server->listen()) {
332 qFatal("HttpServerThread::run is unable to listen");
333 }
334 QMutexLocker lock(&m_mutex);
335 m_port = m_server->serverPort();
336 lock.unlock();
337 m_ready.release();
338
339 const bool doDebug = qgetenv("KDSOAP_DEBUG").toInt();
340
341 if (doDebug) {
342 qDebug() << "HttpServerThread listening on port" << m_port;
343 }
344
345 // Wait for first connection (we'll wait for further ones inside the loop)
346 QTcpSocket *clientSocket = m_server->waitForNextConnectionSocket();
347 Q_ASSERT(clientSocket);
348
349 Q_FOREVER {
350 // get the "request" packet
351 if (doDebug) {
352 qDebug() << "HttpServerThread: waiting for read";
353 }
354 if (clientSocket->state() == QAbstractSocket::UnconnectedState ||
355 !clientSocket->waitForReadyRead(2000)) {
356 if (clientSocket->state() == QAbstractSocket::UnconnectedState) {
357 delete clientSocket;
358 if (doDebug) {
359 qDebug() << "Waiting for next connection...";
360 }
361 clientSocket = m_server->waitForNextConnectionSocket();
362 Q_ASSERT(clientSocket);
363 continue; // go to "waitForReadyRead"
364 } else {
365 qDebug() << "HttpServerThread:" << clientSocket->error() << "waiting for \"request\" packet";
366 break;
367 }
368 }
369 const QByteArray request = m_partialRequest + clientSocket->readAll();
370 if (doDebug) {
371 qDebug() << "HttpServerThread: request:" << request;
372 }
373
374 // Split headers and request xml
375 lock.relock();
376 const bool splitOK = splitHeadersAndData(request, m_receivedHeaders, m_receivedData);
377 if (!splitOK) {
378 //if (doDebug)
379 // qDebug() << "Storing partial request" << request;
380 m_partialRequest = request;
381 continue;
382 }
383
384 m_headers = parseHeaders(m_receivedHeaders);
385
386 if (m_headers.value("Content-Length").toInt() > m_receivedData.size()) {
387 //if (doDebug)
388 // qDebug() << "Storing partial request" << request;
389 m_partialRequest = request;
390 continue;
391 }
392
393 m_partialRequest.clear();
394
395 if (m_headers.value("_path").endsWith("terminateThread")) { // we're asked to exit
396 break; // normal exit
397 }
398
399 // TODO compared with expected SoapAction
400 QList<QByteArray> contentTypes = m_headers.value("Content-Type").split(';');
401 if (contentTypes[0] == "text/xml" && m_headers.value("SoapAction").isEmpty()) {
402 qDebug() << "ERROR: no SoapAction set for Soap 1.1";
403 break;
404 } else if (contentTypes[0] == "application/soap+xml" && !contentTypes[2].startsWith("action")) {
405 qDebug() << "ERROR: no SoapAction set for Soap 1.2";
406 break;
407 }
408 lock.unlock();
409
410 //qDebug() << "headers received:" << m_receivedHeaders;
411 //qDebug() << headers;
412 //qDebug() << "data received:" << m_receivedData;
413
414 if (m_features & BasicAuth) {
415 QByteArray authValue = m_headers.value("Authorization");
416 if (authValue.isEmpty()) {
417 authValue = m_headers.value("authorization"); // as sent by Qt-4.5
418 }
419 bool authOk = false;
420 if (!authValue.isEmpty()) {
421 //qDebug() << "got authValue=" << authValue; // looks like "Basic <base64 of user:pass>"
422 Method method;
423 QString headerVal;
424 parseAuthLine(QString::fromLatin1(authValue.data(), authValue.size()), &method, &headerVal);
425 //qDebug() << "method=" << method << "headerVal=" << headerVal;
426 switch (method) {
427 case None: // we want auth, so reject "None"
428 break;
429 case Basic: {
430 const QByteArray userPass = QByteArray::fromBase64(headerVal.toLatin1());
431 //qDebug() << userPass;
432 // TODO if (validateAuth(userPass)) {
433 if (userPass == ("kdab:testpass")) {
434 authOk = true;
435 }
436 break;
437 }
438 default:
439 qWarning("Unsupported authentication mechanism %s", authValue.constData());
440 }
441 }
442
443 if (!authOk) {
444 // send auth request (Qt supports basic, ntlm and digest)
445 const QByteArray unauthorized = "HTTP/1.1 401 Authorization Required\r\nWWW-Authenticate: Basic realm=\"example\"\r\nContent-Length: 0\r\n\r\n";
446 clientSocket->write(unauthorized);
447 if (!clientSocket->waitForBytesWritten(2000)) {
448 qDebug() << "HttpServerThread:" << clientSocket->error() << "writing auth request";
449 break;
450 }
451 continue;
452 }
453 }
454
455 // send response
456 QByteArray response = makeHttpResponse(m_dataToSend);
457 if (doDebug) {
458 qDebug() << "HttpServerThread: writing" << response;
459 }
460 clientSocket->write(response);
461
462 clientSocket->flush();
463 }
464 // all done...
465 delete clientSocket;
466 delete m_server;
467 if (doDebug) {
468 qDebug() << "HttpServerThread terminated";
469 }
470 }
471
xmlEnvBegin11()472 const char *KDSoapUnitTestHelpers::xmlEnvBegin11()
473 {
474 return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
475 "<soap:Envelope"
476 " xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\""
477 " xmlns:soap-enc=\"http://schemas.xmlsoap.org/soap/encoding/\""
478 " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\""
479 " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"";
480 }
481
xmlEnvBegin12()482 const char *KDSoapUnitTestHelpers::xmlEnvBegin12()
483 {
484 return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
485 "<soap:Envelope"
486 " xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\""
487 " xmlns:soap-enc=\"http://www.w3.org/2003/05/soap-encoding\""
488 " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\""
489 " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"";
490 }
491
xmlEnvEnd()492 const char *KDSoapUnitTestHelpers::xmlEnvEnd()
493 {
494 return "</soap:Envelope>";
495 }
496
497 #include "moc_httpserver_p.cpp"
498 #include "httpserver_p.moc"
499