QGIS API Documentation  3.16.0-Hannover (43b64b13f3)
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) 2020by 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 
29 //for CMAKE_INSTALL_PREFIX
30 #include "qgsconfig.h"
31 #include "qgsserver.h"
32 #include "qgsbufferserverrequest.h"
34 #include "qgsapplication.h"
35 #include "qgsmessagelog.h"
36 
37 #include <QFontDatabase>
38 #include <QString>
39 #include <QTcpServer>
40 #include <QTcpSocket>
41 #include <QNetworkInterface>
42 #include <QCommandLineParser>
43 #include <QObject>
44 
45 
46 #ifndef Q_OS_WIN
47 #include <csignal>
48 #endif
49 
50 #include <string>
51 #include <chrono>
52 
54 
55 // For the signal exit handler
56 QAtomicInt IS_RUNNING = 1;
57 
58 
62 class HttpException: public std::exception
63 {
64 
65  public:
66 
70  HttpException( const QString &message )
71  : mMessage( message )
72  {
73  }
74 
78  QString message( )
79  {
80  return mMessage;
81  }
82 
83  private:
84 
85  QString mMessage;
86 
87 };
88 
89 int main( int argc, char *argv[] )
90 {
91  // Test if the environ variable DISPLAY is defined
92  // if it's not, the server is running in offscreen mode
93  // Qt supports using various QPA (Qt Platform Abstraction) back ends
94  // for rendering. You can specify the back end to use with the environment
95  // variable QT_QPA_PLATFORM when invoking a Qt-based application.
96  // Available platform plugins are: directfbegl, directfb, eglfs, linuxfb,
97  // minimal, minimalegl, offscreen, wayland-egl, wayland, xcb.
98  // https://www.ics.com/blog/qt-tips-and-tricks-part-1
99  // http://doc.qt.io/qt-5/qpa.html
100  const QString display { qgetenv( "DISPLAY" ) };
101  bool withDisplay = true;
102  if ( display.isEmpty() )
103  {
104  withDisplay = false;
105  qputenv( "QT_QPA_PLATFORM", "offscreen" );
106  }
107 
108  // since version 3.0 QgsServer now needs a qApp so initialize QgsApplication
109  QgsApplication app( argc, argv, withDisplay, QString(), QStringLiteral( "QGIS Development Server" ) );
110 
111  QCoreApplication::setOrganizationName( QgsApplication::QGIS_ORGANIZATION_NAME );
112  QCoreApplication::setOrganizationDomain( QgsApplication::QGIS_ORGANIZATION_DOMAIN );
113  QCoreApplication::setApplicationName( "QGIS Development Server" );
114  QCoreApplication::setApplicationVersion( VERSION );
115 
116  if ( ! withDisplay )
117  {
118  QgsMessageLog::logMessage( "DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
119  "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.", "Server", Qgis::Warning );
120  }
121 
122 #ifdef Q_OS_WIN
123  // Initialize font database before fcgi_accept.
124  // When using FCGI with IIS, environment variables (QT_QPA_FONTDIR in this case) are lost after fcgi_accept().
125  QFontDatabase fontDB;
126 #endif
127 
128  // The port to listen
129  QString serverPort { qgetenv( "QGIS_SERVER_PORT" ) };
130  // The address to listen
131  QString ipAddress { qgetenv( "QGIS_SERVER_ADDRESS" ) };
132 
133  if ( serverPort.isEmpty() )
134  {
135  serverPort = QStringLiteral( "8000" );
136  }
137 
138  if ( ipAddress.isEmpty() )
139  {
140  ipAddress = QStringLiteral( "localhost" );
141  }
142 
143  QCommandLineParser parser;
144  parser.setApplicationDescription( QObject::tr( "QGIS Development Server %1" ).arg( VERSION ) );
145  parser.addHelpOption();
146  parser.addVersionOption();
147  parser.addPositionalArgument( QStringLiteral( "addressAndPort" ),
148  QObject::tr( "Address and port (default: \"localhost:8000\")\n"
149  "address and port can also be specified with the environment\n"
150  "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT." ), QStringLiteral( "[address:port]" ) );
151  QCommandLineOption logLevelOption( "l", QObject::tr( "Log level (default: 0)\n"
152  "0: INFO\n"
153  "1: WARNING\n"
154  "2: CRITICAL" ), "logLevel", "0" );
155  parser.addOption( logLevelOption );
156 
157  QCommandLineOption projectOption( "p", QObject::tr( "Path to a QGIS project file (*.qgs or *.qgz),\n"
158  "if specified it will override the query string MAP argument\n"
159  "and the QGIS_PROJECT_FILE environment variable." ), "projectPath", "" );
160  parser.addOption( projectOption );
161 
162  parser.process( app );
163  const QStringList args = parser.positionalArguments();
164 
165  if ( args.size() == 1 )
166  {
167  QStringList addressAndPort { args.at( 0 ).split( ':' ) };
168  if ( addressAndPort.size() == 2 )
169  {
170  ipAddress = addressAndPort.at( 0 );
171  serverPort = addressAndPort.at( 1 );
172  }
173  }
174 
175  QString logLevel = parser.value( logLevelOption );
176  qunsetenv( "QGIS_SERVER_LOG_FILE" );
177  qputenv( "QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
178  qputenv( "QGIS_SERVER_LOG_STDERR", "1" );
179 
180  if ( ! parser.value( projectOption ).isEmpty( ) )
181  {
182  // Check it!
183  const QString projectFilePath { parser.value( projectOption ) };
184  if ( ! QFile::exists( projectFilePath ) )
185  {
186  std::cout << QObject::tr( "Project file not found, the option will be ignored." ).toStdString() << std::endl;
187  }
188  else
189  {
190  qputenv( "QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
191  }
192  }
193 
194  // Disable parallel rendering because if its internal loop
195  //qputenv( "QGIS_SERVER_PARALLEL_RENDERING", "0" );
196 
197  // Create server
198  QTcpServer tcpServer;
199 
200  QHostAddress address { QHostAddress::AnyIPv4 };
201  address.setAddress( ipAddress );
202 
203  if ( ! tcpServer.listen( address, serverPort.toInt( ) ) )
204  {
205  std::cerr << QObject::tr( "Unable to start the server: %1." )
206  .arg( tcpServer.errorString() ).toStdString() << std::endl;
207  tcpServer.close();
208  app.exitQgis();
209  return 1;
210  }
211  else
212  {
213  const int port { tcpServer.serverPort() };
214 
215  QAtomicInt connCounter { 0 };
216 
217  static const QMap<int, QString> knownStatuses
218  {
219  { 200, QStringLiteral( "OK" ) },
220  { 201, QStringLiteral( "Created" ) },
221  { 202, QStringLiteral( "Accepted" ) },
222  { 204, QStringLiteral( "No Content" ) },
223  { 301, QStringLiteral( "Moved Permanently" ) },
224  { 302, QStringLiteral( "Moved Temporarily" ) },
225  { 304, QStringLiteral( "Not Modified" ) },
226  { 400, QStringLiteral( "Bad Request" ) },
227  { 401, QStringLiteral( "Unauthorized" ) },
228  { 403, QStringLiteral( "Forbidden" ) },
229  { 404, QStringLiteral( "Not Found" ) },
230  { 500, QStringLiteral( "Internal Server Error" ) },
231  { 501, QStringLiteral( "Not Implemented" ) },
232  { 502, QStringLiteral( "Bad Gateway" ) },
233  { 503, QStringLiteral( "Service Unavailable" ) }
234  };
235 
236  QgsServer server;
237 
238 #ifdef HAVE_SERVER_PYTHON_PLUGINS
239  server.initPython();
240 #endif
241 
242  std::cout << QObject::tr( "QGIS Development Server listening on http://%1:%2" )
243  .arg( ipAddress ).arg( port ).toStdString() << std::endl;
244 #ifndef Q_OS_WIN
245  std::cout << QObject::tr( "CTRL+C to exit" ).toStdString() << std::endl;
246 #endif
247 
248  // Poor man's synchronous HTTP handler
249  // The reason why this cannot be implemented using signals is that
250  // WMS provider (and probably others) run its own event loop and this
251  // crashes in case the project contains a WMS layer (aka: cascading)
252  auto httpHandler = [ & ]( QTcpSocket * clientConnection )
253  {
254 
255  connCounter++;
256 
257  //qDebug() << clientConnection << "Active connection" << connCounter;
258 
259  QString incomingData;
260 
261  // Incoming connection parser
262  while ( IS_RUNNING && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
263  {
264 
265  if ( ! clientConnection->bytesAvailable() )
266  {
267  qApp->processEvents();
268  continue;
269  }
270 
271  // Read all incoming data
272  while ( IS_RUNNING && clientConnection->bytesAvailable() > 0 )
273  {
274  incomingData.append( clientConnection->readAll() );
275  }
276 
277  try
278  {
279  // Parse protocol and URL GET /path HTTP/1.1
280  int firstLinePos { incomingData.indexOf( "\r\n" ) };
281  if ( firstLinePos == -1 )
282  {
283  throw HttpException( QStringLiteral( "HTTP error finding protocol header" ) );
284  }
285 
286  const QString firstLine { incomingData.left( firstLinePos ) };
287  const QStringList firstLinePieces { firstLine.split( ' ' ) };
288  if ( firstLinePieces.size() != 3 )
289  {
290  throw HttpException( QStringLiteral( "HTTP error splitting protocol header" ) );
291  }
292 
293  const QString methodString { firstLinePieces.at( 0 ) };
294 
296  if ( methodString == "GET" )
297  {
298  method = QgsServerRequest::Method::GetMethod;
299  }
300  else if ( methodString == "POST" )
301  {
302  method = QgsServerRequest::Method::PostMethod;
303  }
304  else if ( methodString == "HEAD" )
305  {
306  method = QgsServerRequest::Method::HeadMethod;
307  }
308  else if ( methodString == "PUT" )
309  {
310  method = QgsServerRequest::Method::PutMethod;
311  }
312  else if ( methodString == "PATCH" )
313  {
314  method = QgsServerRequest::Method::PatchMethod;
315  }
316  else if ( methodString == "DELETE" )
317  {
318  method = QgsServerRequest::Method::DeleteMethod;
319  }
320  else
321  {
322  throw HttpException( QStringLiteral( "HTTP error unsupported method: %1" ).arg( methodString ) );
323  }
324 
325  const QString protocol { firstLinePieces.at( 2 )};
326  if ( protocol != QLatin1String( "HTTP/1.0" ) && protocol != QLatin1String( "HTTP/1.1" ) )
327  {
328  throw HttpException( QStringLiteral( "HTTP error unsupported protocol: %1" ).arg( protocol ) );
329  }
330 
331  // Headers
333  int endHeadersPos { incomingData.indexOf( "\r\n\r\n" ) };
334 
335  if ( endHeadersPos == -1 )
336  {
337  throw HttpException( QStringLiteral( "HTTP error finding headers" ) );
338  }
339 
340  const QStringList httpHeaders { incomingData.mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split( "\r\n" ) };
341 
342  for ( const auto &headerLine : httpHeaders )
343  {
344  const int headerColonPos { headerLine.indexOf( ':' ) };
345  if ( headerColonPos > 0 )
346  {
347  headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
348  }
349  }
350 
351  const int headersSize { endHeadersPos + 4 };
352 
353  // Check for content length and if we have got all data
354  if ( headers.contains( QStringLiteral( "Content-Length" ) ) )
355  {
356  bool ok;
357  const int contentLength { headers.value( QStringLiteral( "Content-Length" ) ).toInt( &ok ) };
358  if ( ok && contentLength > incomingData.length() - headersSize )
359  {
360  break;
361  }
362  }
363 
364  // At this point we should have read all data:
365 
366  // Build URL from env ...
367  QString url { qgetenv( "REQUEST_URI" ) };
368  // ... or from server ip/port and request path
369  if ( url.isEmpty() )
370  {
371  const QString path { firstLinePieces.at( 1 )};
372  // Take Host header if defined
373  if ( headers.contains( QStringLiteral( "Host" ) ) )
374  {
375  url = QStringLiteral( "http://%1%2" ).arg( headers.value( QStringLiteral( "Host" ) ) ).arg( path );
376  }
377  else
378  {
379  url = QStringLiteral( "http://%1:%2%3" ).arg( ipAddress ).arg( port ).arg( path );
380  }
381  }
382 
383  // Inefficient copy :(
384  QByteArray data { incomingData.mid( headersSize ).toUtf8() };
385 
386  auto start = std::chrono::steady_clock::now();
387 
388  QgsBufferServerRequest request { url, method, headers, &data };
389  QgsBufferServerResponse response;
390 
391  server.handleRequest( request, response );
392 
393  // The QGIS server machinery calls processEvents and has internal loop events
394  // that might change the connection state
395  if ( clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
396  {
397  break;
398  }
399 
400  auto elapsedTime { std::chrono::steady_clock::now() - start };
401 
402  if ( ! knownStatuses.contains( response.statusCode() ) )
403  {
404  throw HttpException( QStringLiteral( "HTTP error unsupported status code: %1" ).arg( response.statusCode() ) );
405  }
406 
407  // Output stream
408  clientConnection->write( QStringLiteral( "HTTP/1.0 %1 %2\r\n" ).arg( response.statusCode() ).arg( knownStatuses.value( response.statusCode() ) ).toUtf8() );
409  clientConnection->write( QStringLiteral( "Server: QGIS\r\n" ).toUtf8() );
410  const auto responseHeaders { response.headers() };
411  for ( auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
412  {
413  clientConnection->write( QStringLiteral( "%1: %2\r\n" ).arg( it.key(), it.value() ).toUtf8() );
414  }
415  clientConnection->write( "\r\n" );
416  const QByteArray body { response.body() };
417  clientConnection->write( body );
418 
419  // 10.185.248.71 [09/Jan/2015:19:12:06 +0000] 808840 <time> "GET / HTTP/1.1" 500"
420  std::cout << QStringLiteral( "%1 [%2] %3 %4ms \"%5\" %6" )
421  .arg( clientConnection->peerAddress().toString(),
422  QDateTime::currentDateTime().toString(),
423  QString::number( body.size() ),
424  QString::number( std::chrono::duration_cast<std::chrono::milliseconds>( elapsedTime ).count() ),
425  firstLinePieces.join( ' ' ),
426  QString::number( response.statusCode() ) )
427  .toStdString()
428  << std::endl;
429 
430  clientConnection->disconnectFromHost();
431  }
432  catch ( HttpException &ex )
433  {
434 
435  if ( clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
436  {
437  break;
438  }
439 
440  // Output stream: send error
441  clientConnection->write( QStringLiteral( "HTTP/1.0 %1 %2\r\n" ).arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
442  clientConnection->write( QStringLiteral( "Server: QGIS\r\n" ).toUtf8() );
443  clientConnection->write( "\r\n" );
444  clientConnection->write( ex.message().toUtf8() );
445 
446  std::cout << QStringLiteral( "%1 [%2] \"%3\" - - 500" )
447  .arg( clientConnection->peerAddress().toString() )
448  .arg( QDateTime::currentDateTime().toString() )
449  .arg( ex.message() ).toStdString() << std::endl;
450 
451  clientConnection->disconnectFromHost();
452 
453  }
454  };
455 
456  clientConnection->deleteLater();
457  connCounter--;
458 
459  };
460 
461  // Starts HTTP handler loop
462  QTimer::singleShot( 0, [ & ]
463  {
464  while ( IS_RUNNING )
465  {
466  if ( tcpServer.hasPendingConnections() )
467  {
468  QTcpSocket *clientConnection = tcpServer.nextPendingConnection();
469  if ( clientConnection )
470  {
471  httpHandler( clientConnection );
472  }
473  }
474  else
475  {
476  qApp->processEvents( );
477  std::this_thread::sleep_for( std::chrono::milliseconds( 1 ) );
478  }
479  }
480  } );
481  }
482 
483  // Exit handlers
484 #ifndef Q_OS_WIN
485 
486  auto exitHandler = [ ]( int signal )
487  {
488  std::cout << QStringLiteral( "Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
489  IS_RUNNING = 0;
490  qApp->quit( );
491  };
492 
493  signal( SIGTERM, exitHandler );
494  signal( SIGABRT, exitHandler );
495  signal( SIGINT, exitHandler );
496  signal( SIGPIPE, [ ]( int )
497  {
498  std::cerr << QStringLiteral( "Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
499  } );
500 
501 #endif
502 
503  app.exec();
504  app.exitQgis();
505  return 0;
506 }
507 
QgsApplication::QGIS_ORGANIZATION_NAME
static const char * QGIS_ORGANIZATION_NAME
Definition: qgsapplication.h:151
main
int main(int argc, char *argv[])
Definition: qgis_map_serv.cpp:44
QgsBufferServerResponse
Class defining buffered response.
Definition: qgsbufferserverresponse.h:38
QgsServer::handleRequest
void handleRequest(QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project=nullptr)
Handles the request.
Definition: qgsserver.cpp:297
Qgis::Warning
@ Warning
Definition: qgis.h:91
QgsBufferServerRequest
Class defining request with data.
Definition: qgsbufferserverrequest.h:32
qgsapplication.h
QgsBufferServerResponse::headers
QMap< QString, QString > headers() const override
Returns all the headers.
Definition: qgsbufferserverresponse.h:67
QgsBufferServerResponse::body
QByteArray body() const
Returns body.
Definition: qgsbufferserverresponse.h:140
qgsserver.h
QgsBufferServerResponse::statusCode
int statusCode() const override
Returns the http status code.
Definition: qgsbufferserverresponse.h:83
qgsbufferserverrequest.h
QgsApplication
Extends QApplication to provide access to QGIS specific resources such as theme paths,...
Definition: qgsapplication.h:83
QgsServer
The QgsServer class provides OGC web services.
Definition: qgsserver.h:49
QgsMessageLog::logMessage
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
Definition: qgsmessagelog.cpp:27
QgsServerRequest::Method
Method
HTTP Method (or equivalent) used for the request.
Definition: qgsserverrequest.h:51
QgsServerRequest::Headers
QMap< QString, QString > Headers
Definition: qgsserverrequest.h:45
qgsbufferserverresponse.h
QgsApplication::QGIS_ORGANIZATION_DOMAIN
static const char * QGIS_ORGANIZATION_DOMAIN
Definition: qgsapplication.h:152
qgsmessagelog.h