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