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