1 /*
2     SPDX-FileCopyrightText: 2015-2019 Krzysztof Nowicki <krissn@op.pl>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "fakeewsconnection.h"
8 
9 #include <QBuffer>
10 #include <QRegularExpression>
11 #include <QTcpSocket>
12 #include <QXmlNamePool>
13 #include <QXmlQuery>
14 #include <QXmlResultItems>
15 #include <QXmlSerializer>
16 
17 #include "fakeewsserver_debug.h"
18 
19 static const QHash<uint, QString> responseCodes = {
20     {200, QStringLiteral("OK")},
21     {400, QStringLiteral("Bad Request")},
22     {401, QStringLiteral("Unauthorized")},
23     {403, QStringLiteral("Forbidden")},
24     {404, QStringLiteral("Not Found")},
25     {405, QStringLiteral("Method Not Allowed")},
26     {500, QStringLiteral("Internal Server Error")},
27 };
28 
29 static constexpr int streamingEventsHeartbeatIntervalSeconds = 5;
30 
FakeEwsConnection(QTcpSocket * sock,FakeEwsServer * parent)31 FakeEwsConnection::FakeEwsConnection(QTcpSocket *sock, FakeEwsServer *parent)
32     : QObject(parent)
33     , mSock(sock)
34     , mContentLength(0)
35     , mKeepAlive(false)
36     , mState(Initial)
37     , mAuthenticated(false)
38 {
39     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got new EWS connection.");
40     connect(mSock.data(), &QTcpSocket::disconnected, this, &FakeEwsConnection::disconnected);
41     connect(mSock.data(), &QTcpSocket::readyRead, this, &FakeEwsConnection::dataAvailable);
42     connect(&mDataTimer, &QTimer::timeout, this, &FakeEwsConnection::dataTimeout);
43     connect(&mStreamingRequestHeartbeat, &QTimer::timeout, this, &FakeEwsConnection::streamingRequestHeartbeat);
44     connect(&mStreamingRequestTimeout, &QTimer::timeout, this, &FakeEwsConnection::streamingRequestTimeout);
45 }
46 
~FakeEwsConnection()47 FakeEwsConnection::~FakeEwsConnection()
48 {
49     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Connection closed.");
50 }
51 
disconnected()52 void FakeEwsConnection::disconnected()
53 {
54     deleteLater();
55 }
56 
dataAvailable()57 void FakeEwsConnection::dataAvailable()
58 {
59     if (mState == Initial) {
60         QByteArray line = mSock->readLine();
61         QList<QByteArray> tokens = line.split(' ');
62         mKeepAlive = false;
63 
64         if (tokens.size() < 3) {
65             sendError(QStringLiteral("Invalid request header"));
66             return;
67         }
68         if (tokens.at(0) != "POST") {
69             sendError(QStringLiteral("Expected POST request"));
70             return;
71         }
72         if (tokens.at(1) != "/EWS/Exchange.asmx") {
73             sendError(QStringLiteral("Invalid EWS URL"));
74             return;
75         }
76         mState = RequestReceived;
77     }
78 
79     if (mState == RequestReceived) {
80         QByteArray line;
81         do {
82             line = mSock->readLine();
83             if (line.toLower().startsWith(QByteArray("content-length: "))) {
84                 bool ok;
85                 mContentLength = line.trimmed().mid(16).toUInt(&ok);
86                 if (!ok) {
87                     sendError(QStringLiteral("Failed to parse content length."));
88                     return;
89                 }
90             } else if (line.toLower().startsWith(QByteArray("authorization: basic "))) {
91                 if (line.trimmed().mid(21) == "dGVzdDp0ZXN0") {
92                     mAuthenticated = true;
93                 }
94             } else if (line.toLower() == "connection: keep-alive\r\n") {
95                 mKeepAlive = true;
96             }
97         } while (!line.trimmed().isEmpty());
98 
99         if (line == "\r\n") {
100             mState = HeadersReceived;
101         }
102     }
103 
104     if (mState == HeadersReceived) {
105         if (mContentLength == 0) {
106             sendError(QStringLiteral("Expected content"));
107             return;
108         }
109 
110         mContent += mSock->read(mContentLength - mContent.size());
111 
112         if (mContent.size() >= static_cast<int>(mContentLength)) {
113             mDataTimer.stop();
114 
115             if (!mAuthenticated) {
116                 QString codeStr = responseCodes.value(401);
117                 QString response(QStringLiteral("HTTP/1.1 %1 %2\r\n"
118                                                 "WWW-Authenticate: Basic realm=\"Fake EWS Server\"\r\n"
119                                                 "Connection: close\r\n"
120                                                 "\r\n")
121                                      .arg(401)
122                                      .arg(codeStr));
123                 response += codeStr;
124                 mSock->write(response.toLatin1());
125                 mSock->disconnectFromHost();
126                 return;
127             }
128 
129             FakeEwsServer::DialogEntry::HttpResponse resp = FakeEwsServer::EmptyResponse;
130 
131             const auto server = qobject_cast<FakeEwsServer *>(parent());
132             const auto overrideReplyCallback = server->overrideReplyCallback();
133             if (overrideReplyCallback) {
134                 QXmlResultItems ri;
135                 QXmlNamePool namePool;
136                 resp = overrideReplyCallback(QString::fromUtf8(mContent), ri, namePool);
137             }
138 
139             if (resp == FakeEwsServer::EmptyResponse) {
140                 resp = parseRequest(QString::fromUtf8(mContent));
141             }
142             bool chunked = false;
143 
144             if (resp == FakeEwsServer::EmptyResponse) {
145                 resp = handleGetEventsRequest(QString::fromUtf8(mContent));
146             }
147 
148             if (resp == FakeEwsServer::EmptyResponse) {
149                 resp = handleGetStreamingEventsRequest(QString::fromUtf8(mContent));
150                 if (resp.second > 1000) {
151                     chunked = true;
152                     resp.second %= 1000;
153                 }
154             }
155 
156             auto defaultReplyCallback = server->defaultReplyCallback();
157             if (defaultReplyCallback && (resp == FakeEwsServer::EmptyResponse)) {
158                 QXmlResultItems ri;
159                 QXmlNamePool namePool;
160                 resp = defaultReplyCallback(QString::fromUtf8(mContent), ri, namePool);
161                 qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning response from default callback ") << resp.second << QStringLiteral(": ") << resp.first;
162             }
163 
164             if (resp == FakeEwsServer::EmptyResponse) {
165                 qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning default response 500.");
166                 resp = {QLatin1String(""), 500};
167             }
168 
169             QByteArray buffer;
170             QString codeStr = responseCodes.value(resp.second);
171             QByteArray respContent = resp.first.toUtf8();
172             buffer += QStringLiteral("HTTP/1.1 %1 %2\r\n").arg(resp.second).arg(codeStr).toLatin1();
173             if (chunked) {
174                 buffer += "Transfer-Encoding: chunked\r\n";
175                 buffer += "\r\n";
176                 buffer += QByteArray::number(respContent.size(), 16) + "\r\n";
177                 buffer += respContent + "\r\n";
178             } else {
179                 buffer += "Content-Length: " + QByteArray::number(respContent.size()) + "\r\n";
180                 buffer += mKeepAlive ? "Connection: Keep-Alive\n" : "Connection: Close\r\n";
181                 buffer += "\r\n";
182                 buffer += respContent;
183             }
184             mSock->write(buffer);
185 
186             if (!mKeepAlive && !chunked) {
187                 mSock->disconnectFromHost();
188             }
189             mContent.clear();
190             mState = Initial;
191         } else {
192             mDataTimer.start(3000);
193         }
194     }
195 }
196 
sendError(const QString & msg,ushort code)197 void FakeEwsConnection::sendError(const QString &msg, ushort code)
198 {
199     qCWarningNC(EWSFAKE_LOG) << msg;
200     QString codeStr = responseCodes.value(code);
201     QByteArray response(QStringLiteral("HTTP/1.1 %1 %2\nConnection: close\n\n").arg(code).arg(codeStr).toLatin1());
202     response += msg.toLatin1();
203     mSock->write(response);
204     mSock->disconnectFromHost();
205 }
206 
dataTimeout()207 void FakeEwsConnection::dataTimeout()
208 {
209     qCWarning(EWSFAKE_LOG) << QLatin1String("Timeout waiting for content.");
210     sendError(QStringLiteral("Timeout waiting for content."));
211 }
212 
parseRequest(const QString & content)213 FakeEwsServer::DialogEntry::HttpResponse FakeEwsConnection::parseRequest(const QString &content)
214 {
215     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got request: ") << content;
216 
217     auto server = qobject_cast<FakeEwsServer *>(parent());
218     FakeEwsServer::DialogEntry::HttpResponse resp = FakeEwsServer::EmptyResponse;
219     const auto dialogs{server->dialog()};
220     for (const FakeEwsServer::DialogEntry &de : dialogs) {
221         QXmlResultItems ri;
222         QByteArray resultBytes;
223         QString result;
224         QBuffer resultBuffer(&resultBytes);
225         resultBuffer.open(QIODevice::WriteOnly);
226         QXmlQuery query;
227         QXmlSerializer xser(query, &resultBuffer);
228         if (!de.xQuery.isNull()) {
229             query.setFocus(content);
230             query.setQuery(de.xQuery);
231             query.evaluateTo(&xser);
232             query.evaluateTo(&ri);
233             if (ri.hasError()) {
234                 qCDebugNC(EWSFAKE_LOG) << QStringLiteral("XQuery failed due to errors - skipping");
235                 continue;
236             }
237             result = QString::fromUtf8(resultBytes);
238         }
239 
240         if (!result.trimmed().isEmpty()) {
241             qCDebugNC(EWSFAKE_LOG) << QStringLiteral("Got match for \"") << de.description << QStringLiteral("\"");
242             if (de.replyCallback) {
243                 resp = de.replyCallback(content, ri, query.namePool());
244                 qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning response from callback ") << resp.second << QStringLiteral(": ") << resp.first;
245             } else {
246                 resp = {result.trimmed(), 200};
247                 qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning response from XQuery ") << resp.second << QStringLiteral(": ") << resp.first;
248             }
249             break;
250         }
251     }
252 
253     if (resp == FakeEwsServer::EmptyResponse) {
254         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning empty response.");
255         qCInfoNC(EWSFAKE_LOG) << content;
256     }
257 
258     return resp;
259 }
260 
handleGetEventsRequest(const QString & content)261 FakeEwsServer::DialogEntry::HttpResponse FakeEwsConnection::handleGetEventsRequest(const QString &content)
262 {
263     const QRegularExpression re(QStringLiteral(
264         "<?xml .*<\\w*:?GetEvents[ "
265         ">].*<\\w*:?SubscriptionId>(?<subid>[^<]*)</\\w*:?SubscriptionId><\\w*:?Watermark>(?<watermark>[^<]*)</\\w*:?Watermark></\\w*:?GetEvents>.*"));
266 
267     QRegularExpressionMatch match = re.match(content);
268     if (!match.hasMatch() || match.hasPartialMatch()) {
269         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Not a valid GetEvents request.");
270         return FakeEwsServer::EmptyResponse;
271     }
272 
273     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got valid GetEvents request.");
274 
275     QString resp = QStringLiteral(
276         "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
277         "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
278         "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
279         "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
280         "xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" "
281         "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
282         "<soap:Header>"
283         "<t:ServerVersionInfo MajorVersion=\"8\" MinorVersion=\"0\" MajorBuildNumber=\"628\" MinorBuildNumber=\"0\" />"
284         "</soap:Header>"
285         "<soap:Body>"
286         "<m:GetEventsResponse xmlns=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
287         "<m:ResponseMessages>"
288         "<m:GetEventsResponseMessage ResponseClass=\"Success\">"
289         "<m:ResponseCode>NoError</m:ResponseCode>"
290         "<m:Notification>");
291 
292     if (match.captured(QStringLiteral("subid")).isEmpty() || match.captured(QStringLiteral("watermark")).isEmpty()) {
293         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Missing subscription id or watermark.");
294         const QString errorResp = QStringLiteral(
295             "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
296             "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
297             "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
298             "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
299             "xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" "
300             "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
301             "<soap:Header>"
302             "<t:ServerVersionInfo MajorVersion=\"8\" MinorVersion=\"0\" MajorBuildNumber=\"628\" MinorBuildNumber=\"0\" />"
303             "</soap:Header>"
304             "<soap:Body>"
305             "<m:GetEventsResponse>"
306             "<m:ResponseMessages>"
307             "<m:GetEventsResponseMessage ResponseClass=\"Error\">"
308             "<m:MessageText>Missing subscription id or watermark.</m:MessageText>"
309             "<m:ResponseCode>ErrorInvalidPullSubscriptionId</m:ResponseCode>"
310             "<m:DescriptiveLinkKey>0</m:DescriptiveLinkKey>"
311             "</m:GetEventsResponseMessage>"
312             "</m:ResponseMessages>"
313             "</m:GetEventsResponse>"
314             "</soap:Body>"
315             "</soap:Envelope>");
316         return {errorResp, 200};
317     }
318 
319     resp += QLatin1String("<SubscriptionId>") + match.captured(QStringLiteral("subid")) + QLatin1String("<SubscriptionId>");
320     resp += QLatin1String("<PreviousWatermark>") + match.captured(QStringLiteral("watermark")) + QLatin1String("<PreviousWatermark>");
321     resp += QStringLiteral("<MoreEvents>false<MoreEvents>");
322 
323     auto server = qobject_cast<FakeEwsServer *>(parent());
324     const QStringList events = server->retrieveEventsXml();
325     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning %1 events.").arg(events.size());
326     for (const QString &eventXml : events) {
327         resp += eventXml;
328     }
329 
330     resp += QStringLiteral(
331         "</m:Notification></m:GetEventsResponseMessage></m:ResponseMessages>"
332         "</m:GetEventsResponse></soap:Body></soap:Envelope>");
333 
334     return {resp, 200};
335 }
336 
prepareEventsResponse(const QStringList & events)337 QString FakeEwsConnection::prepareEventsResponse(const QStringList &events)
338 {
339     QString resp = QStringLiteral(
340         "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
341         "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
342         "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
343         "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
344         "xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" "
345         "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
346         "<soap:Header>"
347         "<t:ServerVersionInfo MajorVersion=\"8\" MinorVersion=\"0\" MajorBuildNumber=\"628\" MinorBuildNumber=\"0\" />"
348         "</soap:Header>"
349         "<soap:Body>"
350         "<m:GetStreamingEventsResponse>"
351         "<m:ResponseMessages>"
352         "<m:GetStreamingEventsResponseMessage ResponseClass=\"Success\">"
353         "<m:ResponseCode>NoError</m:ResponseCode>"
354         "<m:ConnectionStatus>OK</m:ConnectionStatus>");
355 
356     if (!events.isEmpty()) {
357         resp += QLatin1String("<m:Notifications><m:Notification><SubscriptionId>") + mStreamingSubId + QLatin1String("<SubscriptionId>");
358 
359         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning %1 events.").arg(events.size());
360         for (const QString &eventXml : std::as_const(events)) {
361             resp += eventXml;
362         }
363 
364         resp += QStringLiteral("</m:Notification></m:Notifications>");
365     }
366     resp += QStringLiteral(
367         "</m:GetStreamingEventsResponseMessage></m:ResponseMessages>"
368         "</m:GetStreamingEventsResponse></soap:Body></soap:Envelope>");
369 
370     return resp;
371 }
372 
handleGetStreamingEventsRequest(const QString & content)373 FakeEwsServer::DialogEntry::HttpResponse FakeEwsConnection::handleGetStreamingEventsRequest(const QString &content)
374 {
375     const QRegularExpression re(
376         QStringLiteral("<?xml .*<\\w*:?GetStreamingEvents[ "
377                        ">].*<\\w*:?SubscriptionIds><\\w*:?SubscriptionId>(?<subid>[^<]*)</\\w*:?SubscriptionId></"
378                        "\\w*:?SubscriptionIds>.*<\\w*:?ConnectionTimeout>(?<timeout>[^<]*)</\\w*:?ConnectionTimeout></\\w*:?GetStreamingEvents>.*"));
379 
380     QRegularExpressionMatch match = re.match(content);
381     if (!match.hasMatch() || match.hasPartialMatch()) {
382         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Not a valid GetStreamingEvents request.");
383         return FakeEwsServer::EmptyResponse;
384     }
385 
386     qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got valid GetStreamingEvents request.");
387 
388     if (match.captured(QStringLiteral("subid")).isEmpty() || match.captured(QStringLiteral("timeout")).isEmpty()) {
389         qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Missing subscription id or timeout.");
390         const QString errorResp = QStringLiteral(
391             "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
392             "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
393             "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
394             "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
395             "xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" "
396             "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
397             "<soap:Header>"
398             "<t:ServerVersionInfo MajorVersion=\"8\" MinorVersion=\"0\" MajorBuildNumber=\"628\" MinorBuildNumber=\"0\" />"
399             "</soap:Header>"
400             "<soap:Body>"
401             "<m:GetStreamingEventsResponse>"
402             "<m:ResponseMessages>"
403             "<m:GetStreamingEventsResponseMessage ResponseClass=\"Error\">"
404             "<m:MessageText>Missing subscription id or timeout.</m:MessageText>"
405             "<m:ResponseCode>ErrorInvalidSubscription</m:ResponseCode>"
406             "<m:DescriptiveLinkKey>0</m:DescriptiveLinkKey>"
407             "</m:GetEventsResponseMessage>"
408             "</m:ResponseMessages>"
409             "</m:GetEventsResponse>"
410             "</soap:Body>"
411             "</soap:Envelope>");
412         return {errorResp, 200};
413     }
414 
415     mStreamingSubId = match.captured(QStringLiteral("subid"));
416 
417     auto server = qobject_cast<FakeEwsServer *>(parent());
418     const QStringList events = server->retrieveEventsXml();
419 
420     QString resp = prepareEventsResponse(events);
421 
422     mStreamingRequestTimeout.start(match.captured(QStringLiteral("timeout")).toInt() * 1000 * 60);
423     mStreamingRequestHeartbeat.setSingleShot(false);
424     mStreamingRequestHeartbeat.start(streamingEventsHeartbeatIntervalSeconds * 1000);
425 
426     Q_EMIT streamingRequestStarted(this);
427 
428     return {resp, 1200};
429 }
430 
streamingRequestHeartbeat()431 void FakeEwsConnection::streamingRequestHeartbeat()
432 {
433     sendEvents(QStringList());
434 }
435 
streamingRequestTimeout()436 void FakeEwsConnection::streamingRequestTimeout()
437 {
438     mStreamingRequestTimeout.stop();
439     mStreamingRequestHeartbeat.stop();
440     mSock->write("0\r\n\r\n");
441     mSock->disconnectFromHost();
442 }
443 
sendEvents(const QStringList & events)444 void FakeEwsConnection::sendEvents(const QStringList &events)
445 {
446     QByteArray resp = prepareEventsResponse(events).toUtf8();
447 
448     mSock->write(QByteArray::number(resp.size(), 16) + "\r\n" + resp + "\r\n");
449 }
450