File indexing completed on 2025-02-16 04:50:06

0001 /*
0002     SPDX-FileCopyrightText: 2015-2019 Krzysztof Nowicki <krissn@op.pl>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "fakeewsconnection.h"
0008 
0009 #include <QBuffer>
0010 #include <QRegularExpression>
0011 #include <QTcpSocket>
0012 #include <QXmlNamePool>
0013 #include <QXmlQuery>
0014 #include <QXmlResultItems>
0015 #include <QXmlSerializer>
0016 
0017 #include "fakeewsserver_debug.h"
0018 
0019 static const QHash<uint, QString> responseCodes = {
0020     {200, QStringLiteral("OK")},
0021     {400, QStringLiteral("Bad Request")},
0022     {401, QStringLiteral("Unauthorized")},
0023     {403, QStringLiteral("Forbidden")},
0024     {404, QStringLiteral("Not Found")},
0025     {405, QStringLiteral("Method Not Allowed")},
0026     {500, QStringLiteral("Internal Server Error")},
0027 };
0028 
0029 static constexpr int streamingEventsHeartbeatIntervalSeconds = 5;
0030 
0031 FakeEwsConnection::FakeEwsConnection(QTcpSocket *sock, FakeEwsServer *parent)
0032     : QObject(parent)
0033     , mSock(sock)
0034     , mContentLength(0)
0035     , mKeepAlive(false)
0036     , mState(Initial)
0037     , mAuthenticated(false)
0038 {
0039     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got new EWS connection.");
0040     connect(mSock.data(), &QTcpSocket::disconnected, this, &FakeEwsConnection::disconnected);
0041     connect(mSock.data(), &QTcpSocket::readyRead, this, &FakeEwsConnection::dataAvailable);
0042     connect(&mDataTimer, &QTimer::timeout, this, &FakeEwsConnection::dataTimeout);
0043     connect(&mStreamingRequestHeartbeat, &QTimer::timeout, this, &FakeEwsConnection::streamingRequestHeartbeat);
0044     connect(&mStreamingRequestTimeout, &QTimer::timeout, this, &FakeEwsConnection::streamingRequestTimeout);
0045 }
0046 
0047 FakeEwsConnection::~FakeEwsConnection()
0048 {
0049     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Connection closed.");
0050 }
0051 
0052 void FakeEwsConnection::disconnected()
0053 {
0054     deleteLater();
0055 }
0056 
0057 void FakeEwsConnection::dataAvailable()
0058 {
0059     if (mState == Initial) {
0060         QByteArray line = mSock->readLine();
0061         QList<QByteArray> tokens = line.split(' ');
0062         mKeepAlive = false;
0063 
0064         if (tokens.size() < 3) {
0065             sendError(QStringLiteral("Invalid request header"));
0066             return;
0067         }
0068         if (tokens.at(0) != "POST") {
0069             sendError(QStringLiteral("Expected POST request"));
0070             return;
0071         }
0072         if (tokens.at(1) != "/EWS/Exchange.asmx") {
0073             sendError(QStringLiteral("Invalid EWS URL"));
0074             return;
0075         }
0076         mState = RequestReceived;
0077     }
0078 
0079     if (mState == RequestReceived) {
0080         QByteArray line;
0081         do {
0082             line = mSock->readLine();
0083             if (line.toLower().startsWith(QByteArray("content-length: "))) {
0084                 bool ok;
0085                 mContentLength = line.trimmed().mid(16).toUInt(&ok);
0086                 if (!ok) {
0087                     sendError(QStringLiteral("Failed to parse content length."));
0088                     return;
0089                 }
0090             } else if (line.toLower().startsWith(QByteArray("authorization: basic "))) {
0091                 if (line.trimmed().mid(21) == "dGVzdDp0ZXN0") {
0092                     mAuthenticated = true;
0093                 }
0094             } else if (line.toLower() == "connection: keep-alive\r\n") {
0095                 mKeepAlive = true;
0096             }
0097         } while (!line.trimmed().isEmpty());
0098 
0099         if (line == "\r\n") {
0100             mState = HeadersReceived;
0101         }
0102     }
0103 
0104     if (mState == HeadersReceived) {
0105         if (mContentLength == 0) {
0106             sendError(QStringLiteral("Expected content"));
0107             return;
0108         }
0109 
0110         mContent += mSock->read(mContentLength - mContent.size());
0111 
0112         if (mContent.size() >= static_cast<int>(mContentLength)) {
0113             mDataTimer.stop();
0114 
0115             if (!mAuthenticated) {
0116                 QString codeStr = responseCodes.value(401);
0117                 QString response(QStringLiteral("HTTP/1.1 %1 %2\r\n"
0118                                                 "WWW-Authenticate: Basic realm=\"Fake EWS Server\"\r\n"
0119                                                 "Connection: close\r\n"
0120                                                 "\r\n")
0121                                      .arg(401)
0122                                      .arg(codeStr));
0123                 response += codeStr;
0124                 mSock->write(response.toLatin1());
0125                 mSock->disconnectFromHost();
0126                 return;
0127             }
0128 
0129             FakeEwsServer::DialogEntry::HttpResponse resp = FakeEwsServer::EmptyResponse;
0130 
0131             const auto server = qobject_cast<FakeEwsServer *>(parent());
0132             const auto overrideReplyCallback = server->overrideReplyCallback();
0133             if (overrideReplyCallback) {
0134                 QXmlResultItems ri;
0135                 QXmlNamePool namePool;
0136                 resp = overrideReplyCallback(QString::fromUtf8(mContent), ri, namePool);
0137             }
0138 
0139             if (resp == FakeEwsServer::EmptyResponse) {
0140                 resp = parseRequest(QString::fromUtf8(mContent));
0141             }
0142             bool chunked = false;
0143 
0144             if (resp == FakeEwsServer::EmptyResponse) {
0145                 resp = handleGetEventsRequest(QString::fromUtf8(mContent));
0146             }
0147 
0148             if (resp == FakeEwsServer::EmptyResponse) {
0149                 resp = handleGetStreamingEventsRequest(QString::fromUtf8(mContent));
0150                 if (resp.second > 1000) {
0151                     chunked = true;
0152                     resp.second %= 1000;
0153                 }
0154             }
0155 
0156             auto defaultReplyCallback = server->defaultReplyCallback();
0157             if (defaultReplyCallback && (resp == FakeEwsServer::EmptyResponse)) {
0158                 QXmlResultItems ri;
0159                 QXmlNamePool namePool;
0160                 resp = defaultReplyCallback(QString::fromUtf8(mContent), ri, namePool);
0161                 qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning response from default callback ") << resp.second << QStringLiteral(": ") << resp.first;
0162             }
0163 
0164             if (resp == FakeEwsServer::EmptyResponse) {
0165                 qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning default response 500.");
0166                 resp = {QLatin1StringView(""), 500};
0167             }
0168 
0169             QByteArray buffer;
0170             QString codeStr = responseCodes.value(resp.second);
0171             QByteArray respContent = resp.first.toUtf8();
0172             buffer += QStringLiteral("HTTP/1.1 %1 %2\r\n").arg(resp.second).arg(codeStr).toLatin1();
0173             if (chunked) {
0174                 buffer += "Transfer-Encoding: chunked\r\n";
0175                 buffer += "\r\n";
0176                 buffer += QByteArray::number(respContent.size(), 16) + "\r\n";
0177                 buffer += respContent + "\r\n";
0178             } else {
0179                 buffer += "Content-Length: " + QByteArray::number(respContent.size()) + "\r\n";
0180                 buffer += mKeepAlive ? "Connection: Keep-Alive\n" : "Connection: Close\r\n";
0181                 buffer += "\r\n";
0182                 buffer += respContent;
0183             }
0184             mSock->write(buffer);
0185 
0186             if (!mKeepAlive && !chunked) {
0187                 mSock->disconnectFromHost();
0188             }
0189             mContent.clear();
0190             mState = Initial;
0191         } else {
0192             mDataTimer.start(3000);
0193         }
0194     }
0195 }
0196 
0197 void FakeEwsConnection::sendError(const QString &msg, ushort code)
0198 {
0199     qCWarningNC(EWSFAKE_LOG) << msg;
0200     QString codeStr = responseCodes.value(code);
0201     QByteArray response(QStringLiteral("HTTP/1.1 %1 %2\nConnection: close\n\n").arg(code).arg(codeStr).toLatin1());
0202     response += msg.toLatin1();
0203     mSock->write(response);
0204     mSock->disconnectFromHost();
0205 }
0206 
0207 void FakeEwsConnection::dataTimeout()
0208 {
0209     qCWarning(EWSFAKE_LOG) << QLatin1StringView("Timeout waiting for content.");
0210     sendError(QStringLiteral("Timeout waiting for content."));
0211 }
0212 
0213 FakeEwsServer::DialogEntry::HttpResponse FakeEwsConnection::parseRequest(const QString &content)
0214 {
0215     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got request: ") << content;
0216 
0217     auto server = qobject_cast<FakeEwsServer *>(parent());
0218     FakeEwsServer::DialogEntry::HttpResponse resp = FakeEwsServer::EmptyResponse;
0219     const auto dialogs{server->dialog()};
0220     for (const FakeEwsServer::DialogEntry &de : dialogs) {
0221         QXmlResultItems ri;
0222         QByteArray resultBytes;
0223         QString result;
0224         QBuffer resultBuffer(&resultBytes);
0225         resultBuffer.open(QIODevice::WriteOnly);
0226         QXmlQuery query;
0227         QXmlSerializer xser(query, &resultBuffer);
0228         if (!de.xQuery.isNull()) {
0229             query.setFocus(content);
0230             query.setQuery(de.xQuery);
0231             query.evaluateTo(&xser);
0232             query.evaluateTo(&ri);
0233             if (ri.hasError()) {
0234                 qCDebugNC(EWSFAKE_LOG) << QStringLiteral("XQuery failed due to errors - skipping");
0235                 continue;
0236             }
0237             result = QString::fromUtf8(resultBytes);
0238         }
0239 
0240         if (!result.trimmed().isEmpty()) {
0241             qCDebugNC(EWSFAKE_LOG) << QStringLiteral("Got match for \"") << de.description << QStringLiteral("\"");
0242             if (de.replyCallback) {
0243                 resp = de.replyCallback(content, ri, query.namePool());
0244                 qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning response from callback ") << resp.second << QStringLiteral(": ") << resp.first;
0245             } else {
0246                 resp = {result.trimmed(), 200};
0247                 qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning response from XQuery ") << resp.second << QStringLiteral(": ") << resp.first;
0248             }
0249             break;
0250         }
0251     }
0252 
0253     if (resp == FakeEwsServer::EmptyResponse) {
0254         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning empty response.");
0255         qCInfoNC(EWSFAKE_LOG) << content;
0256     }
0257 
0258     return resp;
0259 }
0260 
0261 FakeEwsServer::DialogEntry::HttpResponse FakeEwsConnection::handleGetEventsRequest(const QString &content)
0262 {
0263     const QRegularExpression re(QStringLiteral(
0264         "<?xml .*<\\w*:?GetEvents[ "
0265         ">].*<\\w*:?SubscriptionId>(?<subid>[^<]*)</\\w*:?SubscriptionId><\\w*:?Watermark>(?<watermark>[^<]*)</\\w*:?Watermark></\\w*:?GetEvents>.*"));
0266 
0267     QRegularExpressionMatch match = re.match(content);
0268     if (!match.hasMatch() || match.hasPartialMatch()) {
0269         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Not a valid GetEvents request.");
0270         return FakeEwsServer::EmptyResponse;
0271     }
0272 
0273     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got valid GetEvents request.");
0274 
0275     QString resp = QStringLiteral(
0276         "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
0277         "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
0278         "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
0279         "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
0280         "xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" "
0281         "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
0282         "<soap:Header>"
0283         "<t:ServerVersionInfo MajorVersion=\"8\" MinorVersion=\"0\" MajorBuildNumber=\"628\" MinorBuildNumber=\"0\" />"
0284         "</soap:Header>"
0285         "<soap:Body>"
0286         "<m:GetEventsResponse xmlns=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
0287         "<m:ResponseMessages>"
0288         "<m:GetEventsResponseMessage ResponseClass=\"Success\">"
0289         "<m:ResponseCode>NoError</m:ResponseCode>"
0290         "<m:Notification>");
0291 
0292     if (match.captured(QStringLiteral("subid")).isEmpty() || match.captured(QStringLiteral("watermark")).isEmpty()) {
0293         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Missing subscription id or watermark.");
0294         const QString errorResp = QStringLiteral(
0295             "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
0296             "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
0297             "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
0298             "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
0299             "xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" "
0300             "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
0301             "<soap:Header>"
0302             "<t:ServerVersionInfo MajorVersion=\"8\" MinorVersion=\"0\" MajorBuildNumber=\"628\" MinorBuildNumber=\"0\" />"
0303             "</soap:Header>"
0304             "<soap:Body>"
0305             "<m:GetEventsResponse>"
0306             "<m:ResponseMessages>"
0307             "<m:GetEventsResponseMessage ResponseClass=\"Error\">"
0308             "<m:MessageText>Missing subscription id or watermark.</m:MessageText>"
0309             "<m:ResponseCode>ErrorInvalidPullSubscriptionId</m:ResponseCode>"
0310             "<m:DescriptiveLinkKey>0</m:DescriptiveLinkKey>"
0311             "</m:GetEventsResponseMessage>"
0312             "</m:ResponseMessages>"
0313             "</m:GetEventsResponse>"
0314             "</soap:Body>"
0315             "</soap:Envelope>");
0316         return {errorResp, 200};
0317     }
0318 
0319     resp += QLatin1StringView("<SubscriptionId>") + match.captured(QStringLiteral("subid")) + QLatin1StringView("<SubscriptionId>");
0320     resp += QLatin1StringView("<PreviousWatermark>") + match.captured(QStringLiteral("watermark")) + QLatin1StringView("<PreviousWatermark>");
0321     resp += QStringLiteral("<MoreEvents>false<MoreEvents>");
0322 
0323     auto server = qobject_cast<FakeEwsServer *>(parent());
0324     const QStringList events = server->retrieveEventsXml();
0325     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning %1 events.").arg(events.size());
0326     for (const QString &eventXml : events) {
0327         resp += eventXml;
0328     }
0329 
0330     resp += QStringLiteral(
0331         "</m:Notification></m:GetEventsResponseMessage></m:ResponseMessages>"
0332         "</m:GetEventsResponse></soap:Body></soap:Envelope>");
0333 
0334     return {resp, 200};
0335 }
0336 
0337 QString FakeEwsConnection::prepareEventsResponse(const QStringList &events)
0338 {
0339     QString resp = QStringLiteral(
0340         "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
0341         "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
0342         "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
0343         "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
0344         "xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" "
0345         "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
0346         "<soap:Header>"
0347         "<t:ServerVersionInfo MajorVersion=\"8\" MinorVersion=\"0\" MajorBuildNumber=\"628\" MinorBuildNumber=\"0\" />"
0348         "</soap:Header>"
0349         "<soap:Body>"
0350         "<m:GetStreamingEventsResponse>"
0351         "<m:ResponseMessages>"
0352         "<m:GetStreamingEventsResponseMessage ResponseClass=\"Success\">"
0353         "<m:ResponseCode>NoError</m:ResponseCode>"
0354         "<m:ConnectionStatus>OK</m:ConnectionStatus>");
0355 
0356     if (!events.isEmpty()) {
0357         resp += QLatin1StringView("<m:Notifications><m:Notification><SubscriptionId>") + mStreamingSubId + QLatin1StringView("<SubscriptionId>");
0358 
0359         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning %1 events.").arg(events.size());
0360         for (const QString &eventXml : std::as_const(events)) {
0361             resp += eventXml;
0362         }
0363 
0364         resp += QStringLiteral("</m:Notification></m:Notifications>");
0365     }
0366     resp += QStringLiteral(
0367         "</m:GetStreamingEventsResponseMessage></m:ResponseMessages>"
0368         "</m:GetStreamingEventsResponse></soap:Body></soap:Envelope>");
0369 
0370     return resp;
0371 }
0372 
0373 FakeEwsServer::DialogEntry::HttpResponse FakeEwsConnection::handleGetStreamingEventsRequest(const QString &content)
0374 {
0375     const QRegularExpression re(
0376         QStringLiteral("<?xml .*<\\w*:?GetStreamingEvents[ "
0377                        ">].*<\\w*:?SubscriptionIds><\\w*:?SubscriptionId>(?<subid>[^<]*)</\\w*:?SubscriptionId></"
0378                        "\\w*:?SubscriptionIds>.*<\\w*:?ConnectionTimeout>(?<timeout>[^<]*)</\\w*:?ConnectionTimeout></\\w*:?GetStreamingEvents>.*"));
0379 
0380     QRegularExpressionMatch match = re.match(content);
0381     if (!match.hasMatch() || match.hasPartialMatch()) {
0382         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Not a valid GetStreamingEvents request.");
0383         return FakeEwsServer::EmptyResponse;
0384     }
0385 
0386     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got valid GetStreamingEvents request.");
0387 
0388     if (match.captured(QStringLiteral("subid")).isEmpty() || match.captured(QStringLiteral("timeout")).isEmpty()) {
0389         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Missing subscription id or timeout.");
0390         const QString errorResp = QStringLiteral(
0391             "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
0392             "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
0393             "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
0394             "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
0395             "xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" "
0396             "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
0397             "<soap:Header>"
0398             "<t:ServerVersionInfo MajorVersion=\"8\" MinorVersion=\"0\" MajorBuildNumber=\"628\" MinorBuildNumber=\"0\" />"
0399             "</soap:Header>"
0400             "<soap:Body>"
0401             "<m:GetStreamingEventsResponse>"
0402             "<m:ResponseMessages>"
0403             "<m:GetStreamingEventsResponseMessage ResponseClass=\"Error\">"
0404             "<m:MessageText>Missing subscription id or timeout.</m:MessageText>"
0405             "<m:ResponseCode>ErrorInvalidSubscription</m:ResponseCode>"
0406             "<m:DescriptiveLinkKey>0</m:DescriptiveLinkKey>"
0407             "</m:GetEventsResponseMessage>"
0408             "</m:ResponseMessages>"
0409             "</m:GetEventsResponse>"
0410             "</soap:Body>"
0411             "</soap:Envelope>");
0412         return {errorResp, 200};
0413     }
0414 
0415     mStreamingSubId = match.captured(QStringLiteral("subid"));
0416 
0417     auto server = qobject_cast<FakeEwsServer *>(parent());
0418     const QStringList events = server->retrieveEventsXml();
0419 
0420     QString resp = prepareEventsResponse(events);
0421 
0422     mStreamingRequestTimeout.start(match.captured(QStringLiteral("timeout")).toInt() * 1000 * 60);
0423     mStreamingRequestHeartbeat.setSingleShot(false);
0424     mStreamingRequestHeartbeat.start(streamingEventsHeartbeatIntervalSeconds * 1000);
0425 
0426     Q_EMIT streamingRequestStarted(this);
0427 
0428     return {resp, 1200};
0429 }
0430 
0431 void FakeEwsConnection::streamingRequestHeartbeat()
0432 {
0433     sendEvents(QStringList());
0434 }
0435 
0436 void FakeEwsConnection::streamingRequestTimeout()
0437 {
0438     mStreamingRequestTimeout.stop();
0439     mStreamingRequestHeartbeat.stop();
0440     mSock->write("0\r\n\r\n");
0441     mSock->disconnectFromHost();
0442 }
0443 
0444 void FakeEwsConnection::sendEvents(const QStringList &events)
0445 {
0446     QByteArray resp = prepareEventsResponse(events).toUtf8();
0447 
0448     mSock->write(QByteArray::number(resp.size(), 16) + "\r\n" + resp + "\r\n");
0449 }
0450 
0451 #include "moc_fakeewsconnection.cpp"