30 #include <condition_variable> 
   34 #include "qgsconfig.h" 
   41 #include <QFontDatabase> 
   45 #include <QNetworkInterface> 
   46 #include <QCommandLineParser> 
   59 QAtomicInt IS_RUNNING = 1;
 
   64 std::condition_variable REQUEST_WAIT_CONDITION;
 
   65 std::mutex REQUEST_QUEUE_MUTEX;
 
   66 std::mutex SERVER_MUTEX;
 
   70   QPointer<QTcpSocket> clientConnection;
 
   72   std::chrono::steady_clock::time_point startTime;
 
   78 QQueue<RequestContext *> REQUEST_QUEUE;
 
   80 const QMap<int, QString> knownStatuses
 
   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" ) }
 
  102 class HttpException: 
public std::exception
 
  110     HttpException( 
const QString &message )
 
  111       : mMessage( message )
 
  130 class TcpServerWorker: 
public QObject
 
  136     TcpServerWorker( 
const QString &ipAddress, 
int port )
 
  138       QHostAddress address { QHostAddress::AnyIPv4 };
 
  139       address.setAddress( ipAddress );
 
  141       if ( ! mTcpServer.listen( address, port ) )
 
  143         std::cerr << tr( 
"Unable to start the server: %1." )
 
  144                   .arg( mTcpServer.errorString() ).toStdString() << std::endl;
 
  148         const int port { mTcpServer.serverPort() };
 
  150         std::cout << tr( 
"QGIS Development Server listening on http://%1:%2" ).arg( ipAddress ).arg( port ).toStdString() << std::endl;
 
  152         std::cout << tr( 
"CTRL+C to exit" ).toStdString() << std::endl;
 
  158         mTcpServer.connect( &mTcpServer, &QTcpServer::newConnection, 
this, [ = ]
 
  160           QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
 
  162           mConnectionCounter++;
 
  166           QString *incomingData = 
new QString();
 
  169           QObject *context { 
new QObject };
 
  172           auto connectionDeleter = [ = ]()
 
  174             clientConnection->deleteLater();
 
  175             mConnectionCounter--;
 
  180           clientConnection->connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
 
  183           clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, [ = ]( QAbstractSocket::SocketError socketError )
 
  185             qDebug() << 
"Socket error #" << socketError;
 
  186           }, Qt::QueuedConnection );
 
  190           clientConnection->connect( clientConnection, &QIODevice::readyRead, context, [ = ] {
 
  193             while ( clientConnection->bytesAvailable() > 0 )
 
  195               incomingData->append( clientConnection->readAll() );
 
  201               const int firstLinePos { incomingData->indexOf( 
"\r\n" ) };
 
  202               if ( firstLinePos == -1 )
 
  204                 throw HttpException( QStringLiteral( 
"HTTP error finding protocol header" ) );
 
  207               const QString firstLine { incomingData->left( firstLinePos ) };
 
  208               const QStringList firstLinePieces { firstLine.split( 
' ' ) };
 
  209               if ( firstLinePieces.size() != 3 )
 
  211                 throw HttpException( QStringLiteral( 
"HTTP error splitting protocol header" ) );
 
  214               const QString methodString { firstLinePieces.at( 0 ) };
 
  217               if ( methodString == 
"GET" )
 
  219                 method = QgsServerRequest::Method::GetMethod;
 
  221               else if ( methodString == 
"POST" )
 
  223                 method = QgsServerRequest::Method::PostMethod;
 
  225               else if ( methodString == 
"HEAD" )
 
  227                 method = QgsServerRequest::Method::HeadMethod;
 
  229               else if ( methodString == 
"PUT" )
 
  231                 method = QgsServerRequest::Method::PutMethod;
 
  233               else if ( methodString == 
"PATCH" )
 
  235                 method = QgsServerRequest::Method::PatchMethod;
 
  237               else if ( methodString == 
"DELETE" )
 
  239                 method = QgsServerRequest::Method::DeleteMethod;
 
  243                 throw HttpException( QStringLiteral( 
"HTTP error unsupported method: %1" ).arg( methodString ) );
 
  246               const QString protocol { firstLinePieces.at( 2 )};
 
  247               if ( protocol != QLatin1String( 
"HTTP/1.0" ) && protocol != QLatin1String( 
"HTTP/1.1" ) )
 
  249                 throw HttpException( QStringLiteral( 
"HTTP error unsupported protocol: %1" ).arg( protocol ) );
 
  254               const int endHeadersPos { incomingData->indexOf( 
"\r\n\r\n" ) };
 
  256               if ( endHeadersPos == -1 )
 
  258                 throw HttpException( QStringLiteral( 
"HTTP error finding headers" ) );
 
  261               const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split( 
"\r\n" ) };
 
  263               for ( 
const auto &headerLine : httpHeaders )
 
  265                 const int headerColonPos { headerLine.indexOf( 
':' ) };
 
  266                 if ( headerColonPos > 0 )
 
  268                   headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
 
  272               const int headersSize { endHeadersPos + 4 };
 
  275               if ( headers.contains( QStringLiteral( 
"Content-Length" ) ) )
 
  278                 const int contentLength { headers.value( QStringLiteral( 
"Content-Length" ) ).toInt( &ok ) };
 
  279                 if ( ok && contentLength > incomingData->length() - headersSize )
 
  290               QString url { qgetenv( 
"REQUEST_URI" ) };
 
  294                 const QString path { firstLinePieces.at( 1 )};
 
  296                 if ( headers.contains( QStringLiteral( 
"Host" ) ) )
 
  298                   url = QStringLiteral( 
"http://%1%2" ).arg( headers.value( QStringLiteral( 
"Host" ) ), path );
 
  302                   url = QStringLiteral( 
"http://%1:%2%3" ).arg( ipAddress ).arg( port ).arg( path );
 
  307               QByteArray data { incomingData->mid( headersSize ).toUtf8() };
 
  309               if ( !incomingData->isEmpty() && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
 
  311                 auto requestContext = 
new RequestContext
 
  314                   firstLinePieces.join( 
' ' ),
 
  315                   std::chrono::steady_clock::now(),
 
  316                   { url, method, headers, &data },
 
  319                 REQUEST_QUEUE_MUTEX.lock();
 
  320                 REQUEST_QUEUE.enqueue( requestContext );
 
  321                 REQUEST_QUEUE_MUTEX.unlock();
 
  322                 REQUEST_WAIT_CONDITION.notify_one();
 
  325             catch ( HttpException &ex )
 
  327               if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
 
  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() );
 
  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;
 
  340                 clientConnection->disconnectFromHost();
 
  353     bool isListening()
 const 
  361     void responseReady( RequestContext *requestContext )  
 
  363       std::unique_ptr<RequestContext> request { requestContext };
 
  364       const auto elapsedTime { std::chrono::steady_clock::now() - request->startTime };
 
  366       const auto &response { request->response };
 
  367       const auto &clientConnection { request->clientConnection };
 
  369       if ( ! clientConnection ||
 
  370            clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
 
  372         std::cout << 
"Connection reset by peer" << std::endl;
 
  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() ) )
 
  379         std::cout << 
