Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgsblockingnetworkrequest.cpp
3 : : -----------------------------
4 : : begin : November 2018
5 : : copyright : (C) 2018 by Nyall Dawson
6 : : email : nyall dot dawson at gmail dot com
7 : : ***************************************************************************
8 : : * *
9 : : * This program is free software; you can redistribute it and/or modify *
10 : : * it under the terms of the GNU General Public License as published by *
11 : : * the Free Software Foundation; either version 2 of the License, or *
12 : : * (at your option) any later version. *
13 : : * *
14 : : ***************************************************************************/
15 : :
16 : : #include "qgsblockingnetworkrequest.h"
17 : : #include "qgslogger.h"
18 : : #include "qgsapplication.h"
19 : : #include "qgsnetworkaccessmanager.h"
20 : : #include "qgsauthmanager.h"
21 : : #include "qgsmessagelog.h"
22 : : #include "qgsfeedback.h"
23 : : #include <QUrl>
24 : : #include <QNetworkRequest>
25 : : #include <QNetworkReply>
26 : : #include <QMutex>
27 : : #include <QWaitCondition>
28 : : #include <QNetworkCacheMetaData>
29 : : #include <QAuthenticator>
30 : :
31 : 0 : QgsBlockingNetworkRequest::QgsBlockingNetworkRequest()
32 : 0 : {
33 : 0 : connect( QgsNetworkAccessManager::instance(), qOverload< QNetworkReply * >( &QgsNetworkAccessManager::requestTimedOut ), this, &QgsBlockingNetworkRequest::requestTimedOut );
34 : 0 : }
35 : :
36 : 0 : QgsBlockingNetworkRequest::~QgsBlockingNetworkRequest()
37 : 0 : {
38 : 0 : abort();
39 : 0 : }
40 : :
41 : 0 : void QgsBlockingNetworkRequest::requestTimedOut( QNetworkReply *reply )
42 : : {
43 : 0 : if ( reply == mReply )
44 : 0 : mTimedout = true;
45 : 0 : }
46 : :
47 : 0 : QString QgsBlockingNetworkRequest::authCfg() const
48 : : {
49 : 0 : return mAuthCfg;
50 : : }
51 : :
52 : 0 : void QgsBlockingNetworkRequest::setAuthCfg( const QString &authCfg )
53 : : {
54 : 0 : mAuthCfg = authCfg;
55 : 0 : }
56 : :
57 : 0 : QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::get( QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback )
58 : : {
59 : 0 : return doRequest( Get, request, forceRefresh, feedback );
60 : : }
61 : :
62 : 0 : QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::post( QNetworkRequest &request, const QByteArray &data, bool forceRefresh, QgsFeedback *feedback )
63 : : {
64 : 0 : mPayloadData = data;
65 : 0 : return doRequest( Post, request, forceRefresh, feedback );
66 : : }
67 : :
68 : 0 : QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::head( QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback )
69 : : {
70 : 0 : return doRequest( Head, request, forceRefresh, feedback );
71 : : }
72 : :
73 : 0 : QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::put( QNetworkRequest &request, const QByteArray &data, QgsFeedback *feedback )
74 : : {
75 : 0 : mPayloadData = data;
76 : 0 : return doRequest( Put, request, true, feedback );
77 : : }
78 : :
79 : 0 : QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::deleteResource( QNetworkRequest &request, QgsFeedback *feedback )
80 : : {
81 : 0 : return doRequest( Delete, request, true, feedback );
82 : : }
83 : :
84 : 0 : void QgsBlockingNetworkRequest::sendRequestToNetworkAccessManager( const QNetworkRequest &request )
85 : : {
86 : 0 : switch ( mMethod )
87 : : {
88 : : case Get:
89 : 0 : mReply = QgsNetworkAccessManager::instance()->get( request );
90 : 0 : break;
91 : :
92 : : case Post:
93 : 0 : mReply = QgsNetworkAccessManager::instance()->post( request, mPayloadData );
94 : 0 : break;
95 : :
96 : : case Head:
97 : 0 : mReply = QgsNetworkAccessManager::instance()->head( request );
98 : 0 : break;
99 : :
100 : : case Put:
101 : 0 : mReply = QgsNetworkAccessManager::instance()->put( request, mPayloadData );
102 : 0 : break;
103 : :
104 : : case Delete:
105 : 0 : mReply = QgsNetworkAccessManager::instance()->deleteResource( request );
106 : 0 : break;
107 : : };
108 : 0 : }
109 : :
110 : 0 : QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::doRequest( QgsBlockingNetworkRequest::Method method, QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback )
111 : : {
112 : 0 : mMethod = method;
113 : 0 : mFeedback = feedback;
114 : :
115 : 0 : abort(); // cancel previous
116 : 0 : mIsAborted = false;
117 : 0 : mTimedout = false;
118 : 0 : mGotNonEmptyResponse = false;
119 : :
120 : 0 : mErrorMessage.clear();
121 : 0 : mErrorCode = NoError;
122 : 0 : mForceRefresh = forceRefresh;
123 : 0 : mReplyContent.clear();
124 : :
125 : 0 : if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
126 : : {
127 : 0 : mErrorCode = NetworkError;
128 : 0 : mErrorMessage = errorMessageFailedAuth();
129 : 0 : QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
130 : 0 : return NetworkError;
131 : : }
132 : :
133 : 0 : QgsDebugMsgLevel( QStringLiteral( "Calling: %1" ).arg( request.url().toString() ), 2 );
134 : :
135 : 0 : request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, forceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
136 : 0 : request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
137 : :
138 : 0 : QWaitCondition authRequestBufferNotEmpty;
139 : 0 : QMutex waitConditionMutex;
140 : :
141 : 0 : bool threadFinished = false;
142 : 0 : bool success = false;
143 : :
144 : 0 : const bool requestMadeFromMainThread = QThread::currentThread() == QApplication::instance()->thread();
145 : :
146 : 0 : if ( mFeedback )
147 : 0 : connect( mFeedback, &QgsFeedback::canceled, this, &QgsBlockingNetworkRequest::abort );
148 : :
149 : 0 : std::function<void()> downloaderFunction = [ this, request, &waitConditionMutex, &authRequestBufferNotEmpty, &threadFinished, &success, requestMadeFromMainThread ]()
150 : : {
151 : : // this function will always be run in worker threads -- either the blocking call is being made in a worker thread,
152 : : // or the blocking call has been made from the main thread and we've fired up a new thread for this function
153 : : Q_ASSERT( QThread::currentThread() != QgsApplication::instance()->thread() );
154 : :
155 : 0 : QgsNetworkAccessManager::instance( Qt::DirectConnection );
156 : :
157 : 0 : success = true;
158 : :
159 : 0 : sendRequestToNetworkAccessManager( request );
160 : :
161 : 0 : if ( mFeedback )
162 : 0 : connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
163 : :
164 : 0 : if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
165 : : {
166 : 0 : mErrorCode = NetworkError;
167 : 0 : mErrorMessage = errorMessageFailedAuth();
168 : 0 : QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
169 : 0 : if ( requestMadeFromMainThread )
170 : 0 : authRequestBufferNotEmpty.wakeAll();
171 : 0 : success = false;
172 : 0 : }
173 : : else
174 : : {
175 : : // We are able to use direct connection here, because we
176 : : // * either run on the thread mReply lives in, so DirectConnection is standard and safe anyway
177 : : // * or the owner thread of mReply is currently not doing anything because it's blocked in future.waitForFinished() (if it is the main thread)
178 : 0 : connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
179 : 0 : connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
180 : :
181 : 0 : auto resumeMainThread = [&waitConditionMutex, &authRequestBufferNotEmpty ]()
182 : : {
183 : : // when this method is called we have "produced" a single authentication request -- so the buffer is now full
184 : : // and it's time for the "consumer" (main thread) to do its part
185 : 0 : waitConditionMutex.lock();
186 : 0 : authRequestBufferNotEmpty.wakeAll();
187 : 0 : waitConditionMutex.unlock();
188 : :
189 : : // note that we don't need to handle waking this thread back up - that's done automatically by QgsNetworkAccessManager
190 : 0 : };
191 : :
192 : 0 : if ( requestMadeFromMainThread )
193 : : {
194 : 0 : connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::authRequestOccurred, this, resumeMainThread, Qt::DirectConnection );
195 : 0 : connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::proxyAuthenticationRequired, this, resumeMainThread, Qt::DirectConnection );
196 : :
197 : : #ifndef QT_NO_SSL
198 : 0 : connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::sslErrorsOccurred, this, resumeMainThread, Qt::DirectConnection );
199 : : #endif
200 : 0 : }
201 : 0 : QEventLoop loop;
202 : : // connecting to aboutToQuit avoids an on-going request to remain stalled
203 : : // when QThreadPool::globalInstance()->waitForDone()
204 : : // is called at process termination
205 : 0 : connect( qApp, &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit, Qt::DirectConnection );
206 : 0 : connect( this, &QgsBlockingNetworkRequest::downloadFinished, &loop, &QEventLoop::quit, Qt::DirectConnection );
207 : 0 : loop.exec();
208 : 0 : }
209 : :
210 : 0 : if ( requestMadeFromMainThread )
211 : : {
212 : 0 : waitConditionMutex.lock();
213 : 0 : threadFinished = true;
214 : 0 : authRequestBufferNotEmpty.wakeAll();
215 : 0 : waitConditionMutex.unlock();
216 : 0 : }
217 : 0 : };
218 : :
219 : 0 : if ( requestMadeFromMainThread )
220 : : {
221 : 0 : std::unique_ptr<DownloaderThread> downloaderThread = std::make_unique<DownloaderThread>( downloaderFunction );
222 : 0 : downloaderThread->start();
223 : :
224 : 0 : while ( true )
225 : : {
226 : 0 : waitConditionMutex.lock();
227 : 0 : if ( threadFinished )
228 : : {
229 : 0 : waitConditionMutex.unlock();
230 : 0 : break;
231 : : }
232 : 0 : authRequestBufferNotEmpty.wait( &waitConditionMutex );
233 : :
234 : : // If the downloader thread wakes us (the main thread) up and is not yet finished
235 : : // then it has "produced" an authentication request which we need to now "consume".
236 : : // The processEvents() call gives the auth manager the chance to show a dialog and
237 : : // once done with that, we can wake the downloaderThread again and continue the download.
238 : 0 : if ( !threadFinished )
239 : 0 : {
240 : 0 : waitConditionMutex.unlock();
241 : :
242 : 0 : QgsApplication::instance()->processEvents();
243 : : // we don't need to wake up the worker thread - it will automatically be woken when
244 : 0 : // the auth request has been dealt with by QgsNetworkAccessManager
245 : 0 : }
246 : : else
247 : 0 : {
248 : 0 : waitConditionMutex.unlock();
249 : : }
250 : 0 : }
251 : : // wait for thread to gracefully exit
252 : 0 : downloaderThread->wait();
253 : 0 : }
254 : : else
255 : 0 : {
256 : 0 : downloaderFunction();
257 : : }
258 : 0 : return mErrorCode;
259 : 0 : }
260 : :
261 : 0 : void QgsBlockingNetworkRequest::abort()
262 : : {
263 : 0 : mIsAborted = true;
264 : 0 : if ( mReply )
265 : : {
266 : 0 : mReply->deleteLater();
267 : 0 : mReply = nullptr;
268 : 0 : }
269 : 0 : }
270 : :
271 : 0 : void QgsBlockingNetworkRequest::replyProgress( qint64 bytesReceived, qint64 bytesTotal )
272 : : {
273 : 0 : QgsDebugMsgLevel( QStringLiteral( "%1 of %2 bytes downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QStringLiteral( "unknown number of" ) : QString::number( bytesTotal ) ), 2 );
274 : :
275 : 0 : if ( bytesReceived != 0 )
276 : 0 : mGotNonEmptyResponse = true;
277 : :
278 : 0 : if ( !mIsAborted && mReply && ( !mFeedback || !mFeedback->isCanceled() ) )
279 : : {
280 : 0 : if ( mReply->error() == QNetworkReply::NoError )
281 : : {
282 : 0 : QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
283 : 0 : if ( !redirect.isNull() )
284 : : {
285 : : // We don't want to emit downloadProgress() for a redirect
286 : 0 : return;
287 : : }
288 : 0 : }
289 : 0 : }
290 : :
291 : 0 : emit downloadProgress( bytesReceived, bytesTotal );
292 : 0 : }
293 : :
294 : 0 : void QgsBlockingNetworkRequest::replyFinished()
295 : : {
296 : 0 : if ( !mIsAborted && mReply )
297 : : {
298 : :
299 : 0 : if ( mReply->error() == QNetworkReply::NoError && ( !mFeedback || !mFeedback->isCanceled() ) )
300 : : {
301 : 0 : QgsDebugMsgLevel( QStringLiteral( "reply OK" ), 2 );
302 : 0 : QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
303 : 0 : if ( !redirect.isNull() )
304 : : {
305 : 0 : QgsDebugMsgLevel( QStringLiteral( "Request redirected." ), 2 );
306 : :
307 : 0 : const QUrl &toUrl = redirect.toUrl();
308 : 0 : mReply->request();
309 : 0 : if ( toUrl == mReply->url() )
310 : : {
311 : 0 : mErrorMessage = tr( "Redirect loop detected: %1" ).arg( toUrl.toString() );
312 : 0 : QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
313 : 0 : mReplyContent.clear();
314 : 0 : }
315 : : else
316 : : {
317 : 0 : QNetworkRequest request( toUrl );
318 : :
319 : 0 : if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
320 : : {
321 : 0 : mReplyContent.clear();
322 : 0 : mErrorMessage = errorMessageFailedAuth();
323 : 0 : mErrorCode = NetworkError;
324 : 0 : QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
325 : 0 : emit downloadFinished();
326 : 0 : return;
327 : : }
328 : :
329 : 0 : request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, mForceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
330 : 0 : request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
331 : :
332 : 0 : mReply->deleteLater();
333 : 0 : mReply = nullptr;
334 : :
335 : 0 : QgsDebugMsgLevel( QStringLiteral( "redirected: %1 forceRefresh=%2" ).arg( redirect.toString() ).arg( mForceRefresh ), 2 );
336 : :
337 : 0 : sendRequestToNetworkAccessManager( request );
338 : :
339 : 0 : if ( mFeedback )
340 : 0 : connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
341 : :
342 : 0 : if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
343 : : {
344 : 0 : mReplyContent.clear();
345 : 0 : mErrorMessage = errorMessageFailedAuth();
346 : 0 : mErrorCode = NetworkError;
347 : 0 : QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
348 : 0 : emit downloadFinished();
349 : 0 : return;
350 : : }
351 : :
352 : 0 : connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
353 : 0 : connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
354 : 0 : return;
355 : 0 : }
356 : 0 : }
357 : : else
358 : : {
359 : 0 : const QgsNetworkAccessManager *nam = QgsNetworkAccessManager::instance();
360 : :
361 : 0 : if ( nam->cache() )
362 : : {
363 : 0 : QNetworkCacheMetaData cmd = nam->cache()->metaData( mReply->request().url() );
364 : :
365 : 0 : QNetworkCacheMetaData::RawHeaderList hl;
366 : 0 : const auto constRawHeaders = cmd.rawHeaders();
367 : 0 : for ( const QNetworkCacheMetaData::RawHeader &h : constRawHeaders )
368 : : {
369 : 0 : if ( h.first != "Cache-Control" )
370 : 0 : hl.append( h );
371 : : }
372 : 0 : cmd.setRawHeaders( hl );
373 : :
374 : 0 : QgsDebugMsgLevel( QStringLiteral( "expirationDate:%1" ).arg( cmd.expirationDate().toString() ), 2 );
375 : 0 : if ( cmd.expirationDate().isNull() )
376 : : {
377 : 0 : cmd.setExpirationDate( QDateTime::currentDateTime().addSecs( mExpirationSec ) );
378 : 0 : }
379 : :
380 : 0 : nam->cache()->updateMetaData( cmd );
381 : 0 : }
382 : : else
383 : : {
384 : 0 : QgsDebugMsgLevel( QStringLiteral( "No cache!" ), 2 );
385 : : }
386 : :
387 : : #ifdef QGISDEBUG
388 : : bool fromCache = mReply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ).toBool();
389 : : QgsDebugMsgLevel( QStringLiteral( "Reply was cached: %1" ).arg( fromCache ), 2 );
390 : : #endif
391 : :
392 : 0 : mReplyContent = QgsNetworkReplyContent( mReply );
393 : 0 : const QByteArray content = mReply->readAll();
394 : 0 : if ( content.isEmpty() && !mGotNonEmptyResponse && mMethod == Get )
395 : : {
396 : 0 : mErrorMessage = tr( "empty response: %1" ).arg( mReply->errorString() );
397 : 0 : mErrorCode = ServerExceptionError;
398 : 0 : QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
399 : 0 : }
400 : 0 : mReplyContent.setContent( content );
401 : 0 : }
402 : 0 : }
403 : : else
404 : : {
405 : 0 : if ( mReply->error() != QNetworkReply::OperationCanceledError )
406 : : {
407 : 0 : mErrorMessage = mReply->errorString();
408 : 0 : mErrorCode = ServerExceptionError;
409 : 0 : QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
410 : 0 : }
411 : 0 : mReplyContent = QgsNetworkReplyContent( mReply );
412 : 0 : mReplyContent.setContent( mReply->readAll() );
413 : : }
414 : 0 : }
415 : 0 : if ( mTimedout )
416 : 0 : mErrorCode = TimeoutError;
417 : :
418 : 0 : if ( mReply )
419 : : {
420 : 0 : mReply->deleteLater();
421 : 0 : mReply = nullptr;
422 : 0 : }
423 : :
424 : 0 : emit downloadFinished();
425 : 0 : }
426 : :
427 : 0 : QString QgsBlockingNetworkRequest::errorMessageFailedAuth()
428 : : {
429 : 0 : return tr( "network request update failed for authentication config" );
430 : : }
|