collabora-online/wsd/COOLWSD.cpp

4774 lines
163 KiB
C++
Raw Normal View History

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* Copyright the Collabora Online contributors.
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <config.h>
#include <config_version.h>
#include "COOLWSD.hpp"
#if ENABLE_FEATURE_LOCK
#include "CommandControl.hpp"
#endif
#if !MOBILEAPP
#include <HostUtil.hpp>
#endif // !MOBILEAPP
/* Default host used in the start test URI */
#define COOLWSD_TEST_HOST "localhost"
/* Default cool UI used in the admin console URI */
#define COOLWSD_TEST_ADMIN_CONSOLE "/browser/dist/admin/admin.html"
/* Default cool UI used in for monitoring URI */
#define COOLWSD_TEST_METRICS "/cool/getMetrics"
/* Default cool UI used in the start test URI */
#define COOLWSD_TEST_COOL_UI "/browser/" COOLWSD_VERSION_HASH "/debug.html"
/* Default document used in the start test URI */
#define COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_WRITER "test/data/hello-world.odt"
#define COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_CALC "test/data/hello-world.ods"
#define COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_IMPRESS "test/data/hello-world.odp"
#define COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_DRAW "test/data/hello-world.odg"
/* Default ciphers used, when not specified otherwise */
#define DEFAULT_CIPHER_SET "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"
// This is the main source for the coolwsd program. COOL uses several coolwsd processes: one main
// parent process that listens on the TCP port and accepts connections from COOL clients, and a
2015-04-10 14:20:04 +02:00
// number of child processes, each which handles a viewing (editing) session for one document.
#include <unistd.h>
#include <stdlib.h>
#include <sysexits.h>
#include <sys/types.h>
2016-04-04 08:36:27 +02:00
#include <sys/wait.h>
#include <sys/resource.h>
#include <cassert>
2016-08-31 16:02:29 +02:00
#include <clocale>
#include <condition_variable>
2015-03-28 12:55:35 +01:00
#include <cstdlib>
#include <cstring>
2016-08-31 16:02:29 +02:00
#include <ctime>
#include <chrono>
#include <iostream>
#include <map>
#include <memory>
2015-07-13 16:13:06 +02:00
#include <mutex>
#include <regex>
#include <sstream>
#include <string>
#include <thread>
#if !MOBILEAPP
#include <Poco/Net/Context.h>
#include <Poco/Net/HTTPRequest.h>
#include <Poco/Net/HTTPResponse.h>
#include <Poco/Net/IPAddress.h>
#include <Poco/Net/MessageHeader.h>
#include <Poco/Net/NameValueCollection.h>
#include <Poco/Net/Net.h>
#include <Poco/Net/NetException.h>
#include <Poco/Net/PartHandler.h>
#include <Poco/Net/SocketAddress.h>
#include <Poco/Net/AcceptCertificateHandler.h>
#include <Poco/Net/Context.h>
#include <Poco/Net/KeyConsoleHandler.h>
#include <Poco/Net/SSLManager.h>
using Poco::Net::PartHandler;
#include <cerrno>
#include <stdexcept>
#include <fstream>
#include <unordered_map>
#include "Admin.hpp"
#include "Auth.hpp"
#include "FileServer.hpp"
#include "UserMessages.hpp"
#endif
#include <Poco/DateTimeFormatter.h>
#include <Poco/DirectoryIterator.h>
#include <Poco/Exception.h>
#include <Poco/File.h>
#include <Poco/FileStream.h>
#include <Poco/MemoryStream.h>
#include <Poco/Net/HostEntry.h>
#include <Poco/Path.h>
#include <Poco/TemporaryFile.h>
#include <Poco/URI.h>
#include <Poco/Util/AbstractConfiguration.h>
#include <Poco/Util/HelpFormatter.h>
#include <Poco/Util/MapConfiguration.h>
#include <Poco/Util/Option.h>
#include <Poco/Util/OptionException.h>
#include <Poco/Util/OptionSet.h>
#include <Poco/Util/ServerApplication.h>
#include <Poco/Util/XMLConfiguration.h>
#include "ClientSession.hpp"
#include <ClientRequestDispatcher.hpp>
#include <Common.hpp>
#include <Clipboard.hpp>
#include <Crypto.hpp>
#include <DelaySocket.hpp>
#include "DocumentBroker.hpp"
#include <common/JsonUtil.hpp>
wsd: faster jail setup via bind-mount loolmount now works and supports mounting and unmounting, plus numerous improvements, refactoring, logging, etc.. When enabled, binding improves the jail setup time by anywhere from 2x to orders of magnitude (in docker, f.e.). A new config entry mount_jail_tree controls whether mounting is used or the old method of linking/copying of jail contents. It is set to true by default and falls back to linking/copying. A test mount is done when the setting is enabled, and if mounting fails, it's disabled to avoid noise. Temporarily disabled for unit-tests until we can cleanup lingering mounts after Jenkins aborts our build job. In a future patch we will have mount/jail cleanup as part of make. The network/system files in /etc that need frequent refreshing are now updated in systemplate to make their most recent version available in the jails. These files can change during the course of loolwsd lifetime, and are unlikely to be updated in systemplate after installation at all. We link to them in the systemplate/etc directory, and if that fails, we copy them before forking each kit instance to have the latest. This reworks the approach used to bind-mount the jails and the templates such that the total is now down to only three mounts: systemplate, lo, tmp. As now systemplate and lotemplate are shared, they must be mounted as readonly, this means that user/ must now be moved into tmp/user/ which is writable. The mount-points must be recursive, because we mount lo/ within the mount-point of systemplate (which is the root of the jail). But because we (re)bind recursively, and because both systemplate and lotemplate are mounted for each jails, we need to make them unbindable, so they wouldn't multiply the mount-points for each jails (an explosive growth!) Contrarywise, we don't want the mount-points to be shared, because we don't expect to add/remove mounts after a jail is created. The random temp directory is now created and set correctly, plus many logging and other improvements. Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829 Tested-by: Jenkins Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 15:02:58 +02:00
#include <common/FileUtil.hpp>
#include <common/JailUtil.hpp>
#include <common/Watchdog.hpp>
#if MOBILEAPP
# include <Kit.hpp>
#endif
#include <Log.hpp>
#include <MobileApp.hpp>
#include <Protocol.hpp>
#include <Session.hpp>
#if ENABLE_SSL
# include <SslSocket.hpp>
#endif
#include "Storage.hpp"
#include "TraceFile.hpp"
#include <Unit.hpp>
#include <Util.hpp>
#include <common/ConfigUtil.hpp>
#include <common/TraceEvent.hpp>
#include <common/SigUtil.hpp>
#include <RequestVettingStation.hpp>
#include <ServerSocket.hpp>
#if MOBILEAPP
#ifdef IOS
#include "ios.h"
#elif defined(GTKAPP)
#include "gtk.hpp"
#elif defined(__ANDROID__)
#include "androidapp.hpp"
#elif WASMAPP
#include "wasmapp.hpp"
#endif
#endif // MOBILEAPP
#ifdef __linux__
#if !MOBILEAPP
#include <sys/inotify.h>
#endif
#endif
using namespace COOLProtocol;
using Poco::DirectoryIterator;
2015-05-29 07:49:49 +02:00
using Poco::Exception;
using Poco::File;
using Poco::Path;
using Poco::URI;
using Poco::Net::HTTPRequest;
using Poco::Net::HTTPResponse;
using Poco::Net::MessageHeader;
using Poco::Net::NameValueCollection;
using Poco::Util::Application;
using Poco::Util::HelpFormatter;
using Poco::Util::LayeredConfiguration;
using Poco::Util::MissingOptionException;
using Poco::Util::Option;
using Poco::Util::OptionSet;
using Poco::Util::ServerApplication;
using Poco::Util::XMLConfiguration;
/// Port for external clients to connect to
int ClientPortNumber = 0;
/// Protocols to listen on
Socket::Type ClientPortProto = Socket::Type::All;
/// INET address to listen on
ServerSocket::Type ClientListenAddr = ServerSocket::Type::Public;
#if !MOBILEAPP
/// UDS address for kits to connect to.
std::string MasterLocation;
std::string COOLWSD::BuyProductUrl;
std::string COOLWSD::LatestVersion;
std::mutex COOLWSD::FetchUpdateMutex;
std::mutex COOLWSD::RemoteConfigMutex;
#endif
// Tracks the set of prisoners / children waiting to be used.
static std::mutex NewChildrenMutex;
static std::condition_variable NewChildrenCV;
static std::vector<std::shared_ptr<ChildProcess> > NewChildren;
static std::chrono::steady_clock::time_point LastForkRequestTime = std::chrono::steady_clock::now();
static std::atomic<int> OutstandingForks(0);
std::map<std::string, std::shared_ptr<DocumentBroker>> DocBrokers;
std::mutex DocBrokersMutex;
static Poco::AutoPtr<Poco::Util::XMLConfiguration> KitXmlConfig;
extern "C"
{
void dump_state(void); /* easy for gdb */
void forwardSigUsr2();
}
#if ENABLE_DEBUG && !MOBILEAPP
static std::chrono::milliseconds careerSpanMs(std::chrono::milliseconds::zero());
#endif
/// The timeout for a child to spawn, initially high, then reset to the default.
int ChildSpawnTimeoutMs = CHILD_TIMEOUT_MS * 4;
std::atomic<unsigned> COOLWSD::NumConnections;
std::unordered_set<std::string> COOLWSD::EditFileExtensions;
std::unordered_set<std::string> COOLWSD::ViewWithCommentsFileExtensions;
#if MOBILEAPP
// Or can this be retrieved in some other way?
int COOLWSD::prisonerServerSocketFD;
#else
/// Funky latency simulation basic delay (ms)
wsd: avoid static SocketPoll The lifetime management of static objects is extremely unpredictable and depends on many variables outside of our control or even reliable reproducibility. Complex static objects that own threads and other objects are doubly problematic because of their dependency and/or interaction with other objects. Here we replace the static DelayPoll instance with one we control its lifetime in the LOOLWSD main body, such that it is destroyed properly. Specifically, DelayPoll's dtor was accessing Poco's Logging subsystem out of order. That is, after Poco had been destroyed. Another advantage to this approach is that we don't even create the DelayPoll at all if we don't need it. The onus now is on the user of DelayPoll to make sure they create a Delay object with a long-enough lifetime to encompase it use. For completeness, here is the stacktrace: terminate called after throwing an instance of 'std::bad_alloc' what(): std::bad_alloc Program received signal SIGABRT, Aborted. __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51 51 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory. (gdb) bt #0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51 #1 0x00007ffff613f801 in __GI_abort () at abort.c:79 #2 0x00007ffff6d51957 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #3 0x00007ffff6d57ae6 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #4 0x00007ffff6d56b49 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #5 0x00007ffff6d574b8 in __gxx_personality_v0 () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #6 0x00007ffff671f573 in ?? () from /lib/x86_64-linux-gnu/libgcc_s.so.1 #7 0x00007ffff671fad1 in _Unwind_RaiseException () from /lib/x86_64-linux-gnu/libgcc_s.so.1 #8 0x00007ffff6d57d47 in __cxa_throw () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #9 0x00007ffff6d582dc in operator new(unsigned long) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #10 0x00005555556b5927 in void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_construct<char*>(char*, char*, std::forward_iterator_tag) [clone .isra.41] () #11 0x0000555555982a14 in Poco::Message::Message(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Poco::Message::Priority) () #12 0x000055555585a909 in SocketPoll::~SocketPoll (this=0x555555d10f60 <DelayPoll>, __in_chrg=<optimized out>) at net/Socket.cpp:145 #13 0x00007ffff6142041 in __run_exit_handlers (status=0, listp=0x7ffff64ea718 <__exit_funcs>, run_list_atexit=run_list_atexit@entry=true, run_dtors=run_dtors@entry=true) at exit.c:108 #14 0x00007ffff614213a in __GI_exit (status=<optimized out>) at exit.c:139 #15 0x0000555555752d78 in LOOLWSD::innerInitialize (this=<optimized out>, self=...) at wsd/LOOLWSD.cpp:1213 #16 0x0000555555788b07 in LOOLWSD::initialize (this=<optimized out>, self=...) at wsd/LOOLWSD.hpp:432 #17 0x00005555558dd7e4 in Poco::Util::Application::run() () #18 0x00005555556b6574 in main (argc=2, argv=0x7fffffffe528) at wsd/LOOLWSD.cpp:4276 Change-Id: Ifda55fe869fa6734b9c2490da4497d2551ac21c1 Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
2021-03-13 14:52:16 +01:00
static std::size_t SimulatedLatencyMs = 0;
#endif
void COOLWSD::appendAllowedHostsFrom(LayeredConfiguration& conf, const std::string& root, std::vector<std::string>& allowed)
{
for (size_t i = 0; ; ++i)
{
const std::string path = root + ".host[" + std::to_string(i) + ']';
if (!conf.has(path))
{
break;
}
const std::string host = getConfigValue<std::string>(conf, path, "");
if (!host.empty())
{
LOG_INF_S("Adding trusted LOK_ALLOW host: [" << host << ']');
allowed.push_back(host);
}
}
}
namespace {
std::string removeProtocolAndPort(const std::string& host)
{
std::string result;
// protocol
size_t nPos = host.find("//");
if (nPos != std::string::npos)
result = host.substr(nPos + 2);
else
result = host;
// port
nPos = result.find(":");
if (nPos != std::string::npos)
{
if (nPos == 0)
return "";
result = result.substr(0, nPos);
}
return result;
}
bool isValidRegex(const std::string& expression)
{
try
{
std::regex regex(expression);
return true;
}
catch (const std::regex_error& e) {}
return false;
}
}
void COOLWSD::appendAllowedAliasGroups(LayeredConfiguration& conf, std::vector<std::string>& allowed)
{
for (size_t i = 0;; i++)
{
const std::string path = "storage.wopi.alias_groups.group[" + std::to_string(i) + ']';
if (!conf.has(path + ".host"))
{
break;
}
std::string host = conf.getString(path + ".host", "");
bool allow = conf.getBool(path + ".host[@allow]", false);
if (!allow)
{
break;
}
host = removeProtocolAndPort(host);
if (!host.empty())
{
LOG_INF_S("Adding trusted LOK_ALLOW host: [" << host << ']');
allowed.push_back(host);
}
for (size_t j = 0;; j++)
{
const std::string aliasPath = path + ".alias[" + std::to_string(j) + ']';
if (!conf.has(aliasPath))
{
break;
}
std::string alias = getConfigValue<std::string>(conf, aliasPath, "");
alias = removeProtocolAndPort(alias);
if (!alias.empty())
{
LOG_INF_S("Adding trusted LOK_ALLOW alias: [" << alias << ']');
allowed.push_back(alias);
}
}
}
}
/// Internal implementation to alert all clients
/// connected to any document.
void COOLWSD::alertAllUsersInternal(const std::string& msg)
{
if (Util::isMobileApp())
return;
std::lock_guard<std::mutex> docBrokersLock(DocBrokersMutex);
LOG_INF("Alerting all users: [" << msg << ']');
if (UnitWSD::get().filterAlertAllusers(msg))
return;
for (auto& brokerIt : DocBrokers)
{
std::shared_ptr<DocumentBroker> docBroker = brokerIt.second;
docBroker->addCallback([msg, docBroker](){ docBroker->alertAllUsers(msg); });
}
}
void COOLWSD::alertUserInternal(const std::string& dockey, const std::string& msg)
{
if (Util::isMobileApp())
return;
std::lock_guard<std::mutex> docBrokersLock(DocBrokersMutex);
LOG_INF("Alerting document users with dockey: [" << dockey << ']' << " msg: [" << msg << ']');
for (auto& brokerIt : DocBrokers)
{
std::shared_ptr<DocumentBroker> docBroker = brokerIt.second;
if (docBroker->getDocKey() == dockey)
docBroker->addCallback([msg, docBroker](){ docBroker->alertAllUsers(msg); });
}
}
void COOLWSD::writeTraceEventRecording(const char *data, std::size_t nbytes)
{
static std::mutex traceEventFileMutex;
std::unique_lock<std::mutex> lock(traceEventFileMutex);
fwrite(data, nbytes, 1, COOLWSD::TraceEventFile);
}
void COOLWSD::writeTraceEventRecording(const std::string &recording)
{
writeTraceEventRecording(recording.data(), recording.length());
}
void COOLWSD::checkSessionLimitsAndWarnClients()
{
#if !ENABLE_SUPPORT_KEY
#if !MOBILEAPP
ssize_t docBrokerCount = DocBrokers.size() - ConvertToBroker::getInstanceCount();
if (COOLWSD::MaxDocuments < 10000 &&
(docBrokerCount > static_cast<ssize_t>(COOLWSD::MaxDocuments) || COOLWSD::NumConnections >= COOLWSD::MaxConnections))
{
const std::string info = Poco::format(PAYLOAD_INFO_LIMIT_REACHED, COOLWSD::MaxDocuments, COOLWSD::MaxConnections);
LOG_INF("Sending client 'limitreached' message: " << info);
try
{
Util::alertAllUsers(info);
}
catch (const std::exception& ex)
{
LOG_ERR("Error while shutting down socket on reaching limit: " << ex.what());
}
}
#endif
#endif
}
void COOLWSD::checkDiskSpaceAndWarnClients(const bool cacheLastCheck)
{
#if !MOBILEAPP
try
{
const std::string fs = FileUtil::checkDiskSpaceOnRegisteredFileSystems(cacheLastCheck);
if (!fs.empty())
{
LOG_WRN("File system of [" << fs << "] is dangerously low on disk space.");
COOLWSD::alertAllUsersInternal("error: cmd=internal kind=diskfull");
}
}
catch (const std::exception& exc)
{
LOG_ERR("Exception while checking disk-space and warning clients: " << exc.what());
}
#else
(void) cacheLastCheck;
#endif
}
/// Remove dead and idle DocBrokers.
/// The client of idle document should've greyed-out long ago.
void cleanupDocBrokers()
{
Util::assertIsLocked(DocBrokersMutex);
const size_t count = DocBrokers.size();
for (auto it = DocBrokers.begin(); it != DocBrokers.end(); )
{
std::shared_ptr<DocumentBroker> docBroker = it->second;
// Remove only when not alive.
if (!docBroker->isAlive())
{
LOG_INF("Removing DocumentBroker for docKey [" << it->first << "].");
docBroker->dispose();
it = DocBrokers.erase(it);
continue;
} else {
++it;
}
}
if (count != DocBrokers.size())
{
LOG_TRC("Have " << DocBrokers.size() << " DocBrokers after cleanup.\n"
<<
[&](auto& log)
{
for (auto& pair : DocBrokers)
{
log << "DocumentBroker [" << pair.first << "].\n";
}
});
#if !MOBILEAPP && ENABLE_DEBUG
if (COOLWSD::SingleKit && DocBrokers.empty())
{
LOG_DBG("Setting ShutdownRequestFlag: No more docs left in single-kit mode.");
SigUtil::requestShutdown();
}
#endif
}
}
#if !MOBILEAPP
/// Forks as many children as requested.
/// Returns the number of children requested to spawn,
/// -1 for error.
static int forkChildren(const int number)
{
if (Util::isKitInProcess())
return 0;
LOG_TRC("Request forkit to spawn " << number << " new child(ren)");
Util::assertIsLocked(NewChildrenMutex);
if (number > 0)
{
COOLWSD::checkDiskSpaceAndWarnClients(false);
const std::string aMessage = "spawn " + std::to_string(number) + '\n';
LOG_DBG("MasterToForKit: " << aMessage.substr(0, aMessage.length() - 1));
COOLWSD::sendMessageToForKit(aMessage);
OutstandingForks += number;
LastForkRequestTime = std::chrono::steady_clock::now();
return number;
}
return 0;
}
/// Cleans up dead children.
/// Returns true if removed at least one.
static bool cleanupChildren()
{
if (Util::isKitInProcess())
return 0;
Util::assertIsLocked(NewChildrenMutex);
const int count = NewChildren.size();
for (int i = count - 1; i >= 0; --i)
{
if (!NewChildren[i]->isAlive())
{
LOG_WRN("Removing dead spare child [" << NewChildren[i]->getPid() << "].");
NewChildren.erase(NewChildren.begin() + i);
}
}
return static_cast<int>(NewChildren.size()) != count;
}
/// Decides how many children need spawning and spawns.
/// Returns the number of children requested to spawn,
/// -1 for error.
static int rebalanceChildren(int balance)
{
Util::assertIsLocked(NewChildrenMutex);
const size_t available = NewChildren.size();
LOG_TRC("Rebalance children to " << balance << ", have " << available << " and "
<< OutstandingForks << " outstanding requests");
2017-03-30 22:55:17 +02:00
// Do the cleanup first.
const bool rebalance = cleanupChildren();
const auto duration = (std::chrono::steady_clock::now() - LastForkRequestTime);
const auto durationMs = std::chrono::duration_cast<std::chrono::milliseconds>(duration);
if (OutstandingForks != 0 && durationMs >= std::chrono::milliseconds(ChildSpawnTimeoutMs))
{
// Children taking too long to spawn.
// Forget we had requested any, and request anew.
LOG_WRN("ForKit not responsive for " << durationMs << " forking " << OutstandingForks
<< " children. Resetting.");
OutstandingForks = 0;
}
balance -= available;
balance -= OutstandingForks;
if (balance > 0 && (rebalance || OutstandingForks == 0))
{
LOG_DBG("prespawnChildren: Have " << available << " spare "
<< (available == 1 ? "child" : "children") << ", and "
<< OutstandingForks << " outstanding, forking " << balance
<< " more. Time since last request: " << durationMs);
return forkChildren(balance);
}
return 0;
}
/// Proactively spawn children processes
/// to load documents with alacrity.
/// Returns true only if at least one child was requested to spawn.
static bool prespawnChildren()
{
// Rebalance if not forking already.
std::unique_lock<std::mutex> lock(NewChildrenMutex, std::defer_lock);
return lock.try_lock() && (rebalanceChildren(COOLWSD::NumPreSpawnedChildren) > 0);
}
#endif
static size_t addNewChild(std::shared_ptr<ChildProcess> child)
{
assert(child && "Adding null child");
const auto pid = child->getPid();
std::unique_lock<std::mutex> lock(NewChildrenMutex);
--OutstandingForks;
// Prevent from going -ve if we have unexpected children.
if (OutstandingForks < 0)
++OutstandingForks;
if (COOLWSD::IsBindMountingEnabled)
{
// Reset the child-spawn timeout to the default, now that we're set.
// But only when mounting is enabled. Otherwise, copying is always slow.
ChildSpawnTimeoutMs = CHILD_TIMEOUT_MS;
}
LOG_TRC("Adding a new child " << pid << " to NewChildren, have " << OutstandingForks
<< " outstanding requests");
NewChildren.emplace_back(std::move(child));
const size_t count = NewChildren.size();
lock.unlock();
LOG_INF("Have " << count << " spare " << (count == 1 ? "child" : "children")
<< " after adding [" << pid << "]. Notifying.");
NewChildrenCV.notify_one();
return count;
}
#if !MOBILEAPP
namespace
{
#if ENABLE_DEBUG
inline std::string getLaunchBase(bool asAdmin = false)
{
std::ostringstream oss;
oss << " ";
oss << ((COOLWSD::isSSLEnabled() || COOLWSD::isSSLTermination()) ? "https://" : "http://");
if (asAdmin)
{
auto user = COOLWSD::getConfigValue<std::string>("admin_console.username", "");
auto passwd = COOLWSD::getConfigValue<std::string>("admin_console.password", "");
if (user.empty() || passwd.empty())
return "";
oss << user << ':' << passwd << '@';
}
oss << COOLWSD_TEST_HOST ":";
oss << ClientPortNumber;
return oss.str();
}
inline std::string getLaunchURI(const std::string &document, bool readonly = false)
{
std::ostringstream oss;
oss << getLaunchBase();
oss << COOLWSD::ServiceRoot;
oss << COOLWSD_TEST_COOL_UI;
oss << "?file_path=";
oss << DEBUG_ABSSRCDIR "/";
oss << document;
if (readonly)
oss << "&permission=readonly";
return oss.str();
}
inline std::string getServiceURI(const std::string &sub, bool asAdmin = false)
{
std::ostringstream oss;
oss << getLaunchBase(asAdmin);
oss << COOLWSD::ServiceRoot;
oss << sub;
return oss.str();
}
#endif
} // anonymous namespace
#endif // MOBILEAPP
std::atomic<uint64_t> COOLWSD::NextConnectionId(1);
#if !MOBILEAPP
std::atomic<int> COOLWSD::ForKitProcId(-1);
std::shared_ptr<ForKitProcess> COOLWSD::ForKitProc;
bool COOLWSD::NoCapsForKit = false;
bool COOLWSD::NoSeccomp = false;
bool COOLWSD::AdminEnabled = true;
bool COOLWSD::UnattendedRun = false;
bool COOLWSD::SignalParent = false;
bool COOLWSD::UseEnvVarOptions = false;
std::string COOLWSD::RouteToken;
#if ENABLE_DEBUG
bool COOLWSD::SingleKit = false;
bool COOLWSD::ForceCaching = false;
#endif
COOLWSD::WASMActivationState COOLWSD::WASMState = COOLWSD::WASMActivationState::Disabled;
std::unordered_map<std::string, std::chrono::steady_clock::time_point> COOLWSD::Uri2WasmModeMap;
#endif
std::string COOLWSD::SysTemplate;
std::string COOLWSD::LoTemplate = LO_PATH;
std::string COOLWSD::CleanupChildRoot;
std::string COOLWSD::ChildRoot;
std::string COOLWSD::ServerName;
std::string COOLWSD::FileServerRoot;
std::string COOLWSD::ServiceRoot;
std::string COOLWSD::TmpFontDir;
std::string COOLWSD::LOKitVersion;
std::string COOLWSD::ConfigFile = COOLWSD_CONFIGDIR "/coolwsd.xml";
std::string COOLWSD::ConfigDir = COOLWSD_CONFIGDIR "/conf.d";
bool COOLWSD::EnableTraceEventLogging = false;
bool COOLWSD::EnableAccessibility = false;
FILE *COOLWSD::TraceEventFile = NULL;
std::string COOLWSD::LogLevel = "trace";
std::string COOLWSD::LogLevelStartup = "trace";
std::string COOLWSD::LogToken;
std::string COOLWSD::MostVerboseLogLevelSettableFromClient = "notice";
std::string COOLWSD::LeastVerboseLogLevelSettableFromClient = "fatal";
std::string COOLWSD::UserInterface = "default";
bool COOLWSD::AnonymizeUserData = false;
bool COOLWSD::CheckCoolUser = true;
bool COOLWSD::CleanupOnly = false; //< If we should cleanup and exit.
bool COOLWSD::IsProxyPrefixEnabled = false;
#if ENABLE_SSL
Util::RuntimeConstant<bool> COOLWSD::SSLEnabled;
Util::RuntimeConstant<bool> COOLWSD::SSLTermination;
#endif
unsigned COOLWSD::MaxConnections;
unsigned COOLWSD::MaxDocuments;
std::string COOLWSD::OverrideWatermark;
std::set<const Poco::Util::AbstractConfiguration*> COOLWSD::PluginConfigurations;
std::chrono::steady_clock::time_point COOLWSD::StartTime;
bool COOLWSD::IsBindMountingEnabled = true;
// If you add global state please update dumpState below too
2016-04-08 10:00:58 +02:00
static std::string UnitTestLibrary;
2015-07-13 16:13:06 +02:00
unsigned int COOLWSD::NumPreSpawnedChildren = 0;
std::unique_ptr<TraceFileWriter> COOLWSD::TraceDumper;
#if !MOBILEAPP
std::unique_ptr<ClipboardCache> COOLWSD::SavedClipboards;
/// The file request handler used for file-serving.
std::unique_ptr<FileServerRequestHandler> COOLWSD::FileRequestHandler;
#endif
/// This thread polls basic web serving, and handling of
/// websockets before upgrade: when upgraded they go to the
/// relevant DocumentBroker poll instead.
static std::shared_ptr<TerminatingPoll> WebServerPoll;
class PrisonPoll : public TerminatingPoll
{
public:
PrisonPoll() : TerminatingPoll("prisoner_poll") {}
/// Check prisoners are still alive and balanced.
void wakeupHook() override;
#if !MOBILEAPP
// Resets the forkit process object
void setForKitProcess(const std::weak_ptr<ForKitProcess>& forKitProc)
{
assertCorrectThread(__FILE__, __LINE__);
_forKitProc = forKitProc;
}
void sendMessageToForKit(const std::string& msg)
{
if (std::this_thread::get_id() == getThreadOwner())
{
// Speed up sending the message if the request comes from owner thread
std::shared_ptr<ForKitProcess> forKitProc = _forKitProc.lock();
if (forKitProc)
{
forKitProc->sendTextFrame(msg);
}
}
else
{
// Put the message in the owner's thread queue to be send later
// because WebSocketHandler is not thread safe and otherwise we
// should synchronize inside WebSocketHandler.
addCallback([this, msg]{
std::shared_ptr<ForKitProcess> forKitProc = _forKitProc.lock();
if (forKitProc)
{
forKitProc->sendTextFrame(msg);
}
});
}
}
private:
std::weak_ptr<ForKitProcess> _forKitProc;
#endif
};
/// This thread listens for and accepts prisoner kit processes.
/// And also cleans up and balances the correct number of children.
static std::shared_ptr<PrisonPoll> PrisonerPoll;
#if MOBILEAPP
#ifndef IOS
std::mutex COOLWSD::lokit_main_mutex;
#endif
#endif
std::shared_ptr<ChildProcess> getNewChild_Blocks(SocketPoll &destPoll, unsigned mobileAppDocId)
{
(void)mobileAppDocId;
const auto startTime = std::chrono::steady_clock::now();
std::unique_lock<std::mutex> lock(NewChildrenMutex);
#if !MOBILEAPP
assert(mobileAppDocId == 0 && "Unexpected to have mobileAppDocId in the non-mobile build");
int numPreSpawn = COOLWSD::NumPreSpawnedChildren;
++numPreSpawn; // Replace the one we'll dispatch just now.
LOG_DBG("getNewChild: Rebalancing children to " << numPreSpawn);
if (rebalanceChildren(numPreSpawn) < 0)
{
LOG_DBG("getNewChild: rebalancing of children failed. Scheduling housekeeping to recover.");
COOLWSD::doHousekeeping();
// Let the caller retry after a while.
return nullptr;
}
const auto timeout = std::chrono::milliseconds(ChildSpawnTimeoutMs / 2);
LOG_TRC("Waiting for a new child for a max of " << timeout);
#else // MOBILEAPP
const auto timeout = std::chrono::hours(100);
#ifdef IOS
assert(mobileAppDocId > 0 && "Unexpected to have no mobileAppDocId in the iOS build");
#endif
std::thread([&]
{
#ifndef IOS
std::lock_guard<std::mutex> lock(COOLWSD::lokit_main_mutex);
Util::setThreadName("lokit_main");
#else
Util::setThreadName("lokit_main_" + Util::encodeId(mobileAppDocId, 3));
#endif
// Ugly to have that static global COOLWSD::prisonerServerSocketFD, Otoh we know
// there is just one COOLWSD object. (Even in real Online.)
lokit_main(COOLWSD::prisonerServerSocketFD, COOLWSD::UserInterface, mobileAppDocId);
}).detach();
#endif // MOBILEAPP
// FIXME: blocks ...
// Unfortunately we need to wait after spawning children to avoid bombing the system.
// If we fail fast and return, the next document will spawn more children without knowing
// there are some on the way already. And if the system is slow already, that wouldn't help.
LOG_TRC("Waiting for NewChildrenCV");
if (NewChildrenCV.wait_for(lock, timeout, []()
{
LOG_TRC("Predicate for NewChildrenCV wait: NewChildren.size()=" << NewChildren.size());
return !NewChildren.empty();
}))
{
LOG_TRC("NewChildrenCV wait successful");
std::shared_ptr<ChildProcess> child = NewChildren.back();
NewChildren.pop_back();
const size_t available = NewChildren.size();
// Release early before moving sockets.
lock.unlock();
// Validate before returning.
if (child && child->isAlive())
{
LOG_DBG("getNewChild: Have "
<< available << " spare " << (available == 1 ? "child" : "children")
<< " after popping [" << child->getPid() << "] to return in "
<< std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - startTime));
// Change ownership now.
child->moveSocketFromTo(PrisonerPoll, destPoll);
return child;
}
LOG_WRN("getNewChild: popped dead child, need to find another.");
}
else
{
LOG_TRC("NewChildrenCV wait failed");
LOG_WRN("getNewChild: No child available. Sending spawn request to forkit and failing.");
}
LOG_DBG("getNewChild: Timed out while waiting for new child.");
return nullptr;
}
#ifdef __linux__
#if !MOBILEAPP
class InotifySocket : public Socket
{
public:
InotifySocket():
Socket(inotify_init1(IN_NONBLOCK), Socket::Type::Unix)
, m_stopOnConfigChange(true)
{
if (getFD() == -1)
{
LOG_WRN("Inotify - Failed to start a watcher for the configuration, disabling "
"stop_on_config_change");
m_stopOnConfigChange = false;
return;
}
watch(COOLWSD_CONFIGDIR);
}
/// Check for file changes, stop the server if we find any
void handlePoll(SocketDisposition &disposition, std::chrono::steady_clock::time_point now, int events) override;
int getPollEvents(std::chrono::steady_clock::time_point /* now */,
int64_t & /* timeoutMaxMicroS */) override
{
return POLLIN;
}
bool watch(std::string configFile);
private:
bool m_stopOnConfigChange;
int m_watchedCount = 0;
};
bool InotifySocket::watch(const std::string configFile)
{
LOG_TRC("Inotify - Attempting to watch " << configFile << ", in addition to current "
<< m_watchedCount << " watched files");
if (getFD() == -1)
{
LOG_WRN("Inotify - Trying to watch config file " << configFile
<< " without an inotify file descriptor");
return false;
}
int watchedStatus;
watchedStatus = inotify_add_watch(getFD(), configFile.c_str(), IN_MODIFY);
if (watchedStatus == -1)
LOG_WRN("Inotify - Failed to watch config file " << configFile);
else
m_watchedCount++;
return watchedStatus != -1;
}
void InotifySocket::handlePoll(SocketDisposition & /* disposition */, std::chrono::steady_clock::time_point /* now */, int /* events */)
{
LOG_TRC("InotifyPoll - woken up. Reload on config change: "
<< m_stopOnConfigChange << ", Watching " << m_watchedCount << " files");
if (!m_stopOnConfigChange)
return;
char buf[4096];
static_assert(sizeof(buf) >= sizeof(struct inotify_event) + NAME_MAX + 1, "see man 7 inotify");
const struct inotify_event* event;
LOG_TRC("InotifyPoll - Checking for config changes...");
while (true)
{
ssize_t len = read(getFD(), buf, sizeof(buf));
if (len == -1 && errno != EAGAIN)
{
// Some read error, EAGAIN is when there is no data so let's not warn for it
LOG_WRN("InotifyPoll - Read error " << std::strerror(errno)
<< " when trying to get events");
}
else if (len == -1)
{
LOG_TRC("InotifyPoll - Got to end of data when reading inotify");
}
if (len <= 0)
break;
assert(buf[len - 1] == 0 && "see man 7 inotify");
for (char* ptr = buf; ptr < buf + len; ptr += sizeof(struct inotify_event) + event->len)
{
event = (const struct inotify_event*)ptr;
LOG_WRN("InotifyPoll - Config file " << event->name << " was modified, stopping COOLWSD");
SigUtil::requestShutdown();
}
}
}
#endif // if !MOBILEAPP
#endif // #ifdef __linux__
/// The Web Server instance with the accept socket poll thread.
class COOLWSDServer;
static std::unique_ptr<COOLWSDServer> Server;
/// Helper class to hold default configuration entries.
class AppConfigMap final : public Poco::Util::MapConfiguration
{
public:
AppConfigMap(const std::map<std::string, std::string>& map)
{
for (const auto& pair : map)
{
setRaw(pair.first, pair.second);
}
}
void reset(const std::map<std::string, std::string>& map)
{
clear();
for (const auto& pair : map)
{
setRaw(pair.first, pair.second);
}
}
};
#if !MOBILEAPP
void ForKitProcWSHandler::handleMessage(const std::vector<char> &data)
{
LOG_TRC("ForKitProcWSHandler: handling incoming [" << COOLProtocol::getAbbreviatedMessage(&data[0], data.size()) << "].");
const std::string firstLine = COOLProtocol::getFirstLine(&data[0], data.size());
const StringVector tokens = StringVector::tokenize(firstLine.data(), firstLine.size());
if (tokens.equals(0, "segfaultcount"))
{
int count = std::stoi(tokens[1]);
if (count >= 0)
{
Admin::instance().addSegFaultCount(count);
LOG_INF(count << " coolkit processes crashed with segmentation fault.");
}
else
{
LOG_WRN("Invalid 'segfaultcount' message received.");
}
}
else
{
LOG_ERR("ForKitProcWSHandler: unknown command: " << tokens[0]);
}
}
#endif
COOLWSD::COOLWSD()
{
}
COOLWSD::~COOLWSD()
{
}
#if !MOBILEAPP
/**
A custom socket poll to fetch remote config every 60 seconds.
If config changes it applies the new config using LayeredConfiguration.
The URI to fetch is read from the configuration too - it is either the
remote config itself, or the font configuration. It si passed via the
uriConfigKey param.
*/
class RemoteJSONPoll : public SocketPoll
{
public:
RemoteJSONPoll(LayeredConfiguration& config, const std::string& uriConfigKey, const std::string& name, const std::string& kind)
: SocketPoll(name)
, _conf(config)
, _configKey(uriConfigKey)
, _expectedKind(kind)
{ }
virtual ~RemoteJSONPoll() { }
virtual void handleJSON(const Poco::JSON::Object::Ptr& json) = 0;
virtual void handleUnchangedJSON()
{ }
void start()
{
Poco::URI remoteServerURI(_conf.getString(_configKey));
if (_expectedKind == "configuration")
{
if (remoteServerURI.empty())
{
LOG_INF("Remote " << _expectedKind << " is not specified in coolwsd.xml");
return; // no remote config server setup.
}
#if !ENABLE_DEBUG
if (Util::iequal(remoteServerURI.getScheme(), "http"))
{
LOG_ERR("Remote config url should only use HTTPS protocol: " << remoteServerURI.toString());
return;
}
#endif
}
startThread();
}
void pollingThread()
{
while (!isStop() && !SigUtil::getShutdownRequestFlag())
{
Poco::URI remoteServerURI(_conf.getString(_configKey));
// don't try to fetch from an empty URI
bool valid = !remoteServerURI.empty();
#if !ENABLE_DEBUG
if (Util::iequal(remoteServerURI.getScheme(), "http"))
{
LOG_ERR("Remote config url should only use HTTPS protocol: " << remoteServerURI.toString());
valid = false;
}
#endif
if (valid)
{
try
{
std::shared_ptr<http::Session> httpSession(
StorageBase::getHttpSession(remoteServerURI));
http::Request request(remoteServerURI.getPathAndQuery());
//we use ETag header to check whether JSON is modified or not
if (!_eTagValue.empty())
{
request.set("If-None-Match", _eTagValue);
}
const std::shared_ptr<const http::Response> httpResponse =
httpSession->syncRequest(request);
const http::StatusCode statusCode = httpResponse->statusLine().statusCode();
if (statusCode == http::StatusCode::OK)
{
_eTagValue = httpResponse->get("ETag");
std::string body = httpResponse->getBody();
LOG_DBG("Got " << body.size() << " bytes for " << remoteServerURI.toString());
Poco::JSON::Object::Ptr remoteJson;
if (JsonUtil::parseJSON(body, remoteJson))
{
std::string kind;
JsonUtil::findJSONValue(remoteJson, "kind", kind);
if (kind == _expectedKind)
{
handleJSON(remoteJson);
}
else
{
LOG_ERR("Make sure that " << remoteServerURI.toString() << " contains a property 'kind' with "
"value '" << _expectedKind << "'");
}
}
else
{
LOG_ERR("Could not parse the remote config JSON");
}
}
else if (statusCode == http::StatusCode::NotModified)
{
LOG_DBG("Not modified since last time: " << remoteServerURI.toString());
handleUnchangedJSON();
}
else
{
LOG_ERR("Remote config server has response status code: " << statusCode);
}
}
catch (...)
{
LOG_ERR("Failed to fetch remote config JSON, Please check JSON format");
}
}
poll(std::chrono::seconds(60));
}
}
protected:
LayeredConfiguration& _conf;
std::string _eTagValue;
private:
std::string _configKey;
std::string _expectedKind;
};
class RemoteConfigPoll : public RemoteJSONPoll
{
public:
RemoteConfigPoll(LayeredConfiguration& config) :
RemoteJSONPoll(config, "remote_config.remote_url", "remoteconfig_poll", "configuration")
{
constexpr int PRIO_JSON = -200; // highest priority
_persistConfig = new AppConfigMap(std::map<std::string, std::string>{});
_conf.addWriteable(_persistConfig, PRIO_JSON);
}
virtual ~RemoteConfigPoll() { }
void handleJSON(const Poco::JSON::Object::Ptr& remoteJson) override
{
std::map<std::string, std::string> newAppConfig;
fetchAliasGroups(newAppConfig, remoteJson);
#if ENABLE_FEATURE_LOCK
fetchLockedHostPatterns(newAppConfig, remoteJson);
fetchLockedTranslations(newAppConfig, remoteJson);
fetchUnlockImageUrl(newAppConfig, remoteJson);
#endif
fetchIndirectionEndpoint(newAppConfig, remoteJson);
fetchMonitors(newAppConfig, remoteJson);
fetchRemoteFontConfig(newAppConfig, remoteJson);
// before resetting get monitors list
std::vector<std::pair<std::string,int>> oldMonitors = Admin::instance().getMonitorList();
_persistConfig->reset(newAppConfig);
#if ENABLE_FEATURE_LOCK
CommandControl::LockManager::parseLockedHost(_conf);
#endif
Admin::instance().updateMonitors(oldMonitors);
HostUtil::parseAliases(_conf);
handleOptions(remoteJson);
}
void fetchLockedHostPatterns(std::map<std::string, std::string>& newAppConfig,
Poco::JSON::Object::Ptr remoteJson)
{
try
{
Poco::JSON::Object::Ptr lockedHost;
Poco::JSON::Array::Ptr lockedHostPatterns;
try
{
lockedHost = remoteJson->getObject("feature_locking")->getObject("locked_hosts");
lockedHostPatterns = lockedHost->getArray("hosts");
}
catch (const Poco::NullPointerException&)
{
LOG_INF("Overriding locked_hosts failed because feature_locking->locked_hosts->hosts array does not exist");
return;
}
if (lockedHostPatterns.isNull() || lockedHostPatterns->size() == 0)
{
LOG_INF(
"Overriding locked_hosts failed because locked_hosts->hosts array is empty or null");
return;
}
//use feature_lock.locked_hosts[@allow] entry from coolwsd.xml if feature_lock.locked_hosts.allow key doesnot exist in json
Poco::Dynamic::Var allow = !lockedHost->has("allow") ? Poco::Dynamic::Var(_conf.getBool("feature_lock.locked_hosts[@allow]"))
: lockedHost->get("allow");
newAppConfig.insert(std::make_pair("feature_lock.locked_hosts[@allow]", booleanToString(allow)));
if (booleanToString(allow) == "false")
{
LOG_INF("locked_hosts feature is disabled, set feature_lock->locked_hosts->allow to true to enable");
return;
}
std::size_t i;
for (i = 0; i < lockedHostPatterns->size(); i++)
{
std::string host;
Poco::JSON::Object::Ptr subObject = lockedHostPatterns->getObject(i);
JsonUtil::findJSONValue(subObject, "host", host);
Poco::Dynamic::Var readOnly = subObject->get("read_only");
Poco::Dynamic::Var disabledCommands = subObject->get("disabled_commands");
const std::string path =
"feature_lock.locked_hosts.host[" + std::to_string(i) + "]";
newAppConfig.insert(std::make_pair(path, host));
newAppConfig.insert(std::make_pair(path + "[@read_only]", booleanToString(readOnly)));
newAppConfig.insert(std::make_pair(path + "[@disabled_commands]",
booleanToString(disabledCommands)));
}
//if number of locked wopi host patterns defined in coolwsd.xml are greater than number of host
//fetched from json, overwrite the remaining host from config file to empty strings and
//set read_only and disabled_commands to false
for (;; ++i)
{
const std::string path =
"feature_lock.locked_hosts.host[" + std::to_string(i) + "]";
if (!_conf.has(path))
{
break;
}
newAppConfig.insert(std::make_pair(path, ""));
newAppConfig.insert(std::make_pair(path + "[@read_only]", "false"));
newAppConfig.insert(std::make_pair(path + "[@disabled_commands]", "false"));
}
}
catch (const std::exception& exc)
{
LOG_ERR("Failed to fetch locked_hosts, please check JSON format: " << exc.what());
}
}
void fetchAliasGroups(std::map<std::string, std::string>& newAppConfig,
const Poco::JSON::Object::Ptr& remoteJson)
{
try
{
Poco::JSON::Object::Ptr aliasGroups;
Poco::JSON::Array::Ptr groups;
try
{
aliasGroups = remoteJson->getObject("storage")->getObject("wopi")->getObject("alias_groups");
groups = aliasGroups->getArray("groups");
}
catch (const Poco::NullPointerException&)
{
LOG_INF("Overriding alias_groups failed because storage->wopi->alias_groups->groups array does not exist");
return;
}
if (groups.isNull() || groups->size() == 0)
{
LOG_INF("Overriding alias_groups failed because alias_groups->groups array is empty or null");
return;
}
std::string mode = "first";
JsonUtil::findJSONValue(aliasGroups, "mode", mode);
newAppConfig.insert(std::make_pair("storage.wopi.alias_groups[@mode]", mode));
std::size_t i;
for (i = 0; i < groups->size(); i++)
{
Poco::JSON::Object::Ptr group = groups->getObject(i);
std::string host;
JsonUtil::findJSONValue(group, "host", host);
Poco::Dynamic::Var allow = group->get("allow");
const std::string path =
"storage.wopi.alias_groups.group[" + std::to_string(i) + ']';
newAppConfig.insert(std::make_pair(path + ".host", host));
newAppConfig.insert(std::make_pair(path + ".host[@allow]", booleanToString(allow)));
#if ENABLE_FEATURE_LOCK
std::string unlockLink;
JsonUtil::findJSONValue(group, "unlock_link", unlockLink);
newAppConfig.insert(std::make_pair(path + ".unlock_link", unlockLink));
#endif
Poco::JSON::Array::Ptr aliases = group->getArray("aliases");
size_t j = 0;
if (aliases)
{
auto it = aliases->begin();
for (; j < aliases->size(); j++)
{
const std::string aliasPath = path + ".alias[" + std::to_string(j) + ']';
newAppConfig.insert(std::make_pair(aliasPath, it->toString()));
it++;
}
}
for (;; j++)
{
const std::string aliasPath = path + ".alias[" + std::to_string(j) + ']';
if (!_conf.has(aliasPath))
{
break;
}
newAppConfig.insert(std::make_pair(aliasPath, ""));
}
}
//if number of alias_groups defined in configuration are greater than number of alias_group
//fetched from json, overwrite the remaining alias_groups from config file to empty strings and
for (;; i++)
{
const std::string path =
"storage.wopi.alias_groups.group[" + std::to_string(i) + "].host";
if (!_conf.has(path))
{
break;
}
newAppConfig.insert(std::make_pair(path, ""));
newAppConfig.insert(std::make_pair(path + "[@allowed]", "false"));
}
}
catch (const std::exception& exc)
{
LOG_ERR("Fetching of alias groups failed with error: " << exc.what()
<< ", please check JSON format");
}
}
void fetchRemoteFontConfig(std::map<std::string, std::string>& newAppConfig,
const Poco::JSON::Object::Ptr& remoteJson)
{
try
{
Poco::JSON::Object::Ptr remoteFontConfig = remoteJson->getObject("remote_font_config");
std::string url;
if (JsonUtil::findJSONValue(remoteFontConfig, "url", url))
newAppConfig.insert(std::make_pair("remote_font_config.url", url));
}
catch (const Poco::NullPointerException&)
{
LOG_INF("Overriding the remote font config URL failed because the remove_font_config entry does not exist");
}
catch (const std::exception& exc)
{
LOG_ERR("Failed to fetch remote_font_config, please check JSON format: " << exc.what());
}
}
void fetchLockedTranslations(std::map<std::string, std::string>& newAppConfig,
const Poco::JSON::Object::Ptr& remoteJson)
{
try
{
Poco::JSON::Array::Ptr lockedTranslations;
try
{
lockedTranslations =
remoteJson->getObject("feature_locking")->getArray("translations");
}
catch (const Poco::NullPointerException&)
{
LOG_INF(
"Overriding translations failed because feature_locking->translations array "
"does not exist");
return;
}
if (lockedTranslations.isNull() || lockedTranslations->size() == 0)
{
LOG_INF("Overriding feature_locking->translations failed because array is empty or "
"null");
return;
}
std::size_t i;
for (i = 0; i < lockedTranslations->size(); i++)
{
Poco::JSON::Object::Ptr translation = lockedTranslations->getObject(i);
std::string language;
//default values if the one of the entry is missing in json
std::string title = _conf.getString("feature_lock.unlock_title", "");
std::string description = _conf.getString("feature_lock.unlock_description", "");
std::string writerHighlights =
_conf.getString("feature_lock.writer_unlock_highlights", "");
std::string impressHighlights =
_conf.getString("feature_lock.impress_unlock_highlights", "");
std::string calcHighlights =
_conf.getString("feature_lock.calc_unlock_highlights", "");
std::string drawHighlights =
_conf.getString("feature_lock.draw_unlock_highlights", "");
JsonUtil::findJSONValue(translation, "language", language);
JsonUtil::findJSONValue(translation, "unlock_title", title);
JsonUtil::findJSONValue(translation, "unlock_description", description);
JsonUtil::findJSONValue(translation, "writer_unlock_highlights", writerHighlights);
JsonUtil::findJSONValue(translation, "calc_unlock_highlights", calcHighlights);
JsonUtil::findJSONValue(translation, "impress_unlock_highlights",
impressHighlights);
JsonUtil::findJSONValue(translation, "draw_unlock_highlights", drawHighlights);
const std::string path =
"feature_lock.translations.language[" + std::to_string(i) + ']';
newAppConfig.insert(std::make_pair(path + "[@name]", language));
newAppConfig.insert(std::make_pair(path + ".unlock_title", title));
newAppConfig.insert(std::make_pair(path + ".unlock_description", description));
newAppConfig.insert(
std::make_pair(path + ".writer_unlock_highlights", writerHighlights));
newAppConfig.insert(
std::make_pair(path + ".calc_unlock_highlights", calcHighlights));
newAppConfig.insert(
std::make_pair(path + ".impress_unlock_highlights", impressHighlights));
newAppConfig.insert(
std::make_pair(path + ".draw_unlock_highlights", drawHighlights));
}
//if number of translations defined in configuration are greater than number of translation
//fetched from json, overwrite the remaining translations from config file to empty strings
for (;; i++)
{
const std::string path =
"feature_lock.translations.language[" + std::to_string(i) + "][@name]";
if (!_conf.has(path))
{
break;
}
newAppConfig.insert(std::make_pair(path, ""));
}
}
catch (const std::exception& exc)
{
LOG_ERR("Failed to fetch feature_locking->translations, please check JSON format: " << exc.what());
}
}
void fetchUnlockImageUrl(std::map<std::string, std::string>& newAppConfig,
const Poco::JSON::Object::Ptr& remoteJson)
{
try
{
Poco::JSON::Object::Ptr featureLocking = remoteJson->getObject("feature_locking");
std::string unlockImage;
if (JsonUtil::findJSONValue(featureLocking, "unlock_image", unlockImage))
{
newAppConfig.insert(std::make_pair("feature_lock.unlock_image", unlockImage));
}
}
catch (const Poco::NullPointerException&)
{
LOG_INF("Overriding unlock_image URL failed because the unlock_image entry does not "
"exist");
}
catch (const std::exception& exc)
{
LOG_ERR("Failed to fetch unlock_image, please check JSON format: " << exc.what());
}
}
void fetchIndirectionEndpoint(std::map<std::string, std::string>& newAppConfig,
const Poco::JSON::Object::Ptr& remoteJson)
{
try
{
Poco::JSON::Object::Ptr indirectionEndpoint =
remoteJson->getObject("indirection_endpoint");
std::string url;
if (JsonUtil::findJSONValue(indirectionEndpoint, "url", url))
{
newAppConfig.insert(std::make_pair("indirection_endpoint.url", url));
}
}
catch (const Poco::NullPointerException&)
{
LOG_INF("Overriding indirection_endpoint.url failed because the indirection_endpoint.url "
"entry does not "
"exist");
}
catch (const std::exception& exc)
{
LOG_ERR(
"Failed to fetch indirection_endpoint, please check JSON format: " << exc.what());
}
}
void fetchMonitors(std::map<std::string, std::string>& newAppConfig,
const Poco::JSON::Object::Ptr& remoteJson)
{
Poco::JSON::Array::Ptr monitors;
try
{
monitors = remoteJson->getArray("monitors");
}
catch (const Poco::NullPointerException&)
{
LOG_INF("Overriding monitor failed because array "
"does not exist");
return;
}
if (monitors.isNull() || monitors->size() == 0)
{
LOG_INF("Overriding monitors failed because array is empty or "
"null");
return;
}
std::size_t i;
for (i = 0; i < monitors->size(); i++)
newAppConfig.insert(
std::make_pair("monitors.monitor[" + std::to_string(i) + ']', monitors->get(i).toString()));
//if number of monitors defined in configuration are greater than number of monitors
//fetched from json or if the number of monitors shrinks with new json,
//overwrite the remaining monitors from config file to empty strings
for (;; i++)
{
const std::string path =
"monitors.monitor[" + std::to_string(i) + ']';
if (!_conf.has(path))
{
break;
}
newAppConfig.insert(std::make_pair(path, ""));
}
}
void handleOptions(const Poco::JSON::Object::Ptr& remoteJson)
{
try
{
std::string buyProduct;
JsonUtil::findJSONValue(remoteJson, "buy_product_url", buyProduct);
Poco::URI buyProductUri(buyProduct);
{
std::lock_guard<std::mutex> lock(COOLWSD::RemoteConfigMutex);
COOLWSD::BuyProductUrl = buyProductUri.toString();
}
}
catch(const Poco::Exception& exc)
{
LOG_ERR("handleOptions: Exception " << exc.what());
}
}
//sets property to false if it is missing from JSON
//and returns std::string
std::string booleanToString(Poco::Dynamic::Var& booleanFlag)
{
if (booleanFlag.isEmpty())
{
booleanFlag = "false";
}
return booleanFlag.toString();
}
private:
// keeps track of remote config layer
Poco::AutoPtr<AppConfigMap> _persistConfig = nullptr;
};
class RemoteFontConfigPoll : public RemoteJSONPoll
{
public:
RemoteFontConfigPoll(LayeredConfiguration& config)
: RemoteJSONPoll(config, "remote_font_config.url", "remotefontconfig_poll", "fontconfiguration")
{
}
virtual ~RemoteFontConfigPoll() { }
void handleJSON(const Poco::JSON::Object::Ptr& remoteJson) override
{
// First mark all fonts we have downloaded previously as "inactive" to be able to check if
// some font gets deleted from the list in the JSON file.
for (auto& it : fonts)
it.second.active = false;
// Just pick up new fonts.
auto fontsPtr = remoteJson->getArray("fonts");
if (!fontsPtr)
{
LOG_WRN("The 'fonts' property does not exist or is not an array");
return;
}
for (std::size_t i = 0; i < fontsPtr->size(); i++)
{
if (!fontsPtr->isObject(i))
LOG_WRN("Element " << i << " in fonts array is not an object");
else
{
const auto fontPtr = fontsPtr->getObject(i);
const auto uriPtr = fontPtr->get("uri");
if (uriPtr.isEmpty() || !uriPtr.isString())
LOG_WRN("Element in fonts array does not have an 'uri' property or it is not a string");
else
{
const std::string uri = uriPtr.toString();
const auto stampPtr = fontPtr->get("stamp");
if (!stampPtr.isEmpty() && !stampPtr.isString())
LOG_WRN("Element in fonts array with uri '" << uri << "' has a stamp property that is not a string, ignored");
else if (fonts.count(uri) == 0)
{
// First case: This font has not been downloaded.
if (!stampPtr.isEmpty())
{
if (downloadPlain(uri))
{
fonts[uri].stamp = stampPtr.toString();
fonts[uri].active = true;
}
}
else
{
if (downloadWithETag(uri, ""))
{
fonts[uri].active = true;
}
}
}
else if (!stampPtr.isEmpty() && stampPtr.toString() != fonts[uri].stamp)
{
// Second case: Font has been downloaded already, has a "stamp" property,
// and that has been changed in the JSON since it was downloaded.
restartForKitAndReDownloadConfigFile();
break;
}
else if (!stampPtr.isEmpty())
{
// Third case: Font has been downloaded already, has a "stamp" property, and
// that has *not* changed in the JSON since it was downloaded.
fonts[uri].active = true;
}
else
{
// Last case: Font has been downloaded but does not have a "stamp" property.
// Use ETag.
if (!eTagUnchanged(uri, fonts[uri].eTag))
{
restartForKitAndReDownloadConfigFile();
break;
}
fonts[uri].active = true;
}
}
}
}
// Any font that has been deleted from the JSON needs to be removed on this side, too.
for (const auto &it : fonts)
{
if (!it.second.active)
{
LOG_DBG("Font no longer mentioned in the remote font config: " << it.first);
restartForKitAndReDownloadConfigFile();
break;
}
}
}
void handleUnchangedJSON() override
{
// Iterate over the fonts that were mentioned in the JSON file when it was last downloaded.
for (auto& it : fonts)
{
// If the JSON has a "stamp" for the font, and we have already downloaded it, by
// definition we don't need to do anything when the JSON file has not changed.
if (it.second.stamp != "" && it.second.pathName != "")
continue;
// If the JSON has a "stamp" it must have been downloaded already. Should we even
// assert() that?
if (it.second.stamp != "" && it.second.pathName == "")
{
LOG_WRN("Font at " << it.first << " was not downloaded, should have been");
continue;
}
// Otherwise use the ETag to check if the font file needs re-downloading.
if (!eTagUnchanged(it.first, it.second.eTag))
{
restartForKitAndReDownloadConfigFile();
break;
}
}
}
private:
bool downloadPlain(const std::string& uri)
{
const Poco::URI fontUri{uri};
std::shared_ptr<http::Session> httpSession(StorageBase::getHttpSession(fontUri));
http::Request request(fontUri.getPathAndQuery());
request.set("User-Agent", http::getAgentString());
const std::shared_ptr<const http::Response> httpResponse
= httpSession->syncRequest(request);
return finishDownload(uri, httpResponse);
}
bool eTagUnchanged(const std::string& uri, const std::string& oldETag)
{
const Poco::URI fontUri{uri};
std::shared_ptr<http::Session> httpSession(StorageBase::getHttpSession(fontUri));
http::Request request(fontUri.getPathAndQuery());
if (!oldETag.empty())
{
request.set("If-None-Match", oldETag);
}
request.set("User-Agent", http::getAgentString());
const std::shared_ptr<const http::Response> httpResponse
= httpSession->syncRequest(request);
if (httpResponse->statusLine().statusCode() == http::StatusCode::NotModified)
{
LOG_DBG("Not modified since last time: " << uri);
return true;
}
return false;
}
bool downloadWithETag(const std::string& uri, const std::string& oldETag)
{
const Poco::URI fontUri{uri};
std::shared_ptr<http::Session> httpSession(StorageBase::getHttpSession(fontUri));
http::Request request(fontUri.getPathAndQuery());
if (!oldETag.empty())
{
request.set("If-None-Match", oldETag);
}
request.set("User-Agent", http::getAgentString());
const std::shared_ptr<const http::Response> httpResponse
= httpSession->syncRequest(request);
if (httpResponse->statusLine().statusCode() == http::StatusCode::NotModified)
{
LOG_DBG("Not modified since last time: " << uri);
return true;
}
if (!finishDownload(uri, httpResponse))
return false;
fonts[uri].eTag = httpResponse->get("ETag");
return true;
}
bool finishDownload(const std::string& uri, const std::shared_ptr<const http::Response> httpResponse)
{
if (httpResponse->statusLine().statusCode() != http::StatusCode::OK)
{
LOG_WRN("Could not fetch " << uri);
return false;
}
const std::string body = httpResponse->getBody();
std::string fontFile;
// We intentionally use a new file name also when an updated version of a font is
// downloaded. It causes trouble to rewrite the same file, in case it is in use in some Kit
// process at the moment.
// We don't remove the old file either as that also causes problems.
// And in reality, it is a bit unclear how likely it even is that fonts downloaded through
// this mechanism even will be updated.
fontFile = COOLWSD::TmpFontDir + "/" + Util::encodeId(Util::rng::getNext()) + ".ttf";
std::ofstream fontStream(fontFile);
fontStream.write(body.data(), body.size());
if (!fontStream.good())
{
LOG_ERR("Could not write to " << fontFile);
return false;
}
LOG_DBG("Got " << body.size() << " bytes for " << uri << " and wrote to " << fontFile);
fonts[uri].pathName = fontFile;
COOLWSD::sendMessageToForKit("addfont " + fontFile);
return true;
}
void restartForKitAndReDownloadConfigFile()
{
LOG_DBG("Downloaded font has been updated or a font has been removed. ForKit must be restarted.");
fonts.clear();
// Clear the saved ETag of the remote font configuration file so that it will be
// re-downloaded, and all fonts mentioned in it re-downloaded and fed to ForKit.
_eTagValue.clear();
COOLWSD::sendMessageToForKit("exit");
}
struct FontData
{
// Each font can have a "stamp" in the JSON that we treat just as a string. In practice it
// can be some timestamp, but we don't parse it. If the stamp is changed, we re-download the
// font file.
std::string stamp;
// If the font has no "stamp" property, we use the ETag mechanism to see if the font file
// needs to be re-downloaded.
std::string eTag;
// Where the font has been stored
std::string pathName;
// Flag that tells whether the font is mentioned in the JSON file that is being handled.
// Used only in handleJSON() when the JSON has been (re-)downloaded, not when the JSON was
// unchanged in handleUnchangedJSON().
bool active;
};
// The key of this map is the download URI of the font.
std::map<std::string, FontData> fonts;
};
#endif
void COOLWSD::innerInitialize(Application& self)
{
if (!Util::isMobileApp() && geteuid() == 0 && CheckCoolUser)
{
throw std::runtime_error("Do not run as root. Please run as cool user.");
}
wsd: faster jail setup via bind-mount loolmount now works and supports mounting and unmounting, plus numerous improvements, refactoring, logging, etc.. When enabled, binding improves the jail setup time by anywhere from 2x to orders of magnitude (in docker, f.e.). A new config entry mount_jail_tree controls whether mounting is used or the old method of linking/copying of jail contents. It is set to true by default and falls back to linking/copying. A test mount is done when the setting is enabled, and if mounting fails, it's disabled to avoid noise. Temporarily disabled for unit-tests until we can cleanup lingering mounts after Jenkins aborts our build job. In a future patch we will have mount/jail cleanup as part of make. The network/system files in /etc that need frequent refreshing are now updated in systemplate to make their most recent version available in the jails. These files can change during the course of loolwsd lifetime, and are unlikely to be updated in systemplate after installation at all. We link to them in the systemplate/etc directory, and if that fails, we copy them before forking each kit instance to have the latest. This reworks the approach used to bind-mount the jails and the templates such that the total is now down to only three mounts: systemplate, lo, tmp. As now systemplate and lotemplate are shared, they must be mounted as readonly, this means that user/ must now be moved into tmp/user/ which is writable. The mount-points must be recursive, because we mount lo/ within the mount-point of systemplate (which is the root of the jail). But because we (re)bind recursively, and because both systemplate and lotemplate are mounted for each jails, we need to make them unbindable, so they wouldn't multiply the mount-points for each jails (an explosive growth!) Contrarywise, we don't want the mount-points to be shared, because we don't expect to add/remove mounts after a jail is created. The random temp directory is now created and set correctly, plus many logging and other improvements. Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829 Tested-by: Jenkins Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 15:02:58 +02:00
Util::setApplicationPath(Poco::Path(Application::instance().commandPath()).parent().toString());
StartTime = std::chrono::steady_clock::now();
LayeredConfiguration& conf = config();
// Add default values of new entries here, so there is a sensible default in case
// the setting is missing from the config file. It is possible that users do not
// update their config files, and we are backward compatible.
// These defaults should be the same
// 1) here
// 2) in the 'default' attribute in coolwsd.xml, which is for documentation
// 3) the default parameter of getConfigValue() call. That is used when the
// setting is present in coolwsd.xml, but empty (i.e. use the default).
static const std::map<std::string, std::string> DefAppConfig = {
{ "accessibility.enable", "false" },
{ "allowed_languages", "de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru" },
{ "admin_console.enable_pam", "false" },
{ "child_root_path", "jails" },
{ "file_server_root_path", "browser/.." },
{ "enable_websocket_urp", "false" },
{ "hexify_embedded_urls", "false" },
{ "experimental_features", "false" },
{ "logging.protocol", "false" },
// { "logging.anonymize.anonymize_user_data", "false" }, // Do not set to fallback on filename/username.
{ "logging.color", "true" },
{ "logging.file.property[0]", "coolwsd.log" },
{ "logging.file.property[0][@name]", "path" },
{ "logging.file.property[1]", "never" },
{ "logging.file.property[1][@name]", "rotation" },
{ "logging.file.property[2]", "true" },
{ "logging.file.property[2][@name]", "compress" },
{ "logging.file.property[3]", "false" },
{ "logging.file.property[3][@name]", "flush" },
{ "logging.file.property[4]", "10 days" },
{ "logging.file.property[4][@name]", "purgeAge" },
{ "logging.file.property[5]", "10" },
{ "logging.file.property[5][@name]", "purgeCount" },
{ "logging.file.property[6]", "true" },
{ "logging.file.property[6][@name]", "rotateOnOpen" },
{ "logging.file.property[7]", "false" },
{ "logging.file.property[7][@name]", "archive" },
{ "logging.file[@enable]", "false" },
{ "logging.level", "trace" },
{ "logging.level_startup", "trace" },
{ "logging.lokit_sal_log", "-INFO-WARN" },
{ "logging.docstats", "false" },
{ "logging.userstats", "false" },
{ "browser_logging", "false" },
{ "mount_jail_tree", "true" },
{ "net.connection_timeout_secs", "30" },
{ "net.listen", "any" },
{ "net.proto", "all" },
{ "net.service_root", "" },
{ "net.proxy_prefix", "false" },
{ "net.content_security_policy", "" },
{ "net.frame_ancestors", "" },
{ "num_prespawn_children", "1" },
{ "per_document.always_save_on_exit", "false" },
{ "per_document.autosave_duration_secs", "300" },
{ "per_document.cleanup.cleanup_interval_ms", "10000" },
{ "per_document.cleanup.bad_behavior_period_secs", "60" },
{ "per_document.cleanup.idle_time_secs", "300" },
{ "per_document.cleanup.limit_dirty_mem_mb", "3072" },
{ "per_document.cleanup.limit_cpu_per", "85" },
{ "per_document.cleanup.lost_kit_grace_period_secs", "120" },
{ "per_document.cleanup[@enable]", "false" },
{ "per_document.idle_timeout_secs", "3600" },
{ "per_document.idlesave_duration_secs", "30" },
{ "per_document.limit_file_size_mb", "0" },
{ "per_document.limit_num_open_files", "0" },
{ "per_document.limit_load_secs", "100" },
{ "per_document.limit_store_failures", "5" },
{ "per_document.limit_convert_secs", "100" },
{ "per_document.limit_stack_mem_kb", "8000" },
{ "per_document.limit_virt_mem_mb", "0" },
{ "per_document.max_concurrency", "4" },
{ "per_document.min_time_between_saves_ms", "500" },
{ "per_document.min_time_between_uploads_ms", "5000" },
{ "per_document.batch_priority", "5" },
{ "per_document.pdf_resolution_dpi", "96" },
{ "per_document.redlining_as_comments", "false" },
{ "per_view.idle_timeout_secs", "900" },
{ "per_view.out_of_focus_timeout_secs", "120" },
{ "per_view.custom_os_info", "" },
{ "security.capabilities", "true" },
{ "security.seccomp", "true" },
{ "security.jwt_expiry_secs", "1800" },
{ "security.enable_metrics_unauthenticated", "false" },
{ "certificates.database_path", "" },
{ "server_name", "" },
{ "ssl.ca_file_path", COOLWSD_CONFIGDIR "/ca-chain.cert.pem" },
{ "ssl.cert_file_path", COOLWSD_CONFIGDIR "/cert.pem" },
{ "ssl.enable", "true" },
{ "ssl.hpkp.max_age[@enable]", "true" },
{ "ssl.hpkp.report_uri[@enable]", "false" },
{ "ssl.hpkp[@enable]", "false" },
{ "ssl.hpkp[@report_only]", "false" },
{ "ssl.sts.enabled", "false" },
{ "ssl.sts.max_age", "31536000" },
{ "ssl.key_file_path", COOLWSD_CONFIGDIR "/key.pem" },
{ "ssl.termination", "true" },
{ "stop_on_config_change", "false" },
{ "storage.filesystem[@allow]", "false" },
// "storage.ssl.enable" - deliberately not set; for back-compat
{ "storage.wopi.max_file_size", "0" },
{ "storage.wopi[@allow]", "true" },
{ "storage.wopi.locking.refresh", "900" },
{ "storage.wopi.is_legacy_server", "false" },
{ "sys_template_path", "systemplate" },
{ "trace_event[@enable]", "false" },
{ "trace.path[@compress]", "true" },
{ "trace.path[@snapshot]", "false" },
{ "trace[@enable]", "false" },
{ "welcome.enable", "false" },
{ "home_mode.enable", "false" },
{ "feedback.show", "true" },
{ "overwrite_mode.enable", "true" },
#if ENABLE_FEATURE_LOCK
{ "feature_lock.locked_hosts[@allow]", "false" },
{ "feature_lock.locked_hosts.fallback[@read_only]", "false" },
{ "feature_lock.locked_hosts.fallback[@disabled_commands]", "false" },
{ "feature_lock.locked_hosts.host[0]", "localhost" },
{ "feature_lock.locked_hosts.host[0][@read_only]", "false" },
{ "feature_lock.locked_hosts.host[0][@disabled_commands]", "false" },
{ "feature_lock.is_lock_readonly", "false" },
{ "feature_lock.locked_commands", LOCKED_COMMANDS },
{ "feature_lock.unlock_title", UNLOCK_TITLE },
{ "feature_lock.unlock_link", UNLOCK_LINK },
{ "feature_lock.unlock_description", UNLOCK_DESCRIPTION },
{ "feature_lock.writer_unlock_highlights", WRITER_UNLOCK_HIGHLIGHTS },
{ "feature_lock.calc_unlock_highlights", CALC_UNLOCK_HIGHLIGHTS },
{ "feature_lock.impress_unlock_highlights", IMPRESS_UNLOCK_HIGHLIGHTS },
{ "feature_lock.draw_unlock_highlights", DRAW_UNLOCK_HIGHLIGHTS },
#endif
#if ENABLE_FEATURE_RESTRICTION
{ "restricted_commands", "" },
#endif
{ "user_interface.mode", "default" },
{ "user_interface.use_integration_theme", "true" },
{ "quarantine_files[@enable]", "false" },
{ "quarantine_files.limit_dir_size_mb", "250" },
{ "quarantine_files.max_versions_to_maintain", "2" },
{ "quarantine_files.path", "quarantine" },
{ "quarantine_files.expiry_min", "30" },
{ "remote_config.remote_url", "" },
{ "storage.wopi.alias_groups[@mode]", "first" },
{ "languagetool.base_url", "" },
{ "languagetool.api_key", "" },
{ "languagetool.user_name", "" },
{ "languagetool.enabled", "false" },
{ "languagetool.ssl_verification", "true" },
{ "languagetool.rest_protocol", "" },
{ "deepl.api_url", "" },
{ "deepl.auth_key", "" },
{ "deepl.enabled", "false" },
{ "zotero.enable", "true" },
{ "indirection_endpoint.url", "" },
#if !MOBILEAPP
{ "help_url", HELP_URL },
#endif
{ "product_name", APP_NAME },
{ "admin_console.logging.admin_login", "true" },
{ "admin_console.logging.metrics_fetch", "true" },
{ "admin_console.logging.monitor_connect", "true" },
{ "admin_console.logging.admin_action", "true" },
{ "wasm.enable", "false" },
{ "wasm.force", "false" },
};
// Set default values, in case they are missing from the config file.
Poco::AutoPtr<AppConfigMap> defConfig(new AppConfigMap(DefAppConfig));
conf.addWriteable(defConfig, PRIO_SYSTEM); // Lowest priority
#if !MOBILEAPP
// Load default configuration files, with name independent
// of Poco's view of app-name, from local file if present.
Poco::Path configPath("coolwsd.xml");
if (Application::findFile(configPath))
loadConfiguration(configPath.toString(), PRIO_DEFAULT);
else
{
// Fallback to the COOLWSD_CONFIGDIR or --config-file path.
loadConfiguration(ConfigFile, PRIO_DEFAULT);
}
// Load extra ("plug-in") configuration files, if present
File dir(ConfigDir);
if (dir.exists() && dir.isDirectory())
{
for (auto configFileIterator = DirectoryIterator(dir); configFileIterator != DirectoryIterator(); ++configFileIterator)
{
// Only accept configuration files ending in .xml
const std::string configFile = configFileIterator.path().getFileName();
if (configFile.length() > 4 && strcasecmp(configFile.substr(configFile.length() - 4).data(), ".xml") == 0)
{
const std::string fullFileName = dir.path() + "/" + configFile;
PluginConfigurations.insert(new XMLConfiguration(fullFileName));
}
}
}
// Override any settings passed on the command-line or via environment variables
if (UseEnvVarOptions)
initializeEnvOptions();
Poco::AutoPtr<AppConfigMap> overrideConfig(new AppConfigMap(_overrideSettings));
conf.addWriteable(overrideConfig, PRIO_APPLICATION); // Highest priority
if (!UnitTestLibrary.empty())
{
UnitWSD::defaultConfigure(conf);
}
// Experimental features.
EnableExperimental = getConfigValue<bool>(conf, "experimental_features", false);
EnableAccessibility = getConfigValue<bool>(conf, "accessibility.enable", false);
// Setup user interface mode
UserInterface = getConfigValue<std::string>(conf, "user_interface.mode", "default");
if (UserInterface == "compact")
UserInterface = "classic";
if (UserInterface == "tabbed")
UserInterface = "notebookbar";
if (EnableAccessibility)
UserInterface = "notebookbar";
// Set the log-level after complete initialization to force maximum details at startup.
LogLevel = getConfigValue<std::string>(conf, "logging.level", "trace");
MostVerboseLogLevelSettableFromClient = getConfigValue<std::string>(conf, "logging.most_verbose_level_settable_from_client", "notice");
LeastVerboseLogLevelSettableFromClient = getConfigValue<std::string>(conf, "logging.least_verbose_level_settable_from_client", "fatal");
setenv("COOL_LOGLEVEL", LogLevel.c_str(), true);
#if !ENABLE_DEBUG
const std::string salLog = getConfigValue<std::string>(conf, "logging.lokit_sal_log", "-INFO-WARN");
setenv("SAL_LOG", salLog.c_str(), 0);
#endif
#if WASMAPP
// In WASM, we want to log to the Log Console.
// Disable logging to file to log to stdout and
// disable color since this isn't going to the terminal.
constexpr bool withColor = false;
constexpr bool logToFile = false;
#else
const bool withColor = getConfigValue<bool>(conf, "logging.color", true) && isatty(fileno(stderr));
if (withColor)
{
setenv("COOL_LOGCOLOR", "1", true);
}
const auto logToFile = getConfigValue<bool>(conf, "logging.file[@enable]", false);
std::map<std::string, std::string> logProperties;
for (std::size_t i = 0; ; ++i)
{
const std::string confPath = "logging.file.property[" + std::to_string(i) + ']';
const std::string confName = config().getString(confPath + "[@name]", "");
2016-08-13 14:59:14 +02:00
if (!confName.empty())
{
const std::string value = config().getString(confPath, "");
2016-08-13 14:59:14 +02:00
logProperties.emplace(confName, value);
}
else if (!config().has(confPath))
{
break;
}
}
// Setup the logfile envar for the kit processes.
if (logToFile)
{
const auto it = logProperties.find("path");
if (it != logProperties.end())
{
setenv("COOL_LOGFILE", "1", true);
setenv("COOL_LOGFILENAME", it->second.c_str(), true);
std::cerr << "\nLogging at " << LogLevel << " level to file: " << it->second.c_str()
<< std::endl;
}
}
#endif
// Log at trace level until we complete the initialization.
LogLevelStartup = getConfigValue<std::string>(conf, "logging.level_startup", "trace");
setenv("COOL_LOGLEVEL_STARTUP", LogLevelStartup.c_str(), true);
Log::initialize("wsd", LogLevelStartup, withColor, logToFile, logProperties);
if (LogLevel != LogLevelStartup)
{
LOG_INF("Setting log-level to [" << LogLevelStartup << "] and delaying setting to ["
<< LogLevel << "] until after WSD initialization.");
}
if (getConfigValue<bool>(conf, "browser_logging", "false"))
{
LogToken = Util::rng::getHexString(16);
}
// First log entry.
ServerName = config().getString("server_name");
LOG_INF("Initializing coolwsd server [" << ServerName << "]. Experimental features are "
<< (EnableExperimental ? "enabled." : "disabled."));
// Initialize the UnitTest subsystem.
if (!UnitWSD::init(UnitWSD::UnitType::Wsd, UnitTestLibrary))
{
throw std::runtime_error("Failed to load wsd unit test library.");
}
// Allow UT to manipulate before using configuration values.
UnitWSD::get().configure(conf);
// Trace Event Logging.
EnableTraceEventLogging = getConfigValue<bool>(conf, "trace_event[@enable]", false);
if (EnableTraceEventLogging)
{
const auto traceEventFile = getConfigValue<std::string>(conf, "trace_event.path", COOLWSD_TRACEEVENTFILE);
LOG_INF("Trace Event file is " << traceEventFile << ".");
TraceEventFile = fopen(traceEventFile.c_str(), "w");
if (TraceEventFile != NULL)
{
if (fcntl(fileno(TraceEventFile), F_SETFD, FD_CLOEXEC) == -1)
{
fclose(TraceEventFile);
TraceEventFile = NULL;
}
else
{
fprintf(TraceEventFile, "[\n");
// Output a metadata event that tells that this is the WSD process
fprintf(TraceEventFile, "{\"name\":\"process_name\",\"ph\":\"M\",\"args\":{\"name\":\"WSD\"},\"pid\":%d,\"tid\":%ld},\n",
getpid(), (long) Util::getThreadId());
fprintf(TraceEventFile, "{\"name\":\"thread_name\",\"ph\":\"M\",\"args\":{\"name\":\"Main\"},\"pid\":%d,\"tid\":%ld},\n",
getpid(), (long) Util::getThreadId());
}
}
}
// Check deprecated settings.
bool reuseCookies = false;
if (getSafeConfig(conf, "storage.wopi.reuse_cookies", reuseCookies))
LOG_WRN("NOTE: Deprecated config option storage.wopi.reuse_cookies - no longer supported.");
#if !MOBILEAPP
COOLWSD::WASMState = getConfigValue<bool>(conf, "wasm.enable", false)
? COOLWSD::WASMActivationState::Enabled
: COOLWSD::WASMActivationState::Disabled;
if (getConfigValue<bool>(conf, "wasm.force", false))
{
if (COOLWSD::WASMState != COOLWSD::WASMActivationState::Enabled)
{
LOG_FTL(
"WASM is not enabled; cannot force serving WASM. Please set wasm.enabled to true "
"in coolwsd.xml first");
Util::forcedExit(EX_SOFTWARE);
}
LOG_INF("WASM is force-enabled. All documents will be loaded through WASM");
COOLWSD::WASMState = COOLWSD::WASMActivationState::Forced;
}
#endif // !MOBILEAPP
// Get anonymization settings.
#if COOLWSD_ANONYMIZE_USER_DATA
AnonymizeUserData = true;
LOG_INF("Anonymization of user-data is permanently enabled.");
#else
LOG_INF("Anonymization of user-data is configurable.");
bool haveAnonymizeUserDataConfig = false;
if (getSafeConfig(conf, "logging.anonymize.anonymize_user_data", AnonymizeUserData))
haveAnonymizeUserDataConfig = true;
bool anonymizeFilenames = false;
bool anonymizeUsernames = false;
if (getSafeConfig(conf, "logging.anonymize.usernames", anonymizeFilenames) ||
getSafeConfig(conf, "logging.anonymize.filenames", anonymizeUsernames))
{
LOG_WRN("NOTE: both logging.anonymize.usernames and logging.anonymize.filenames are deprecated and superseded by "
"logging.anonymize.anonymize_user_data. Please remove username and filename entries from the config and use only anonymize_user_data.");
if (haveAnonymizeUserDataConfig)
LOG_WRN("Since logging.anonymize.anonymize_user_data is provided (" << AnonymizeUserData << ") in the config, it will be used.");
else
{
AnonymizeUserData = (anonymizeFilenames || anonymizeUsernames);
}
}
#endif
if (AnonymizeUserData && LogLevel == "trace" && !CleanupOnly)
{
if (getConfigValue<bool>(conf, "logging.anonymize.allow_logging_user_data", false))
{
LOG_WRN("Enabling trace logging while anonymization is enabled due to logging.anonymize.allow_logging_user_data setting. "
"This will leak user-data!");
// Disable anonymization as it's useless now.
AnonymizeUserData = false;
}
else
{
static const char failure[] = "Anonymization and trace-level logging are incompatible. "
"Please reduce logging level to debug or lower in coolwsd.xml to prevent leaking sensitive user data.";
LOG_FTL(failure);
std::cerr << '\n' << failure << std::endl;
#if ENABLE_DEBUG
std::cerr << "\nIf you have used 'make run', edit coolwsd.xml and make sure you have removed "
"'--o:logging.level=trace' from the command line in Makefile.am.\n" << std::endl;
#endif
Util::forcedExit(EX_SOFTWARE);
}
}
std::uint64_t anonymizationSalt = 82589933;
LOG_INF("Anonymization of user-data is " << (AnonymizeUserData ? "enabled." : "disabled."));
if (AnonymizeUserData)
{
// Get the salt, if set, otherwise default, and set as envar, so the kits inherit it.
anonymizationSalt = getConfigValue<std::uint64_t>(conf, "logging.anonymize.anonymization_salt", 82589933);
const std::string anonymizationSaltStr = std::to_string(anonymizationSalt);
setenv("COOL_ANONYMIZATION_SALT", anonymizationSaltStr.c_str(), true);
}
FileUtil::setUrlAnonymization(AnonymizeUserData, anonymizationSalt);
{
bool enableWebsocketURP =
COOLWSD::getConfigValue<bool>("security.enable_websocket_urp", false);
setenv("ENABLE_WEBSOCKET_URP", enableWebsocketURP ? "true" : "false", 1);
}
{
std::string proto = getConfigValue<std::string>(conf, "net.proto", "");
if (Util::iequal(proto, "ipv4"))
ClientPortProto = Socket::Type::IPv4;
else if (Util::iequal(proto, "ipv6"))
ClientPortProto = Socket::Type::IPv6;
else if (Util::iequal(proto, "all"))
ClientPortProto = Socket::Type::All;
else
LOG_WRN("Invalid protocol: " << proto);
}
{
std::string listen = getConfigValue<std::string>(conf, "net.listen", "");
if (Util::iequal(listen, "any"))
ClientListenAddr = ServerSocket::Type::Public;
else if (Util::iequal(listen, "loopback"))
ClientListenAddr = ServerSocket::Type::Local;
else
LOG_WRN("Invalid listen address: " << listen << ". Falling back to default: 'any'" );
}
// Prefix for the coolwsd pages; should not end with a '/'
ServiceRoot = getPathFromConfig("net.service_root");
while (ServiceRoot.length() > 0 && ServiceRoot[ServiceRoot.length() - 1] == '/')
ServiceRoot.pop_back();
IsProxyPrefixEnabled = getConfigValue<bool>(conf, "net.proxy_prefix", false);
#if ENABLE_SSL
COOLWSD::SSLEnabled.set(getConfigValue<bool>(conf, "ssl.enable", true));
COOLWSD::SSLTermination.set(getConfigValue<bool>(conf, "ssl.termination", true));
2016-09-01 16:24:22 +02:00
#endif
LOG_INF("SSL support: SSL is " << (COOLWSD::isSSLEnabled() ? "enabled." : "disabled."));
LOG_INF("SSL support: termination is " << (COOLWSD::isSSLTermination() ? "enabled." : "disabled."));
std::string allowedLanguages(config().getString("allowed_languages"));
// Core <= 7.0.
setenv("LOK_WHITELIST_LANGUAGES", allowedLanguages.c_str(), 1);
// Core >= 7.1.
setenv("LOK_ALLOWLIST_LANGUAGES", allowedLanguages.c_str(), 1);
#endif
int pdfResolution = getConfigValue<int>(conf, "per_document.pdf_resolution_dpi", 96);
if (pdfResolution > 0)
{
constexpr int MaxPdfResolutionDpi = 384;
if (pdfResolution > MaxPdfResolutionDpi)
{
// Avoid excessive memory consumption.
LOG_WRN("The PDF resolution specified in per_document.pdf_resolution_dpi ("
<< pdfResolution << ") is larger than the maximum (" << MaxPdfResolutionDpi
<< "). Using " << MaxPdfResolutionDpi << " instead.");
pdfResolution = MaxPdfResolutionDpi;
}
const std::string pdfResolutionStr = std::to_string(pdfResolution);
LOG_DBG("Setting envar PDFIMPORT_RESOLUTION_DPI="
<< pdfResolutionStr << " per config per_document.pdf_resolution_dpi");
::setenv("PDFIMPORT_RESOLUTION_DPI", pdfResolutionStr.c_str(), 1);
}
SysTemplate = getPathFromConfig("sys_template_path");
wsd: faster jail setup via bind-mount loolmount now works and supports mounting and unmounting, plus numerous improvements, refactoring, logging, etc.. When enabled, binding improves the jail setup time by anywhere from 2x to orders of magnitude (in docker, f.e.). A new config entry mount_jail_tree controls whether mounting is used or the old method of linking/copying of jail contents. It is set to true by default and falls back to linking/copying. A test mount is done when the setting is enabled, and if mounting fails, it's disabled to avoid noise. Temporarily disabled for unit-tests until we can cleanup lingering mounts after Jenkins aborts our build job. In a future patch we will have mount/jail cleanup as part of make. The network/system files in /etc that need frequent refreshing are now updated in systemplate to make their most recent version available in the jails. These files can change during the course of loolwsd lifetime, and are unlikely to be updated in systemplate after installation at all. We link to them in the systemplate/etc directory, and if that fails, we copy them before forking each kit instance to have the latest. This reworks the approach used to bind-mount the jails and the templates such that the total is now down to only three mounts: systemplate, lo, tmp. As now systemplate and lotemplate are shared, they must be mounted as readonly, this means that user/ must now be moved into tmp/user/ which is writable. The mount-points must be recursive, because we mount lo/ within the mount-point of systemplate (which is the root of the jail). But because we (re)bind recursively, and because both systemplate and lotemplate are mounted for each jails, we need to make them unbindable, so they wouldn't multiply the mount-points for each jails (an explosive growth!) Contrarywise, we don't want the mount-points to be shared, because we don't expect to add/remove mounts after a jail is created. The random temp directory is now created and set correctly, plus many logging and other improvements. Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829 Tested-by: Jenkins Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 15:02:58 +02:00
if (SysTemplate.empty())
{
LOG_FTL("Missing sys_template_path config entry.");
throw MissingOptionException("systemplate");
}
ChildRoot = getPathFromConfig("child_root_path");
wsd: faster jail setup via bind-mount loolmount now works and supports mounting and unmounting, plus numerous improvements, refactoring, logging, etc.. When enabled, binding improves the jail setup time by anywhere from 2x to orders of magnitude (in docker, f.e.). A new config entry mount_jail_tree controls whether mounting is used or the old method of linking/copying of jail contents. It is set to true by default and falls back to linking/copying. A test mount is done when the setting is enabled, and if mounting fails, it's disabled to avoid noise. Temporarily disabled for unit-tests until we can cleanup lingering mounts after Jenkins aborts our build job. In a future patch we will have mount/jail cleanup as part of make. The network/system files in /etc that need frequent refreshing are now updated in systemplate to make their most recent version available in the jails. These files can change during the course of loolwsd lifetime, and are unlikely to be updated in systemplate after installation at all. We link to them in the systemplate/etc directory, and if that fails, we copy them before forking each kit instance to have the latest. This reworks the approach used to bind-mount the jails and the templates such that the total is now down to only three mounts: systemplate, lo, tmp. As now systemplate and lotemplate are shared, they must be mounted as readonly, this means that user/ must now be moved into tmp/user/ which is writable. The mount-points must be recursive, because we mount lo/ within the mount-point of systemplate (which is the root of the jail). But because we (re)bind recursively, and because both systemplate and lotemplate are mounted for each jails, we need to make them unbindable, so they wouldn't multiply the mount-points for each jails (an explosive growth!) Contrarywise, we don't want the mount-points to be shared, because we don't expect to add/remove mounts after a jail is created. The random temp directory is now created and set correctly, plus many logging and other improvements. Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829 Tested-by: Jenkins Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 15:02:58 +02:00
if (ChildRoot.empty())
{
LOG_FTL("Missing child_root_path config entry.");
throw MissingOptionException("childroot");
}
else
{
#if !MOBILEAPP
if (CleanupOnly)
{
// Cleanup and exit.
JailUtil::cleanupJails(ChildRoot);
Util::forcedExit(EX_OK);
}
#endif
wsd: faster jail setup via bind-mount loolmount now works and supports mounting and unmounting, plus numerous improvements, refactoring, logging, etc.. When enabled, binding improves the jail setup time by anywhere from 2x to orders of magnitude (in docker, f.e.). A new config entry mount_jail_tree controls whether mounting is used or the old method of linking/copying of jail contents. It is set to true by default and falls back to linking/copying. A test mount is done when the setting is enabled, and if mounting fails, it's disabled to avoid noise. Temporarily disabled for unit-tests until we can cleanup lingering mounts after Jenkins aborts our build job. In a future patch we will have mount/jail cleanup as part of make. The network/system files in /etc that need frequent refreshing are now updated in systemplate to make their most recent version available in the jails. These files can change during the course of loolwsd lifetime, and are unlikely to be updated in systemplate after installation at all. We link to them in the systemplate/etc directory, and if that fails, we copy them before forking each kit instance to have the latest. This reworks the approach used to bind-mount the jails and the templates such that the total is now down to only three mounts: systemplate, lo, tmp. As now systemplate and lotemplate are shared, they must be mounted as readonly, this means that user/ must now be moved into tmp/user/ which is writable. The mount-points must be recursive, because we mount lo/ within the mount-point of systemplate (which is the root of the jail). But because we (re)bind recursively, and because both systemplate and lotemplate are mounted for each jails, we need to make them unbindable, so they wouldn't multiply the mount-points for each jails (an explosive growth!) Contrarywise, we don't want the mount-points to be shared, because we don't expect to add/remove mounts after a jail is created. The random temp directory is now created and set correctly, plus many logging and other improvements. Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829 Tested-by: Jenkins Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 15:02:58 +02:00
if (ChildRoot[ChildRoot.size() - 1] != '/')
ChildRoot += '/';
#if CODE_COVERAGE
::setenv("BASE_CHILD_ROOT", Poco::Path(ChildRoot).absolute().toString().c_str(), 1);
#endif
// We need to cleanup other people's expired jails
CleanupChildRoot = ChildRoot;
// Encode the process id into the path for parallel re-use of jails/
ChildRoot += std::to_string(getpid()) + '-' + Util::rng::getHexString(8) + '/';
LOG_INF("Creating childroot: " + ChildRoot);
wsd: faster jail setup via bind-mount loolmount now works and supports mounting and unmounting, plus numerous improvements, refactoring, logging, etc.. When enabled, binding improves the jail setup time by anywhere from 2x to orders of magnitude (in docker, f.e.). A new config entry mount_jail_tree controls whether mounting is used or the old method of linking/copying of jail contents. It is set to true by default and falls back to linking/copying. A test mount is done when the setting is enabled, and if mounting fails, it's disabled to avoid noise. Temporarily disabled for unit-tests until we can cleanup lingering mounts after Jenkins aborts our build job. In a future patch we will have mount/jail cleanup as part of make. The network/system files in /etc that need frequent refreshing are now updated in systemplate to make their most recent version available in the jails. These files can change during the course of loolwsd lifetime, and are unlikely to be updated in systemplate after installation at all. We link to them in the systemplate/etc directory, and if that fails, we copy them before forking each kit instance to have the latest. This reworks the approach used to bind-mount the jails and the templates such that the total is now down to only three mounts: systemplate, lo, tmp. As now systemplate and lotemplate are shared, they must be mounted as readonly, this means that user/ must now be moved into tmp/user/ which is writable. The mount-points must be recursive, because we mount lo/ within the mount-point of systemplate (which is the root of the jail). But because we (re)bind recursively, and because both systemplate and lotemplate are mounted for each jails, we need to make them unbindable, so they wouldn't multiply the mount-points for each jails (an explosive growth!) Contrarywise, we don't want the mount-points to be shared, because we don't expect to add/remove mounts after a jail is created. The random temp directory is now created and set correctly, plus many logging and other improvements. Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829 Tested-by: Jenkins Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 15:02:58 +02:00
}
#if !MOBILEAPP
// Copy and serialize the config into XML to pass to forkit.
KitXmlConfig.reset(new Poco::Util::XMLConfiguration);
for (const auto& pair : DefAppConfig)
{
try
{
KitXmlConfig->setString(pair.first, config().getRawString(pair.first));
}
catch (const std::exception&)
{
// Nothing to do.
}
}
// Fixup some config entries to match out decisions/overrides.
KitXmlConfig->setBool("ssl.enable", isSSLEnabled());
KitXmlConfig->setBool("ssl.termination", isSSLTermination());
// We don't pass the config via command-line
// to avoid dealing with escaping and other traps.
std::ostringstream oss;
KitXmlConfig->save(oss);
setenv("COOL_CONFIG", oss.str().c_str(), true);
// Initialize the config subsystem too.
config::initialize(&config());
// For some reason I can't get at this setting in ChildSession::loKitCallback().
std::string fontsMissingHandling = config::getString("fonts_missing.handling", "log");
setenv("FONTS_MISSING_HANDLING", fontsMissingHandling.c_str(), 1);
IsBindMountingEnabled = getConfigValue<bool>(conf, "mount_jail_tree", true);
#if CODE_COVERAGE
// Code coverage is not supported with bind-mounting.
if (IsBindMountingEnabled)
{
LOG_WRN("Mounting is not compatible with code-coverage. Disabling.");
IsBindMountingEnabled = false;
}
#endif // CODE_COVERAGE
wsd: faster jail setup via bind-mount loolmount now works and supports mounting and unmounting, plus numerous improvements, refactoring, logging, etc.. When enabled, binding improves the jail setup time by anywhere from 2x to orders of magnitude (in docker, f.e.). A new config entry mount_jail_tree controls whether mounting is used or the old method of linking/copying of jail contents. It is set to true by default and falls back to linking/copying. A test mount is done when the setting is enabled, and if mounting fails, it's disabled to avoid noise. Temporarily disabled for unit-tests until we can cleanup lingering mounts after Jenkins aborts our build job. In a future patch we will have mount/jail cleanup as part of make. The network/system files in /etc that need frequent refreshing are now updated in systemplate to make their most recent version available in the jails. These files can change during the course of loolwsd lifetime, and are unlikely to be updated in systemplate after installation at all. We link to them in the systemplate/etc directory, and if that fails, we copy them before forking each kit instance to have the latest. This reworks the approach used to bind-mount the jails and the templates such that the total is now down to only three mounts: systemplate, lo, tmp. As now systemplate and lotemplate are shared, they must be mounted as readonly, this means that user/ must now be moved into tmp/user/ which is writable. The mount-points must be recursive, because we mount lo/ within the mount-point of systemplate (which is the root of the jail). But because we (re)bind recursively, and because both systemplate and lotemplate are mounted for each jails, we need to make them unbindable, so they wouldn't multiply the mount-points for each jails (an explosive growth!) Contrarywise, we don't want the mount-points to be shared, because we don't expect to add/remove mounts after a jail is created. The random temp directory is now created and set correctly, plus many logging and other improvements. Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829 Tested-by: Jenkins Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 15:02:58 +02:00
// Setup the jails.
JailUtil::cleanupJails(CleanupChildRoot);
JailUtil::setupChildRoot(IsBindMountingEnabled, ChildRoot, SysTemplate);
LOG_DBG("FileServerRoot before config: " << FileServerRoot);
FileServerRoot = getPathFromConfig("file_server_root_path");
LOG_DBG("FileServerRoot after config: " << FileServerRoot);
//creating quarantine directory
if (getConfigValue<bool>(conf, "quarantine_files[@enable]", false))
{
std::string path = Util::trimmed(getPathFromConfig("quarantine_files.path"));
LOG_INF("Quarantine path is set to [" << path << "] in config");
if (path.empty())
{
LOG_WRN("Quarantining is enabled via quarantine_files config, but no path is set in "
"quarantine_files.path. Disabling quarantine");
}
else
{
if (path[path.size() - 1] != '/')
path += '/';
if (path[0] != '/')
LOG_WRN("Quarantine path is relative. Please use an absolute path for better "
"reliability");
Poco::File p(path);
try
{
LOG_TRC("Creating quarantine directory [" + path << ']');
p.createDirectories();
LOG_DBG("Created quarantine directory [" + path << ']');
}
catch (const std::exception& ex)
{
LOG_WRN("Failed to create quarantine directory [" << path
<< "]. Disabling quaratine");
}
if (FileUtil::Stat(path).exists())
{
LOG_INF("Initializing quarantine at [" + path << ']');
Quarantine::initialize(path);
}
}
}
else
{
LOG_INF("Quarantine is disabled in config");
}
NumPreSpawnedChildren = getConfigValue<int>(conf, "num_prespawn_children", 1);
if (NumPreSpawnedChildren < 1)
{
LOG_WRN("Invalid num_prespawn_children in config (" << NumPreSpawnedChildren << "). Resetting to 1.");
NumPreSpawnedChildren = 1;
}
LOG_INF("NumPreSpawnedChildren set to " << NumPreSpawnedChildren << '.');
wsd: faster jail setup via bind-mount loolmount now works and supports mounting and unmounting, plus numerous improvements, refactoring, logging, etc.. When enabled, binding improves the jail setup time by anywhere from 2x to orders of magnitude (in docker, f.e.). A new config entry mount_jail_tree controls whether mounting is used or the old method of linking/copying of jail contents. It is set to true by default and falls back to linking/copying. A test mount is done when the setting is enabled, and if mounting fails, it's disabled to avoid noise. Temporarily disabled for unit-tests until we can cleanup lingering mounts after Jenkins aborts our build job. In a future patch we will have mount/jail cleanup as part of make. The network/system files in /etc that need frequent refreshing are now updated in systemplate to make their most recent version available in the jails. These files can change during the course of loolwsd lifetime, and are unlikely to be updated in systemplate after installation at all. We link to them in the systemplate/etc directory, and if that fails, we copy them before forking each kit instance to have the latest. This reworks the approach used to bind-mount the jails and the templates such that the total is now down to only three mounts: systemplate, lo, tmp. As now systemplate and lotemplate are shared, they must be mounted as readonly, this means that user/ must now be moved into tmp/user/ which is writable. The mount-points must be recursive, because we mount lo/ within the mount-point of systemplate (which is the root of the jail). But because we (re)bind recursively, and because both systemplate and lotemplate are mounted for each jails, we need to make them unbindable, so they wouldn't multiply the mount-points for each jails (an explosive growth!) Contrarywise, we don't want the mount-points to be shared, because we don't expect to add/remove mounts after a jail is created. The random temp directory is now created and set correctly, plus many logging and other improvements. Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829 Tested-by: Jenkins Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 15:02:58 +02:00
FileUtil::registerFileSystemForDiskSpaceChecks(ChildRoot);
int nThreads = std::max<int>(std::thread::hardware_concurrency(), 1);
int maxConcurrency = getConfigValue<int>(conf, "per_document.max_concurrency", 4);
if (maxConcurrency > 16)
{
LOG_WRN("Using a large number of threads for every document puts pressure on "
"the scheduler, and consumes memory, while providing marginal gains "
"consider lowering max_concurrency from " << maxConcurrency);
}
if (maxConcurrency > nThreads)
{
LOG_ERR("Setting concurrency above the number of physical "
"threads yields extra latency and memory usage for no benefit. "
"Clamping " << maxConcurrency << " to " << nThreads << " threads.");
maxConcurrency = nThreads;
}
if (maxConcurrency > 0)
{
setenv("MAX_CONCURRENCY", std::to_string(maxConcurrency).c_str(), 1);
}
LOG_INF("MAX_CONCURRENCY set to " << maxConcurrency << '.');
#elif defined(__EMSCRIPTEN__)
// disable threaded image scaling for wasm for now
setenv("VCL_NO_THREAD_SCALE", "1", 1);
#endif
const auto redlining = getConfigValue<bool>(conf, "per_document.redlining_as_comments", false);
if (!redlining)
{
setenv("DISABLE_REDLINE", "1", 1);
LOG_INF("DISABLE_REDLINE set");
}
// Otherwise we profile the soft-device at jail creation time.
setenv("SAL_DISABLE_OPENCL", "true", 1);
// Disable getting the OS print queue and default printer
setenv("SAL_DISABLE_PRINTERLIST", "true", 1);
setenv("SAL_DISABLE_DEFAULTPRINTER", "true", 1);
// Log the connection and document limits.
#if ENABLE_WELCOME_MESSAGE
if (getConfigValue<bool>(conf, "home_mode.enable", false))
{
COOLWSD::MaxConnections = 20;
COOLWSD::MaxDocuments = 10;
}
else
{
conf.setString("feedback.show", "true");
conf.setString("welcome.enable", "true");
COOLWSD::MaxConnections = MAX_CONNECTIONS;
COOLWSD::MaxDocuments = MAX_DOCUMENTS;
}
#else
{
COOLWSD::MaxConnections = MAX_CONNECTIONS;
COOLWSD::MaxDocuments = MAX_DOCUMENTS;
}
#endif
#if !MOBILEAPP
NoSeccomp = Util::isKitInProcess() || !getConfigValue<bool>(conf, "security.seccomp", true);
NoCapsForKit =
Util::isKitInProcess() || !getConfigValue<bool>(conf, "security.capabilities", true);
AdminEnabled = getConfigValue<bool>(conf, "admin_console.enable", true);
#if ENABLE_DEBUG
if (Util::isKitInProcess())
SingleKit = true;
#endif
#endif
// LanguageTool configuration
bool enableLanguageTool = getConfigValue<bool>(conf, "languagetool.enabled", false);
setenv("LANGUAGETOOL_ENABLED", enableLanguageTool ? "true" : "false", 1);
const std::string baseAPIUrl = getConfigValue<std::string>(conf, "languagetool.base_url", "");
setenv("LANGUAGETOOL_BASEURL", baseAPIUrl.c_str(), 1);
const std::string userName = getConfigValue<std::string>(conf, "languagetool.user_name", "");
setenv("LANGUAGETOOL_USERNAME", userName.c_str(), 1);
const std::string apiKey = getConfigValue<std::string>(conf, "languagetool.api_key", "");
setenv("LANGUAGETOOL_APIKEY", apiKey.c_str(), 1);
bool sslVerification = getConfigValue<bool>(conf, "languagetool.ssl_verification", true);
setenv("LANGUAGETOOL_SSL_VERIFICATION", sslVerification ? "true" : "false", 1);
const std::string restProtocol = getConfigValue<std::string>(conf, "languagetool.rest_protocol", "");
setenv("LANGUAGETOOL_RESTPROTOCOL", restProtocol.c_str(), 1);
// DeepL configuration
const std::string apiURL = getConfigValue<std::string>(conf, "deepl.api_url", "");
const std::string authKey = getConfigValue<std::string>(conf, "deepl.auth_key", "");
setenv("DEEPL_API_URL", apiURL.c_str(), 1);
setenv("DEEPL_AUTH_KEY", authKey.c_str(), 1);
#if !MOBILEAPP
const std::string helpUrl = getConfigValue<std::string>(conf, "help_url", HELP_URL);
setenv("LOK_HELP_URL", helpUrl.c_str(), 1);
#else
// On mobile UI there should be no tunnelled dialogs. But if there are some, by mistake,
// at least they should not have a non-working Help button.
setenv("LOK_HELP_URL", "", 1);
#endif
#if ENABLE_SUPPORT_KEY
const std::string supportKeyString = getConfigValue<std::string>(conf, "support_key", "");
if (supportKeyString.empty())
{
LOG_WRN("Support key not set, please use 'coolconfig set-support-key'.");
std::cerr << "Support key not set, please use 'coolconfig set-support-key'." << std::endl;
COOLWSD::OverrideWatermark = "Unsupported, the support key is missing.";
}
else
{
SupportKey key(supportKeyString);
if (!key.verify())
{
LOG_WRN("Invalid support key, please use 'coolconfig set-support-key'.");
std::cerr << "Invalid support key, please use 'coolconfig set-support-key'." << std::endl;
COOLWSD::OverrideWatermark = "Unsupported, the support key is invalid.";
}
else
{
int validDays = key.validDaysRemaining();
if (validDays <= 0)
{
LOG_WRN("Your support key has expired, please ask for a new one, and use 'coolconfig set-support-key'.");
std::cerr << "Your support key has expired, please ask for a new one, and use 'coolconfig set-support-key'." << std::endl;
COOLWSD::OverrideWatermark = "Unsupported, the support key has expired.";
}
else
{
LOG_INF("Your support key is valid for " << validDays << " days");
COOLWSD::MaxConnections = 1000;
COOLWSD::MaxDocuments = 200;
COOLWSD::OverrideWatermark.clear();
}
}
}
#endif
if (COOLWSD::MaxConnections < 3)
{
LOG_ERR("MAX_CONNECTIONS must be at least 3");
COOLWSD::MaxConnections = 3;
}
if (COOLWSD::MaxDocuments > COOLWSD::MaxConnections)
{
LOG_ERR("MAX_DOCUMENTS cannot be bigger than MAX_CONNECTIONS");
COOLWSD::MaxDocuments = COOLWSD::MaxConnections;
}
#if !WASMAPP
struct rlimit rlim;
::getrlimit(RLIMIT_NOFILE, &rlim);
LOG_INF("Maximum file descriptor supported by the system: " << rlim.rlim_cur - 1);
// 4 fds per document are used for client connection, Kit process communication, and
// a wakeup pipe with 2 fds. 32 fds (i.e. 8 documents) are reserved.
LOG_INF("Maximum number of open documents supported by the system: " << rlim.rlim_cur / 4 - 8);
#endif
LOG_INF("Maximum concurrent open Documents limit: " << COOLWSD::MaxDocuments);
LOG_INF("Maximum concurrent client Connections limit: " << COOLWSD::MaxConnections);
COOLWSD::NumConnections = 0;
// Command Tracing.
if (getConfigValue<bool>(conf, "trace[@enable]", false))
{
const auto& path = getConfigValue<std::string>(conf, "trace.path", "");
const auto recordOutgoing = getConfigValue<bool>(conf, "trace.outgoing.record", false);
std::vector<std::string> filters;
for (size_t i = 0; ; ++i)
{
const std::string confPath = "trace.filter.message[" + std::to_string(i) + ']';
const std::string regex = config().getString(confPath, "");
if (!regex.empty())
{
filters.push_back(regex);
}
else if (!config().has(confPath))
{
break;
}
}
const auto compress = getConfigValue<bool>(conf, "trace.path[@compress]", false);
const auto takeSnapshot = getConfigValue<bool>(conf, "trace.path[@snapshot]", false);
TraceDumper = std::make_unique<TraceFileWriter>(path, recordOutgoing, compress,
takeSnapshot, filters);
}
// Allowed hosts for being external data source in the documents
std::vector<std::string> lokAllowedHosts;
appendAllowedHostsFrom(conf, "net.lok_allow", lokAllowedHosts);
// For backward compatibility post_allow hosts are also allowed
bool postAllowed = conf.getBool("net.post_allow[@allow]", false);
if (postAllowed)
appendAllowedHostsFrom(conf, "net.post_allow", lokAllowedHosts);
// For backward compatibility wopi hosts are also allowed
bool wopiAllowed = conf.getBool("storage.wopi[@allow]", false);
if (wopiAllowed)
{
appendAllowedHostsFrom(conf, "storage.wopi", lokAllowedHosts);
appendAllowedAliasGroups(conf, lokAllowedHosts);
}
if (lokAllowedHosts.size())
{
std::string allowedRegex;
for (size_t i = 0; i < lokAllowedHosts.size(); i++)
{
if (isValidRegex(lokAllowedHosts[i]))
allowedRegex += (i != 0 ? "|" : "") + lokAllowedHosts[i];
else
LOG_ERR("Invalid regular expression for allowed host: \"" << lokAllowedHosts[i] << "\"");
}
setenv("LOK_HOST_ALLOWLIST", allowedRegex.c_str(), true);
}
#if !MOBILEAPP
SavedClipboards = std::make_unique<ClipboardCache>();
LOG_TRC("Initialize FileServerRequestHandler");
COOLWSD::FileRequestHandler =
std::make_unique<FileServerRequestHandler>(COOLWSD::FileServerRoot);
#endif
WebServerPoll = std::make_unique<TerminatingPoll>("websrv_poll");
PrisonerPoll = std::make_unique<PrisonPoll>();
Server = std::make_unique<COOLWSDServer>();
LOG_TRC("Initialize StorageBase");
StorageBase::initialize();
#if !MOBILEAPP
// Check for smaps_rollup bug where rewinding and rereading gives
// bogus doubled results
if (FILE* fp = fopen("/proc/self/smaps_rollup", "r"))
{
std::size_t memoryDirty1 = Util::getPssAndDirtyFromSMaps(fp).second;
(void)Util::getPssAndDirtyFromSMaps(fp); // interleave another rewind+read to margin
std::size_t memoryDirty2 = Util::getPssAndDirtyFromSMaps(fp).second;
LOG_TRC("Comparing smaps_rollup read and rewind+read: " << memoryDirty1 << " vs " << memoryDirty2);
if (memoryDirty2 >= memoryDirty1 * 2)
{
// Believed to be fixed in >= v4.19, bug seen in 4.15.0 and not in 6.5.10
// https://github.com/torvalds/linux/commit/258f669e7e88c18edbc23fe5ce00a476b924551f
LOG_WRN("Reading smaps_rollup twice reports Private_Dirty doubled, smaps_rollup is unreliable on this kernel");
setenv("COOL_DISABLE_SMAPS_ROLLUP", "1", true);
}
fclose(fp);
}
ServerApplication::initialize(self);
DocProcSettings docProcSettings;
docProcSettings.setLimitVirtMemMb(getConfigValue<int>("per_document.limit_virt_mem_mb", 0));
docProcSettings.setLimitStackMemKb(getConfigValue<int>("per_document.limit_stack_mem_kb", 0));
docProcSettings.setLimitFileSizeMb(getConfigValue<int>("per_document.limit_file_size_mb", 0));
docProcSettings.setLimitNumberOpenFiles(getConfigValue<int>("per_document.limit_num_open_files", 0));
DocCleanupSettings &docCleanupSettings = docProcSettings.getCleanupSettings();
docCleanupSettings.setEnable(getConfigValue<bool>("per_document.cleanup[@enable]", false));
docCleanupSettings.setCleanupInterval(getConfigValue<int>("per_document.cleanup.cleanup_interval_ms", 10000));
docCleanupSettings.setBadBehaviorPeriod(getConfigValue<int>("per_document.cleanup.bad_behavior_period_secs", 60));
docCleanupSettings.setIdleTime(getConfigValue<int>("per_document.cleanup.idle_time_secs", 300));
docCleanupSettings.setLimitDirtyMem(getConfigValue<int>("per_document.cleanup.limit_dirty_mem_mb", 3072));
docCleanupSettings.setLimitCpu(getConfigValue<int>("per_document.cleanup.limit_cpu_per", 85));
docCleanupSettings.setLostKitGracePeriod(getConfigValue<int>("per_document.cleanup.lost_kit_grace_period_secs", 120));
Admin::instance().setDefDocProcSettings(docProcSettings, false);
#else
(void) self;
#endif
}
void COOLWSD::initializeSSL()
{
#if ENABLE_SSL
if (!COOLWSD::isSSLEnabled())
return;
const std::string ssl_cert_file_path = getPathFromConfig("ssl.cert_file_path");
LOG_INF("SSL Cert file: " << ssl_cert_file_path);
const std::string ssl_key_file_path = getPathFromConfig("ssl.key_file_path");
LOG_INF("SSL Key file: " << ssl_key_file_path);
const std::string ssl_ca_file_path = getPathFromConfig("ssl.ca_file_path");
LOG_INF("SSL CA file: " << ssl_ca_file_path);
std::string ssl_cipher_list = config().getString("ssl.cipher_list", "");
if (ssl_cipher_list.empty())
ssl_cipher_list = DEFAULT_CIPHER_SET;
LOG_INF("SSL Cipher list: " << ssl_cipher_list);
// Initialize the non-blocking server socket SSL context.
ssl::Manager::initializeServerContext(ssl_cert_file_path, ssl_key_file_path, ssl_ca_file_path,
ssl_cipher_list, ssl::CertificateVerification::Disabled);
if (!ssl::Manager::isServerContextInitialized())
LOG_ERR("Failed to initialize Server SSL.");
else
LOG_INF("Initialized Server SSL.");
#else
LOG_INF("SSL is unavailable in this build.");
#endif
}
void COOLWSD::dumpNewSessionTrace(const std::string& id, const std::string& sessionId, const std::string& uri, const std::string& path)
{
if (TraceDumper)
{
try
{
TraceDumper->newSession(id, sessionId, uri, path);
}
catch (const std::exception& exc)
{
LOG_ERR("Exception in tracer newSession: " << exc.what());
}
}
}
void COOLWSD::dumpEndSessionTrace(const std::string& id, const std::string& sessionId, const std::string& uri)
{
if (TraceDumper)
{
try
{
TraceDumper->endSession(id, sessionId, uri);
}
catch (const std::exception& exc)
{
LOG_ERR("Exception in tracer newSession: " << exc.what());
}
}
}
void COOLWSD::dumpEventTrace(const std::string& id, const std::string& sessionId, const std::string& data)
{
if (TraceDumper)
{
TraceDumper->writeEvent(id, sessionId, data);
}
}
void COOLWSD::dumpIncomingTrace(const std::string& id, const std::string& sessionId, const std::string& data)
{
if (TraceDumper)
{
TraceDumper->writeIncoming(id, sessionId, data);
}
}
void COOLWSD::dumpOutgoingTrace(const std::string& id, const std::string& sessionId, const std::string& data)
{
if (TraceDumper)
{
TraceDumper->writeOutgoing(id, sessionId, data);
}
}
void COOLWSD::defineOptions(OptionSet& optionSet)
{
if (Util::isMobileApp())
return;
2015-10-28 10:55:03 +01:00
ServerApplication::defineOptions(optionSet);
2015-10-28 10:55:03 +01:00
optionSet.addOption(Option("help", "", "Display help information on command line arguments.")
.required(false)
.repeatable(false));
optionSet.addOption(Option("version-hash", "", "Display product version-hash information and exit.")
.required(false)
.repeatable(false));
optionSet.addOption(Option("version", "", "Display version and hash information.")
.required(false)
.repeatable(false));
optionSet.addOption(Option("cleanup", "", "Cleanup jails and other temporary data and exit.")
.required(false)
.repeatable(false));
optionSet.addOption(Option("port", "", "Port number to listen to (default: " +
std::to_string(DEFAULT_CLIENT_PORT_NUMBER) + "),")
2015-10-28 10:55:03 +01:00
.required(false)
.repeatable(false)
.argument("port_number"));
2015-10-28 10:55:03 +01:00
optionSet.addOption(Option("disable-ssl", "", "Disable SSL security layer.")
.required(false)
.repeatable(false));
optionSet.addOption(Option("disable-cool-user-checking", "", "Don't check whether coolwsd is running under the user 'cool'. NOTE: This is insecure, use only when you know what you are doing!")
.required(false)
.repeatable(false));
optionSet.addOption(Option("override", "o", "Override any setting by providing full xmlpath=value.")
.required(false)
.repeatable(true)
.argument("xmlpath"));
optionSet.addOption(Option("config-file", "", "Override configuration file path.")
.required(false)
.repeatable(false)
.argument("path"));
optionSet.addOption(Option("config-dir", "", "Override extra configuration directory path.")
.required(false)
.repeatable(false)
.argument("path"));
optionSet.addOption(Option("lo-template-path", "", "Override the LOK core installation directory path.")
.required(false)
.repeatable(false)
.argument("path"));
optionSet.addOption(Option("unattended", "", "Unattended run, won't wait for a debugger on faulting.")
.required(false)
.repeatable(false));
optionSet.addOption(Option("signal", "", "Send signal SIGUSR2 to parent process when server is ready to accept connections")
.required(false)
.repeatable(false));
optionSet.addOption(Option("use-env-vars", "",
"Use the environment variables defined on "
"https://sdk.collaboraonline.com/docs/installation/"
"CODE_Docker_image.html#setting-the-application-configuration-"
"dynamically-via-environment-variables to set options. "
"'DONT_GEN_SSL_CERT' is forcibly enabled and 'extra_params' is "
"ignored even when using this option.")
.required(false)
.repeatable(false));
#if ENABLE_DEBUG
optionSet.addOption(Option("unitlib", "", "Unit testing library path.")
.required(false)
.repeatable(false)
.argument("unitlib"));
optionSet.addOption(Option("careerspan", "", "How many seconds to run.")
.required(false)
.repeatable(false)
.argument("seconds"));
optionSet.addOption(Option("singlekit", "", "Spawn one libreoffice kit.")
.required(false)
.repeatable(false));
optionSet.addOption(Option("forcecaching", "", "Force HTML & asset caching even in debug mode: accelerates cypress.")
.required(false)
.repeatable(false));
#endif
}
void COOLWSD::handleOption(const std::string& optionName,
const std::string& value)
2015-03-25 13:39:58 +01:00
{
#if !MOBILEAPP
2015-10-28 10:55:03 +01:00
ServerApplication::handleOption(optionName, value);
2015-03-25 13:39:58 +01:00
2015-10-28 10:55:03 +01:00
if (optionName == "help")
2015-03-25 13:39:58 +01:00
{
displayHelp();
Util::forcedExit(EX_OK);
2015-03-25 13:39:58 +01:00
}
else if (optionName == "version-hash")
{
std::string version, hash;
Util::getVersionInfo(version, hash);
std::cout << hash << std::endl;
Util::forcedExit(EX_OK);
}
else if (optionName == "version")
; // ignore for compatibility
else if (optionName == "cleanup")
CleanupOnly = true; // Flag for later as we need the config.
2015-10-28 10:55:03 +01:00
else if (optionName == "port")
ClientPortNumber = std::stoi(value);
else if (optionName == "disable-ssl")
_overrideSettings["ssl.enable"] = "false";
else if (optionName == "disable-cool-user-checking")
CheckCoolUser = false;
else if (optionName == "override")
{
std::string optName;
std::string optValue;
COOLProtocol::parseNameValuePair(value, optName, optValue);
_overrideSettings[optName] = optValue;
}
else if (optionName == "config-file")
ConfigFile = value;
else if (optionName == "config-dir")
ConfigDir = value;
else if (optionName == "lo-template-path")
LoTemplate = value;
else if (optionName == "signal")
SignalParent = true;
else if (optionName == "use-env-vars")
UseEnvVarOptions = true;
#if ENABLE_DEBUG
else if (optionName == "unitlib")
UnitTestLibrary = value;
else if (optionName == "unattended")
{
UnattendedRun = true;
SigUtil::setUnattended();
}
else if (optionName == "careerspan")
careerSpanMs = std::chrono::seconds(std::stoi(value)); // Convert second to ms
else if (optionName == "singlekit")
{
SingleKit = true;
NumPreSpawnedChildren = 1;
}
else if (optionName == "forcecaching")
ForceCaching = true;
static const char* latencyMs = std::getenv("COOL_DELAY_SOCKET_MS");
if (latencyMs)
SimulatedLatencyMs = std::stoi(latencyMs);
#endif
#else
(void) optionName;
(void) value;
#endif
2015-03-25 13:39:58 +01:00
}
void COOLWSD::initializeEnvOptions()
{
int n = 0;
char* aliasGroup;
while ((aliasGroup = std::getenv(("aliasgroup" + std::to_string(n + 1)).c_str())) != nullptr)
{
bool first = true;
std::istringstream aliasGroupStream;
aliasGroupStream.str(aliasGroup);
for (std::string alias; std::getline(aliasGroupStream, alias, ',');)
{
if (first)
{
const std::string path = "storage.wopi.alias_groups.group[" + std::to_string(n) + "].host";
_overrideSettings[path] = alias;
_overrideSettings[path + "[@allow]"] = "true";
first = false;
}
else
{
_overrideSettings["storage.wopi.alias_groups.group[" + std::to_string(n) +
"].alias"] = alias;
}
}
n++;
}
if (n >= 1)
{
_overrideSettings["alias_groups[@mode]"] = "groups";
}
char* optionValue;
if ((optionValue = std::getenv("username")) != nullptr) _overrideSettings["admin_console.username"] = optionValue;
if ((optionValue = std::getenv("password")) != nullptr) _overrideSettings["admin_console.password"] = optionValue;
if ((optionValue = std::getenv("server_name")) != nullptr) _overrideSettings["server_name"] = optionValue;
if ((optionValue = std::getenv("dictionaries")) != nullptr) _overrideSettings["allowed_languages"] = optionValue;
if ((optionValue = std::getenv("remoteconfigurl")) != nullptr) _overrideSettings["remote_config.remote_url"] = optionValue;
}
2015-03-25 13:39:58 +01:00
#if !MOBILEAPP
void COOLWSD::displayHelp()
2015-03-25 13:39:58 +01:00
{
HelpFormatter helpFormatter(options());
helpFormatter.setCommand(commandName());
helpFormatter.setUsage("OPTIONS");
helpFormatter.setHeader("Collabora Online WebSocket server.");
2015-03-25 13:39:58 +01:00
helpFormatter.format(std::cout);
}
bool COOLWSD::checkAndRestoreForKit()
{
// clang issues warning for WIF*() macro usages below:
// "equality comparison with extraneous parentheses [-Werror,-Wparentheses-equality]"
// https://bugs.llvm.org/show_bug.cgi?id=22949
#if defined __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wparentheses-equality"
#endif
if (ForKitProcId == -1)
{
// Fire the ForKit process for the first time.
if (!SigUtil::getShutdownRequestFlag() && !createForKit())
{
// Should never fail.
LOG_FTL("Setting ShutdownRequestFlag: Failed to spawn coolforkit.");
SigUtil::requestShutdown();
}
}
if (Util::isKitInProcess())
return true;
int status;
const pid_t pid = waitpid(ForKitProcId, &status, WUNTRACED | WNOHANG);
if (pid > 0)
{
if (pid == ForKitProcId)
{
if (WIFEXITED(status) || WIFSIGNALED(status))
{
if (WIFEXITED(status))
{
LOG_INF("Forkit process [" << pid << "] exited with code: " <<
WEXITSTATUS(status) << '.');
}
else
{
LOG_ERR("Forkit process [" << pid << "] " <<
(WCOREDUMP(status) ? "core-dumped" : "died") <<
" with " << SigUtil::signalName(WTERMSIG(status)));
}
// Spawn a new forkit and try to dust it off and resume.
if (!SigUtil::getShutdownRequestFlag() && !createForKit())
{
LOG_FTL("Setting ShutdownRequestFlag: Failed to spawn forkit instance.");
SigUtil::requestShutdown();
}
}
else if (WIFSTOPPED(status))
{
LOG_INF("Forkit process [" << pid << "] stopped with " <<
SigUtil::signalName(WSTOPSIG(status)));
}
else if (WIFCONTINUED(status))
{
LOG_INF("Forkit process [" << pid << "] resumed with SIGCONT.");
}
else
{
LOG_WRN("Unknown status returned by waitpid: " << std::hex << status << std::dec);
}
return true;
}
else
{
LOG_ERR("An unknown child process [" << pid << "] died.");
}
}
else if (pid < 0)
{
LOG_SYS("Forkit waitpid failed");
if (errno == ECHILD)
{
// No child processes.
// Spawn a new forkit and try to dust it off and resume.
if (!SigUtil::getShutdownRequestFlag() && !createForKit())
{
LOG_FTL("Setting ShutdownRequestFlag: Failed to spawn forkit instance.");
SigUtil::requestShutdown();
}
}
return true;
}
return false;
#if defined __clang__
#pragma clang diagnostic pop
#endif
}
#endif
void COOLWSD::doHousekeeping()
{
if (PrisonerPoll)
{
PrisonerPoll->wakeup();
}
}
void COOLWSD::closeDocument(const std::string& docKey, const std::string& message)
{
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto docBrokerIt = DocBrokers.find(docKey);
if (docBrokerIt != DocBrokers.end())
{
std::shared_ptr<DocumentBroker> docBroker = docBrokerIt->second;
docBroker->addCallback([docBroker, message]() {
docBroker->closeDocument(message);
});
}
}
void COOLWSD::autoSave(const std::string& docKey)
{
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto docBrokerIt = DocBrokers.find(docKey);
if (docBrokerIt != DocBrokers.end())
{
std::shared_ptr<DocumentBroker> docBroker = docBrokerIt->second;
docBroker->addCallback(
[docBroker]() { docBroker->autoSave(/*force=*/true, /*dontSaveIfUnmodified=*/true); });
}
}
void COOLWSD::setLogLevelsOfKits(const std::string& level)
{
std::lock_guard<std::mutex> docBrokersLock(DocBrokersMutex);
LOG_INF("Changing kits' log levels: [" << level << ']');
for (const auto& brokerIt : DocBrokers)
{
std::shared_ptr<DocumentBroker> docBroker = brokerIt.second;
docBroker->addCallback([docBroker, level]() {
docBroker->setKitLogLevel(level);
});
}
}
/// Really do the house-keeping
void PrisonPoll::wakeupHook()
{
#if !MOBILEAPP
LOG_TRC("PrisonerPoll - wakes up with " << NewChildren.size() <<
" new children and " << DocBrokers.size() << " brokers and " <<
OutstandingForks << " kits forking");
if (!COOLWSD::checkAndRestoreForKit())
{
// No children have died.
// Make sure we have sufficient reserves.
prespawnChildren();
}
#endif
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex, std::defer_lock);
if (docBrokersLock.try_lock())
{
cleanupDocBrokers();
SigUtil::checkForwardSigUsr2(forwardSigUsr2);
}
}
#if !MOBILEAPP
bool COOLWSD::createForKit()
2015-07-13 16:13:06 +02:00
{
LOG_INF("Creating new forkit process.");
// Creating a new forkit is always a slow process.
ChildSpawnTimeoutMs = CHILD_TIMEOUT_MS * 4;
std::unique_lock<std::mutex> newChildrenLock(NewChildrenMutex);
StringVector args;
std::string parentPath = Path(Application::instance().commandPath()).parent().toString();
#if STRACE_COOLFORKIT
// if you want to use this, you need to sudo setcap cap_fowner,cap_chown,cap_mknod,cap_sys_chroot=ep /usr/bin/strace
args.push_back("-o");
args.push_back("strace.log");
args.push_back("-f");
args.push_back("-tt");
args.push_back("-s");
args.push_back("256");
args.push_back(parentPath + "coolforkit");
#elif VALGRIND_COOLFORKIT
NoCapsForKit = true;
NoSeccomp = true;
// args.push_back("--log-file=valgrind.log");
// args.push_back("--track-fds=all");
// for massif: can connect with (gdb) target remote | vgdb
// and then monitor snapshot <filename> before kit exit
// args.push_back("--tool=massif");
// args.push_back("--vgdb=yes");
// args.push_back("--vgdb-error=0");
args.push_back("--trace-children=yes");
args.push_back("--error-limit=no");
args.push_back("--num-callers=128");
std::string nocapsCopy = parentPath + "coolforkit-nocaps";
FileUtil::copy(parentPath + "coolforkit", nocapsCopy, true, true);
args.push_back(nocapsCopy);
#endif
args.push_back("--systemplate=" + SysTemplate);
args.push_back("--lotemplate=" + LoTemplate);
args.push_back("--childroot=" + ChildRoot);
args.push_back("--clientport=" + std::to_string(ClientPortNumber));
args.push_back("--masterport=" + MasterLocation);
const DocProcSettings& docProcSettings = Admin::instance().getDefDocProcSettings();
std::ostringstream ossRLimits;
ossRLimits << "limit_virt_mem_mb:" << docProcSettings.getLimitVirtMemMb();
ossRLimits << ";limit_stack_mem_kb:" << docProcSettings.getLimitStackMemKb();
ossRLimits << ";limit_file_size_mb:" << docProcSettings.getLimitFileSizeMb();
ossRLimits << ";limit_num_open_files:" << docProcSettings.getLimitNumberOpenFiles();
args.push_back("--rlimits=" + ossRLimits.str());
if (UnitWSD::get().hasKitHooks())
args.push_back("--unitlib=" + UnitTestLibrary);
args.push_back("--version");
if (NoCapsForKit)
args.push_back("--nocaps");
if (NoSeccomp)
args.push_back("--noseccomp");
args.push_back("--ui=" + UserInterface);
if (!CheckCoolUser)
args.push_back("--disable-cool-user-checking");
if (UnattendedRun)
args.push_back("--unattended");
#if ENABLE_DEBUG
if (SingleKit)
args.push_back("--singlekit");
#endif
#if STRACE_COOLFORKIT
std::string forKitPath = "/usr/bin/strace";
#elif VALGRIND_COOLFORKIT
std::string forKitPath = "/usr/bin/valgrind";
#else
std::string forKitPath = parentPath + "coolforkit";
#endif
// Always reap first, in case we haven't done so yet.
if (ForKitProcId != -1)
{
if (Util::isKitInProcess())
return true;
int status;
waitpid(ForKitProcId, &status, WUNTRACED | WNOHANG);
ForKitProcId = -1;
Admin::instance().setForKitPid(ForKitProcId);
}
// Below line will be executed by PrisonerPoll thread.
ForKitProc = nullptr;
PrisonerPoll->setForKitProcess(ForKitProc);
// ForKit always spawns one.
++OutstandingForks;
LOG_INF("Launching forkit process: " << forKitPath << ' ' << args.cat(' ', 0));
LastForkRequestTime = std::chrono::steady_clock::now();
int child = createForkit(forKitPath, args);
ForKitProcId = child;
LOG_INF("Forkit process launched: " << ForKitProcId);
// Init the Admin manager
Admin::instance().setForKitPid(ForKitProcId);
const int balance = COOLWSD::NumPreSpawnedChildren - OutstandingForks;
if (balance > 0)
rebalanceChildren(balance);
return ForKitProcId != -1;
2015-07-13 16:13:06 +02:00
}
void COOLWSD::sendMessageToForKit(const std::string& message)
{
if (PrisonerPoll)
{
PrisonerPoll->sendMessageToForKit(message);
}
}
#endif // !MOBILEAPP
2017-03-16 19:32:12 +01:00
/// Handles the socket that the prisoner kit connected to WSD on.
class PrisonerRequestDispatcher final : public WebSocketHandler
{
std::weak_ptr<ChildProcess> _childProcess;
int _pid; //< The Kit's PID (for logging).
int _socketFD; //< The socket FD to the Kit (for logging).
bool _associatedWithDoc; //< True when/if we get a DocBroker.
public:
PrisonerRequestDispatcher()
: WebSocketHandler(/* isClient = */ false, /* isMasking = */ true)
, _pid(0)
, _socketFD(0)
, _associatedWithDoc(false)
{
LOG_TRC_S("PrisonerRequestDispatcher");
}
~PrisonerRequestDispatcher()
{
LOG_TRC("~PrisonerRequestDispatcher");
// Notify the broker that we're done.
// Note: since this class is the default WebScoketHandler
// for all incoming connections, for ForKit we have to
// replace it (once we receive 'GET /coolws/forkit') with
// ForKitProcWSHandler (see ForKitProcess) and nothing to disconnect.
std::shared_ptr<ChildProcess> child = _childProcess.lock();
if (child && child->getPid() > 0)
onDisconnect();
}
private:
/// Keep our socket around ...
void onConnect(const std::shared_ptr<StreamSocket>& socket) override
{
WebSocketHandler::onConnect(socket);
LOG_TRC("Prisoner connected");
}
void onDisconnect() override
{
LOG_DBG("Prisoner connection disconnected");
// Notify the broker that we're done.
std::shared_ptr<ChildProcess> child = _childProcess.lock();
std::shared_ptr<DocumentBroker> docBroker =
child && child->getPid() > 0 ? child->getDocumentBroker() : nullptr;
if (docBroker)
{
assert(child->getPid() == _pid && "Child PID changed unexpectedly");
const bool unexpected = !docBroker->isUnloading() && !SigUtil::getShutdownRequestFlag();
if (unexpected)
{
LOG_WRN("DocBroker [" << docBroker->getDocKey()
<< "] got disconnected from its Kit (" << child->getPid()
<< ") unexpectedly. Closing");
}
else
{
LOG_DBG("DocBroker [" << docBroker->getDocKey() << "] disconnected from its Kit ("
<< child->getPid() << ") as expected");
}
docBroker->disconnectedFromKit(unexpected);
}
else if (!_associatedWithDoc && !SigUtil::getShutdownRequestFlag())
{
LOG_WRN("Unassociated Kit (" << _pid << ") disconnected unexpectedly");
std::unique_lock<std::mutex> lock(NewChildrenMutex);
auto it = std::find(NewChildren.begin(), NewChildren.end(), child);
if (it != NewChildren.end())
NewChildren.erase(it);
else
LOG_WRN("Unknown Kit process closed with pid " << child->getPid());
#if !MOBILEAPP
rebalanceChildren(COOLWSD::NumPreSpawnedChildren);
#endif
}
}
/// Called after successful socket reads.
void handleIncomingMessage(SocketDisposition &disposition) override
{
if (_childProcess.lock())
{
// FIXME: inelegant etc. - derogate to websocket code
WebSocketHandler::handleIncomingMessage(disposition);
return;
}
std::shared_ptr<StreamSocket> socket = getSocket().lock();
if (!socket)
{
LOG_ERR("Invalid socket while reading incoming message");
return;
}
Buffer& data = socket->getInBuffer();
if (data.empty())
{
LOG_DBG("No data to process from the socket");
return;
}
#ifdef LOG_SOCKET_DATA
LOG_TRC("HandleIncomingMessage: buffer has:\n"
<< Util::dumpHex(std::string(data.data(), std::min(data.size(), 256UL))));
#endif
// Consume the incoming data by parsing and processing the body.
http::Request request;
#if !MOBILEAPP
const int64_t read = request.readData(data.data(), data.size());
if (read < 0)
{
LOG_ERR("Error parsing prisoner socket data");
return;
}
if (read == 0)
{
// Not enough data.
return;
}
assert(read > 0 && "Must have read some data!");
// Remove consumed data.
data.eraseFirst(read);
#endif
try
{
#if !MOBILEAPP
LOG_TRC("Child connection with URI [" << COOLWSD::anonymizeUrl(request.getUrl())
<< ']');
Poco::URI requestURI(request.getUrl());
if (requestURI.getPath() == FORKIT_URI)
{
if (socket->getPid() != COOLWSD::ForKitProcId)
{
LOG_WRN("Connection request received on "
<< FORKIT_URI << " endpoint from unexpected ForKit process. Skipped");
return;
}
COOLWSD::ForKitProc = std::make_shared<ForKitProcess>(COOLWSD::ForKitProcId, socket, request);
LOG_ASSERT_MSG(socket->getInBuffer().empty(), "Unexpected data in prisoner socket");
socket->getInBuffer().clear();
PrisonerPoll->setForKitProcess(COOLWSD::ForKitProc);
return;
}
if (requestURI.getPath() != NEW_CHILD_URI)
{
LOG_ERR("Invalid incoming child URI [" << requestURI.getPath() << ']');
return;
}
const auto duration = (std::chrono::steady_clock::now() - LastForkRequestTime);
const auto durationMs = std::chrono::duration_cast<std::chrono::milliseconds>(duration);
LOG_TRC("New child spawned after " << durationMs << " of requesting");
// New Child is spawned.
const Poco::URI::QueryParameters params = requestURI.getQueryParameters();
const int pid = socket->getPid();
std::string jailId;
for (const auto& param : params)
{
if (param.first == "jailid")
jailId = param.second;
else if (param.first == "version")
COOLWSD::LOKitVersion = param.second;
}
if (pid <= 0)
{
LOG_ERR("Invalid PID in child URI [" << COOLWSD::anonymizeUrl(request.getUrl())
<< ']');
return;
}
if (jailId.empty())
{
LOG_ERR("Invalid JailId in child URI [" << COOLWSD::anonymizeUrl(request.getUrl())
<< ']');
return;
}
LOG_ASSERT_MSG(socket->getInBuffer().empty(), "Unexpected data in prisoner socket");
socket->getInBuffer().clear();
LOG_INF("New child [" << pid << "], jailId: " << jailId);
#else
pid_t pid = 100;
std::string jailId = "jail";
socket->getInBuffer().clear();
#endif
LOG_TRC("Calling make_shared<ChildProcess>, for NewChildren?");
auto child = std::make_shared<ChildProcess>(pid, jailId, socket, request);
if (!Util::isMobileApp())
UnitWSD::get().newChild(child);
_pid = pid;
_socketFD = socket->getFD();
child->setSMapsFD(socket->getIncomingFD(SMAPS));
_childProcess = child; // weak
addNewChild(child);
}
catch (const std::bad_weak_ptr&)
{
// Using shared_from_this() from a constructor is not good.
assert(!"Got std::bad_weak_ptr. Are we using shared_from_this() from a constructor?");
}
catch (const std::exception& exc)
{
// Probably don't have enough data just yet.
// TODO: timeout if we never get enough.
}
}
/// Prisoner websocket fun ... (for now)
virtual void handleMessage(const std::vector<char> &data) override
{
if (UnitWSD::isUnitTesting() && UnitWSD::get().filterChildMessage(data))
return;
auto message = std::make_shared<Message>(data.data(), data.size(), Message::Dir::Out);
std::shared_ptr<StreamSocket> socket = getSocket().lock();
if (socket)
{
assert(socket->getFD() == _socketFD && "Socket FD changed unexpectedly");
LOG_TRC("Prisoner message [" << message->abbr() << ']');
}
else
LOG_WRN("Message handler called but without valid socket. Expected #" << _socketFD);
std::shared_ptr<ChildProcess> child = _childProcess.lock();
std::shared_ptr<DocumentBroker> docBroker =
child && child->getPid() > 0 ? child->getDocumentBroker() : nullptr;
if (docBroker)
{
assert(child->getPid() == _pid && "Child PID changed unexpectedly");
_associatedWithDoc = true;
docBroker->handleInput(message);
}
else if (child && child->getPid() > 0)
{
const std::string abbreviatedMessage = COOLWSD::AnonymizeUserData ? "..." : message->abbr();
LOG_WRN("Child " << child->getPid() << " has no DocBroker to handle message: ["
<< abbreviatedMessage << ']');
}
else
{
const std::string abbreviatedMessage = COOLWSD::AnonymizeUserData ? "..." : message->abbr();
LOG_ERR("Cannot handle message with unassociated Kit (PID " << _pid << "): ["
<< abbreviatedMessage);
}
}
int getPollEvents(std::chrono::steady_clock::time_point /* now */,
int64_t & /* timeoutMaxMs */) override
{
return POLLIN;
}
void performWrites(std::size_t /*capacity*/) override {}
};
class PlainSocketFactory final : public SocketFactory
{
std::shared_ptr<Socket> create(const int physicalFd, Socket::Type type) override
{
2017-03-23 18:14:51 +01:00
int fd = physicalFd;
#if !MOBILEAPP
2017-03-23 18:14:51 +01:00
if (SimulatedLatencyMs > 0)
{
int delayfd = Delay::create(SimulatedLatencyMs, physicalFd);
if (delayfd == -1)
LOG_ERR("DelaySocket creation failed, using physicalFd " << physicalFd << " instead.");
else
fd = delayfd;
}
#endif
return StreamSocket::create<StreamSocket>(
std::string(), fd, type, false,
std::make_shared<ClientRequestDispatcher>());
}
};
#if ENABLE_SSL
class SslSocketFactory final : public SocketFactory
{
std::shared_ptr<Socket> create(const int physicalFd, Socket::Type type) override
{
2017-03-23 18:14:51 +01:00
int fd = physicalFd;
#if !MOBILEAPP
2017-03-23 18:14:51 +01:00
if (SimulatedLatencyMs > 0)
fd = Delay::create(SimulatedLatencyMs, physicalFd);
#endif
2017-03-23 18:14:51 +01:00
return StreamSocket::create<SslStreamSocket>(std::string(), fd, type, false,
std::make_shared<ClientRequestDispatcher>());
}
};
#endif
class PrisonerSocketFactory final : public SocketFactory
{
std::shared_ptr<Socket> create(const int fd, Socket::Type type) override
{
2017-03-23 18:14:51 +01:00
// No local delay.
return StreamSocket::create<StreamSocket>(std::string(), fd, type, false,
std::make_shared<PrisonerRequestDispatcher>(),
StreamSocket::ReadType::UseRecvmsgExpectFD);
}
};
/// The main server thread.
///
/// Waits for the connections from the cools, and creates the
/// websockethandlers accordingly.
class COOLWSDServer
{
COOLWSDServer(COOLWSDServer&& other) = delete;
const COOLWSDServer& operator=(COOLWSDServer&& other) = delete;
// allocate port & hold temporarily.
std::shared_ptr<ServerSocket> _serverSocket;
public:
COOLWSDServer()
: _acceptPoll("accept_poll")
#if !MOBILEAPP
, _admin(Admin::instance())
#endif
{
}
~COOLWSDServer()
{
stop();
}
void findClientPort()
{
_serverSocket = findServerPort();
}
void startPrisoners()
{
PrisonerPoll->startThread();
PrisonerPoll->insertNewSocket(findPrisonerServerPort());
}
static void stopPrisoners()
{
PrisonerPoll->joinThread();
}
void start()
{
_acceptPoll.startThread();
_acceptPoll.insertNewSocket(_serverSocket);
#if MOBILEAPP
coolwsd_server_socket_fd = _serverSocket->getFD();
#endif
_serverSocket.reset();
WebServerPoll->startThread();
#if !MOBILEAPP
_admin.start();
#endif
}
void stop()
{
_acceptPoll.joinThread();
if (WebServerPoll)
WebServerPoll->joinThread();
#if !MOBILEAPP
_admin.stop();
#endif
}
void dumpState(std::ostream& os) const
{
// FIXME: add some stop-world magic before doing the dump(?)
Socket::InhibitThreadChecks = true;
SocketPoll::InhibitThreadChecks = true;
std::string version, hash;
Util::getVersionInfo(version, hash);
os << "COOLWSDServer: " << version << " - " << hash
#if !MOBILEAPP
<< "\n Kit version: " << COOLWSD::LOKitVersion
<< "\n Ports: server " << ClientPortNumber << " prisoner " << MasterLocation
<< "\n SSL: " << (COOLWSD::isSSLEnabled() ? "https" : "http")
<< "\n SSL-Termination: " << (COOLWSD::isSSLTermination() ? "yes" : "no")
<< "\n Security " << (COOLWSD::NoCapsForKit ? "no" : "") << " chroot, "
<< (COOLWSD::NoSeccomp ? "no" : "") << " api lockdown"
<< "\n Admin: " << (COOLWSD::AdminEnabled ? "enabled" : "disabled")
<< "\n RouteToken: " << COOLWSD::RouteToken
#endif
<< "\n TerminationFlag: " << SigUtil::getTerminationFlag()
<< "\n isShuttingDown: " << SigUtil::getShutdownRequestFlag()
<< "\n NewChildren: " << NewChildren.size()
<< "\n OutstandingForks: " << OutstandingForks
<< "\n NumPreSpawnedChildren: " << COOLWSD::NumPreSpawnedChildren
<< "\n ChildSpawnTimeoutMs: " << ChildSpawnTimeoutMs
<< "\n Document Brokers: " << DocBrokers.size()
#if !MOBILEAPP
<< "\n of which ConvertTo: " << ConvertToBroker::getInstanceCount()
#endif
<< "\n vs. MaxDocuments: " << COOLWSD::MaxDocuments
<< "\n NumConnections: " << COOLWSD::NumConnections
<< "\n vs. MaxConnections: " << COOLWSD::MaxConnections
<< "\n SysTemplate: " << COOLWSD::SysTemplate
<< "\n LoTemplate: " << COOLWSD::LoTemplate
<< "\n ChildRoot: " << COOLWSD::ChildRoot
<< "\n FileServerRoot: " << COOLWSD::FileServerRoot
<< "\n ServiceRoot: " << COOLWSD::ServiceRoot
<< "\n LOKitVersion: " << COOLWSD::LOKitVersion
<< "\n HostIdentifier: " << Util::getProcessIdentifier()
<< "\n ConfigFile: " << COOLWSD::ConfigFile
<< "\n ConfigDir: " << COOLWSD::ConfigDir
<< "\n LogLevel: " << COOLWSD::LogLevel
<< "\n AnonymizeUserData: " << (COOLWSD::AnonymizeUserData ? "yes" : "no")
<< "\n CheckCoolUser: " << (COOLWSD::CheckCoolUser ? "yes" : "no")
<< "\n IsProxyPrefixEnabled: " << (COOLWSD::IsProxyPrefixEnabled ? "yes" : "no")
<< "\n OverrideWatermark: " << COOLWSD::OverrideWatermark
<< "\n UserInterface: " << COOLWSD::UserInterface
;
os << "\nServer poll:\n";
_acceptPoll.dumpState(os);
os << "Web Server poll:\n";
WebServerPoll->dumpState(os);
2017-03-06 16:45:34 +01:00
os << "Prisoner poll:\n";
PrisonerPoll->dumpState(os);
#if !MOBILEAPP
os << "Admin poll:\n";
_admin.dumpState(os);
2017-03-23 18:14:51 +01:00
// If we have any delaying work going on.
Delay::dumpState(os);
COOLWSD::SavedClipboards->dumpState(os);
#endif
2017-03-23 18:14:51 +01:00
os << "Document Broker polls "
<< "[ " << DocBrokers.size() << " ]:\n";
2017-03-06 16:45:34 +01:00
for (auto &i : DocBrokers)
i.second->dumpState(os);
#if !MOBILEAPP
os << "Converter count: " << ConvertToBroker::getInstanceCount() << '\n';
#endif
Socket::InhibitThreadChecks = false;
SocketPoll::InhibitThreadChecks = false;
}
private:
class AcceptPoll : public TerminatingPoll {
public:
AcceptPoll(const std::string &threadName) :
TerminatingPoll(threadName) {}
void wakeupHook() override
{
SigUtil::checkDumpGlobalState(dump_state);
}
};
/// This thread & poll accepts incoming connections.
AcceptPoll _acceptPoll;
#if !MOBILEAPP
Admin& _admin;
#endif
/// Create the internal only, local socket for forkit / kits prisoners to talk to.
std::shared_ptr<ServerSocket> findPrisonerServerPort()
{
std::shared_ptr<SocketFactory> factory = std::make_shared<PrisonerSocketFactory>();
#if !MOBILEAPP
auto socket = std::make_shared<LocalServerSocket>(*PrisonerPoll, factory);
const std::string location = socket->bind();
if (!location.length())
{
LOG_FTL("Failed to create local unix domain socket. Exiting.");
Util::forcedExit(EX_SOFTWARE);
return nullptr;
}
if (!socket->listen())
{
LOG_FTL("Failed to listen on local unix domain socket at " << location << ". Exiting.");
Util::forcedExit(EX_SOFTWARE);
}
LOG_INF("Listening to prisoner connections on " << location);
MasterLocation = location;
#ifndef HAVE_ABSTRACT_UNIX_SOCKETS
if(!socket->link(COOLWSD::SysTemplate + "/0" + MasterLocation))
{
LOG_FTL("Failed to hardlink local unix domain socket into a jail. Exiting.");
Util::forcedExit(EX_SOFTWARE);
}
#endif
#else
constexpr int DEFAULT_MASTER_PORT_NUMBER = 9981;
std::shared_ptr<ServerSocket> socket
= ServerSocket::create(ServerSocket::Type::Public, DEFAULT_MASTER_PORT_NUMBER,
ClientPortProto, *PrisonerPoll, factory);
COOLWSD::prisonerServerSocketFD = socket->getFD();
LOG_INF("Listening to prisoner connections on #" << COOLWSD::prisonerServerSocketFD);
#endif
return socket;
}
/// Create the externally listening public socket
std::shared_ptr<ServerSocket> findServerPort()
{
std::shared_ptr<SocketFactory> factory;
if (ClientPortNumber <= 0)
{
// Avoid using the default port for unit-tests altogether.
// This avoids interfering with a running test instance.
ClientPortNumber = DEFAULT_CLIENT_PORT_NUMBER + (UnitWSD::isUnitTesting() ? 1 : 0);
}
#if ENABLE_SSL
if (COOLWSD::isSSLEnabled())
factory = std::make_shared<SslSocketFactory>();
else
#endif
factory = std::make_shared<PlainSocketFactory>();
std::shared_ptr<ServerSocket> socket = ServerSocket::create(
ClientListenAddr, ClientPortNumber, ClientPortProto, *WebServerPoll, factory);
const int firstPortNumber = ClientPortNumber;
while (!socket &&
#ifdef BUILDING_TESTS
true
#else
UnitWSD::isUnitTesting()
#endif
)
{
++ClientPortNumber;
LOG_INF("Client port " << (ClientPortNumber - 1) << " is busy, trying "
<< ClientPortNumber);
socket = ServerSocket::create(ClientListenAddr, ClientPortNumber, ClientPortProto,
*WebServerPoll, factory);
}
if (!socket)
{
LOG_FTL("Failed to listen on Server port(s) (" << firstPortNumber << '-'
<< ClientPortNumber << "). Exiting");
Util::forcedExit(EX_SOFTWARE);
}
#if !MOBILEAPP
LOG_INF('#' << socket->getFD() << " Listening to client connections on port "
<< ClientPortNumber);
#else
LOG_INF("Listening to client connections on #" << socket->getFD());
#endif
return socket;
}
};
#if !MOBILEAPP
void COOLWSD::processFetchUpdate()
{
try
{
const std::string url(INFOBAR_URL);
if (url.empty())
return; // No url, nothing to do.
Poco::URI uriFetch(url);
uriFetch.addQueryParameter("product", config::getString("product_name", APP_NAME));
uriFetch.addQueryParameter("version", COOLWSD_VERSION);
LOG_TRC("Infobar update request from " << uriFetch.toString());
std::shared_ptr<http::Session> sessionFetch = StorageBase::getHttpSession(uriFetch);
if (!sessionFetch)
return;
http::Request request(uriFetch.getPathAndQuery());
request.add("Accept", "application/json");
const std::shared_ptr<const http::Response> httpResponse =
sessionFetch->syncRequest(request);
if (httpResponse->statusLine().statusCode() == http::StatusCode::OK)
{
LOG_DBG("Infobar update returned: " << httpResponse->getBody());
std::lock_guard<std::mutex> lock(COOLWSD::FetchUpdateMutex);
COOLWSD::LatestVersion = httpResponse->getBody();
}
else
LOG_WRN("Failed to update the infobar. Got: "
<< httpResponse->statusLine().statusCode() << ' '
<< httpResponse->statusLine().reasonPhrase());
}
catch(const Poco::Exception& exc)
{
LOG_DBG("FetchUpdate: " << exc.displayText());
}
catch(std::exception& exc)
{
LOG_DBG("FetchUpdate: " << exc.what());
}
catch(...)
{
LOG_DBG("FetchUpdate: Unknown exception");
}
}
#if ENABLE_DEBUG
std::string COOLWSD::getServerURL()
{
return getServiceURI(COOLWSD_TEST_COOL_UI);
}
#endif
#endif
int COOLWSD::innerMain()
2015-07-13 16:13:06 +02:00
{
#if !MOBILEAPP
# ifdef __linux__
// down-pay all the forkit linking cost once & early.
setenv("LD_BIND_NOW", "1", 1);
# endif
std::string version, hash;
Util::getVersionInfo(version, hash);
LOG_INF("Coolwsd version details: " << version << " - " << hash << " - id " << Util::getProcessIdentifier() << " - on " << Util::getLinuxVersion());
#endif
initializeSSL();
#if !MOBILEAPP
// Fetch remote settings from server if configured
RemoteConfigPoll remoteConfigThread(config());
remoteConfigThread.start();
#endif
#ifndef IOS
// We can open files with non-ASCII names just fine on iOS without this, and this code is
// heavily Linux-specific anyway.
// Force a uniform UTF-8 locale for ourselves & our children.
char* locale = std::setlocale(LC_ALL, "C.UTF-8");
if (!locale)
{
// rhbz#1590680 - C.UTF-8 is unsupported on RH7
LOG_WRN("Could not set locale to C.UTF-8, will try en_US.UTF-8");
locale = std::setlocale(LC_ALL, "en_US.UTF-8");
if (!locale)
LOG_WRN("Could not set locale to en_US.UTF-8. Without UTF-8 support documents with non-ASCII file names cannot be opened.");
}
if (locale)
{
LOG_INF("Locale is set to " + std::string(locale));
::setenv("LC_ALL", locale, 1);
}
#endif
#if !MOBILEAPP
// We use the same option set for both parent and child coolwsd,
// so must check options required in the parent (but not in the
// child) separately now. Also check for options that are
// meaningless for the parent.
if (LoTemplate.empty())
2016-10-14 11:57:27 +02:00
{
wsd: faster jail setup via bind-mount loolmount now works and supports mounting and unmounting, plus numerous improvements, refactoring, logging, etc.. When enabled, binding improves the jail setup time by anywhere from 2x to orders of magnitude (in docker, f.e.). A new config entry mount_jail_tree controls whether mounting is used or the old method of linking/copying of jail contents. It is set to true by default and falls back to linking/copying. A test mount is done when the setting is enabled, and if mounting fails, it's disabled to avoid noise. Temporarily disabled for unit-tests until we can cleanup lingering mounts after Jenkins aborts our build job. In a future patch we will have mount/jail cleanup as part of make. The network/system files in /etc that need frequent refreshing are now updated in systemplate to make their most recent version available in the jails. These files can change during the course of loolwsd lifetime, and are unlikely to be updated in systemplate after installation at all. We link to them in the systemplate/etc directory, and if that fails, we copy them before forking each kit instance to have the latest. This reworks the approach used to bind-mount the jails and the templates such that the total is now down to only three mounts: systemplate, lo, tmp. As now systemplate and lotemplate are shared, they must be mounted as readonly, this means that user/ must now be moved into tmp/user/ which is writable. The mount-points must be recursive, because we mount lo/ within the mount-point of systemplate (which is the root of the jail). But because we (re)bind recursively, and because both systemplate and lotemplate are mounted for each jails, we need to make them unbindable, so they wouldn't multiply the mount-points for each jails (an explosive growth!) Contrarywise, we don't want the mount-points to be shared, because we don't expect to add/remove mounts after a jail is created. The random temp directory is now created and set correctly, plus many logging and other improvements. Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829 Tested-by: Jenkins Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 15:02:58 +02:00
LOG_FTL("Missing --lo-template-path option");
throw MissingOptionException("lotemplate");
2016-10-14 11:57:27 +02:00
}
if (FileServerRoot.empty())
wsd: faster jail setup via bind-mount loolmount now works and supports mounting and unmounting, plus numerous improvements, refactoring, logging, etc.. When enabled, binding improves the jail setup time by anywhere from 2x to orders of magnitude (in docker, f.e.). A new config entry mount_jail_tree controls whether mounting is used or the old method of linking/copying of jail contents. It is set to true by default and falls back to linking/copying. A test mount is done when the setting is enabled, and if mounting fails, it's disabled to avoid noise. Temporarily disabled for unit-tests until we can cleanup lingering mounts after Jenkins aborts our build job. In a future patch we will have mount/jail cleanup as part of make. The network/system files in /etc that need frequent refreshing are now updated in systemplate to make their most recent version available in the jails. These files can change during the course of loolwsd lifetime, and are unlikely to be updated in systemplate after installation at all. We link to them in the systemplate/etc directory, and if that fails, we copy them before forking each kit instance to have the latest. This reworks the approach used to bind-mount the jails and the templates such that the total is now down to only three mounts: systemplate, lo, tmp. As now systemplate and lotemplate are shared, they must be mounted as readonly, this means that user/ must now be moved into tmp/user/ which is writable. The mount-points must be recursive, because we mount lo/ within the mount-point of systemplate (which is the root of the jail). But because we (re)bind recursively, and because both systemplate and lotemplate are mounted for each jails, we need to make them unbindable, so they wouldn't multiply the mount-points for each jails (an explosive growth!) Contrarywise, we don't want the mount-points to be shared, because we don't expect to add/remove mounts after a jail is created. The random temp directory is now created and set correctly, plus many logging and other improvements. Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829 Tested-by: Jenkins Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 15:02:58 +02:00
FileServerRoot = Util::getApplicationPath();
FileServerRoot = Poco::Path(FileServerRoot).absolute().toString();
LOG_DBG("FileServerRoot: " << FileServerRoot);
wsd: avoid static SocketPoll The lifetime management of static objects is extremely unpredictable and depends on many variables outside of our control or even reliable reproducibility. Complex static objects that own threads and other objects are doubly problematic because of their dependency and/or interaction with other objects. Here we replace the static DelayPoll instance with one we control its lifetime in the LOOLWSD main body, such that it is destroyed properly. Specifically, DelayPoll's dtor was accessing Poco's Logging subsystem out of order. That is, after Poco had been destroyed. Another advantage to this approach is that we don't even create the DelayPoll at all if we don't need it. The onus now is on the user of DelayPoll to make sure they create a Delay object with a long-enough lifetime to encompase it use. For completeness, here is the stacktrace: terminate called after throwing an instance of 'std::bad_alloc' what(): std::bad_alloc Program received signal SIGABRT, Aborted. __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51 51 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory. (gdb) bt #0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51 #1 0x00007ffff613f801 in __GI_abort () at abort.c:79 #2 0x00007ffff6d51957 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #3 0x00007ffff6d57ae6 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #4 0x00007ffff6d56b49 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #5 0x00007ffff6d574b8 in __gxx_personality_v0 () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #6 0x00007ffff671f573 in ?? () from /lib/x86_64-linux-gnu/libgcc_s.so.1 #7 0x00007ffff671fad1 in _Unwind_RaiseException () from /lib/x86_64-linux-gnu/libgcc_s.so.1 #8 0x00007ffff6d57d47 in __cxa_throw () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #9 0x00007ffff6d582dc in operator new(unsigned long) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #10 0x00005555556b5927 in void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_construct<char*>(char*, char*, std::forward_iterator_tag) [clone .isra.41] () #11 0x0000555555982a14 in Poco::Message::Message(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Poco::Message::Priority) () #12 0x000055555585a909 in SocketPoll::~SocketPoll (this=0x555555d10f60 <DelayPoll>, __in_chrg=<optimized out>) at net/Socket.cpp:145 #13 0x00007ffff6142041 in __run_exit_handlers (status=0, listp=0x7ffff64ea718 <__exit_funcs>, run_list_atexit=run_list_atexit@entry=true, run_dtors=run_dtors@entry=true) at exit.c:108 #14 0x00007ffff614213a in __GI_exit (status=<optimized out>) at exit.c:139 #15 0x0000555555752d78 in LOOLWSD::innerInitialize (this=<optimized out>, self=...) at wsd/LOOLWSD.cpp:1213 #16 0x0000555555788b07 in LOOLWSD::initialize (this=<optimized out>, self=...) at wsd/LOOLWSD.hpp:432 #17 0x00005555558dd7e4 in Poco::Util::Application::run() () #18 0x00005555556b6574 in main (argc=2, argv=0x7fffffffe528) at wsd/LOOLWSD.cpp:4276 Change-Id: Ifda55fe869fa6734b9c2490da4497d2551ac21c1 Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
2021-03-13 14:52:16 +01:00
LOG_DBG("Initializing DelaySocket with " << SimulatedLatencyMs << "ms.");
Delay delay(SimulatedLatencyMs);
const auto fetchUpdateCheck = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::hours(std::max(getConfigValue<int>("fetch_update_check", 10), 0)));
#endif
ClientRequestDispatcher::InitStaticFileContentCache();
// Allocate our port - passed to prisoners.
assert(Server && "The COOLWSDServer instance does not exist.");
Server->findClientPort();
TmpFontDir = ChildRoot + JailUtil::CHILDROOT_TMP_INCOMING_PATH;
// Start the internal prisoner server and spawn forkit,
// which in turn forks first child.
Server->startPrisoners();
// No need to "have at least one child" beforehand on mobile
#if !MOBILEAPP
if (!Util::isKitInProcess())
{
// Make sure we have at least one child before moving forward.
std::unique_lock<std::mutex> lock(NewChildrenMutex);
// If we are debugging, it's not uncommon to wait for several minutes before first
// child is born. Don't use an expiry timeout in that case.
const bool debugging = std::getenv("SLEEPFORDEBUGGER") || std::getenv("SLEEPKITFORDEBUGGER");
if (debugging)
{
LOG_DBG("Waiting for new child without timeout.");
NewChildrenCV.wait(lock, []() { return !NewChildren.empty(); });
}
else
{
int retry = (COOLWSD::NoCapsForKit ? 150 : 50);
const auto timeout = std::chrono::milliseconds(ChildSpawnTimeoutMs);
while (retry-- > 0 && !SigUtil::getShutdownRequestFlag())
{
LOG_INF("Waiting for a new child for a max of " << timeout);
if (NewChildrenCV.wait_for(lock, timeout, []() { return !NewChildren.empty(); }))
{
break;
}
}
}
// Check we have at least one.
LOG_TRC("Have " << NewChildren.size() << " new children.");
if (NewChildren.empty())
{
if (SigUtil::getShutdownRequestFlag())
LOG_FTL("Shutdown requested while starting up. Exiting.");
else
LOG_FTL("No child process could be created. Exiting.");
Util::forcedExit(EX_SOFTWARE);
}
assert(NewChildren.size() > 0);
}
if (LogLevel != "trace")
{
LOG_INF("WSD initialization complete: setting log-level to [" << LogLevel << "] as configured.");
Log::logger().setLevel(LogLevel);
}
if (Log::logger().getLevel() >= Poco::Message::Priority::PRIO_INFORMATION)
LOG_ERR("Log level is set very high to '" << LogLevel << "' this will have a "
"significant performance impact. Do not use this in production.");
// Start the remote font downloading polling thread.
std::unique_ptr<RemoteFontConfigPoll> remoteFontConfigThread;
try
{
// Fetch font settings from server if configured
remoteFontConfigThread = std::make_unique<RemoteFontConfigPoll>(config());
remoteFontConfigThread->start();
}
catch (const Poco::Exception&)
{
LOG_DBG("No remote_font_config");
}
#endif
// URI with /contents are public and we don't need to anonymize them.
Util::mapAnonymized("contents", "contents");
// Start the server.
Server->start();
#if WASMAPP
Make the temporary "WASM app" work Now I hope things are initialised in the right order and the plumbing gets set up so that messages are passed as expected. It seems to work most of the time. Main changes are: - The online WASM executable is built using the -s MODULARIZE -s EXPORT_NAME=createOnlineModule options. This means that the WASM runtime is not automatically initialized and the main() function is not automatically started. Only when the createOnlineModule() function is called is that done. Calling exported C/C++ functions is a little bit more complicated. - Code to actually Base64-encode strings to be executed as JavaScript when expected is now present in wasmapp.cpp. (After being passed through the Base64ToArrayBuffer function on the JS side.) Whether this is actually necessary is not fully clear, but to keep the code similar to that in the GTK, iOS, and Android apps, this is kept as such for now. It would probably work fine to just directly create the ArrayBuffer in the C++ (using the EM_ASM magic). - The COOLWSD::run() function is now run in a separate thread so that main() can return. - The FakeWebSocket's onopen() function is now called from innerMain(), where the HULLO message is sent. It remains a bit unclear if this really is the ideal place. In the mobile apps the HULLO message is sent and the onopen() function is called in the window.socket.onopen() function in global.js. But note that despite that the WASM app and the mobile apps are largely quite similarly constructed and the FakeSocket and FakeWebSocket plumbing is the same, there is an important difference. In a mobile app the C++ code is what runs first, and that then loads the HTML page into WebKit, in which the JS runs. In the WASM app it is the other way around. The web page is naturaly the one that is loaded and the JS code then starts running the C++ code as WASM. Finally, note that the whole concept that there is a separate "WASM app" is temporary. What we eventually want to achieve is that the COOL webpage upon loading will connect a COOL server. As it does currently. The COOL server runs the online and core C++ code to load a document, and renders document tiles and sends those to the client JS code to dispay. The new thing will be that, if enabled, in addition to the HTML and JS resources, the client will also download the WASM code and data resources. Also, the document and updates to it will be downloaded while being edited so that a copy can be kept in client memory. But the WASM code and the downloaded document will remain unused most of the time. Only if the connection to the COOL server breaks will the JS start running the WASM code and the JS will talk to online code running locally as WASM instead of to a COOL server. Obviously there are still lots of things hanging in the air here regarding how exactly this will work. Signed-off-by: Tor Lillqvist <tml@collabora.com> Change-Id: Ib1786a0b485d51797b0f2302d4296aa1ff9df5c1
2023-01-05 19:11:01 +01:00
// It is not at all obvious that this is the ideal place to do the HULLO thing and call onopen
// on TheFakeWebSocket. But it seems to work.
handle_cool_message("HULLO");
MAIN_THREAD_EM_ASM(window.TheFakeWebSocket.onopen());
#endif
/// The main-poll does next to nothing:
SocketPoll mainWait("main");
#if !MOBILEAPP
std::cerr << "Ready to accept connections on port " << ClientPortNumber << ".\n" << std::endl;
if (SignalParent)
{
kill(getppid(), SIGUSR2);
}
#endif
#if !MOBILEAPP && ENABLE_DEBUG
const std::string postMessageURI =
getServiceURI("/browser/dist/framed.doc.html?file_path=" DEBUG_ABSSRCDIR
"/" COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_WRITER);
std::ostringstream oss;
oss << "\nLaunch one of these in your browser:\n\n"
<< "Edit mode:" << '\n'
<< " Writer: " << getLaunchURI(COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_WRITER) << '\n'
<< " Calc: " << getLaunchURI(COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_CALC) << '\n'
<< " Impress: " << getLaunchURI(COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_IMPRESS) << '\n'
<< " Draw: " << getLaunchURI(COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_DRAW) << '\n'
<< "\nReadonly mode:" << '\n'
<< " Writer readonly: " << getLaunchURI(COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_WRITER, true) << '\n'
<< " Calc readonly: " << getLaunchURI(COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_CALC, true) << '\n'
<< " Impress readonly: " << getLaunchURI(COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_IMPRESS, true) << '\n'
<< " Draw readonly: " << getLaunchURI(COOLWSD_TEST_DOCUMENT_RELATIVE_PATH_DRAW, true) << '\n'
<< "\npostMessage: " << postMessageURI << std::endl;
const std::string adminURI = getServiceURI(COOLWSD_TEST_ADMIN_CONSOLE, true);
if (!adminURI.empty())
oss << "\nOr for the admin, monitoring, capabilities & discovery:\n\n"
<< adminURI << '\n'
<< getServiceURI(COOLWSD_TEST_METRICS, true) << '\n'
<< getServiceURI("/hosting/capabilities") << '\n'
<< getServiceURI("/hosting/discovery") << '\n';
oss << std::endl;
std::cerr << oss.str();
#endif
const auto startStamp = std::chrono::steady_clock::now();
#if !MOBILEAPP
auto stampFetch = startStamp - (fetchUpdateCheck - std::chrono::milliseconds(60000));
#ifdef __linux__
if (getConfigValue<bool>("stop_on_config_change", false)) {
std::shared_ptr<InotifySocket> inotifySocket = std::make_shared<InotifySocket>();
mainWait.insertNewSocket(inotifySocket);
}
#endif
#endif
while (!SigUtil::getShutdownRequestFlag())
{
// This timeout affects the recovery time of prespawned children.
std::chrono::microseconds waitMicroS = SocketPoll::DefaultPollTimeoutMicroS * 4;
if (UnitWSD::isUnitTesting() && !SigUtil::getShutdownRequestFlag())
{
UnitWSD::get().invokeTest();
// More frequent polling while testing, to reduce total test time.
waitMicroS =
std::min(UnitWSD::get().getTimeoutMilliSeconds(), std::chrono::milliseconds(1000));
waitMicroS /= 4;
}
mainWait.poll(waitMicroS);
// Wake the prisoner poll to spawn some children, if necessary.
PrisonerPoll->wakeup();
const auto timeNow = std::chrono::steady_clock::now();
const std::chrono::milliseconds timeSinceStartMs
= std::chrono::duration_cast<std::chrono::milliseconds>(timeNow - startStamp);
// Unit test timeout
if (UnitWSD::isUnitTesting() && !SigUtil::getShutdownRequestFlag())
{
UnitWSD::get().checkTimeout(timeSinceStartMs);
}
#if !MOBILEAPP
SavedClipboards->checkexpiry();
const std::chrono::milliseconds durationFetch
= std::chrono::duration_cast<std::chrono::milliseconds>(timeNow - stampFetch);
if (fetchUpdateCheck > std::chrono::milliseconds::zero() && durationFetch > fetchUpdateCheck)
{
processFetchUpdate();
stampFetch = timeNow;
}
#endif
#if ENABLE_DEBUG && !MOBILEAPP
if (careerSpanMs > std::chrono::milliseconds::zero() && timeSinceStartMs > careerSpanMs)
{
LOG_INF("Setting ShutdownRequestFlag: " << timeSinceStartMs << " gone, career of "
<< careerSpanMs << " expired.");
SigUtil::requestShutdown();
}
#endif
}
COOLWSD::alertAllUsersInternal("close: shuttingdown");
// Lots of polls will stop; stop watching them first.
SocketPoll::PollWatchdog.reset();
// Stop the listening to new connections
// and wait until sockets close.
LOG_INF("Stopping server socket listening. ShutdownRequestFlag: " <<
SigUtil::getShutdownRequestFlag() << ", TerminationFlag: " << SigUtil::getTerminationFlag());
if (!UnitWSD::isUnitTesting())
{
// When running unit-tests the listening port will
// get recycled and another test will be listening.
// This is very problematic if a DocBroker here is
// saving and uploading before shutting down, because
// the test that gets the same port will receive this
// unexpected upload and fail.
// Otherwise, in production, we should probably respond
// with some error that we are recycling. But for now,
// don't change the behavior and stop listening.
Server->stop();
}
// atexit handlers tend to free Admin before Documents
LOG_INF("Exiting. Cleaning up lingering documents.");
#if !MOBILEAPP
if (!SigUtil::getShutdownRequestFlag())
{
// This shouldn't happen, but it's fail safe to always cleanup properly.
LOG_WRN("Setting ShutdownRequestFlag: Exiting WSD without ShutdownRequestFlag. Setting it "
"now.");
SigUtil::requestShutdown();
}
#endif
// Wait until documents are saved and sessions closed.
// Don't stop the DocBroker, they will exit.
constexpr size_t sleepMs = 200;
constexpr size_t count = (COMMAND_TIMEOUT_MS * 6) / sleepMs;
for (size_t i = 0; i < count; ++i)
{
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
if (DocBrokers.empty())
break;
LOG_DBG("Waiting for " << DocBrokers.size() << " documents to stop.");
cleanupDocBrokers();
docBrokersLock.unlock();
// Give them time to save and cleanup.
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
}
if (UnitWSD::isUnitTesting() && !SigUtil::getTerminationFlag())
{
LOG_INF("Setting TerminationFlag to avoid deadlocking unittest.");
SigUtil::setTerminationFlag();
}
// Disable thread checking - we'll now cleanup lots of things if we can
Socket::InhibitThreadChecks = true;
SocketPoll::InhibitThreadChecks = true;
// Wait for the DocumentBrokers. They must be saving/uploading now.
// Do not stop them! Otherwise they might not save/upload the document.
// We block until they finish, or the service stopping times out.
{
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
for (auto& docBrokerIt : DocBrokers)
{
std::shared_ptr<DocumentBroker> docBroker = docBrokerIt.second;
if (docBroker && docBroker->isAlive())
{
LOG_DBG("Joining docBroker [" << docBrokerIt.first << "].");
docBroker->joinThread();
}
}
// Now should be safe to destroy what's left.
cleanupDocBrokers();
DocBrokers.clear();
}
if (TraceEventFile != NULL)
{
// If we have written any objects to it, it ends with a comma and newline. Back over those.
if (ftell(TraceEventFile) > 2)
(void)fseek(TraceEventFile, -2, SEEK_CUR);
// Close the JSON array.
fprintf(TraceEventFile, "\n]\n");
fclose(TraceEventFile);
TraceEventFile = NULL;
}
#if !MOBILEAPP
if (!Util::isKitInProcess())
{
// Terminate child processes
LOG_INF("Requesting forkit process " << ForKitProcId << " to terminate.");
#if CODE_COVERAGE || VALGRIND_COOLFORKIT
constexpr auto signal = SIGTERM;
#else
constexpr auto signal = SIGKILL;
#endif
SigUtil::killChild(ForKitProcId, signal);
}
#endif
Server->stopPrisoners();
if (UnitWSD::isUnitTesting())
{
Server->stop();
Server.reset();
}
PrisonerPoll.reset();
WebServerPoll.reset();
// Terminate child processes
LOG_INF("Requesting child processes to terminate.");
for (auto& child : NewChildren)
{
child->terminate();
}
NewChildren.clear();
#if !MOBILEAPP
if (!Util::isKitInProcess())
{
// Wait for forkit process finish.
LOG_INF("Waiting for forkit process to exit");
int status = 0;
waitpid(ForKitProcId, &status, WUNTRACED);
ForKitProcId = -1;
ForKitProc.reset();
}
JailUtil::cleanupJails(CleanupChildRoot);
#endif // !MOBILEAPP
const int returnValue = UnitBase::uninit();
LOG_INF("Process [coolwsd] finished with exit status: " << returnValue);
// At least on centos7, Poco deadlocks while
// cleaning up its SSL context singleton.
Util::forcedExit(returnValue);
return returnValue;
}
std::shared_ptr<TerminatingPoll> COOLWSD:: getWebServerPoll ()
{
return WebServerPoll;
}
void COOLWSD::cleanup()
{
try
{
Server.reset();
wsd: avoid static LOOLWSDServer instance LOOLWSDServer needs to shutdown its accept_poll SocketPoll in its destructor, which may be called after Poco's Logging subsystem has been destroyed. Instead of managing the lifetime of accept_poll member of LOOLWSDServer, it is safer to manage the lifetime of LOOLWSDServer itself, and destroy it in the cleanup stage. This makes sure that its other members, or indeed LOOLWSDServer itself, can't have any late-destoryed objects that can cause trouble. The stacktrace for this crash: terminate called after throwing an instance of 'std::bad_alloc' what(): std::bad_alloc Program received signal SIGABRT, Aborted. __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51 51 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory. (gdb) bt #0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51 #1 0x00007ffff613f801 in __GI_abort () at abort.c:79 #2 0x00007ffff6d51957 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #3 0x00007ffff6d57ae6 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #4 0x00007ffff6d56b49 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #5 0x00007ffff6d574b8 in __gxx_personality_v0 () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #6 0x00007ffff671f573 in ?? () from /lib/x86_64-linux-gnu/libgcc_s.so.1 #7 0x00007ffff671fad1 in _Unwind_RaiseException () from /lib/x86_64-linux-gnu/libgcc_s.so.1 #8 0x00007ffff6d57d47 in __cxa_throw () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #9 0x00007ffff6d582dc in operator new(unsigned long) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #10 0x00005555556b6127 in void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_construct<char*>(char*, char*, std::forward_iterator_tag) [clone .isra.41] () #11 0x0000555555986b94 in Poco::Message::Message(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Poco::Message::Priority) () #12 0x000055555585ea49 in SocketPoll::~SocketPoll (this=0x555555d13490 <srv+16>, __in_chrg=<optimized out>) at net/Socket.cpp:145 #13 0x000055555575d794 in TerminatingPoll::~TerminatingPoll (this=0x555555d13490 <srv+16>, __in_chrg=<optimized out>) at ./net/Socket.hpp:832 #14 LOOLWSDServer::AcceptPoll::~AcceptPoll (this=0x555555d13490 <srv+16>, __in_chrg=<optimized out>) at wsd/LOOLWSD.cpp:3766 #15 LOOLWSDServer::~LOOLWSDServer (this=0x555555d13480 <srv>, __in_chrg=<optimized out>) at wsd/LOOLWSD.cpp:3640 #16 0x00007ffff6142041 in __run_exit_handlers (status=0, listp=0x7ffff64ea718 <__exit_funcs>, run_list_atexit=run_list_atexit@entry=true, run_dtors=run_dtors@entry=true) at exit.c:108 #17 0x00007ffff614213a in __GI_exit (status=<optimized out>) at exit.c:139 #18 0x0000555555753658 in LOOLWSD::innerInitialize (this=<optimized out>, self=...) at wsd/LOOLWSD.cpp:1213 #19 0x0000555555789527 in LOOLWSD::initialize (this=<optimized out>, self=...) at wsd/LOOLWSD.hpp:439 #20 0x00005555558e1964 in Poco::Util::Application::run() () #21 0x00005555556b6d74 in main (argc=3, argv=0x7fffffffe4f8) at wsd/LOOLWSD.cpp:4286 Change-Id: I28ea6215ce49c752cbb90bc33269ab3b662accf1 Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
2021-03-13 19:25:10 +01:00
PrisonerPoll.reset();
WebServerPoll.reset();
#if !MOBILEAPP
SavedClipboards.reset();
FileRequestHandler.reset();
JWTAuth::cleanup();
#if ENABLE_SSL
// Finally, we no longer need SSL.
if (COOLWSD::isSSLEnabled())
{
Poco::Net::uninitializeSSL();
Poco::Crypto::uninitializeCrypto();
ssl::Manager::uninitializeClientContext();
ssl::Manager::uninitializeServerContext();
}
#endif
#endif
TraceDumper.reset();
Socket::InhibitThreadChecks = true;
SocketPoll::InhibitThreadChecks = true;
2020-01-22 08:52:55 +01:00
// Delete these while the static Admin instance is still alive.
std::lock_guard<std::mutex> docBrokersLock(DocBrokersMutex);
DocBrokers.clear();
}
catch (const std::exception& ex)
{
LOG_ERR("Failed to uninitialize: " << ex.what());
}
}
int COOLWSD::main(const std::vector<std::string>& /*args*/)
{
#if MOBILEAPP && !defined IOS
SigUtil::resetTerminationFlags();
#endif
int returnValue;
try {
returnValue = innerMain();
}
catch (const std::exception& e)
{
LOG_FTL("Exception: " << e.what());
cleanup();
throw;
} catch (...) {
cleanup();
throw;
}
cleanup();
returnValue = UnitBase::uninit();
LOG_INF("Process [coolwsd] finished with exit status: " << returnValue);
#if CODE_COVERAGE
__gcov_dump();
#endif
return returnValue;
}
int COOLWSD::getClientPortNumber()
{
return ClientPortNumber;
}
#if !MOBILEAPP
std::vector<std::shared_ptr<DocumentBroker>> COOLWSD::getBrokersTestOnly()
{
std::lock_guard<std::mutex> docBrokersLock(DocBrokersMutex);
std::vector<std::shared_ptr<DocumentBroker>> result;
result.reserve(DocBrokers.size());
for (auto& brokerIt : DocBrokers)
result.push_back(brokerIt.second);
return result;
}
std::set<pid_t> COOLWSD::getKitPids()
{
std::set<pid_t> pids = getSpareKitPids();
pids.merge(getDocKitPids());
return pids;
}
std::set<pid_t> COOLWSD::getSpareKitPids()
{
std::set<pid_t> pids;
pid_t pid;
{
std::unique_lock<std::mutex> lock(NewChildrenMutex);
for (const auto &child : NewChildren)
{
pid = child->getPid();
if (pid > 0)
pids.emplace(pid);
}
}
return pids;
}
std::set<pid_t> COOLWSD::getDocKitPids()
{
std::set<pid_t> pids;
pid_t pid;
{
std::unique_lock<std::mutex> lock(DocBrokersMutex);
for (const auto &it : DocBrokers)
{
pid = it.second->getPid();
if (pid > 0)
pids.emplace(pid);
}
}
return pids;
}
#if !defined(BUILDING_TESTS)
namespace Util
{
void alertAllUsers(const std::string& cmd, const std::string& kind)
{
alertAllUsers("error: cmd=" + cmd + " kind=" + kind);
}
void alertAllUsers(const std::string& msg)
{
COOLWSD::alertAllUsersInternal(msg);
}
}
#endif
#endif
void dump_state()
{
std::ostringstream oss;
wsd: avoid static LOOLWSDServer instance LOOLWSDServer needs to shutdown its accept_poll SocketPoll in its destructor, which may be called after Poco's Logging subsystem has been destroyed. Instead of managing the lifetime of accept_poll member of LOOLWSDServer, it is safer to manage the lifetime of LOOLWSDServer itself, and destroy it in the cleanup stage. This makes sure that its other members, or indeed LOOLWSDServer itself, can't have any late-destoryed objects that can cause trouble. The stacktrace for this crash: terminate called after throwing an instance of 'std::bad_alloc' what(): std::bad_alloc Program received signal SIGABRT, Aborted. __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51 51 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory. (gdb) bt #0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51 #1 0x00007ffff613f801 in __GI_abort () at abort.c:79 #2 0x00007ffff6d51957 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #3 0x00007ffff6d57ae6 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #4 0x00007ffff6d56b49 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #5 0x00007ffff6d574b8 in __gxx_personality_v0 () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #6 0x00007ffff671f573 in ?? () from /lib/x86_64-linux-gnu/libgcc_s.so.1 #7 0x00007ffff671fad1 in _Unwind_RaiseException () from /lib/x86_64-linux-gnu/libgcc_s.so.1 #8 0x00007ffff6d57d47 in __cxa_throw () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #9 0x00007ffff6d582dc in operator new(unsigned long) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #10 0x00005555556b6127 in void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_construct<char*>(char*, char*, std::forward_iterator_tag) [clone .isra.41] () #11 0x0000555555986b94 in Poco::Message::Message(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Poco::Message::Priority) () #12 0x000055555585ea49 in SocketPoll::~SocketPoll (this=0x555555d13490 <srv+16>, __in_chrg=<optimized out>) at net/Socket.cpp:145 #13 0x000055555575d794 in TerminatingPoll::~TerminatingPoll (this=0x555555d13490 <srv+16>, __in_chrg=<optimized out>) at ./net/Socket.hpp:832 #14 LOOLWSDServer::AcceptPoll::~AcceptPoll (this=0x555555d13490 <srv+16>, __in_chrg=<optimized out>) at wsd/LOOLWSD.cpp:3766 #15 LOOLWSDServer::~LOOLWSDServer (this=0x555555d13480 <srv>, __in_chrg=<optimized out>) at wsd/LOOLWSD.cpp:3640 #16 0x00007ffff6142041 in __run_exit_handlers (status=0, listp=0x7ffff64ea718 <__exit_funcs>, run_list_atexit=run_list_atexit@entry=true, run_dtors=run_dtors@entry=true) at exit.c:108 #17 0x00007ffff614213a in __GI_exit (status=<optimized out>) at exit.c:139 #18 0x0000555555753658 in LOOLWSD::innerInitialize (this=<optimized out>, self=...) at wsd/LOOLWSD.cpp:1213 #19 0x0000555555789527 in LOOLWSD::initialize (this=<optimized out>, self=...) at wsd/LOOLWSD.hpp:439 #20 0x00005555558e1964 in Poco::Util::Application::run() () #21 0x00005555556b6d74 in main (argc=3, argv=0x7fffffffe4f8) at wsd/LOOLWSD.cpp:4286 Change-Id: I28ea6215ce49c752cbb90bc33269ab3b662accf1 Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
2021-03-13 19:25:10 +01:00
if (Server)
Server->dumpState(oss);
const std::string msg = oss.str();
fprintf(stderr, "%s\n", msg.c_str());
LOG_TRC(msg);
}
void forwardSigUsr2()
{
LOG_TRC("forwardSigUsr2");
if (Util::isKitInProcess())
return;
Util::assertIsLocked(DocBrokersMutex);
std::lock_guard<std::mutex> newChildLock(NewChildrenMutex);
#if !MOBILEAPP
if (COOLWSD::ForKitProcId > 0)
{
LOG_INF("Sending SIGUSR2 to forkit " << COOLWSD::ForKitProcId);
::kill(COOLWSD::ForKitProcId, SIGUSR2);
}
#endif
for (const auto& child : NewChildren)
{
if (child && child->getPid() > 0)
{
LOG_INF("Sending SIGUSR2 to child " << child->getPid());
::kill(child->getPid(), SIGUSR2);
}
}
for (const auto& pair : DocBrokers)
{
std::shared_ptr<DocumentBroker> docBroker = pair.second;
if (docBroker)
{
LOG_INF("Sending SIGUSR2 to docBroker " << docBroker->getPid());
::kill(docBroker->getPid(), SIGUSR2);
}
}
}
Add an initial libfuzzer based fuzzer - target ClientSession::_handleInput(), since crashing there would bring down the whole loolwsd (not just a kit process), and it deals with input from untrusted users (browsers) - add a --enable-fuzzers configure switch to build with -fsanitize=fuzzer (compared to normal sanitizers build, this is the only special flag needed) - configuring other sanitizers is not done automatically, either use --with-sanitizer=... or the environment variables from LODE's sanitizer config - run the actual fuzzer like this: ./clientsession_fuzzer -max_len=16384 fuzzer/data/ - note that at least openSUSE Leap 15.1 sadly ships with a clang with libfuzzer static libs removed from the package, so you need a self-built clang to run the fuzzer (either manual build or one from LODE) - <https://chromium.googlesource.com/chromium/src/testing/libfuzzer/+/refs/heads/master/efficient_fuzzing.md#execution-speed> suggests that "You should aim for at least 1,000 exec/s from your fuzz target locally" (i.e. one run should not take more than 1 ms), so try this minimal approach first. The alternative would be to start from the existing loolwsd_fuzzer binary, then step by step cut it down to not fork(), not do any network traffic, etc -- till it's fast enough that the fuzzer can find interesting input - the various configurations start to be really complex (the matrix is just very large), so try to use Util::isFuzzing() for fuzzer-specific changes (this is what core.git does as well), and only resort to ifdefs for the Util::isFuzzing() itself Change-Id: I72dc1193b34c93eacb5d8e39cef42387d42bd72f Reviewed-on: https://gerrit.libreoffice.org/c/online/+/89226 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Michael Meeks <michael.meeks@collabora.com>
2020-02-21 15:52:20 +01:00
// Avoid this in the Util::isFuzzing() case because libfuzzer defines its own main().
#if !MOBILEAPP && !LIBFUZZER
int main(int argc, char** argv)
{
SigUtil::setUserSignals();
SigUtil::setFatalSignals("wsd " COOLWSD_VERSION " " COOLWSD_VERSION_HASH);
setKitInProcess();
try
{
COOLWSD app;
return app.run(argc, argv);
}
catch (Poco::Exception& exc)
{
std::cerr << exc.displayText() << std::endl;
return EX_SOFTWARE;
}
}
#endif
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */