30 #include "qgsconfig.h"
37 #include <QFontDatabase>
41 #include <QNetworkInterface>
42 #include <QCommandLineParser>
56 QAtomicInt IS_RUNNING = 1;
62 class HttpException:
public std::exception
70 HttpException(
const QString &message )
89 int main(
int argc,
char *argv[] )
100 const QString display { qgetenv(
"DISPLAY" ) };
101 bool withDisplay =
true;
102 if ( display.isEmpty() )
105 qputenv(
"QT_QPA_PLATFORM",
"offscreen" );
109 QgsApplication app( argc, argv, withDisplay, QString(), QStringLiteral(
"QGIS Development Server" ) );
113 QCoreApplication::setApplicationName(
"QGIS Development Server" );
114 QCoreApplication::setApplicationVersion( VERSION );
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 );
125 QFontDatabase fontDB;
129 QString serverPort { qgetenv(
"QGIS_SERVER_PORT" ) };
131 QString ipAddress { qgetenv(
"QGIS_SERVER_ADDRESS" ) };
133 if ( serverPort.isEmpty() )
135 serverPort = QStringLiteral(
"8000" );
138 if ( ipAddress.isEmpty() )
140 ipAddress = QStringLiteral(
"localhost" );
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"
154 "2: CRITICAL" ),
"logLevel",
"0" );
155 parser.addOption( logLevelOption );
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 );
162 parser.process( app );
163 const QStringList args = parser.positionalArguments();
165 if ( args.size() == 1 )
167 QStringList addressAndPort { args.at( 0 ).split(
':' ) };
168 if ( addressAndPort.size() == 2 )
170 ipAddress = addressAndPort.at( 0 );
171 serverPort = addressAndPort.at( 1 );
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" );
180 if ( ! parser.value( projectOption ).isEmpty( ) )
183 const QString projectFilePath { parser.value( projectOption ) };
184 if ( ! QFile::exists( projectFilePath ) )
186 std::cout << QObject::tr(
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
190 qputenv(
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
198 QTcpServer tcpServer;
200 QHostAddress address { QHostAddress::AnyIPv4 };
201 address.setAddress( ipAddress );
203 if ( ! tcpServer.listen( address, serverPort.toInt( ) ) )
205 std::cerr << QObject::tr(
"Unable to start the server: %1." )
206 .arg( tcpServer.errorString() ).toStdString() << std::endl;
213 const int port { tcpServer.serverPort() };
215 QAtomicInt connCounter { 0 };
217 static const QMap<int, QString> knownStatuses
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" ) }
238 #ifdef HAVE_SERVER_PYTHON_PLUGINS
242 std::cout << QObject::tr(
"QGIS Development Server listening on http://%1:%2" )
243 .arg( ipAddress ).arg( port ).toStdString() << std::endl;
245 std::cout << QObject::tr(
"CTRL+C to exit" ).toStdString() << std::endl;
252 auto httpHandler = [ & ]( QTcpSocket * clientConnection )
259 QString incomingData;
262 while ( IS_RUNNING && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
265 if ( ! clientConnection->bytesAvailable() )
267 qApp->processEvents();
272 while ( IS_RUNNING && clientConnection->bytesAvailable() > 0 )
274 incomingData.append( clientConnection->readAll() );
280 int firstLinePos { incomingData.indexOf(
"\r\n" ) };
281 if ( firstLinePos == -1 )
283 throw HttpException( QStringLiteral(
"HTTP error finding protocol header" ) );
286 const QString firstLine { incomingData.left( firstLinePos ) };
287 const QStringList firstLinePieces { firstLine.split(
' ' ) };
288 if ( firstLinePieces.size() != 3 )
290 throw HttpException( QStringLiteral(
"HTTP error splitting protocol header" ) );
293 const QString methodString { firstLinePieces.at( 0 ) };
296 if ( methodString ==
"GET" )
298 method = QgsServerRequest::Method::GetMethod;
300 else if ( methodString ==
"POST" )
302 method = QgsServerRequest::Method::PostMethod;
304 else if ( methodString ==
"HEAD" )
306 method = QgsServerRequest::Method::HeadMethod;
308 else if ( methodString ==
"PUT" )
310 method = QgsServerRequest::Method::PutMethod;
312 else if ( methodString ==
"PATCH" )
314 method = QgsServerRequest::Method::PatchMethod;
316 else if ( methodString ==
"DELETE" )
318 method = QgsServerRequest::Method::DeleteMethod;
322 throw HttpException( QStringLiteral(
"HTTP error unsupported method: %1" ).arg( methodString ) );
325 const QString protocol { firstLinePieces.at( 2 )};
326 if ( protocol != QLatin1String(
"HTTP/1.0" ) && protocol != QLatin1String(
"HTTP/1.1" ) )
328 throw HttpException( QStringLiteral(
"HTTP error unsupported protocol: %1" ).arg( protocol ) );
333 int endHeadersPos { incomingData.indexOf(
"\r\n\r\n" ) };
335 if ( endHeadersPos == -1 )
337 throw HttpException( QStringLiteral(
"HTTP error finding headers" ) );
340 const QStringList httpHeaders { incomingData.mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split(
"\r\n" ) };
342 for (
const auto &headerLine : httpHeaders )
344 const int headerColonPos { headerLine.indexOf(
':' ) };
345 if ( headerColonPos > 0 )
347 headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
351 const int headersSize { endHeadersPos + 4 };
354 if ( headers.contains( QStringLiteral(
"Content-Length" ) ) )
357 const int contentLength { headers.value( QStringLiteral(
"Content-Length" ) ).toInt( &ok ) };
358 if ( ok && contentLength > incomingData.length() - headersSize )
367 QString url { qgetenv(
"REQUEST_URI" ) };
371 const QString path { firstLinePieces.at( 1 )};
373 if ( headers.contains( QStringLiteral(
"Host" ) ) )
375 url = QStringLiteral(
"http://%1%2" ).arg( headers.value( QStringLiteral(
"Host" ) ), path );
379 url = QStringLiteral(
"http://%1:%2%3" ).arg( ipAddress ).arg( port ).arg( path );
384 QByteArray data { incomingData.mid( headersSize ).toUtf8() };
386 auto start = std::chrono::steady_clock::now();
395 if ( clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
400 auto elapsedTime { std::chrono::steady_clock::now() - start };
402 if ( ! knownStatuses.contains( response.
statusCode() ) )
404 throw HttpException( QStringLiteral(
"HTTP error unsupported status code: %1" ).arg( response.
statusCode() ) );
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 )
413 clientConnection->write( QStringLiteral(
"%1: %2\r\n" ).arg( it.key(), it.value() ).toUtf8() );
415 clientConnection->write(
"\r\n" );
416 const QByteArray body { response.
body() };
417 clientConnection->write( body );
420 std::cout << QStringLiteral(
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m" )
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(
' ' ),
430 clientConnection->disconnectFromHost();
432 catch ( HttpException &ex )
435 if ( clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
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() );
446 std::cout << QStringLiteral(
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m" )
447 .arg( clientConnection->peerAddress().toString() )
448 .arg( QDateTime::currentDateTime().toString() )
449 .arg( ex.message() ).toStdString() << std::endl;
451 clientConnection->disconnectFromHost();
456 clientConnection->deleteLater();
462 QTimer::singleShot( 0, [ & ]
466 if ( tcpServer.hasPendingConnections() )
468 QTcpSocket *clientConnection = tcpServer.nextPendingConnection();
469 if ( clientConnection )
471 httpHandler( clientConnection );
476 qApp->processEvents( );
477 std::this_thread::sleep_for( std::chrono::milliseconds( 1 ) );
486 auto exitHandler = [ ](
int signal )
488 std::cout << QStringLiteral(
"Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
493 signal( SIGTERM, exitHandler );
494 signal( SIGABRT, exitHandler );
495 signal( SIGINT, exitHandler );
496 signal( SIGPIPE, [ ](
int )
498 std::cerr << QStringLiteral(
"Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
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.
QByteArray body() const
Returns body.
QMap< QString, QString > headers() const override
Returns all the headers.
int statusCode() const override
Returns the http status code.
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).
Method
HTTP Method (or equivalent) used for the request.
QMap< QString, QString > Headers
The QgsServer class provides OGC web services.
void handleRequest(QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project=nullptr)
Handles the request.
int main(int argc, char *argv[])