QGIS API Documentation 4.1.0-Master (31622b25bb0)
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
118 QString message() const { return mMessage; }
119
120 private:
121 QString mMessage;
122};
123
124
125class TcpServerWorker : public QObject
126{
127 Q_OBJECT
128
129 public:
130 TcpServerWorker( const QString &ipAddress, int port )
131 {
132 QHostAddress address { QHostAddress::AnyIPv4 };
133 address.setAddress( ipAddress );
134
135 if ( !mTcpServer.listen( address, port ) )
136 {
137 std::cerr << tr( "Unable to start the server: %1." ).arg( mTcpServer.errorString() ).toStdString() << std::endl;
138 }
139 else
140 {
141 const int port { mTcpServer.serverPort() };
142
143 std::cout << tr( "QGIS Development Server listening on http://%1:%2" ).arg( ipAddress ).arg( port ).toStdString() << std::endl;
144#ifndef Q_OS_WIN
145 std::cout << tr( "CTRL+C to exit" ).toStdString() << std::endl;
146#endif
147
148 mIsListening = true;
149
150 // Incoming connection handler
151 QTcpServer::connect( &mTcpServer, &QTcpServer::newConnection, this, [this, ipAddress, port] {
152 QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
153
154 mConnectionCounter++;
155
156 //qDebug() << "Active connections: " << mConnectionCounter;
157
158 QString *incomingData = new QString();
159
160 // Lambda disconnect context
161 QObject *context { new QObject };
162
163 // Deletes the connection later
164 auto connectionDeleter = [this, clientConnection, incomingData]() {
165 clientConnection->deleteLater();
166 mConnectionCounter--;
167 delete incomingData;
168 };
169
170 // This will delete the connection
171 QObject::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
172
173#if 0 // Debugging output
174 clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, []( QAbstractSocket::SocketError socketError )
175 {
176 qDebug() << "Socket error #" << socketError;
177 }, Qt::QueuedConnection );
178#endif
179
180 // Incoming connection parser
181 QObject::connect( clientConnection, &QIODevice::readyRead, context, [clientConnection, incomingData, context, ipAddress, port] {
182 // Read all incoming data
183 while ( clientConnection->bytesAvailable() > 0 )
184 {
185 incomingData->append( clientConnection->readAll() );
186 }
187
188 try
189 {
190 // Parse protocol and URL GET /path HTTP/1.1
191 const auto firstLinePos { incomingData->indexOf( "\r\n" ) };
192 if ( firstLinePos == -1 )
193 {
194 throw HttpException( u"HTTP error finding protocol header"_s );
195 }
196
197 const QString firstLine { incomingData->left( firstLinePos ) };
198 const QStringList firstLinePieces { firstLine.split( ' ' ) };
199 if ( firstLinePieces.size() != 3 )
200 {
201 throw HttpException( u"HTTP error splitting protocol header"_s );
202 }
203
204 const QString methodString { firstLinePieces.at( 0 ) };
205
207 if ( methodString == "GET" )
208 {
210 }
211 else if ( methodString == "POST" )
212 {
214 }
215 else if ( methodString == "HEAD" )
216 {
218 }
219 else if ( methodString == "PUT" )
220 {
222 }
223 else if ( methodString == "PATCH" )
224 {
226 }
227 else if ( methodString == "DELETE" )
228 {
230 }
231 else if ( methodString == "OPTIONS" )
232 {
234 }
235 else
236 {
237 throw HttpException( u"HTTP error unsupported method: %1"_s.arg( methodString ) );
238 }
239
240 // cppcheck-suppress containerOutOfBounds
241 const QString protocol { firstLinePieces.at( 2 ) };
242 if ( protocol != "HTTP/1.0"_L1 && protocol != "HTTP/1.1"_L1 )
243 {
244 throw HttpException( u"HTTP error unsupported protocol: %1"_s.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( u"HTTP error finding headers"_s );
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( u"Content-Length"_s ) )
271 {
272 bool ok;
273 const int contentLength { headers.value( u"Content-Length"_s ).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( u"Host"_s ) )
293 {
294 url = u"http://%1%2"_s.arg( headers.value( u"Host"_s ), path );
295 }
296 else
297 {
298 url = u"http://%1:%2%3"_s.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( u"HTTP/1.0 %1 %2\r\n"_s.arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
326 clientConnection->write( u"Server: QGIS\r\n"_s.toUtf8() );
327 clientConnection->write( "\r\n" );
328 clientConnection->write( ex.message().toUtf8() );
329
330 std::cout
331 << u"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m"_s.arg( clientConnection->peerAddress().toString() ).arg( QDateTime::currentDateTime().toString() ).arg( ex.message() ).toStdString()
332 << 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 QCoreApplication::setOrganizationName( QgsApplication::QGIS_ORGANIZATION_NAME );
504 QCoreApplication::setOrganizationDomain( QgsApplication::QGIS_ORGANIZATION_DOMAIN );
505 QCoreApplication::setApplicationName( "QGIS Development Server" );
506 QCoreApplication::setApplicationVersion( VERSION );
507
508 if ( !withDisplay )
509 {
511 "DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
512 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
513 "Server",
515 );
516 }
517
518#ifdef Q_OS_WIN
519 // Initialize font database before fcgi_accept.
520 // When using FCGI with IIS, environment variables (QT_QPA_FONTDIR in this case) are lost after fcgi_accept().
521 QFontDatabase fontDB;
522#endif
523
524 // The port to listen
525 serverPort = qgetenv( "QGIS_SERVER_PORT" );
526 // The address to listen
527 ipAddress = qgetenv( "QGIS_SERVER_ADDRESS" );
528
529 if ( serverPort.isEmpty() )
530 {
531 serverPort = u"8000"_s;
532 }
533
534 if ( ipAddress.isEmpty() )
535 {
536 ipAddress = u"127.0.0.1"_s;
537 }
538
539 QCommandLineParser parser;
540 parser.setApplicationDescription( QObject::tr( "QGIS Development Server %1" ).arg( VERSION ) );
541 parser.addHelpOption();
542
543 const QCommandLineOption versionOption( QStringList() << "v" << "version", QObject::tr( "Version of QGIS and libraries" ) );
544 parser.addOption( versionOption );
545
546 parser.addPositionalArgument(
547 u"addressAndPort"_s,
548 QObject::tr(
549 "Address and port (default: \"localhost:8000\")\n"
550 "address and port can also be specified with the environment\n"
551 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT."
552 ),
553 u"[address:port]"_s
554 );
555 const QCommandLineOption logLevelOption(
556 "l",
557 QObject::tr(
558 "Log level (default: 0)\n"
559 "0: INFO\n"
560 "1: WARNING\n"
561 "2: CRITICAL"
562 ),
563 "logLevel",
564 "0"
565 );
566 parser.addOption( logLevelOption );
567
568 const QCommandLineOption projectOption(
569 "p",
570 QObject::tr(
571 "Path to a QGIS project file (*.qgs or *.qgz),\n"
572 "if specified it will override the query string MAP argument\n"
573 "and the QGIS_PROJECT_FILE environment variable."
574 ),
575 "projectPath",
576 ""
577 );
578 parser.addOption( projectOption );
579
580 parser.process( app );
581
582 if ( parser.isSet( versionOption ) )
583 {
584 std::cout << QgsCommandLineUtils::allVersions().toStdString();
585 return 0;
586 }
587
588 const QStringList args = parser.positionalArguments();
589
590 if ( args.size() == 1 )
591 {
592 const QStringList addressAndPort { args.at( 0 ).split( ':' ) };
593 if ( addressAndPort.size() == 2 )
594 {
595 ipAddress = addressAndPort.at( 0 );
596 // cppcheck-suppress containerOutOfBounds
597 serverPort = addressAndPort.at( 1 );
598 }
599 }
600
601 const QString logLevel = parser.value( logLevelOption );
602 qunsetenv( "QGIS_SERVER_LOG_FILE" );
603 qputenv( "QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
604 qputenv( "QGIS_SERVER_LOG_STDERR", "1" );
605
606 QgsServer server;
607
608 if ( !parser.value( projectOption ).isEmpty() )
609 {
610 // Check it!
611 const QString projectFilePath { parser.value( projectOption ) };
614 {
615 std::cout << QObject::tr( "Project file not found, the option will be ignored." ).toStdString() << std::endl;
616 }
617 else
618 {
619 qputenv( "QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
620 }
621 }
622
623 // Disable parallel rendering because if its internal loop
624 //qputenv( "QGIS_SERVER_PARALLEL_RENDERING", "0" );
625
626
627#ifdef HAVE_SERVER_PYTHON_PLUGINS
628 server.initPython();
629#endif
630
631 // TCP thread
632 TcpServerThread tcpServerThread { ipAddress, serverPort.toInt() };
633
634 bool isTcpError = false;
635 TcpServerThread::connect(
636 &tcpServerThread,
637 &TcpServerThread::serverError,
638 qApp,
639 [&] {
640 isTcpError = true;
641 qApp->quit();
642 },
643 Qt::QueuedConnection
644 );
645
646 // Monitoring thread
647 QueueMonitorThread queueMonitorThread;
648 QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [&]( RequestContext *requestContext ) {
649 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
650 {
651 server.handleRequest( requestContext->request, requestContext->response );
652 SERVER_MUTEX.unlock();
653 }
654 else
655 {
656 delete requestContext;
657 SERVER_MUTEX.unlock();
658 return;
659 }
660 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
661 tcpServerThread.emitResponseReady( requestContext ); //#spellok
662 else
663 delete requestContext;
664 } );
665
666 // Exit handlers
667#ifndef Q_OS_WIN
668
669 auto exitHandler = []( int signal ) {
670 std::cout << u"Signal %1 received: quitting"_s.arg( signal ).toStdString() << std::endl;
671 IS_RUNNING = 0;
672 qApp->quit();
673 };
674
675 signal( SIGTERM, exitHandler );
676 signal( SIGABRT, exitHandler );
677 signal( SIGINT, exitHandler );
678 signal( SIGPIPE, []( int ) { std::cerr << u"Signal SIGPIPE received: ignoring"_s.toStdString() << std::endl; } );
679
680#endif
681
682 tcpServerThread.start();
683 queueMonitorThread.start();
684
685 QgsApplication::exec();
686 // Wait for threads
687 tcpServerThread.exit();
688 tcpServerThread.wait();
689 queueMonitorThread.stop();
690 REQUEST_WAIT_CONDITION.notify_all();
691 queueMonitorThread.wait();
693
694 return isTcpError ? 1 : 0;
695}
696
697#include "qgis_mapserver.moc"
698
@ DontLoad3DViews
Skip loading 3D views.
Definition qgis.h:4622
@ DontStoreOriginalStyles
Skip the initial XML style storage for layers. Useful for minimising project load times in non-intera...
Definition qgis.h:4621
@ DontUpgradeAnnotations
Don't upgrade old annotation items to QgsAnnotationItem.
Definition qgis.h:4625
@ DontLoadLayouts
Don't load print layouts. Improves project read time if layouts are not required, and allows projects...
Definition qgis.h:4617
@ DontResolveLayers
Don't resolve layer paths (i.e. don't load any layer content). Dramatically improves project read tim...
Definition qgis.h:4615
@ 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.
int main(int argc, char *argv[])