"Cannot write to output socket" << std::endl;
 
  380         clientConnection->disconnectFromHost();
 
  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 )
 
  388         clientConnection->write( QStringLiteral( 
"%1: %2\r\n" ).arg( it.key(), it.value() ).toUtf8() );
 
  390       clientConnection->write( 
"\r\n" );
 
  391       const QByteArray body { response.body() };
 
  392       clientConnection->write( body );
 
  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() ),
 
  401                       QString::number( response.statusCode() ) )
 
  406       clientConnection->disconnectFromHost();
 
  411     QTcpServer mTcpServer;
 
  412     qlonglong mConnectionCounter = 0;
 
  413     bool mIsListening = 
false;
 
  418 class TcpServerThread: 
public QThread
 
  424     TcpServerThread( 
const QString &ipAddress, 
const int port )
 
  425       : mIpAddress( ipAddress )
 
  430     void emitResponseReady( RequestContext *requestContext )  
 
  432       if ( requestContext->clientConnection )
 
  433         emit responseReady( requestContext );  
 
  438       const TcpServerWorker worker( mIpAddress, mPort );
 
  439       if ( ! worker.isListening() )
 
  446         connect( 
this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady );  
 
  453     void responseReady( RequestContext *requestContext );  
 
  463 class QueueMonitorThread: 
public QThread
 
  473         std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
 
  474         REQUEST_WAIT_CONDITION.wait( requestLocker, [ = ] { 
return ! mIsRunning || ! REQUEST_QUEUE.isEmpty(); } );
 
  479           emit requestReady( REQUEST_QUEUE.dequeue() );
 
  486     void requestReady( RequestContext *requestContext );
 
  497     bool mIsRunning = 
