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