/* * This file is part of the Flowee project * Copyright (C) 2019-2022 Tom Zander * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "FloweeServiceApplication.h" #include #include #include #include #include #include // for SplitHostPort #include #include #include #ifdef Qt6Network_FOUND #include #include #endif namespace { void HandleSigTerm(int) { QCoreApplication::quit(); } void HandleSigHup(int) { FloweeServiceApplication *app = qobject_cast(QCoreApplication::instance()); Q_ASSERT(app); app->handleSigHub(); } } FloweeServiceApplication::FloweeServiceApplication(int &argc, char **argv, short appLogSection) : QCoreApplication(argc, argv), m_debug(QStringList() << "debug", "Use debug level logging"), m_verbose(QStringList() << "verbose" << "v", "Be more verbose"), m_quiet(QStringList() << "quiet" << "q", "Be quiet, only errors are shown"), m_version(QStringList() << "version", "Display version"), m_bindAddress(QStringList() << "bind", "Bind to this IP:port", "IP-ADDRESS"), m_connect("connect", "Server location and port", "Hostname"), m_appLogSection(appLogSection) { } FloweeServiceApplication::~FloweeServiceApplication() { if (!m_logFile.isEmpty()) // only log when a logfile was passed to the setup() logFatal(m_appLogSection) << "Shutdown"; } void FloweeServiceApplication::addServerOptions(QCommandLineParser &parser, Options options) { m_isServer = true; addClientOptions(parser, options); } void FloweeServiceApplication::addClientOptions(QCommandLineParser &parser, Options options) { m_parser = &parser; #ifndef BCH_NO_DEBUG_OUTPUT if (!m_isServer) parser.addOption(m_debug); #endif parser.addOption(m_version); if (!options.testFlag(NoConnect)) parser.addOption(m_connect); if (m_isServer) { parser.addOption(m_bindAddress); } else if (!options.testFlag(NoVerbosity)) { parser.addOption(m_verbose); parser.addOption(m_quiet); } } void FloweeServiceApplication::setup(const char *logFilename, const QString &configFilePath) { if (m_parser && m_parser->isSet(m_version)) { QTextStream out(stdout); out << applicationName() << " " << FormatFullVersion().c_str() << Qt::endl; out << "License GPLv3+: GNU GPL version 3 or later" << Qt::endl; out << "This is free software: you are free to change and redistribute it." << Qt::endl << Qt::endl; ::exit(0); return; } if (m_parser && (!m_isServer && (m_parser->isSet(m_verbose) || m_parser->isSet(m_quiet) #ifndef BCH_NO_DEBUG_OUTPUT || m_parser->isSet(m_debug) #endif ))) { auto *logger = Log::Manager::instance(); logger->clearChannels(); Log::Verbosity v = Log::WarningLevel; #ifndef BCH_NO_DEBUG_OUTPUT if (m_parser->isSet(m_debug)) v = Log::DebugLevel; else #endif if (m_parser->isSet(m_verbose)) v = Log::InfoLevel; else if (m_parser->isSet(m_quiet)) v = Log::FatalLevel; logger->clearLogLevels(v); logger->addConsoleChannel(); } else if (logFilename) { m_logsconf = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "logs.conf"); if (m_logsconf.isEmpty() && !configFilePath.isEmpty()) { int i = configFilePath.lastIndexOf('/'); if (i == -1) m_logsconf = configFilePath + "/logs.conf"; else m_logsconf = configFilePath.left(i + 1) + "logs.conf"; } m_logFile = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QString("/%1").arg(logFilename); if (m_logsconf.isEmpty()) m_logsconf = QStandardPaths::locate(QStandardPaths::ConfigLocation, organizationName() + "/logs.conf"); if (m_logsconf.isEmpty()) { logCritical().nospace() << applicationName() << "] No logs config found"; for (auto &p : QStandardPaths::standardLocations(QStandardPaths::AppConfigLocation)) { logWarning(m_appLogSection).nospace() << " tried " << p << "/logs.conf"; } for (auto &p : QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)) { logWarning(m_appLogSection).nospace() << " tried " << p << "/flowee/logs.conf"; } logCritical().nospace() << "Log output goes to: " << m_logFile; Log::Manager::instance()->setLogLevel(m_appLogSection, Log::WarningLevel); } else { logCritical().nospace() << applicationName() << "] Trying logs config at " << m_logsconf; } Log::Manager::instance()->parseConfig(m_logsconf.toLocal8Bit().toStdString(), m_logFile.toLocal8Bit().toStdString()); logFatal().nospace() << "Flowee " << applicationName() << " starting. Version: " << FormatFullVersion().c_str(); logCritical() << "Main Log-Section:" << m_appLogSection; } // Reopen log on SIGHUP (to allow for log-rotate) struct sigaction sa_hup; sa_hup.sa_handler = HandleSigHup; sigemptyset(&sa_hup.sa_mask); sa_hup.sa_flags = 0; sigaction(SIGHUP, &sa_hup, nullptr); struct sigaction sa; sa.sa_handler = HandleSigTerm; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGTERM, &sa, nullptr); sigaction(SIGINT, &sa, nullptr); // Ignore SIGPIPE, otherwise it will bring the daemon down if the client closes unexpectedly signal(SIGPIPE, SIG_IGN); } EndPoint FloweeServiceApplication::serverAddressFromArguments(uint16_t defaultPort) const { Q_ASSERT(m_parser); EndPoint ep; ep.announcePort = defaultPort; if (m_parser->isSet(m_connect)) SplitHostPort(m_parser->value(m_connect).toStdString(), ep.announcePort, ep.hostname); else ep.ipAddress = boost::asio::ip::address_v4::loopback(); return ep; } QList FloweeServiceApplication::bindingEndPoints(QCommandLineParser &parser, uint16_t defaultPort, DefaultBindOption defaultBind) const { QStringList addresses = parser.values(m_bindAddress); if (addresses.isEmpty()) { switch (defaultBind) { case FloweeServiceApplication::YggAsDefault: addresses << "yggdrasil"; break; case FloweeServiceApplication::LocalhostAsDefault: addresses << "localhost"; break; case FloweeServiceApplication::AllInterfacesAsDefault: addresses << "0.0.0.0"; break; case FloweeServiceApplication::UserSupplied: break; } } QList answer; for (QString &address : addresses) { std::string hostname; uint16_t port = defaultPort; SplitHostPort(address.toStdString(), port, hostname); std::transform(hostname.begin(), hostname.end(), hostname.begin(), ::tolower); const bool all = hostname == "0.0.0.0"; const bool ygg = hostname == "yggdrasil"; using namespace boost::asio; if (hostname.empty() || hostname == "localhost" || all || ygg) { answer.push_back(ip::tcp::endpoint(ip::address_v4::loopback(), port)); answer.push_back(ip::tcp::endpoint(ip::address_v6::loopback(), port)); if (all || ygg) { #ifdef Qt6Network_FOUND for (auto &net : QNetworkInterface::allAddresses()) { if (net.isLoopback()) continue; bool use = all; if (!all && ygg && net.protocol() == QAbstractSocket::IPv6Protocol) { // yggdrasil uses normal IPv6 addresses, but with a specific prefix not // seen on the wider Internet. Q_IPV6ADDR bytes = net.toIPv6Address(); use = (bytes[0] & 0xfe) == 2; // address is in range '0200/7'; } if (!use) continue; boost::system::error_code ec; auto ip = ip::make_address(net.toString().toStdString(), ec); if (ec) { logDebug() << "Internal issue with address:" << net.toString() << ec.message(); continue; } answer.push_back(ip::tcp::endpoint(ip, port)); } #endif } } else { try { answer.append(boost::asio::ip::tcp::endpoint(boost::asio::ip::address::from_string(hostname), port)); } catch (std::runtime_error &e) { logFatal().nospace() << "Bind address didn't parse: `" << address << "'. Skipping."; logDebug() << e; } } } return answer; } void FloweeServiceApplication::handleSigHub() const { Log::Manager::instance()->reopenLogFiles(); Log::Manager::instance()->parseConfig(m_logsconf.toLocal8Bit().toStdString(), m_logFile.toLocal8Bit().toStdString()); emit reparseConfig(); } int FloweeServiceApplication::bindTo(QTcpServer *server, int defaultPort) { #ifdef Qt6Network_FOUND QStringList addresses = m_parser->values(m_bindAddress); QHostAddress address; int port = defaultPort; if (!addresses.empty()) { if (addresses.size() > 1) { logFatal() << "More than one --bind passsed, please limit to one or use 'localhost' / '0.0.0.0' wildcards"; return 1; } QString ip(addresses.front()); int index = ip.indexOf(":"); if (index > 0) { bool ok; port = ip.mid(index + 1).toInt(&ok); if (!ok) { logFatal() << "Could not parse port portion of bind address."; return 2; } ip = ip.left(index); } if (ip.compare("localhost", Qt::CaseInsensitive) == 0) address = QHostAddress::LocalHost; else if (ip == QLatin1String("0.0.0.0")) address = QHostAddress::Any; else { address = QHostAddress(ip); } if (address.isNull()) { logFatal() << "Did not understand bind address"; return 2; } } else { address = QHostAddress::Any; } if (!server->listen(address, port)) { logCritical() << " Failed to listen on interface"; return 1; } logCritical() << "Listening on" << address.toString() << "port:" << port; return 0; #else return 1; // it obviously failed if we didn't do anything. #endif }