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