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