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