Files

424 lines
15 KiB
C++
Raw Permalink Normal View History

2019-04-01 10:53:46 +02:00
/*
* This file is part of the Flowee project
* Copyright (C) 2019-2026 Tom Zander <tom@flowee.org>
2019-04-01 10:53:46 +02:00
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "FloweeServiceApplication.h"
#include <config/flowee-config.h>
2019-04-01 10:53:46 +02:00
#include <QCommandLineParser>
#include <QStandardPaths>
2024-08-26 22:04:01 +02:00
#include <QTextStream>
#include <QDateTime>
2019-04-01 10:53:46 +02:00
#include <signal.h>
2025-02-20 20:32:13 +01:00
#include <QTimer>
2025-02-23 22:34:39 +01:00
#include <QFileInfo>
#include <QDir>
2019-04-01 10:53:46 +02:00
#include <utilstrencodings.h> // for SplitHostPort
2019-04-13 16:23:02 +02:00
#include <clientversion.h>
#include <boost/asio.hpp>
2022-08-20 18:24:55 +02:00
#ifdef Qt6Network_FOUND
#include <QtNetwork/QNetworkInterface>
2020-11-16 23:09:17 +01:00
#include <QTcpServer>
#endif
2019-04-13 16:23:02 +02:00
namespace {
void HandleSigTerm(int) {
2019-04-01 10:53:46 +02:00
QCoreApplication::quit();
}
void HandleSigHup(int) {
FloweeServiceApplication *app = qobject_cast<FloweeServiceApplication*>(QCoreApplication::instance());
Q_ASSERT(app);
app->handleSigHub();
}
}
2025-02-20 20:32:13 +01:00
#ifdef __linux__
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
namespace {
void netWatch()
{
// Create a netlink socket
const int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (sock_fd < 0) {
logFatal() << "Failed to get netlink socket";
return;
}
// Bind to the socket
struct sockaddr_nl sa;
memset(&sa, 0, sizeof(sa));
sa.nl_family = AF_NETLINK;
sa.nl_groups = RTMGRP_LINK; // Subscribe to link events
if (bind(sock_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
logFatal() << "Failed to bind to socket";
close(sock_fd);
return;
}
char buffer[4096];
while (true) {
ssize_t len = recv(sock_fd, buffer, sizeof(buffer), 0);
if (len < 0) {
logFatal() << "Disconnected netlink";
break;
}
for (struct nlmsghdr *nlh = (struct nlmsghdr *) buffer; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
if (nlh->nlmsg_type == RTM_NEWLINK) {
FloweeServiceApplication *app = qobject_cast<FloweeServiceApplication*>(QCoreApplication::instance());
assert(app);
app->networkSetupChanged();
break;
}
}
}
close(sock_fd);
}
}
#endif
2019-09-12 15:23:30 +02:00
FloweeServiceApplication::FloweeServiceApplication(int &argc, char **argv, short appLogSection)
2019-04-01 10:53:46 +02:00
: QCoreApplication(argc, argv),
2019-04-13 16:23:02 +02:00
m_debug(QStringList() << "debug", "Use debug level logging"),
2019-05-13 18:27:09 +02:00
m_verbose(QStringList() << "verbose" << "v", "Be more verbose"),
m_quiet(QStringList() << "quiet" << "q", "Be quiet, only errors are shown"),
2019-04-13 16:23:02 +02:00
m_version(QStringList() << "version", "Display version"),
2019-04-09 19:34:55 +02:00
m_bindAddress(QStringList() << "bind", "Bind to this IP:port", "IP-ADDRESS"),
2019-09-12 15:23:30 +02:00
m_connect("connect", "Server location and port", "Hostname"),
m_appLogSection(appLogSection)
2019-04-01 10:53:46 +02:00
{
2025-02-20 20:32:13 +01:00
connect (this, &FloweeServiceApplication::startRebindTriggered, this, [=]() {
QTimer::singleShot(2000, this, &FloweeServiceApplication::rebind);
}, Qt::QueuedConnection);
2019-04-01 10:53:46 +02:00
}
FloweeServiceApplication::~FloweeServiceApplication()
{
2019-04-10 15:38:37 +02:00
if (!m_logFile.isEmpty()) // only log when a logfile was passed to the setup()
logFatal(m_appLogSection) << "Shutdown";
2019-04-01 10:53:46 +02:00
}
void FloweeServiceApplication::addServerOptions(QCommandLineParser &parser, Options options)
2019-04-01 10:53:46 +02:00
{
2019-05-13 18:27:09 +02:00
m_isServer = true;
addClientOptions(parser, options);
}
void FloweeServiceApplication::addClientOptions(QCommandLineParser &parser, Options options)
{
m_parser = &parser;
2019-05-13 18:27:09 +02:00
#ifndef BCH_NO_DEBUG_OUTPUT
2021-02-22 14:09:19 +01:00
if (!m_isServer)
parser.addOption(m_debug);
2019-05-13 18:27:09 +02:00
#endif
2019-04-13 16:23:02 +02:00
parser.addOption(m_version);
if (!options.testFlag(NoConnect))
parser.addOption(m_connect);
2019-05-13 18:27:09 +02:00
if (m_isServer) {
parser.addOption(m_bindAddress);
} else if (!options.testFlag(NoVerbosity)) {
2019-05-13 18:27:09 +02:00
parser.addOption(m_verbose);
parser.addOption(m_quiet);
}
2019-04-01 10:53:46 +02:00
}
2019-05-09 12:15:34 +02:00
void FloweeServiceApplication::setup(const char *logFilename, const QString &configFilePath) {
2019-04-13 16:23:02 +02:00
if (m_parser && m_parser->isSet(m_version)) {
QTextStream out(stdout);
2023-07-15 22:01:07 +02:00
out << applicationName() << " " << applicationVersion() << Qt::endl;
out << "Flowee Libs version: " << FormatFullVersion().c_str() << Qt::endl;
2022-08-20 19:18:34 +02:00
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;
2019-04-13 16:23:02 +02:00
::exit(0);
return;
}
2019-09-12 15:23:30 +02:00
if (m_parser && (!m_isServer && (m_parser->isSet(m_verbose) || m_parser->isSet(m_quiet)
2019-05-13 18:27:09 +02:00
#ifndef BCH_NO_DEBUG_OUTPUT
|| m_parser->isSet(m_debug)
#endif
2019-09-12 15:23:30 +02:00
))) {
auto *logger = Log::Manager::instance();
logger->clearChannels();
2019-05-13 18:27:09 +02:00
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) {
2019-04-10 15:38:37 +02:00
m_logFile = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
2021-02-07 18:07:22 +01:00
+ QString("/%1").arg(logFilename);
2025-02-23 22:34:39 +01:00
if (!configFilePath.isEmpty()) {
QFileInfo item(configFilePath);
if (item.isDir())
m_logsconf = configFilePath + "/logs.conf";
else if (item.isFile())
m_logsconf = configFilePath;
}
if (m_logsconf.isEmpty())
m_logsconf = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "logs.conf");
2021-02-07 18:07:22 +01:00
if (m_logsconf.isEmpty())
2022-01-19 17:31:25 +01:00
m_logsconf = QStandardPaths::locate(QStandardPaths::ConfigLocation,
organizationName() + "/logs.conf");
if (m_logsconf.isEmpty()) { // last try, homedir (for system installs).
auto inHome = QDir::homePath() + "/logs.conf";
if (QFile::exists(inHome))
m_logsconf = inHome;
}
2019-04-10 15:38:37 +02:00
if (m_logsconf.isEmpty()) {
2019-05-09 12:15:34 +02:00
logCritical().nospace() << applicationName() << "] No logs config found";
2020-12-25 23:51:06 +01:00
for (auto &p : QStandardPaths::standardLocations(QStandardPaths::AppConfigLocation)) {
2019-05-09 12:15:34 +02:00
logWarning(m_appLogSection).nospace() << " tried " << p << "/logs.conf";
}
2021-02-07 18:07:22 +01:00
for (auto &p : QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)) {
logWarning(m_appLogSection).nospace() << " tried " << p << '/' << organizationName() << "/logs.conf";
2021-02-07 18:07:22 +01:00
}
logWarning(m_appLogSection).nospace() << " tried " << QDir::homePath() << "/logs.conf";
2020-08-26 14:05:43 +02:00
logCritical().nospace() << "Log output goes to: " << m_logFile;
2019-06-02 20:23:19 +02:00
Log::Manager::instance()->setLogLevel(m_appLogSection, Log::WarningLevel);
2019-04-10 15:38:37 +02:00
} else {
2019-05-09 12:15:34 +02:00
logCritical().nospace() << applicationName() << "] Trying logs config at " << m_logsconf;
2019-04-10 15:38:37 +02:00
}
2019-05-09 12:15:34 +02:00
Log::Manager::instance()->parseConfig(m_logsconf.toLocal8Bit().toStdString(), m_logFile.toLocal8Bit().toStdString());
2023-07-15 22:01:07 +02:00
logFatal() << organizationName() << applicationName() << "starting. Version:"
<< applicationVersion() << "Libs version:" << FormatFullVersion().c_str();
2024-08-26 22:04:01 +02:00
logFatal() << "Start-time UTC:" << QDateTime::currentDateTimeUtc().toString();
logFatal() << "Machine local :" << QDateTime::currentDateTime().toString();
2020-11-16 18:59:25 +01:00
logCritical() << "Main Log-Section:" << m_appLogSection;
2019-04-10 15:38:37 +02:00
}
2019-04-01 10:53:46 +02:00
// 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;
2019-09-12 15:23:30 +02:00
sigaction(SIGHUP, &sa_hup, nullptr);
2019-04-01 10:53:46 +02:00
struct sigaction sa;
sa.sa_handler = HandleSigTerm;
2019-04-01 10:53:46 +02:00
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
2019-09-12 15:23:30 +02:00
sigaction(SIGTERM, &sa, nullptr);
sigaction(SIGINT, &sa, nullptr);
2019-04-01 10:53:46 +02:00
// Ignore SIGPIPE, otherwise it will bring the daemon down if the client closes unexpectedly
signal(SIGPIPE, SIG_IGN);
}
2019-09-12 15:23:30 +02:00
EndPoint FloweeServiceApplication::serverAddressFromArguments(uint16_t defaultPort) const
2019-04-01 10:53:46 +02:00
{
2019-05-09 12:15:34 +02:00
Q_ASSERT(m_parser);
2019-04-01 10:53:46 +02:00
EndPoint ep;
2019-06-02 20:16:49 +02:00
ep.announcePort = defaultPort;
2019-05-09 12:15:34 +02:00
if (m_parser->isSet(m_connect))
2019-06-02 20:16:49 +02:00
SplitHostPort(m_parser->value(m_connect).toStdString(), ep.announcePort, ep.hostname);
2019-05-09 12:15:34 +02:00
else
ep.ipAddress = boost::asio::ip::address_v4::loopback();
2019-04-01 10:53:46 +02:00
return ep;
}
2025-02-20 20:32:13 +01:00
QList<boost::asio::ip::tcp::endpoint> FloweeServiceApplication::bindingEndPoints(uint16_t defaultPort, DefaultBindOption defaultBind) const
2019-04-09 19:34:55 +02:00
{
2025-02-20 20:32:13 +01:00
assert(m_parser);
QStringList addresses = m_parser->values(m_bindAddress);
2019-08-24 15:36:17 +02:00
if (addresses.isEmpty()) {
switch (defaultBind) {
2022-01-28 17:18:39 +01:00
case FloweeServiceApplication::YggAsDefault:
addresses << "yggdrasil";
break;
2019-08-24 15:36:17 +02:00
case FloweeServiceApplication::LocalhostAsDefault:
addresses << "localhost";
break;
case FloweeServiceApplication::AllInterfacesAsDefault:
addresses << "0.0.0.0";
break;
2019-09-12 15:23:30 +02:00
case FloweeServiceApplication::UserSupplied: break;
2019-08-24 15:36:17 +02:00
}
}
2019-04-09 19:34:55 +02:00
QList<boost::asio::ip::tcp::endpoint> answer;
2020-12-25 23:51:06 +01:00
for (QString &address : addresses) {
2019-04-09 19:34:55 +02:00
std::string hostname;
2019-06-02 20:16:49 +02:00
uint16_t port = defaultPort;
2019-04-09 19:34:55 +02:00
SplitHostPort(address.toStdString(), port, hostname);
std::transform(hostname.begin(), hostname.end(), hostname.begin(), ::tolower);
2022-01-28 17:18:39 +01:00
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) {
2022-08-20 19:18:34 +02:00
#ifdef Qt6Network_FOUND
2020-12-25 23:51:06 +01:00
for (auto &net : QNetworkInterface::allAddresses()) {
2022-01-28 17:18:39 +01:00
if (net.isLoopback())
continue;
2025-11-08 19:59:11 +01:00
if (net.isLinkLocal())
continue;
2022-01-28 17:18:39 +01:00
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';
}
2022-01-28 17:18:39 +01:00
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 {
2025-02-11 16:46:21 +01:00
answer.append(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address(hostname), port));
} catch (std::runtime_error &e) {
logFatal().nospace() << "Bind address didn't parse: `" << address << "'. Skipping.";
2019-09-12 15:23:30 +02:00
logDebug() << e;
}
2019-04-09 19:34:55 +02:00
}
}
return answer;
}
2019-04-01 10:53:46 +02:00
void FloweeServiceApplication::handleSigHub() const
{
Log::Manager::instance()->reopenLogFiles();
Log::Manager::instance()->parseConfig(m_logsconf.toLocal8Bit().toStdString(), m_logFile.toLocal8Bit().toStdString());
emit reparseConfig();
2019-04-01 10:53:46 +02:00
}
2020-11-16 22:45:46 +01:00
2020-11-16 23:09:17 +01:00
int FloweeServiceApplication::bindTo(QTcpServer *server, int defaultPort)
2020-11-16 22:45:46 +01:00
{
2022-08-20 19:18:34 +02:00
#ifdef Qt6Network_FOUND
2020-11-16 23:09:17 +01:00
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;
2022-08-20 19:18:34 +02:00
port = ip.mid(index + 1).toInt(&ok);
2020-11-16 23:09:17 +01:00
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;
}
2021-02-26 15:08:23 +01:00
logCritical() << "Listening on" << address.toString() << "port:" << port;
2020-11-16 23:09:17 +01:00
return 0;
#else
return 1; // it obviously failed if we didn't do anything.
#endif
2020-11-16 22:45:46 +01:00
}
2025-02-20 20:32:13 +01:00
int FloweeServiceApplication::bindWith(uint16_t defaultPort,
DefaultBindOption defaultBind,
const std::function<void(const boost::asio::ip::tcp::endpoint&)> &callback)
{
assert(m_parser);
assert(m_rebind.get() == nullptr);
m_rebind.reset(new NetworkRebindData());
m_rebind->bindOne_function = callback;
m_rebind->defaultPort = defaultPort;
m_rebind->defaultBind = defaultBind;
auto options = bindingEndPoints(defaultPort, defaultBind);
for (auto i = options.begin(); i != options.end(); ++i) {
try {
m_rebind->bindOne_function(*i);
m_rebind->boundInterfaces.push_back(*i);
} catch (const std::exception &e) {
}
}
#ifdef __linux__
m_rebind->netWatch = new std::thread(netWatch);
#endif
return (int) m_rebind->boundInterfaces.size();
}
void FloweeServiceApplication::networkSetupChanged()
{
assert(m_rebind.get());
if (m_rebind->triggeredRebind)
return;
m_rebind->triggeredRebind = true;
emit startRebindTriggered();
}
void FloweeServiceApplication::rebind()
{
m_rebind->triggeredRebind = false;
auto options = bindingEndPoints(m_rebind->defaultPort, m_rebind->defaultBind);
for (auto i = options.begin(); i != options.end(); ++i) {
if (m_rebind->boundInterfaces.contains(*i)) // skip allready bound ones.
continue;
try {
m_rebind->bindOne_function(*i);
m_rebind->boundInterfaces.push_back(*i);
} catch (const std::exception &e) {
}
}
}