true;
 
  501 int main( 
int argc, 
char *argv[] )
 
  512   const QString display { qgetenv( 
"DISPLAY" ) };
 
  513   bool withDisplay = 
true;
 
  514   if ( display.isEmpty() )
 
  517     qputenv( 
"QT_QPA_PLATFORM", 
"offscreen" );
 
  521   const QgsApplication app( argc, argv, withDisplay, QString(), QStringLiteral( 
"QGIS Development Server" ) );
 
  525   QCoreApplication::setApplicationName( 
"QGIS Development Server" );
 
  526   QCoreApplication::setApplicationVersion( VERSION );
 
  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 );
 
  537   QFontDatabase fontDB;
 
  541   serverPort = qgetenv( 
"QGIS_SERVER_PORT" );
 
  543   ipAddress = qgetenv( 
"QGIS_SERVER_ADDRESS" );
 
  545   if ( serverPort.isEmpty() )
 
  547     serverPort = QStringLiteral( 
"8000" );
 
  550   if ( ipAddress.isEmpty() )
 
  552     ipAddress = QStringLiteral( 
"localhost" );
 
  555   QCommandLineParser parser;
 
  556   parser.setApplicationDescription( QObject::tr( 
"QGIS Development Server %1" ).arg( VERSION ) );
 
  557   parser.addHelpOption();
 
  559   const QCommandLineOption versionOption( QStringList() << 
"v" << 
"version", QObject::tr( 
"Version of QGIS and libraries" ) );
 
  560   parser.addOption( versionOption );
 
  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" 
  569       "2: CRITICAL" ), 
"logLevel", 
"0" );
 
  570   parser.addOption( logLevelOption );
 
  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 );
 
  577   parser.process( app );
 
  579   if ( parser.isSet( versionOption ) )
 
  585   const QStringList args = parser.positionalArguments();
 
  587   if ( args.size() == 1 )
 
  589     const QStringList addressAndPort { args.at( 0 ).split( 
':' ) };
 
  590     if ( addressAndPort.size() == 2 )
 
  592       ipAddress = addressAndPort.at( 0 );
 
  593       serverPort = addressAndPort.at( 1 );
 
  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" );
 
  604   if ( ! parser.value( projectOption ).isEmpty( ) )
 
  607     const QString projectFilePath { parser.value( projectOption ) };
 
  610       std::cout << QObject::tr( 
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
 
  614       qputenv( 
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
 
  622 #ifdef HAVE_SERVER_PYTHON_PLUGINS 
  627   TcpServerThread tcpServerThread{ ipAddress, serverPort.toInt() };
 
  629   bool isTcpError = 
false;
 
  630   tcpServerThread.connect( &tcpServerThread, &TcpServerThread::serverError, qApp, [ & ]
 
  634   }, Qt::QueuedConnection );
 
  637   QueueMonitorThread queueMonitorThread;
 
  638   queueMonitorThread.connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [ & ]( RequestContext * requestContext )
 
  640     if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
 
  642       server.handleRequest( requestContext->request, requestContext->response );
 
  643       SERVER_MUTEX.unlock();
 
  647       delete requestContext;
 
  648       SERVER_MUTEX.unlock();
 
  651     if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
 
  652       tcpServerThread.emitResponseReady( requestContext );  
 
  654       delete requestContext;
 
  660   auto exitHandler = [ ]( 
int signal )
 
  662     std::cout << QStringLiteral( 
"Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
 
  667   signal( SIGTERM, exitHandler );
 
  668   signal( SIGABRT, exitHandler );
 
  669   signal( SIGINT, exitHandler );
 
  670   signal( SIGPIPE, [ ]( 
int )
 
  672     std::cerr << QStringLiteral( 
"Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
 
  677   tcpServerThread.start();
 
  678   queueMonitorThread.start();
 
  682   tcpServerThread.exit();
 
  683   tcpServerThread.wait();
 
  684   queueMonitorThread.stop();
 
  685   REQUEST_WAIT_CONDITION.notify_all();
 
  686   queueMonitorThread.wait();
 
  689   return isTcpError ? 1 : 0;
 
  692 #include "qgis_mapserver.moc" 
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 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.
@ 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.
int main(int argc, char *argv[])