QGIS API Documentation 3.37.0-Master (614ebf5f6b2)
Loading...
Searching...
No Matches
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 {
220 }
221 else if ( methodString == "POST" )
222 {
224 }
225 else if ( methodString == "HEAD" )
226 {
228 }
229 else if ( methodString == "PUT" )
230 {
232 }
233 else if ( methodString == "PATCH" )
234 {
236 }
237 else if ( methodString == "DELETE" )
238 {
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...
@ Warning
Warning message.
Definition qgis.h:101
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.
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[])