/* -*- 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 #include #include "COOLWSD.hpp" #if ENABLE_FEATURE_LOCK #include "CommandControl.hpp" #endif #if !MOBILEAPP #include #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 // number of child processes, each which handles a viewing (editing) session for one document. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if !MOBILEAPP #include #include #include #include #include #include #include #include #include #include #include #include #include #include using Poco::Net::PartHandler; #include #include #include #include #include "Admin.hpp" #include "Auth.hpp" #include "FileServer.hpp" #include "UserMessages.hpp" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ClientSession.hpp" #include #include #include #include #include #include "DocumentBroker.hpp" #include #include #include #include #if MOBILEAPP # include #endif #include #include #include #include #if ENABLE_SSL # include #endif #include "Storage.hpp" #include "TraceFile.hpp" #include #include #include #include #include #include #include #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 #endif #endif using namespace COOLProtocol; using Poco::DirectoryIterator; 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 > NewChildren; static std::chrono::steady_clock::time_point LastForkRequestTime = std::chrono::steady_clock::now(); static std::atomic OutstandingForks(0); std::map> DocBrokers; std::mutex DocBrokersMutex; static Poco::AutoPtr 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 COOLWSD::NumConnections; std::unordered_set COOLWSD::EditFileExtensions; std::unordered_set COOLWSD::ViewWithCommentsFileExtensions; #if MOBILEAPP // Or can this be retrieved in some other way? int COOLWSD::prisonerServerSocketFD; #else /// Funky latency simulation basic delay (ms) static std::size_t SimulatedLatencyMs = 0; #endif void COOLWSD::appendAllowedHostsFrom(LayeredConfiguration& conf, const std::string& root, std::vector& 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(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& 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(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 docBrokersLock(DocBrokersMutex); LOG_INF("Alerting all users: [" << msg << ']'); if (UnitWSD::get().filterAlertAllusers(msg)) return; for (auto& brokerIt : DocBrokers) { std::shared_ptr 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 docBrokersLock(DocBrokersMutex); LOG_INF("Alerting document users with dockey: [" << dockey << ']' << " msg: [" << msg << ']'); for (auto& brokerIt : DocBrokers) { std::shared_ptr 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 lock(traceEventFileMutex); fwrite(data, nbytes, 1, COOLWSD::TraceEventFile); } void COOLWSD::writeTraceEventRecording(const std::string &recording) { writeTraceEventRecording(recording.data(), recording.length()); } void COOLWSD::checkSessionLimitsAndWarnClients() { #if !MOBILEAPP if (config::isSupportKeyEnabled()) return; ssize_t docBrokerCount = DocBrokers.size() - ConvertToBroker::getInstanceCount(); if (COOLWSD::MaxDocuments < 10000 && (docBrokerCount > static_cast(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 } 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 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(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"); // Do the cleanup first. const bool rebalance = cleanupChildren(); const auto duration = (std::chrono::steady_clock::now() - LastForkRequestTime); const auto durationMs = std::chrono::duration_cast(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 lock(NewChildrenMutex, std::defer_lock); return lock.try_lock() && (rebalanceChildren(COOLWSD::NumPreSpawnedChildren) > 0); } #endif static size_t addNewChild(std::shared_ptr child) { assert(child && "Adding null child"); const auto pid = child->getPid(); std::unique_lock 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("admin_console.username", ""); auto passwd = COOLWSD::getConfigValue("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 COOLWSD::NextConnectionId(1); #if !MOBILEAPP std::atomic COOLWSD::ForKitProcId(-1); std::shared_ptr 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 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 COOLWSD::SSLEnabled; Util::RuntimeConstant COOLWSD::SSLTermination; #endif unsigned COOLWSD::MaxConnections; unsigned COOLWSD::MaxDocuments; std::string COOLWSD::OverrideWatermark; std::set COOLWSD::PluginConfigurations; std::chrono::steady_clock::time_point COOLWSD::StartTime; bool COOLWSD::IsBindMountingEnabled = true; // If you add global state please update dumpState below too static std::string UnitTestLibrary; unsigned int COOLWSD::NumPreSpawnedChildren = 0; std::unique_ptr COOLWSD::TraceDumper; #if !MOBILEAPP std::unique_ptr COOLWSD::SavedClipboards; /// The file request handler used for file-serving. std::unique_ptr 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 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& 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 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 forKitProc = _forKitProc.lock(); if (forKitProc) { forKitProc->sendTextFrame(msg); } }); } } private: std::weak_ptr _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 PrisonerPoll; #if MOBILEAPP #ifndef IOS std::mutex COOLWSD::lokit_main_mutex; #endif #endif std::shared_ptr getNewChild_Blocks(SocketPoll &destPoll, unsigned mobileAppDocId) { (void)mobileAppDocId; const auto startTime = std::chrono::steady_clock::now(); std::unique_lock 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 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 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::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 Server; /// Helper class to hold default configuration entries. class AppConfigMap final : public Poco::Util::MapConfiguration { public: AppConfigMap(const std::map& map) { for (const auto& pair : map) { setRaw(pair.first, pair.second); } } void reset(const std::map& map) { clear(); for (const auto& pair : map) { setRaw(pair.first, pair.second); } } }; #if !MOBILEAPP void ForKitProcWSHandler::handleMessage(const std::vector &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 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 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{}); _conf.addWriteable(_persistConfig, PRIO_JSON); } virtual ~RemoteConfigPoll() { } void handleJSON(const Poco::JSON::Object::Ptr& remoteJson) override { std::map 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> 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& 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& 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& 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& 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& 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& 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& 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 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 _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 httpSession(StorageBase::getHttpSession(fontUri)); http::Request request(fontUri.getPathAndQuery()); request.set("User-Agent", http::getAgentString()); const std::shared_ptr 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 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 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 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 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 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 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."); } 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 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 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 overrideConfig(new AppConfigMap(_overrideSettings)); conf.addWriteable(overrideConfig, PRIO_APPLICATION); // Highest priority if (!UnitTestLibrary.empty()) { UnitWSD::defaultConfigure(conf); } // Experimental features. EnableExperimental = getConfigValue(conf, "experimental_features", false); EnableAccessibility = getConfigValue(conf, "accessibility.enable", false); // Setup user interface mode UserInterface = getConfigValue(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(conf, "logging.level", "trace"); MostVerboseLogLevelSettableFromClient = getConfigValue(conf, "logging.most_verbose_level_settable_from_client", "notice"); LeastVerboseLogLevelSettableFromClient = getConfigValue(conf, "logging.least_verbose_level_settable_from_client", "fatal"); setenv("COOL_LOGLEVEL", LogLevel.c_str(), true); #if !ENABLE_DEBUG const std::string salLog = getConfigValue(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(conf, "logging.color", true) && isatty(fileno(stderr)); if (withColor) { setenv("COOL_LOGCOLOR", "1", true); } const auto logToFile = getConfigValue(conf, "logging.file[@enable]", false); std::map 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]", ""); if (!confName.empty()) { const std::string value = config().getString(confPath, ""); 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(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(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(conf, "trace_event[@enable]", false); if (EnableTraceEventLogging) { const auto traceEventFile = getConfigValue(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(conf, "wasm.enable", false) ? COOLWSD::WASMActivationState::Enabled : COOLWSD::WASMActivationState::Disabled; if (getConfigValue(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(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(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("security.enable_websocket_urp", false); setenv("ENABLE_WEBSOCKET_URP", enableWebsocketURP ? "true" : "false", 1); } { std::string proto = getConfigValue(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(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(conf, "net.proxy_prefix", false); #if ENABLE_SSL COOLWSD::SSLEnabled.set(getConfigValue(conf, "ssl.enable", true)); COOLWSD::SSLTermination.set(getConfigValue(conf, "ssl.termination", true)); #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(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"); if (SysTemplate.empty()) { LOG_FTL("Missing sys_template_path config entry."); throw MissingOptionException("systemplate"); } ChildRoot = getPathFromConfig("child_root_path"); 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 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); } #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(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 // 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(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(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 << '.'); FileUtil::registerFileSystemForDiskSpaceChecks(ChildRoot); int nThreads = std::max(std::thread::hardware_concurrency(), 1); int maxConcurrency = getConfigValue(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(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(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(conf, "security.seccomp", true); NoCapsForKit = Util::isKitInProcess() || !getConfigValue(conf, "security.capabilities", true); AdminEnabled = getConfigValue(conf, "admin_console.enable", true); #if ENABLE_DEBUG if (Util::isKitInProcess()) SingleKit = true; #endif #endif // LanguageTool configuration bool enableLanguageTool = getConfigValue(conf, "languagetool.enabled", false); setenv("LANGUAGETOOL_ENABLED", enableLanguageTool ? "true" : "false", 1); const std::string baseAPIUrl = getConfigValue(conf, "languagetool.base_url", ""); setenv("LANGUAGETOOL_BASEURL", baseAPIUrl.c_str(), 1); const std::string userName = getConfigValue(conf, "languagetool.user_name", ""); setenv("LANGUAGETOOL_USERNAME", userName.c_str(), 1); const std::string apiKey = getConfigValue(conf, "languagetool.api_key", ""); setenv("LANGUAGETOOL_APIKEY", apiKey.c_str(), 1); bool sslVerification = getConfigValue(conf, "languagetool.ssl_verification", true); setenv("LANGUAGETOOL_SSL_VERIFICATION", sslVerification ? "true" : "false", 1); const std::string restProtocol = getConfigValue(conf, "languagetool.rest_protocol", ""); setenv("LANGUAGETOOL_RESTPROTOCOL", restProtocol.c_str(), 1); // DeepL configuration const std::string apiURL = getConfigValue(conf, "deepl.api_url", ""); const std::string authKey = getConfigValue(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(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 (config::isSupportKeyEnabled()) { const std::string supportKeyString = getConfigValue(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(); } } } } 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(conf, "trace[@enable]", false)) { const auto& path = getConfigValue(conf, "trace.path", ""); const auto recordOutgoing = getConfigValue(conf, "trace.outgoing.record", false); std::vector 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(conf, "trace.path[@compress]", false); const auto takeSnapshot = getConfigValue(conf, "trace.path[@snapshot]", false); TraceDumper = std::make_unique(path, recordOutgoing, compress, takeSnapshot, filters); } // Allowed hosts for being external data source in the documents std::vector 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(); LOG_TRC("Initialize FileServerRequestHandler"); COOLWSD::FileRequestHandler = std::make_unique(COOLWSD::FileServerRoot); #endif WebServerPoll = std::make_unique("websrv_poll"); PrisonerPoll = std::make_unique(); Server = std::make_unique(); 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("per_document.limit_virt_mem_mb", 0)); docProcSettings.setLimitStackMemKb(getConfigValue("per_document.limit_stack_mem_kb", 0)); docProcSettings.setLimitFileSizeMb(getConfigValue("per_document.limit_file_size_mb", 0)); docProcSettings.setLimitNumberOpenFiles(getConfigValue("per_document.limit_num_open_files", 0)); DocCleanupSettings &docCleanupSettings = docProcSettings.getCleanupSettings(); docCleanupSettings.setEnable(getConfigValue("per_document.cleanup[@enable]", false)); docCleanupSettings.setCleanupInterval(getConfigValue("per_document.cleanup.cleanup_interval_ms", 10000)); docCleanupSettings.setBadBehaviorPeriod(getConfigValue("per_document.cleanup.bad_behavior_period_secs", 60)); docCleanupSettings.setIdleTime(getConfigValue("per_document.cleanup.idle_time_secs", 300)); docCleanupSettings.setLimitDirtyMem(getConfigValue("per_document.cleanup.limit_dirty_mem_mb", 3072)); docCleanupSettings.setLimitCpu(getConfigValue("per_document.cleanup.limit_cpu_per", 85)); docCleanupSettings.setLostKitGracePeriod(getConfigValue("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; ServerApplication::defineOptions(optionSet); 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) + "),") .required(false) .repeatable(false) .argument("port_number")); 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) { #if !MOBILEAPP ServerApplication::handleOption(optionName, value); if (optionName == "help") { displayHelp(); Util::forcedExit(EX_OK); } 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. 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 } 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; } #if !MOBILEAPP void COOLWSD::displayHelp() { HelpFormatter helpFormatter(options()); helpFormatter.setCommand(commandName()); helpFormatter.setUsage("OPTIONS"); helpFormatter.setHeader("Collabora Online WebSocket server."); 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 docBrokersLock(DocBrokersMutex); auto docBrokerIt = DocBrokers.find(docKey); if (docBrokerIt != DocBrokers.end()) { std::shared_ptr docBroker = docBrokerIt->second; docBroker->addCallback([docBroker, message]() { docBroker->closeDocument(message); }); } } void COOLWSD::autoSave(const std::string& docKey) { std::unique_lock docBrokersLock(DocBrokersMutex); auto docBrokerIt = DocBrokers.find(docKey); if (docBrokerIt != DocBrokers.end()) { std::shared_ptr docBroker = docBrokerIt->second; docBroker->addCallback( [docBroker]() { docBroker->autoSave(/*force=*/true, /*dontSaveIfUnmodified=*/true); }); } } void COOLWSD::setLogLevelsOfKits(const std::string& level) { std::lock_guard docBrokersLock(DocBrokersMutex); LOG_INF("Changing kits' log levels: [" << level << ']'); for (const auto& brokerIt : DocBrokers) { std::shared_ptr 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 docBrokersLock(DocBrokersMutex, std::defer_lock); if (docBrokersLock.try_lock()) { cleanupDocBrokers(); SigUtil::checkForwardSigUsr2(forwardSigUsr2); } } #if !MOBILEAPP bool COOLWSD::createForKit() { LOG_INF("Creating new forkit process."); // Creating a new forkit is always a slow process. ChildSpawnTimeoutMs = CHILD_TIMEOUT_MS * 4; std::unique_lock 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_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 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; } void COOLWSD::sendMessageToForKit(const std::string& message) { if (PrisonerPoll) { PrisonerPoll->sendMessageToForKit(message); } } #endif // !MOBILEAPP /// Handles the socket that the prisoner kit connected to WSD on. class PrisonerRequestDispatcher final : public WebSocketHandler { std::weak_ptr _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 child = _childProcess.lock(); if (child && child->getPid() > 0) onDisconnect(); } private: /// Keep our socket around ... void onConnect(const std::shared_ptr& 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 child = _childProcess.lock(); std::shared_ptr 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 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 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(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(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, for NewChildren?"); auto child = std::make_shared(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 &data) override { if (UnitWSD::isUnitTesting() && UnitWSD::get().filterChildMessage(data)) return; auto message = std::make_shared(data.data(), data.size(), Message::Dir::Out); std::shared_ptr 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 child = _childProcess.lock(); std::shared_ptr 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 create(const int physicalFd, Socket::Type type) override { int fd = physicalFd; #if !MOBILEAPP 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( std::string(), fd, type, false, std::make_shared()); } }; #if ENABLE_SSL class SslSocketFactory final : public SocketFactory { std::shared_ptr create(const int physicalFd, Socket::Type type) override { int fd = physicalFd; #if !MOBILEAPP if (SimulatedLatencyMs > 0) fd = Delay::create(SimulatedLatencyMs, physicalFd); #endif return StreamSocket::create(std::string(), fd, type, false, std::make_shared()); } }; #endif class PrisonerSocketFactory final : public SocketFactory { std::shared_ptr create(const int fd, Socket::Type type) override { // No local delay. return StreamSocket::create(std::string(), fd, type, false, std::make_shared(), 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; 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); os << "Prisoner poll:\n"; PrisonerPoll->dumpState(os); #if !MOBILEAPP os << "Admin poll:\n"; _admin.dumpState(os); // If we have any delaying work going on. Delay::dumpState(os); COOLWSD::SavedClipboards->dumpState(os); #endif os << "Document Broker polls " << "[ " << DocBrokers.size() << " ]:\n"; 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 findPrisonerServerPort() { std::shared_ptr factory = std::make_shared(); #if !MOBILEAPP auto socket = std::make_shared(*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 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 findServerPort() { std::shared_ptr 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(); else #endif factory = std::make_shared(); std::shared_ptr 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 sessionFetch = StorageBase::getHttpSession(uriFetch); if (!sessionFetch) return; http::Request request(uriFetch.getPathAndQuery()); request.add("Accept", "application/json"); const std::shared_ptr httpResponse = sessionFetch->syncRequest(request); if (httpResponse->statusLine().statusCode() == http::StatusCode::OK) { LOG_DBG("Infobar update returned: " << httpResponse->getBody()); std::lock_guard 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() { #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()) { LOG_FTL("Missing --lo-template-path option"); throw MissingOptionException("lotemplate"); } if (FileServerRoot.empty()) FileServerRoot = Util::getApplicationPath(); FileServerRoot = Poco::Path(FileServerRoot).absolute().toString(); LOG_DBG("FileServerRoot: " << FileServerRoot); LOG_DBG("Initializing DelaySocket with " << SimulatedLatencyMs << "ms."); Delay delay(SimulatedLatencyMs); const auto fetchUpdateCheck = std::chrono::duration_cast( std::chrono::hours(std::max(getConfigValue("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 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 remoteFontConfigThread; try { // Fetch font settings from server if configured remoteFontConfigThread = std::make_unique(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 // 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("stop_on_config_change", false)) { std::shared_ptr inotifySocket = std::make_shared(); 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(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(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 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 docBrokersLock(DocBrokersMutex); for (auto& docBrokerIt : DocBrokers) { std::shared_ptr 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 COOLWSD:: getWebServerPoll () { return WebServerPoll; } void COOLWSD::cleanup() { try { Server.reset(); 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; // Delete these while the static Admin instance is still alive. std::lock_guard docBrokersLock(DocBrokersMutex); DocBrokers.clear(); } catch (const std::exception& ex) { LOG_ERR("Failed to uninitialize: " << ex.what()); } } int COOLWSD::main(const std::vector& /*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> COOLWSD::getBrokersTestOnly() { std::lock_guard docBrokersLock(DocBrokersMutex); std::vector> result; result.reserve(DocBrokers.size()); for (auto& brokerIt : DocBrokers) result.push_back(brokerIt.second); return result; } std::set COOLWSD::getKitPids() { std::set pids = getSpareKitPids(); pids.merge(getDocKitPids()); return pids; } std::set COOLWSD::getSpareKitPids() { std::set pids; pid_t pid; { std::unique_lock lock(NewChildrenMutex); for (const auto &child : NewChildren) { pid = child->getPid(); if (pid > 0) pids.emplace(pid); } } return pids; } std::set COOLWSD::getDocKitPids() { std::set pids; pid_t pid; { std::unique_lock 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; 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 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 docBroker = pair.second; if (docBroker) { LOG_INF("Sending SIGUSR2 to docBroker " << docBroker->getPid()); ::kill(docBroker->getPid(), SIGUSR2); } } } // 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: */