QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgis_mapserver.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgs_mapserver.cpp
3
4A QGIS development HTTP server for testing/development purposes.
5The server listens to localhost:8000, the address and port can be changed with the
6environment variable QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT or passing <address>:<port>
7on the command line.
8
9All requests and application messages are printed to the standard output,
10while QGIS server internal logging is printed to stderr.
11
12 -------------------
13 begin : Jan 17 2020
14 copyright : (C) 2020 by Alessandro Pasotti
15 email : elpaso at itopen dot it
16 ***************************************************************************/
17
18/***************************************************************************
19 * *
20 * This program is free software; you can redistribute it and/or modify *
21 * it under the terms of the GNU General Public License as published by *
22 * the Free Software Foundation; either version 2 of the License, or *
23 * (at your option) any later version. *
24 * *
25 ***************************************************************************/
26
27#include <thread>
28#include <string>
29#include <chrono>
30#include <condition_variable>
31
32//for CMAKE_INSTALL_PREFIX
33#include "qgscommandlineutils.h"
34#include "qgsconfig.h"
35#include "qgsserver.h"
38#include "qgsapplication.h"
39#include "qgsmessagelog.h"
40
41#include <QFontDatabase>
42#include <QString>
43#include <QTcpServer>
44#include <QTcpSocket>
45#include <QNetworkInterface>
46#include <QCommandLineParser>
47#include <QObject>
48#include <QQueue>
49#include <QThread>
50#include <QPointer>
51
52#ifndef Q_OS_WIN
53#include <csignal>
54#endif
55
57
58// For the signal exit handler
59QAtomicInt IS_RUNNING = 1;
60
61QString ipAddress;
62QString serverPort;
63
64std::condition_variable REQUEST_WAIT_CONDITION;
65std::mutex REQUEST_QUEUE_MUTEX;
66std::mutex SERVER_MUTEX;
67
68struct RequestContext
69{
70 QPointer<QTcpSocket> clientConnection;
71 QString httpHeader;
72 std::chrono::steady_clock::time_point startTime;
75};
76
77
78QQueue<RequestContext *> REQUEST_QUEUE;
79
80const QMap<int, QString> knownStatuses
81{
82 { 200, QStringLiteral( "OK" ) },
83 { 201, QStringLiteral( "Created" ) },
84 { 202, QStringLiteral( "Accepted" ) },
85 { 204, QStringLiteral( "No Content" ) },
86 { 301, QStringLiteral( "Moved Permanently" ) },
87 { 302, QStringLiteral( "Moved Temporarily" ) },
88 { 304, QStringLiteral( "Not Modified" ) },
89 { 400, QStringLiteral( "Bad Request" ) },
90 { 401, QStringLiteral( "Unauthorized" ) },
91 { 403, QStringLiteral( "Forbidden" ) },
92 { 404, QStringLiteral( "Not Found" ) },
93 { 500, QStringLiteral( "Internal Server Error" ) },
94 { 501, QStringLiteral( "Not Implemented" ) },
95 { 502, QStringLiteral( "Bad Gateway" ) },
96 { 503, QStringLiteral( "Service Unavailable" ) }
97};
98
102class HttpException: public std::exception
103{
104
105 public:
106
110 HttpException( const QString &message )
111 : mMessage( message )
112 {
113 }
114
118 QString message( )
119 {
120 return mMessage;
121 }
122
123 private:
124
125 QString mMessage;
126
127};
128
129
130class TcpServerWorker: public QObject
131{
132 Q_OBJECT
133
134 public:
135
136 TcpServerWorker( const QString &ipAddress, int port )
137 {
138 QHostAddress address { QHostAddress::AnyIPv4 };
139 address.setAddress( ipAddress );
140
141 if ( ! mTcpServer.listen( address, port ) )
142 {
143 std::cerr << tr( "Unable to start the server: %1." )
144 .arg( mTcpServer.errorString() ).toStdString() << std::endl;
145 }
146 else
147 {
148 const int port { mTcpServer.serverPort() };
149
150 std::cout << tr( "QGIS Development Server listening on http://%1:%2" ).arg( ipAddress ).arg( port ).toStdString() << std::endl;
151#ifndef Q_OS_WIN
152 std::cout << tr( "CTRL+C to exit" ).toStdString() << std::endl;
153#endif
154
155 mIsListening = true;
156
157 // Incoming connection handler
158 QTcpServer::connect( &mTcpServer, &QTcpServer::newConnection, this, [ = ]
159 {
160 QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
161
162 mConnectionCounter++;
163
164 //qDebug() << "Active connections: " << mConnectionCounter;
165
166 QString *incomingData = new QString();
167
168 // Lambda disconnect context
169 QObject *context { new QObject };
170
171 // Deletes the connection later
172 auto connectionDeleter = [ = ]()
173 {
174 clientConnection->deleteLater();
175 mConnectionCounter--;
176 delete incomingData;
177 };
178
179 // This will delete the connection
180 QTcpSocket::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
181
182#if 0 // Debugging output
183 clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, [ = ]( QAbstractSocket::SocketError socketError )
184 {
185 qDebug() << "Socket error #" << socketError;
186 }, Qt::QueuedConnection );
187#endif
188
189 // Incoming connection parser
190 QTcpSocket::connect( clientConnection, &QIODevice::readyRead, context, [ = ] {
191
192 // Read all incoming data
193 while ( clientConnection->bytesAvailable() > 0 )
194 {
195 incomingData->append( clientConnection->readAll() );
196 }
197
198 try
199 {
200 // Parse protocol and URL GET /path HTTP/1.1
201 const auto firstLinePos { incomingData->indexOf( "\r\n" ) };
202 if ( firstLinePos == -1 )
203 {
204 throw HttpException( QStringLiteral( "HTTP error finding protocol header" ) );
205 }
206
207 const QString firstLine { incomingData->left( firstLinePos ) };
208 const QStringList firstLinePieces { firstLine.split( ' ' ) };
209 if ( firstLinePieces.size() != 3 )
210 {
211 throw HttpException( QStringLiteral( "HTTP error splitting protocol header" ) );
212 }
213
214 const QString methodString { firstLinePieces.at( 0 ) };
215
217 if ( methodString == "GET" )
218 {
219 method = QgsServerRequest::Method::GetMethod;
220 }
221 else if ( methodString == "POST" )
222 {
223 method = QgsServerRequest::Method::PostMethod;
224 }
225 else if ( methodString == "HEAD" )
226 {
227 method = QgsServerRequest::Method::HeadMethod;
228 }
229 else if ( methodString == "PUT" )
230 {
231 method = QgsServerRequest::Method::PutMethod;
232 }
233 else if ( methodString == "PATCH" )
234 {
235 method = QgsServerRequest::Method::PatchMethod;
236 }
237 else if ( methodString == "DELETE" )
238 {
239 method = QgsServerRequest::Method::DeleteMethod;
240 }
241 else
242 {
243 throw HttpException( QStringLiteral( "HTTP error unsupported method: %1" ).arg( methodString ) );
244 }
245
246 // cppcheck-suppress containerOutOfBounds
247 const QString protocol { firstLinePieces.at( 2 )};
248 if ( protocol != QLatin1String( "HTTP/1.0" ) && protocol != QLatin1String( "HTTP/1.1" ) )
249 {
250 throw HttpException( QStringLiteral( "HTTP error unsupported protocol: %1" ).arg( protocol ) );
251 }
252
253 // Headers
255 const auto endHeadersPos { incomingData->indexOf( "\r\n\r\n" ) };
256
257 if ( endHeadersPos == -1 )
258 {
259 throw HttpException( QStringLiteral( "HTTP error finding headers" ) );
260 }
261
262 const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split( "\r\n" ) };
263
264 for ( const auto &headerLine : httpHeaders )
265 {
266 const auto headerColonPos { headerLine.indexOf( ':' ) };
267 if ( headerColonPos > 0 )
268 {
269 headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
270 }
271 }
272
273 const auto headersSize { endHeadersPos + 4 };
274
275 // Check for content length and if we have got all data
276 if ( headers.contains( QStringLiteral( "Content-Length" ) ) )
277 {
278 bool ok;
279 const int contentLength { headers.value( QStringLiteral( "Content-Length" ) ).toInt( &ok ) };
280 if ( ok && contentLength > incomingData->length() - headersSize )
281 {
282 return;
283 }
284 }
285
286 // At this point we should have read all data:
287 // disconnect the lambdas
288 delete context;
289
290 // Build URL from env ...
291 QString url { qgetenv( "REQUEST_URI" ) };
292 // ... or from server ip/port and request path
293 if ( url.isEmpty() )
294 {
295 // cppcheck-suppress containerOutOfBounds
296 const QString path { firstLinePieces.at( 1 )};
297 // Take Host header if defined
298 if ( headers.contains( QStringLiteral( "Host" ) ) )
299 {
300 url = QStringLiteral( "http://%1%2" ).arg( headers.value( QStringLiteral( "Host" ) ), path );
301 }
302 else
303 {
304 url = QStringLiteral( "http://%1:%2%3" ).arg( ipAddress ).arg( port ).arg( path );
305 }
306 }
307
308 // Inefficient copy :(
309 QByteArray data { incomingData->mid( headersSize ).toUtf8() };
310
311 if ( !incomingData->isEmpty() && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
312 {
313 auto requestContext = new RequestContext
314 {
315 clientConnection,
316 firstLinePieces.join( ' ' ),
317 std::chrono::steady_clock::now(),
318 { url, method, headers, &data },
319 {},
320 } ;
321 REQUEST_QUEUE_MUTEX.lock();
322 REQUEST_QUEUE.enqueue( requestContext );
323 REQUEST_QUEUE_MUTEX.unlock();
324 REQUEST_WAIT_CONDITION.notify_one();
325 }
326 }
327 catch ( HttpException &ex )
328 {
329 if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
330 {
331 // Output stream: send error
332 clientConnection->write( QStringLiteral( "HTTP/1.0 %1 %2\r\n" ).arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
333 clientConnection->write( QStringLiteral( "Server: QGIS\r\n" ).toUtf8() );
334 clientConnection->write( "\r\n" );
335 clientConnection->write( ex.message().toUtf8() );
336
337 std::cout << QStringLiteral( "\033[1;31m%1 [%2] \"%3\" - - 500\033[0m" )
338 .arg( clientConnection->peerAddress().toString() )
339 .arg( QDateTime::currentDateTime().toString() )
340 .arg( ex.message() ).toStdString() << std::endl;
341
342 clientConnection->disconnectFromHost();
343 }
344 }
345 } );
346 } );
347 }
348 }
349
350 ~TcpServerWorker()
351 {
352 mTcpServer.close();
353 }
354
355 bool isListening() const
356 {
357 return mIsListening;
358 }
359
360 public slots:
361
362 // Outgoing connection handler
363 void responseReady( RequestContext *requestContext ) //#spellok
364 {
365 std::unique_ptr<RequestContext> request { requestContext };
366 const auto elapsedTime { std::chrono::steady_clock::now() - request->startTime };
367
368 const auto &response { request->response };
369 const auto &clientConnection { request->clientConnection };
370
371 if ( ! clientConnection ||
372 clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
373 {
374 std::cout << "Connection reset by peer" << std::endl;
375 return;
376 }
377
378 // Output stream
379 if ( -1 == clientConnection->write( QStringLiteral( "HTTP/1.0 %1 %2\r\n" ).arg( response.statusCode() ).arg( knownStatuses.value( response.statusCode(), QStringLiteral( "Unknown response code" ) ) ).toUtf8() ) )
380 {
381 std::cout << "Cannot write to output socket" << std::endl;
382 clientConnection->disconnectFromHost();
383 return;
384 }
385
386 clientConnection->write( QStringLiteral( "Server: QGIS\r\n" ).toUtf8() );
387 const auto responseHeaders { response.headers() };
388 for ( auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
389 {
390 clientConnection->write( QStringLiteral( "%1: %2\r\n" ).arg( it.key(), it.value() ).toUtf8() );
391 }
392 clientConnection->write( "\r\n" );
393 const QByteArray body { response.body() };
394 clientConnection->write( body );
395
396 // 10.185.248.71 [09/Jan/2015:19:12:06 +0000] 808840 <time> "GET / HTTP/1.1" 500"
397 std::cout << QStringLiteral( "\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m" )
398 .arg( clientConnection->peerAddress().toString(),
399 QDateTime::currentDateTime().toString(),
400 QString::number( body.size() ),
401 QString::number( std::chrono::duration_cast<std::chrono::milliseconds>( elapsedTime ).count() ),
402 request->httpHeader,
403 QString::number( response.statusCode() ) )
404 .toStdString()
405 << std::endl;
406
407 // This will trigger delete later on the socket object
408 clientConnection->disconnectFromHost();
409 }
410
411 private:
412
413 QTcpServer mTcpServer;
414 qlonglong mConnectionCounter = 0;
415 bool mIsListening = false;
416
417};
418
419
420class TcpServerThread: public QThread
421{
422 Q_OBJECT
423
424 public:
425
426 TcpServerThread( const QString &ipAddress, const int port )
427 : mIpAddress( ipAddress )
428 , mPort( port )
429 {
430 }
431
432 void emitResponseReady( RequestContext *requestContext ) //#spellok
433 {
434 if ( requestContext->clientConnection )
435 emit responseReady( requestContext ); //#spellok
436 }
437
438 void run( )
439 {
440 const TcpServerWorker worker( mIpAddress, mPort );
441 if ( ! worker.isListening() )
442 {
443 emit serverError();
444 }
445 else
446 {
447 // Forward signal to worker
448 connect( this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady ); //#spellok
449 QThread::run();
450 }
451 }
452
453 signals:
454
455 void responseReady( RequestContext *requestContext ); //#spellok
456 void serverError( );
457
458 private:
459
460 QString mIpAddress;
461 int mPort;
462};
463
464
465class QueueMonitorThread: public QThread
466{
467
468 Q_OBJECT
469
470 public:
471 void run( )
472 {
473 while ( mIsRunning )
474 {
475 std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
476 REQUEST_WAIT_CONDITION.wait( requestLocker, [ = ] { return ! mIsRunning || ! REQUEST_QUEUE.isEmpty(); } );
477 if ( mIsRunning )
478 {
479 // Lock if server is running
480 SERVER_MUTEX.lock();
481 emit requestReady( REQUEST_QUEUE.dequeue() );
482 }
483 }
484 }
485
486 signals:
487
488 void requestReady( RequestContext *requestContext );
489
490 public slots:
491
492 void stop()
493 {
494 mIsRunning = false;
495 }
496
497 private:
498
499 bool mIsRunning = true;
500
501};
502
503int main( int argc, char *argv[] )
504{
505 // Test if the environ variable DISPLAY is defined
506 // if it's not, the server is running in offscreen mode
507 // Qt supports using various QPA (Qt Platform Abstraction) back ends
508 // for rendering. You can specify the back end to use with the environment
509 // variable QT_QPA_PLATFORM when invoking a Qt-based application.
510 // Available platform plugins are: directfbegl, directfb, eglfs, linuxfb,
511 // minimal, minimalegl, offscreen, wayland-egl, wayland, xcb.
512 // https://www.ics.com/blog/qt-tips-and-tricks-part-1
513 // http://doc.qt.io/qt-5/qpa.html
514 const QString display { qgetenv( "DISPLAY" ) };
515 bool withDisplay = true;
516 if ( display.isEmpty() )
517 {
518 withDisplay = false;
519 qputenv( "QT_QPA_PLATFORM", "offscreen" );
520 }
521
522 // since version 3.0 QgsServer now needs a qApp so initialize QgsApplication
523 const QgsApplication app( argc, argv, withDisplay, QString(), QStringLiteral( "QGIS Development Server" ) );
524
525 QCoreApplication::setOrganizationName( QgsApplication::QGIS_ORGANIZATION_NAME );
526 QCoreApplication::setOrganizationDomain( QgsApplication::QGIS_ORGANIZATION_DOMAIN );
527 QCoreApplication::setApplicationName( "QGIS Development Server" );
528 QCoreApplication::setApplicationVersion( VERSION );
529
530 if ( ! withDisplay )
531 {
532 QgsMessageLog::logMessage( "DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
533 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.", "Server", Qgis::MessageLevel::Warning );
534 }
535
536#ifdef Q_OS_WIN
537 // Initialize font database before fcgi_accept.
538 // When using FCGI with IIS, environment variables (QT_QPA_FONTDIR in this case) are lost after fcgi_accept().
539 QFontDatabase fontDB;
540#endif
541
542 // The port to listen
543 serverPort = qgetenv( "QGIS_SERVER_PORT" );
544 // The address to listen
545 ipAddress = qgetenv( "QGIS_SERVER_ADDRESS" );
546
547 if ( serverPort.isEmpty() )
548 {
549 serverPort = QStringLiteral( "8000" );
550 }
551
552 if ( ipAddress.isEmpty() )
553 {
554 ipAddress = QStringLiteral( "localhost" );
555 }
556
557 QCommandLineParser parser;
558 parser.setApplicationDescription( QObject::tr( "QGIS Development Server %1" ).arg( VERSION ) );
559 parser.addHelpOption();
560
561 const QCommandLineOption versionOption( QStringList() << "v" << "version", QObject::tr( "Version of QGIS and libraries" ) );
562 parser.addOption( versionOption );
563
564 parser.addPositionalArgument( QStringLiteral( "addressAndPort" ),
565 QObject::tr( "Address and port (default: \"localhost:8000\")\n"
566 "address and port can also be specified with the environment\n"
567 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT." ), QStringLiteral( "[address:port]" ) );
568 const QCommandLineOption logLevelOption( "l", QObject::tr( "Log level (default: 0)\n"
569 "0: INFO\n"
570 "1: WARNING\n"
571 "2: CRITICAL" ), "logLevel", "0" );
572 parser.addOption( logLevelOption );
573
574 const QCommandLineOption projectOption( "p", QObject::tr( "Path to a QGIS project file (*.qgs or *.qgz),\n"
575 "if specified it will override the query string MAP argument\n"
576 "and the QGIS_PROJECT_FILE environment variable." ), "projectPath", "" );
577 parser.addOption( projectOption );
578
579 parser.process( app );
580
581 if ( parser.isSet( versionOption ) )
582 {
583 std::cout << QgsCommandLineUtils::allVersions().toStdString();
584 return 0;
585 }
586
587 const QStringList args = parser.positionalArguments();
588
589 if ( args.size() == 1 )
590 {
591 const QStringList addressAndPort { args.at( 0 ).split( ':' ) };
592 if ( addressAndPort.size() == 2 )
593 {
594 ipAddress = addressAndPort.at( 0 );
595 // cppcheck-suppress containerOutOfBounds
596 serverPort = addressAndPort.at( 1 );
597 }
598 }
599
600 const QString logLevel = parser.value( logLevelOption );
601 qunsetenv( "QGIS_SERVER_LOG_FILE" );
602 qputenv( "QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
603 qputenv( "QGIS_SERVER_LOG_STDERR", "1" );
604
605 QgsServer server;
606
607 if ( ! parser.value( projectOption ).isEmpty( ) )
608 {
609 // Check it!
610 const QString projectFilePath { parser.value( projectOption ) };
611 if ( ! QgsProject::instance()->read( projectFilePath,
616 {
617 std::cout << QObject::tr( "Project file not found, the option will be ignored." ).toStdString() << std::endl;
618 }
619 else
620 {
621 qputenv( "QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
622 }
623 }
624
625 // Disable parallel rendering because if its internal loop
626 //qputenv( "QGIS_SERVER_PARALLEL_RENDERING", "0" );
627
628
629#ifdef HAVE_SERVER_PYTHON_PLUGINS
630 server.initPython();
631#endif
632
633 // TCP thread
634 TcpServerThread tcpServerThread{ ipAddress, serverPort.toInt() };
635
636 bool isTcpError = false;
637 TcpServerThread::connect( &tcpServerThread, &TcpServerThread::serverError, qApp, [ & ]
638 {
639 isTcpError = true;
640 qApp->quit();
641 }, Qt::QueuedConnection );
642
643 // Monitoring thread
644 QueueMonitorThread queueMonitorThread;
645 QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [ & ]( RequestContext * requestContext )
646 {
647 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
648 {
649 server.handleRequest( requestContext->request, requestContext->response );
650 SERVER_MUTEX.unlock();
651 }
652 else
653 {
654 delete requestContext;
655 SERVER_MUTEX.unlock();
656 return;
657 }
658 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
659 tcpServerThread.emitResponseReady( requestContext ); //#spellok
660 else
661 delete requestContext;
662 } );
663
664 // Exit handlers
665#ifndef Q_OS_WIN
666
667 auto exitHandler = [ ]( int signal )
668 {
669 std::cout << QStringLiteral( "Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
670 IS_RUNNING = 0;
671 qApp->quit( );
672 };
673
674 signal( SIGTERM, exitHandler );
675 signal( SIGABRT, exitHandler );
676 signal( SIGINT, exitHandler );
677 signal( SIGPIPE, [ ]( int )
678 {
679 std::cerr << QStringLiteral( "Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
680 } );
681
682#endif
683
684 tcpServerThread.start();
685 queueMonitorThread.start();
686
687 QgsApplication::exec();
688 // Wait for threads
689 tcpServerThread.exit();
690 tcpServerThread.wait();
691 queueMonitorThread.stop();
692 REQUEST_WAIT_CONDITION.notify_all();
693 queueMonitorThread.wait();
695
696 return isTcpError ? 1 : 0;
697}
698
699#include "qgis_mapserver.moc"
700
702
703
@ DontLoad3DViews
Skip loading 3D views (since QGIS 3.26)
@ DontStoreOriginalStyles
Skip the initial XML style storage for layers. Useful for minimising project load times in non-intera...
@ DontLoadLayouts
Don't load print layouts. Improves project read time if layouts are not required, and allows projects...
@ DontResolveLayers
Don't resolve layer paths (i.e. don't load any layer content). Dramatically improves project read tim...
Extends QApplication to provide access to QGIS specific resources such as theme paths,...
static void exitQgis()
deletes provider registry and map layer registry
static const char * QGIS_ORGANIZATION_DOMAIN
static const char * QGIS_ORGANIZATION_NAME
Class defining request with data.
Class defining buffered response.
static QString allVersions()
Display all versions in the standard output stream.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:481
Method
HTTP Method (or equivalent) used for the request.
QMap< QString, QString > Headers
The QgsServer class provides OGC web services.
Definition: qgsserver.h:49
int main(int argc, char *argv[])