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