QGIS API Documentation  3.27.0-Master (aef1b1ec20)
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 ) };
608  if ( ! QgsProject::instance()->read( projectFilePath,
609  Qgis::ProjectReadFlag::DontResolveLayers
610  | Qgis::ProjectReadFlag::DontLoadLayouts
611  | Qgis::ProjectReadFlag::DontStoreOriginalStyles
612  | Qgis::ProjectReadFlag::DontLoad3DViews
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( &tcpServerThread, &TcpServerThread::serverError, qApp, [ & ]
636  {
637  isTcpError = true;
638  qApp->quit();
639  }, Qt::QueuedConnection );
640 
641  // Monitoring thread
642  QueueMonitorThread queueMonitorThread;
643  QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [ & ]( RequestContext * requestContext )
644  {
645  if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
646  {
647  server.handleRequest( requestContext->request, requestContext->response );
648  SERVER_MUTEX.unlock();
649  }
650  else
651  {
652  delete requestContext;
653  SERVER_MUTEX.unlock();
654  return;
655  }
656  if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
657  tcpServerThread.emitResponseReady( requestContext ); //#spellok
658  else
659  delete requestContext;
660  } );
661 
662  // Exit handlers
663 #ifndef Q_OS_WIN
664 
665  auto exitHandler = [ ]( int signal )
666  {
667  std::cout << QStringLiteral( "Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
668  IS_RUNNING = 0;
669  qApp->quit( );
670  };
671 
672  signal( SIGTERM, exitHandler );
673  signal( SIGABRT, exitHandler );
674  signal( SIGINT, exitHandler );
675  signal( SIGPIPE, [ ]( int )
676  {
677  std::cerr << QStringLiteral( "Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
678  } );
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 
700 
701 
@ DontLoadProjectStyles
Skip loading project style databases (since QGIS 3.26)
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:479
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[])