libreoffice-online/kit/Kit.cpp
Caolán McNamara b65ded3f6b Allow Namespace use even if BindMounting failed
For Namespaces the parent uses e.g.

jails/195178-46f603be/tmp/cool-82ChWNp1XiISVSpV/

to pass files to what is normally the child's (hidden) bind mount of that as

jails/195178-46f603be/82ChWNp1XiISVSpV/tmp

If BindMounting fails and we fallback to linking, then symlink
jails/195178-46f603be/tmp/cool-82ChWNp1XiISVSpV/ to the fallback
jails/195178-46f603be/82ChWNp1XiISVSpV/tmp so the parent can still
use the path expected for Namespaces.

So we can still get the goodness of namespaces wrt capabilities even
if we have the ills of bind-mounting fail.

Signed-off-by: Caolán McNamara <caolan.mcnamara@collabora.com>
Change-Id: I9a6f9e8db8f6edced18fe20367afc4d599a29ed2
2024-07-17 11:19:11 +02:00

3773 lines
126 KiB
C++

/* -*- 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/.
*/
/*
* The main entry point for the LibreOfficeKit process serving
* a document editing session.
*/
#include <config.h>
#include <config_version.h>
#include <dlfcn.h>
#include <limits>
#ifdef __linux__
#include <ftw.h>
#include <sys/vfs.h>
#include <linux/magic.h>
#include <sys/capability.h>
#include <sys/sysmacros.h>
#endif
#ifdef __FreeBSD__
#include <ftw.h>
#define FTW_CONTINUE 0
#define FTW_STOP (-1)
#define FTW_SKIP_SUBTREE 0
#define FTW_ACTIONRETVAL 0
#endif
#include <unistd.h>
#include <utime.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sysexits.h>
#include <atomic>
#include <cassert>
#include <climits>
#include <condition_variable>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <string>
#include <sstream>
#include <thread>
#include <mutex>
#define LOK_USE_UNSTABLE_API
#include <LibreOfficeKit/LibreOfficeKitInit.h>
#include <Poco/File.h>
#include <Poco/Exception.h>
#include <Poco/URI.h>
#include "ChildSession.hpp"
#include <Common.hpp>
#include <MobileApp.hpp>
#include <FileUtil.hpp>
#include <common/JailUtil.hpp>
#include <common/JsonUtil.hpp>
#include "KitHelper.hpp"
#include "Kit.hpp"
#include <Protocol.hpp>
#include <Log.hpp>
#include <Png.hpp>
#include <Rectangle.hpp>
#include <Unit.hpp>
#include <UserMessages.hpp>
#include <Util.hpp>
#include <JsonUtil.hpp>
#include "Watermark.hpp"
#include "RenderTiles.hpp"
#include "KitWebSocket.hpp"
#include "SetupKitEnvironment.hpp"
#include <common/ConfigUtil.hpp>
#include <common/TraceEvent.hpp>
#include <common/Watchdog.hpp>
#if !MOBILEAPP
#include <common/security.h>
#include <common/SigUtil.hpp>
#include <common/Seccomp.hpp>
#include <utility>
#endif
#if MOBILEAPP
#include "COOLWSD.hpp"
#endif
#ifdef IOS
#include "ios.h"
#endif
#define LIB_SOFFICEAPP "lib" "sofficeapp" ".so"
#define LIB_MERGED "lib" "mergedlo" ".so"
using Poco::Exception;
using Poco::File;
using Poco::JSON::Object;
using Poco::JSON::Parser;
using Poco::URI;
#ifndef BUILDING_TESTS
using Poco::Path;
#endif
using namespace COOLProtocol;
extern "C" { void dump_kit_state(void); /* easy for gdb */ }
#if !MOBILEAPP
// A Kit process hosts only a single document in its lifetime.
class Document;
static Document *singletonDocument = nullptr;
static std::unique_ptr<Util::ThreadCounter> threadCounter;
static std::unique_ptr<Util::FDCounter> fdCounter;
int getCurrentThreadCount()
{
if (threadCounter)
return threadCounter->count();
else
return -1;
}
#endif
_LibreOfficeKit* loKitPtr = nullptr;
/// Used for test code to accelerating waiting until idle and to
/// flush sockets with a 'processtoidle' -> 'idle' reply.
static std::chrono::steady_clock::time_point ProcessToIdleDeadline;
#ifndef BUILDING_TESTS
static bool AnonymizeUserData = false;
static uint64_t AnonymizationSalt = 82589933;
#endif
static bool EnableWebsocketURP = false;
static int URPStartCount = 0;
bool isURPEnabled() { return EnableWebsocketURP; }
/// When chroot is enabled, this is blank as all
/// the paths inside the jail, relative to it's jail.
/// E.g. /tmp/user/docs/...
/// However, without chroot, the jail path is
/// absolute in the system root.
/// I.e. ChildRoot/JailId/tmp/user/docs/...
/// We need to know where the jail really is
/// because WSD doesn't know if chroot will succeed
/// or fail, but it assumes the document path to
/// be relative to the root of the jail (i.e. chroot
/// expected to succeed). If it fails, or when caps
/// are disabled, file paths would be relative to the
/// system root, not the jail.
static std::string JailRoot;
static int URPtoLoFDs[2] { -1, -1 };
static int URPfromLoFDs[2] { -1, -1 };
// Abnormally we get LOK events from another thread, which must be
// push safely into our main poll loop to process to keep all
// socket buffer & event processing in a single, thread.
bool pushToMainThread(LibreOfficeKitCallback cb, int type, const char *p, void *data);
static LokHookFunction2* initFunction = nullptr;
namespace
{
// for later consistency checking.
static std::string UserDirPath;
static std::string InstDirPath;
std::string pathFromFileURL(const std::string &uri)
{
std::string decoded;
Poco::URI::decode(uri, decoded);
if (decoded.rfind("file://", 0) != 0)
{
LOG_ERR("Asked to load a very unusual file path: '" << uri << "' -> '" << decoded << "'");
return std::string();
}
return decoded.substr(7);
}
void consistencyCheckFileExists(const std::string &uri)
{
std::string path = pathFromFileURL(uri);
if (path.empty())
return;
FileUtil::Stat stat(path);
if (!stat.good() && stat.isFile())
LOG_ERR("Fatal system error: created file passed into document doesn't exist: '" << path << "'");
else
LOG_TRC("File path '" << path << "' exists of length " << stat.size());
consistencyCheckJail();
}
#if !defined(BUILDING_TESTS) && !MOBILEAPP
enum class LinkOrCopyType
{
All,
LO
};
LinkOrCopyType linkOrCopyType;
std::string sourceForLinkOrCopy;
Poco::Path destinationForLinkOrCopy;
bool forceInitialCopy; // some stackable file-systems have very slow first hard link creation
std::string linkableForLinkOrCopy; // Place to stash copies that we can hard-link from
std::chrono::time_point<std::chrono::steady_clock> linkOrCopyStartTime;
bool linkOrCopyVerboseLogging = false;
unsigned linkOrCopyFileCount = 0; // Track to help quantify the link-or-copy performance.
constexpr unsigned SlowLinkOrCopyLimitInSecs = 2; // After this many seconds, start spamming the logs.
bool detectSlowStackingFileSystem([[maybe_unused]] const std::string& directory)
{
#ifdef __linux__
#ifndef OVERLAYFS_SUPER_MAGIC
// From linux/magic.h.
#define OVERLAYFS_SUPER_MAGIC 0x794c7630
#endif
struct statfs fs;
if (::statfs(directory.c_str(), &fs) != 0)
{
LOG_SYS("statfs failed on '" << directory << "'");
return false;
}
switch (fs.f_type) {
// case FUSE_SUPER_MAGIC: ?
case OVERLAYFS_SUPER_MAGIC:
return true;
default:
return false;
}
#else
return false;
#endif
}
/// Returns the LinkOrCopyType as a human-readable string (for logging).
std::string linkOrCopyTypeString(LinkOrCopyType type)
{
switch (type)
{
case LinkOrCopyType::LO:
return "LibreOffice";
case LinkOrCopyType::All:
return "all";
default:
assert(!"Unknown LinkOrCopyType.");
return "unknown";
}
}
bool shouldCopyDir(const char *path)
{
switch (linkOrCopyType)
{
case LinkOrCopyType::LO:
return
strcmp(path, "program/wizards") != 0 &&
strcmp(path, "sdk") != 0 &&
strcmp(path, "debugsource") != 0 &&
strcmp(path, "share/basic") != 0 &&
strncmp(path, "share/extensions/dict-", // preloaded
sizeof("share/extensions/dict")) != 0 &&
strcmp(path, "share/Scripts/java") != 0 &&
strcmp(path, "share/Scripts/javascript") != 0 &&
strcmp(path, "share/config/wizard") != 0 &&
strcmp(path, "readmes") != 0 &&
strcmp(path, "help") != 0;
default: // LinkOrCopyType::All
return true;
}
}
bool shouldLinkFile(const char *path)
{
switch (linkOrCopyType)
{
case LinkOrCopyType::LO:
{
if (strstr(path, "LICENSE") || strstr(path, "EULA") || strstr(path, "CREDITS")
|| strstr(path, "NOTICE"))
return false;
const char* dot = strrchr(path, '.');
if (!dot)
return true;
if (!strcmp(dot, ".dbg"))
return false;
if (!strcmp(dot, ".so"))
{
// NSS is problematic ...
if (strstr(path, "libnspr4") ||
strstr(path, "libplds4") ||
strstr(path, "libplc4") ||
strstr(path, "libnss3") ||
strstr(path, "libnssckbi") ||
strstr(path, "libnsutil3") ||
strstr(path, "libssl3") ||
strstr(path, "libsoftokn3") ||
strstr(path, "libsqlite3") ||
strstr(path, "libfreeblpriv3"))
return true;
// As is Python ...
if (strstr(path, "python-core"))
return true;
// otherwise drop the rest of the code.
return false;
}
const char *vers;
if ((vers = strstr(path, ".so."))) // .so.[digit]+
{
for(int i = sizeof (".so."); vers[i] != '\0'; ++i)
if (!isdigit(vers[i]) && vers[i] != '.')
return true;
return false;
}
return true;
}
default: // LinkOrCopyType::All
return true;
}
}
void linkOrCopyFile(const char* fpath, const std::string& newPath)
{
++linkOrCopyFileCount;
if (linkOrCopyVerboseLogging)
LOG_INF("Linking file \"" << fpath << "\" to \"" << newPath << '"');
if (!forceInitialCopy)
{
// first try a simple hard-link
if (link(fpath, newPath.c_str()) == 0)
return;
}
// else always copy before linking to linkable/
// incrementally build our 'linkable/' copy nearby
static bool canChown = true; // only if we can get permissions right
if ((forceInitialCopy || errno == EXDEV) && canChown)
{
// then copy somewhere closer and hard link from there
if (!forceInitialCopy)
LOG_TRC("link(\"" << fpath << "\", \"" << newPath << "\") failed: " << strerror(errno)
<< ". Will try to link template.");
std::string linkableCopy = linkableForLinkOrCopy + fpath;
if (::link(linkableCopy.c_str(), newPath.c_str()) == 0)
return;
if (errno == ENOENT)
{
File(Path(linkableCopy).parent()).createDirectories();
if (!FileUtil::copy(fpath, linkableCopy, /*log=*/false, /*throw_on_error=*/false))
LOG_TRC("Failed to create linkable copy [" << fpath << "] to [" << linkableCopy.c_str() << "]");
else {
// Match system permissions, so a file we can write is not shared across jails.
struct stat ownerInfo;
if (::stat(fpath, &ownerInfo) != 0 ||
::chown(linkableCopy.c_str(), ownerInfo.st_uid, ownerInfo.st_gid) != 0)
{
LOG_ERR("Failed to stat or chown " << ownerInfo.st_uid << ":" << ownerInfo.st_gid <<
" " << linkableCopy << ": " << strerror(errno) << " missing cap_chown?, disabling linkable");
unlink(linkableCopy.c_str());
canChown = false;
}
else if (::link(linkableCopy.c_str(), newPath.c_str()) == 0)
return;
}
}
LOG_TRC("link(\"" << linkableCopy << "\", \"" << newPath << "\") failed: " << strerror(errno)
<< ". Cannot create linkable copy.");
}
static bool warned = false;
if (!warned)
{
LOG_ERR("link(\"" << fpath << "\", \"" << newPath.c_str() << "\") failed: " << strerror(errno)
<< ". Very slow copying path triggered.");
warned = true;
} else
LOG_TRC("link(\"" << fpath << "\", \"" << newPath.c_str() << "\") failed: " << strerror(errno)
<< ". Will copy.");
if (!FileUtil::copy(fpath, newPath.c_str(), /*log=*/false, /*throw_on_error=*/false))
{
LOG_FTL("Failed to copy or link [" << fpath << "] to [" << newPath << "]. Exiting.");
Util::forcedExit(EX_SOFTWARE);
}
}
int linkOrCopyFunction(const char *fpath,
const struct stat* sb,
int typeflag,
struct FTW* /*ftwbuf*/)
{
if (strcmp(fpath, sourceForLinkOrCopy.c_str()) == 0)
{
LOG_TRC("nftw: Skipping redundant path: " << fpath);
return FTW_CONTINUE;
}
if (!linkOrCopyVerboseLogging)
{
const auto durationInSecs = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::now() - linkOrCopyStartTime);
if (durationInSecs.count() > SlowLinkOrCopyLimitInSecs)
{
LOG_WRN("Linking/copying files from "
<< sourceForLinkOrCopy << " to " << destinationForLinkOrCopy.toString()
<< " is taking too much time. Enabling verbose link/copy logging.");
linkOrCopyVerboseLogging = true;
}
}
assert(fpath[strlen(sourceForLinkOrCopy.c_str())] == '/');
const char *relativeOldPath = fpath + strlen(sourceForLinkOrCopy.c_str()) + 1;
const Poco::Path newPath(destinationForLinkOrCopy, Poco::Path(relativeOldPath));
switch (typeflag)
{
case FTW_F:
case FTW_SLN:
Poco::File(newPath.parent()).createDirectories();
if (shouldLinkFile(relativeOldPath))
linkOrCopyFile(fpath, newPath.toString());
break;
case FTW_D:
{
struct stat st;
if (stat(fpath, &st) == -1)
{
LOG_SYS("nftw: stat(\"" << fpath << "\") failed");
return FTW_STOP;
}
if (!shouldCopyDir(relativeOldPath))
{
LOG_TRC("nftw: Skipping redundant path: " << relativeOldPath);
return FTW_SKIP_SUBTREE;
}
Poco::File(newPath).createDirectories();
struct utimbuf ut;
ut.actime = st.st_atime;
ut.modtime = st.st_mtime;
if (utime(newPath.toString().c_str(), &ut) == -1)
{
LOG_SYS("nftw: utime(\"" << newPath.toString() << "\") failed");
return FTW_STOP;
}
}
break;
case FTW_SL:
{
const std::size_t size = sb->st_size;
std::vector<char> target(size + 1);
char* target_data = target.data();
const ssize_t written = readlink(fpath, target_data, size);
if (written <= 0 || static_cast<std::size_t>(written) > size)
{
LOG_SYS("nftw: readlink(\"" << fpath << "\") failed");
Util::forcedExit(EX_SOFTWARE);
}
target_data[written] = '\0';
Poco::File(newPath.parent()).createDirectories();
if (symlink(target_data, newPath.toString().c_str()) == -1)
{
LOG_SYS("nftw: symlink(\"" << target_data << "\", \"" << newPath.toString()
<< "\") failed");
return FTW_STOP;
}
}
break;
case FTW_DNR:
LOG_ERR("nftw: Cannot read directory '" << fpath << '\'');
return FTW_STOP;
case FTW_NS:
LOG_ERR("nftw: stat failed for '" << fpath << '\'');
return FTW_STOP;
default:
LOG_FTL("nftw: unexpected typeflag: '" << typeflag);
assert(!"nftw: unexpected typeflag.");
break;
}
return FTW_CONTINUE;
}
void linkOrCopy(std::string source, const Poco::Path& destination, const std::string& linkable,
LinkOrCopyType type)
{
std::string resolved = FileUtil::realpath(source);
if (resolved != source)
{
LOG_DBG("linkOrCopy: Using real path [" << resolved << "] instead of original link ["
<< source << "].");
source = std::move(resolved);
}
LOG_INF("linkOrCopy " << linkOrCopyTypeString(type) << " from [" << source << "] to ["
<< destination.toString() << "].");
linkOrCopyType = type;
sourceForLinkOrCopy = source;
if (sourceForLinkOrCopy.back() == '/')
sourceForLinkOrCopy.pop_back();
destinationForLinkOrCopy = destination;
linkableForLinkOrCopy = linkable;
linkOrCopyFileCount = 0;
linkOrCopyStartTime = std::chrono::steady_clock::now();
forceInitialCopy = detectSlowStackingFileSystem(destination.toString());
if (nftw(source.c_str(), linkOrCopyFunction, 10, FTW_ACTIONRETVAL|FTW_PHYS) == -1)
{
LOG_ERR("linkOrCopy: nftw() failed for '" << source << '\'');
}
if (linkOrCopyVerboseLogging)
{
linkOrCopyVerboseLogging = false;
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - linkOrCopyStartTime).count();
const double seconds = (ms + 1) / 1000.; // At least 1ms to avoid div-by-zero.
const auto rate = linkOrCopyFileCount / seconds;
LOG_INF("Linking/Copying of " << linkOrCopyFileCount << " files from " << source
<< " to " << destinationForLinkOrCopy.toString()
<< " finished in " << seconds << " seconds, or " << rate
<< " files / second.");
}
}
#if CODE_COVERAGE
std::string childRootForGCDAFiles;
std::string sourceForGCDAFiles;
std::string destForGCDAFiles;
int linkGCDAFilesFunction(const char* fpath, const struct stat*, int typeflag,
struct FTW* /*ftwbuf*/)
{
if (strcmp(fpath, sourceForGCDAFiles.c_str()) == 0)
{
LOG_TRC("nftw: Skipping redundant path: " << fpath);
return FTW_CONTINUE;
}
if (fpath.starts_with(childRootForGCDAFiles))
{
LOG_TRC("nftw: Skipping childRoot subtree: " << fpath);
return FTW_SKIP_SUBTREE;
}
assert(fpath[strlen(sourceForGCDAFiles.c_str())] == '/');
const char* relativeOldPath = fpath + strlen(sourceForGCDAFiles.c_str()) + 1;
const Poco::Path newPath(destForGCDAFiles, Poco::Path(relativeOldPath));
switch (typeflag)
{
case FTW_F:
case FTW_SLN:
{
const char* dot = strrchr(relativeOldPath, '.');
if (dot && !strcmp(dot, ".gcda"))
{
Poco::File(newPath.parent()).createDirectories();
if (link(fpath, newPath.toString().c_str()) != 0)
{
LOG_SYS("nftw: Failed to link [" << fpath << "] -> [" << newPath.toString()
<< ']');
}
}
}
break;
case FTW_D:
case FTW_SL:
break;
case FTW_DNR:
LOG_ERR("nftw: Cannot read directory '" << fpath << '\'');
break;
case FTW_NS:
LOG_ERR("nftw: stat failed for '" << fpath << '\'');
break;
default:
LOG_FTL("nftw: unexpected typeflag: '" << typeflag);
assert(!"nftw: unexpected typeflag.");
break;
}
return FTW_CONTINUE;
}
/// Link .gcda (gcov) files from the src directory into the jail.
/// We need this so we can easily extract the profile data from within
/// the jail. Otherwise, we lose coverage info of the kit process.
void linkGCDAFiles(const std::string& destPath)
{
Poco::Path sourcePathInJail(destPath);
const auto sourcePath = std::string(DEBUG_ABSSRCDIR);
sourcePathInJail.append(sourcePath);
Poco::File(sourcePathInJail).createDirectories();
LOG_INF("Linking .gcda files from " << sourcePath << " -> " << sourcePathInJail.toString());
const auto childRootPtr = std::getenv("BASE_CHILD_ROOT");
if (childRootPtr == nullptr || strlen(childRootPtr) == 0)
{
LOG_ERR("Cannot collect code-coverage stats for the Kit processes. BASE_CHILD_ROOT "
"envar missing.");
return;
}
// Trim the trailing /.
const std::string childRoot = childRootPtr;
const size_t last = childRoot.find_last_not_of('/');
if (last != std::string::npos)
childRootForGCDAFiles = childRoot.substr(0, last + 1);
else
childRootForGCDAFiles = childRoot;
sourceForGCDAFiles = sourcePath;
destForGCDAFiles = sourcePathInJail.toString() + '/';
LOG_INF("nftw .gcda files from " << sourceForGCDAFiles << " -> " << destForGCDAFiles << " ("
<< childRootForGCDAFiles << ')');
if (nftw(sourcePath.c_str(), linkGCDAFilesFunction, 10, FTW_ACTIONRETVAL | FTW_PHYS) == -1)
{
LOG_ERR("linkGCDAFiles: nftw() failed for '" << sourcePath << '\'');
}
}
#endif
#ifndef __FreeBSD__
void dropCapability(cap_value_t capability)
{
cap_t caps;
cap_value_t cap_list[] = { capability };
caps = cap_get_proc();
if (caps == nullptr)
{
LOG_SFL("cap_get_proc() failed");
Util::forcedExit(EX_SOFTWARE);
}
char *capText = cap_to_text(caps, nullptr);
LOG_TRC("Capabilities first: " << capText);
cap_free(capText);
if (cap_set_flag(caps, CAP_EFFECTIVE, N_ELEMENTS(cap_list), cap_list, CAP_CLEAR) == -1 ||
cap_set_flag(caps, CAP_PERMITTED, N_ELEMENTS(cap_list), cap_list, CAP_CLEAR) == -1)
{
LOG_SFL("cap_set_flag() failed");
Util::forcedExit(EX_SOFTWARE);
}
if (cap_set_proc(caps) == -1)
{
LOG_SFL("cap_set_proc() failed");
Util::forcedExit(EX_SOFTWARE);
}
capText = cap_to_text(caps, nullptr);
LOG_TRC("Capabilities now: " << capText);
cap_free(capText);
cap_free(caps);
}
#endif // __FreeBSD__
#endif // BUILDING_TESTS
} // namespace
Document::Document(const std::shared_ptr<lok::Office>& loKit,
const std::string& jailId,
const std::string& docKey,
const std::string& docId,
const std::string& url,
const std::shared_ptr<WebSocketHandler>& websocketHandler,
unsigned mobileAppDocId)
: _loKit(loKit),
_jailId(jailId),
_docKey(docKey),
_docId(docId),
_url(url),
_obfuscatedFileId(Util::getFilenameFromURL(docKey)),
_queue(std::make_shared<KitQueue>()),
_websocketHandler(websocketHandler),
_modified(ModifiedState::UnModified),
_isBgSaveProcess(false),
_isBgSaveDisabled(false),
_haveDocPassword(false),
_isDocPasswordProtected(false),
_docPasswordType(DocumentPasswordType::ToView),
_stop(false),
_deltaGen(new DeltaGenerator()),
_editorId(-1),
_editorChangeWarning(false),
_lastMemTrimTime(std::chrono::steady_clock::now()),
_mobileAppDocId(mobileAppDocId),
_duringLoad(0)
{
LOG_INF("Document ctor for [" << _docKey <<
"] url [" << anonymizeUrl(_url) << "] on child [" << _jailId <<
"] and id [" << _docId << "].");
assert(_loKit);
#if !MOBILEAPP
assert(singletonDocument == nullptr);
singletonDocument = this;
#endif
}
Document::~Document()
{
LOG_INF("~Document dtor for [" << _docKey <<
"] url [" << anonymizeUrl(_url) << "] on child [" << _jailId <<
"] and id [" << _docId << "]. There are " <<
_sessions.size() << " views.");
// Wait for the callback worker to finish.
_stop = true;
for (const auto& session : _sessions)
{
session.second->resetDocManager();
}
#ifdef IOS
DocumentData::deallocate(_mobileAppDocId);
#endif
}
/// Post the message - in the unipoll world we're in the right thread anyway
bool Document::postMessage(const char* data, int size, const WSOpCode code) const
{
if (_isBgSaveProcess)
{
auto socket = _saveProcessParent.lock();
if (socket)
{
LOG_TRC("postMessage forwarding to parent of save process: " << getAbbreviatedMessage(data, size));
if (code != WSOpCode::Text)
{
LOG_WRN("save process unexpectedly sending binary message to parent: " << getAbbreviatedMessage(data, size));
assert(false);
return false;
}
else
return socket->sendMessage(data, size, code, /*flush=*/true) > 0;
}
else
LOG_TRC("Failed to forward to parent of save process: connection closed.");
return false;
}
LOG_TRC("postMessage called with: " << getAbbreviatedMessage(data, size));
if (!_websocketHandler)
{
LOG_ERR("Child Doc: Bad socket while sending [" << getAbbreviatedMessage(data, size) << "].");
return false;
}
_websocketHandler->sendMessage(data, size, code, /*flush=*/true);
return true;
}
bool Document::createSession(const std::string& sessionId)
{
#if defined(BUILDING_TESTS)
LOG_ERR("createSession stubbed for tests for " << sessionId);
return false;
#else
try
{
if (_sessions.find(sessionId) != _sessions.end())
{
LOG_ERR("Session [" << sessionId << "] on url [" << anonymizeUrl(_url) << "] already exists.");
return true;
}
LOG_INF("Creating " << (_sessions.empty() ? "first" : "new") <<
" session for url: " << anonymizeUrl(_url) << " for sessionId: " <<
sessionId << " on jailId: " << _jailId);
auto session = std::make_shared<ChildSession>(
_websocketHandler, sessionId,
_jailId, JailRoot, *this);
_sessions.emplace(sessionId, session);
_deltaGen->setSessionCount(_sessions.size());
const int viewId = session->getViewId();
_lastUpdatedAt[viewId] = std::chrono::steady_clock::now();
_speedCount[viewId] = 0;
LOG_INF("New session [" << sessionId << "] created. Have " << _sessions.size()
<< " sessions now");
updateActivityHeader();
return true;
}
catch (const std::exception& ex)
{
LOG_ERR("Exception while creating session [" << sessionId <<
"] on url [" << anonymizeUrl(_url) << "] - '" << ex.what() << "'.");
return false;
}
#endif
}
std::size_t Document::purgeSessions()
{
std::vector<std::shared_ptr<ChildSession>> deadSessions;
std::size_t num_sessions = 0;
{
// If there are no live sessions, we don't need to do anything at all and can just
// bluntly exit, no need to clean up our own data structures. Also, there is a bug that
// causes the deadSessions.clear() call below to crash in some situations when the last
// session is being removed.
for (auto it = _sessions.cbegin(); it != _sessions.cend(); )
{
if (it->second->isCloseFrame())
{
LOG_DBG("Removing session [" << it->second->getId() << ']');
deadSessions.push_back(it->second);
it = _sessions.erase(it);
}
else
{
++it;
}
}
num_sessions = _sessions.size();
if (!Util::isMobileApp() && num_sessions == 0)
{
LOG_FTL("Document [" << anonymizeUrl(_url) << "] has no more views, exiting bluntly.");
flushAndExit(EX_OK);
}
}
if (deadSessions.size() > 0 )
LOG_TRC("Purging " << deadSessions.size() <<
" dead sessions, with " << num_sessions <<
" active sessions.");
// Don't destroy sessions while holding our lock.
// We may deadlock if a session is waiting on us
// during callback initiated while handling a command
// and the dtor tries to take its lock (which is taken).
deadSessions.clear();
return num_sessions;
}
/// Set Document password for given URL
void Document::setDocumentPassword(int passwordType)
{
// Log whether the document is password protected and a password is provided
LOG_INF("setDocumentPassword: passwordProtected=" << _isDocPasswordProtected <<
" passwordProvided=" << _haveDocPassword);
if (_isDocPasswordProtected && _haveDocPassword)
{
// it means this is the second attempt with the wrong password; abort the load operation
_loKit->setDocumentPassword(_jailedUrl.c_str(), nullptr);
return;
}
// One thing for sure, this is a password protected document
_isDocPasswordProtected = true;
if (passwordType == LOK_CALLBACK_DOCUMENT_PASSWORD)
_docPasswordType = DocumentPasswordType::ToView;
else if (passwordType == LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY)
_docPasswordType = DocumentPasswordType::ToModify;
LOG_INF("Calling _loKit->setDocumentPassword");
if (_haveDocPassword)
_loKit->setDocumentPassword(_jailedUrl.c_str(), _docPassword.c_str());
else
_loKit->setDocumentPassword(_jailedUrl.c_str(), nullptr);
LOG_INF("setDocumentPassword returned.");
}
void Document::renderTiles(TileCombined &tileCombined)
{
// Find a session matching our view / render settings.
const auto session = _sessions.findByCanonicalId(tileCombined.getNormalizedViewId());
if (!session)
{
LOG_ERR("Session is not found. Maybe exited after rendering request.");
return;
}
if (!_loKitDocument)
{
LOG_ERR("Tile rendering requested before loading document.");
return;
}
if (_loKitDocument->getViewsCount() <= 0)
{
LOG_ERR("Tile rendering requested without views.");
return;
}
// if necessary select a suitable rendering view eg. with 'show non-printing chars'
if (tileCombined.getNormalizedViewId())
_loKitDocument->setView(session->getViewId());
const auto blenderFunc = [&](unsigned char* data, int offsetX, int offsetY,
std::size_t pixmapWidth, std::size_t pixmapHeight,
int pixelWidth, int pixelHeight, LibreOfficeKitTileMode mode) {
if (session->watermark())
session->watermark()->blending(data, offsetX, offsetY, pixmapWidth, pixmapHeight,
pixelWidth, pixelHeight, mode);
};
const auto postMessageFunc = [&](const char* buffer, std::size_t length) {
postMessage(buffer, length, WSOpCode::Binary);
};
if (!RenderTiles::doRender(_loKitDocument, *_deltaGen, tileCombined, _deltaPool,
blenderFunc, postMessageFunc, _mobileAppDocId,
session->getCanonicalViewId(), session->getDumpTiles()))
{
LOG_DBG("All tiles skipped, not producing empty tilecombine: message");
return;
}
}
bool Document::sendFrame(const char* buffer, int length, WSOpCode opCode)
{
try
{
return postMessage(buffer, length, opCode);
}
catch (const Exception& exc)
{
LOG_ERR("Document::sendFrame: Exception: " << exc.displayText() <<
(exc.nested() ? "( " + exc.nested()->displayText() + ')' : ""));
}
return false;
}
void Document::trimIfInactive()
{
// Don't perturb memory un-necessarily
if (_isBgSaveProcess)
return;
// FIXME: multi-document mobile optimization ?
for (const auto& it : _sessions)
{
if (it.second->isActive())
{
LOG_TRC("have active session, don't trim");
return;
}
}
// TODO: be more clever - detect if we mutated the documen
// recently, measure memory pressure etc.
LOG_DBG("Sessions are all inactive - trim memory");
SigUtil::addActivity("trimIfInactive");
_loKit->trimMemory(4096);
_deltaGen->dropCache();
}
void Document::trimAfterInactivity()
{
// Don't perturb memory un-necessarily
if (_isBgSaveProcess)
return;
if (std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() -
_lastMemTrimTime) < std::chrono::seconds(30))
{
return;
}
LOG_TRC("Should we trim our caches ?");
double minInactivityMs = std::numeric_limits<double>::max();
for (const auto& it : _sessions)
{
minInactivityMs = std::min(it.second->getInactivityMS(), minInactivityMs);
}
if (minInactivityMs >= 9999)
{
LOG_DBG("Trimming Core caches");
SigUtil::addActivity("trimAfterInactivity");
_loKit->trimMemory(1024);
_lastMemTrimTime = std::chrono::steady_clock::now();
}
}
/* static */ void Document::GlobalCallback(const int type, const char* p, void* data)
{
if (SigUtil::getTerminationFlag())
return;
// unusual LOK event from another thread,
// pData - is Document with process' lifetime.
if (pushToMainThread(GlobalCallback, type, p, data))
return;
const std::string payload = p ? p : "(nil)";
Document* self = static_cast<Document*>(data);
if (type == LOK_CALLBACK_PROFILE_FRAME)
{
// We must send the trace data to the WSD process for output
LOG_TRC("Document::GlobalCallback " << lokCallbackTypeToString(type) << ": " << payload.length() << " bytes.");
self->sendTextFrame("traceevent: \n" + payload);
return;
}
LOG_TRC("Document::GlobalCallback " << lokCallbackTypeToString(type) <<
" [" << payload << "].");
if (type == LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY ||
type == LOK_CALLBACK_DOCUMENT_PASSWORD)
{
// Mark the document password type.
self->setDocumentPassword(type);
return;
}
else if (type == LOK_CALLBACK_STATUS_INDICATOR_START ||
type == LOK_CALLBACK_STATUS_INDICATOR_SET_VALUE ||
type == LOK_CALLBACK_STATUS_INDICATOR_FINISH)
{
for (auto& it : self->_sessions)
{
std::shared_ptr<ChildSession> session = it.second;
if (!session->isCloseFrame())
session->loKitCallback(type, payload);
}
return;
}
else if (type == LOK_CALLBACK_JSDIALOG || type == LOK_CALLBACK_HYPERLINK_CLICKED)
{
if (self->_sessions.size() == 1)
{
auto it = self->_sessions.begin();
std::shared_ptr<ChildSession> session = it->second;
if (session && !session->isCloseFrame())
{
session->loKitCallback(type, payload);
if (self->isLoadOngoing())
LOG_DBG("Enable processing input due to event of " << type << " during load");
session->getProtocol()->enableProcessInput(true);
return;
}
}
}
// Broadcast leftover status indicator callbacks to all clients
self->broadcastCallbackToClients(type, payload);
}
/* static */ void Document::ViewCallback(const int type, const char* p, void* data)
{
if (SigUtil::getTerminationFlag())
return;
// unusual LOK event from another thread.
// pData - is CallbackDescriptors which share process' lifetime.
if (pushToMainThread(ViewCallback, type, p, data))
return;
CallbackDescriptor* descriptor = static_cast<CallbackDescriptor*>(data);
assert(descriptor && "Null callback data.");
assert(descriptor->getDoc() && "Null Document instance.");
std::shared_ptr<KitQueue> queue = descriptor->getDoc()->_queue;
assert(queue && "Null KitQueue.");
const std::string payload = p ? p : "(nil)";
LOG_TRC("Document::ViewCallback [" << descriptor->getViewId() <<
"] [" << lokCallbackTypeToString(type) <<
"] [" << payload << "].");
// when we examine the content of the JSON
std::string targetViewId;
if (type == LOK_CALLBACK_CELL_CURSOR)
{
StringVector tokens(StringVector::tokenize(payload, ','));
// Payload may be 'EMPTY'.
if (tokens.size() == 4)
{
int cursorX = std::stoi(tokens[0]);
int cursorY = std::stoi(tokens[1]);
int cursorWidth = std::stoi(tokens[2]);
int cursorHeight = std::stoi(tokens[3]);
queue->updateCursorPosition(0, 0, cursorX, cursorY, cursorWidth, cursorHeight);
}
}
else if (type == LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR)
{
Poco::JSON::Parser parser;
const Poco::Dynamic::Var result = parser.parse(payload);
const auto& command = result.extract<Poco::JSON::Object::Ptr>();
std::string rectangle = command->get("rectangle").toString();
StringVector tokens(StringVector::tokenize(rectangle, ','));
// Payload may be 'EMPTY'.
if (tokens.size() == 4)
{
int cursorX = std::stoi(tokens[0]);
int cursorY = std::stoi(tokens[1]);
int cursorWidth = std::stoi(tokens[2]);
int cursorHeight = std::stoi(tokens[3]);
queue->updateCursorPosition(0, 0, cursorX, cursorY, cursorWidth, cursorHeight);
}
}
else if (type == LOK_CALLBACK_INVALIDATE_VIEW_CURSOR ||
type == LOK_CALLBACK_CELL_VIEW_CURSOR)
{
Poco::JSON::Parser parser;
const Poco::Dynamic::Var result = parser.parse(payload);
const auto& command = result.extract<Poco::JSON::Object::Ptr>();
targetViewId = command->get("viewId").toString();
std::string part = command->get("part").toString();
std::string text = command->get("rectangle").toString();
StringVector tokens(StringVector::tokenize(text, ','));
// Payload may be 'EMPTY'.
if (tokens.size() == 4)
{
int cursorX = std::stoi(tokens[0]);
int cursorY = std::stoi(tokens[1]);
int cursorWidth = std::stoi(tokens[2]);
int cursorHeight = std::stoi(tokens[3]);
queue->updateCursorPosition(std::stoi(targetViewId), std::stoi(part), cursorX, cursorY, cursorWidth, cursorHeight);
}
}
else if (type == LOK_CALLBACK_DOCUMENT_PASSWORD_RESET)
{
Document* document = dynamic_cast<Document*>(descriptor->getDoc());
Poco::JSON::Object::Ptr object;
if (document && JsonUtil::parseJSON(payload, object))
{
std::string password = JsonUtil::getJSONValue<std::string>(object, "password");
bool isToModify = JsonUtil::getJSONValue<bool>(object, "isToModify");
document->_isDocPasswordProtected = !password.empty();
document->_haveDocPassword = document->_isDocPasswordProtected;
document->_docPassword = password;
document->_docPasswordType =
isToModify ? DocumentPasswordType::ToModify : DocumentPasswordType::ToView;
}
return;
}
else if (type == LOK_CALLBACK_VIEW_RENDER_STATE)
{
Document* document = dynamic_cast<Document*>(descriptor->getDoc());
if (document)
{
std::shared_ptr<ChildSession> session = document->findSessionByViewId(descriptor->getViewId());
if (session)
{
session->setViewRenderState(payload);
document->invalidateCanonicalId(session->getId());
}
else
{
LOG_ERR("Cannot find session for viewId: " << descriptor->getViewId());
}
}
else
{
// This shouldn't happen, but for consistency.
LOG_ERR("Failed to downcast DocumentManagerInterface to Document");
}
return;
}
// merge various callback types together if possible
if (type == LOK_CALLBACK_INVALIDATE_TILES)
{
// all views have to be in sync; FIXME: calc an issue here ?
queue->putCallback(-1, type, payload);
}
else
queue->putCallback(descriptor->getViewId(), type, payload);
LOG_TRC("Document::ViewCallback end.");
}
/// Load a document (or view) and register callbacks.
bool Document::onLoad(const std::string& sessionId,
const std::string& uriAnonym,
const std::string& renderOpts)
{
LOG_INF("Loading url [" << uriAnonym << "] for session [" << sessionId <<
"] which has " << (_sessions.size() - 1) << " sessions.");
Util::ReferenceHolder duringLoad(_duringLoad);
// This shouldn't happen, but for sanity.
const auto it = _sessions.find(sessionId);
if (it == _sessions.end() || !it->second)
{
LOG_ERR("Cannot find session [" << sessionId << "] to load view for.");
return false;
}
std::shared_ptr<ChildSession> session = it->second;
try
{
if (!load(session, renderOpts))
{
return false;
}
}
catch (const std::exception &exc)
{
LOG_ERR("Exception while loading url [" << uriAnonym <<
"] for session [" << sessionId << "]: " << exc.what());
session->sendTextFrameAndLogError("error: cmd=load kind=faileddocloading");
return false;
}
return true;
}
void Document::onUnload(const ChildSession& session)
{
// This is called when we receive 'child-??? disconnect'.
// First, we _sessions.erase(), which destroys the ChildSession instance.
// We are called from ~ChildSession.
const auto& sessionId = session.getId();
LOG_INF("Unloading session [" << sessionId << "] on url [" << anonymizeUrl(_url) << ']');
if (_loKitDocument == nullptr)
{
LOG_ERR("Unloading session [" << sessionId << "] without loKitDocument, exiting bluntly");
flushAndExit(EX_OK);
return;
}
// If we have no more sessions, we have nothing more to do.
if (!Util::isMobileApp() && _sessions.empty())
{
// Sanitiy check.
std::ostringstream msg;
const int views = _loKitDocument->getViewsCount();
if (views > 1 || isBackgroundSaveProcess())
{
// Normally, this is a race between the save notification
// sent from the background-save process being processed
// by DocBroker and we processing the disconnection from
// said background-save process before getting here.
// However, this could also be an indication of a save
// still in progress and DocBroker unloading--a bug.
msg << " but " << views << " views"
<< (isBackgroundSaveProcess() ? " and background-save in progress" : "");
}
LOG_INF("Document [" << anonymizeUrl(_url) << "] has no more sessions" << msg.str()
<< "; exiting bluntly");
flushAndExit(EX_OK);
return;
}
const int viewId = session.getViewId();
_queue->removeCursorPosition(viewId);
// Unload the view.
_loKitDocument->setView(viewId);
_loKitDocument->registerCallback(nullptr, nullptr);
_loKit->registerCallback(nullptr, nullptr);
_loKitDocument->destroyView(viewId);
// Since callback messages are processed on idle-timer,
// we could receive callbacks after destroying a view.
// Retain the CallbackDescriptor object, which is shared with Core.
// Do not: _viewIdToCallbackDescr.erase(viewId);
const int viewCount = _loKitDocument->getViewsCount();
LOG_INF("Document [" << anonymizeUrl(_url) << "] session [" << sessionId << "] unloaded view ["
<< viewId << "]. Have " << viewCount << " view"
<< (viewCount != 1 ? "s" : "") << " and " << _sessions.size() << " session"
<< (_sessions.size() != 1 ? "s" : ""));
if (viewCount > 0)
{
// Broadcast updated view info
notifyViewInfo();
}
}
void Document::updateActivityHeader() const
{
// pre-prepare and set details in case of a signal later
std::stringstream ss;
ss << "Session count: " << _sessions.size() << "\n";
for (const auto& it : _sessions)
ss << "\t" << it.second->getActivityState() << "\n";
ss << "Commands:\n";
SigUtil::setActivityHeader(ss.str());
}
bool Document::joinThreads()
{
if (!getLOKit()->joinThreads())
return false;
if (SocketPoll::PollWatchdog)
SocketPoll::PollWatchdog->joinThread();
_deltaPool.stop();
return true;
}
// Most threads are opportunisticaly created but some need to be started
void Document::startThreads()
{
_deltaPool.start();
getLOKit()->startThreads();
if (SocketPoll::PollWatchdog)
SocketPoll::PollWatchdog->startThread();
}
void Document::handleSaveMessage(const std::string &)
{
LOG_TRC("Check save message");
// if a bgsave process - now we can clean up.
if (_isBgSaveProcess)
{
auto socket = _saveProcessParent.lock();
if (socket)
{
LOG_TRC("Shutting down bgsv child's socket to parent kit post save");
// We don't want to wait around for the parent's websocket
socket->shutdownAfterWriting();
// This means we don't get to send statechanged: .uno:ModifiedStatus
// which is fine - we want to leave that to the Kit process.
}
else
LOG_TRC("Shutting down already shutdown bgsv child's socket to parent kit post save");
// any further messages are not interesting.
if (_queue)
_queue->clear();
// cleanup any lingering file-system pieces
_loKitDocument.reset();
// Next step in the chain is BgSaveChildWebSocketHandler::onDisconnect
}
}
// need to hold a reference on session in case it exits during async save
bool Document::forkToSave(const std::function<void()> &childSave, int viewId)
{
#if MOBILEAPP
return false;
#else // !MOBILEAPP
if (_isBgSaveProcess)
{
LOG_ERR("Serious error bgsv process trying to fork again");
assert(false);
return false;
}
if (_isBgSaveDisabled)
{
LOG_TRC("Skipping background save for bg save disabled process");
return false;
}
if (!joinThreads())
{
LOG_WRN("Failed to join threads before async save");
return false;
}
size_t threads = getCurrentThreadCount();
if (threads != 1)
{
LOG_DBG("Failed to ensure we have just one thread on 1st try, we have: " << threads);
// Potentially the kernel can take time to cleanup after pthread_join
int countDown = 10;
while (true)
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if ((threads = getCurrentThreadCount()) == 1)
break;
countDown--;
if (countDown < 1)
{
LOG_WRN("Failed to ensure we have just one thread for bgsave, "
"we have: " << threads << " synchronously saving");
return false;
}
}
}
// Oddly we have seen this broken in the past.
assert(processInputEnabled());
#if 0
// TODO: compare FD count in a normal process with how
// many we see open now.
int expectFds = 2 // SocketPoll wakeups
+ 1; // socket to coolwsd
int actualFds = fdCounter->count();
if (actualFds != expectFds)
{
LOG_WRN("Can't background save: " << actualFds << " fds open; expect " << expectFds);
return false;
}
#endif
const auto start = std::chrono::steady_clock::now();
// TODO: close URPtoLoFDs and URPfromLoFDs and test
if (isURPEnabled())
{
LOG_WRN("Can't background save with URP enabled");
return false;
}
// FIXME: only do one of these at a time ...
// FIXME: defer and queue a 2nd save if queued during save ...
std::shared_ptr<StreamSocket> parentSocket, childSocket;
if (!StreamSocket::socketpair(parentSocket, childSocket))
return false;
// To encode into the child process id for debugging
static size_t numSaves = 0;
numSaves++;
const pid_t pid = fork();
if (!pid) // Child
{
Log::postFork();
// sort out thread local variables to get logging right from
// as early as possible.
Util::setThreadName("kitbgsv_" + Util::encodeId(_mobileAppDocId, 3) +
"_" + Util::encodeId(numSaves, 3));
_isBgSaveProcess = true;
SigUtil::addActivity("forked background save process: " +
std::to_string(pid));
SigUtil::dieOnParentDeath();
childSocket.reset();
// now we just have a single socket to our parent
Util::sleepFromEnvIfSet("KitBackgroundSave", "SLEEPBACKGROUNDFORDEBUGGER");
UnitKit::get().postBackgroundSaveFork();
// Background save should run at a lower priority
int prio = config::getInt("per_document.bgsave_priority", 5);
Util::setProcessAndThreadPriorities(getpid(), prio);
// other queued messages should be handled in the parent kit
if (_queue)
_queue->clear();
// Hard drop our previous connections to coolwsd and shared wakeups.
KitSocketPoll::cleanupChildProcess();
// close duplicate kit->wsd socket
auto kitWs = std::static_pointer_cast<KitWebSocketHandler>(_websocketHandler);
kitWs->shutdownForBackgroundSave();
// now send messages to the parent instead of the kit.
auto parentWs = std::make_shared<BgSaveChildWebSocketHandler>("bgsv_child_ws");
parentSocket->setHandler(parentWs);
parentSocket->setWebSocket(); // avoid http upgrade.
_saveProcessParent = parentWs;
// hand parentSocket to the main poll
KitSocketPoll::getMainPoll()->insertNewSocket(parentSocket);
parentWs.reset();
getLOKit()->setForkedChild(true);
const auto now = std::chrono::steady_clock::now();
LOG_TRC("Background save process " << getpid() << " fork took " <<
std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() << "ms");
childSave();
SigUtil::addActivity("background save process shutdown");
// Wait now for an async save result from the core,
// and head to handleSaveMessage
}
else // Still us
{
LOG_TRC("Spawned process " << pid << " to do background save");
parentSocket.reset();
// now we have a socket to the child: childSocket
forceDocUnmodifiedForBgSave(viewId);
auto bgSaveChild = std::make_shared<BgSaveParentWebSocketHandler>(
"bgsv_kit_ws", pid, shared_from_this(),
findSessionByViewId(viewId));
childSocket->setHandler(bgSaveChild);
childSocket->setWebSocket(); // avoid http upgrade.
KitSocketPoll::getMainPoll()->insertNewSocket(childSocket);
getLOKit()->setForkedChild(false);
startThreads();
}
return true;
#endif // !MOBILEAPP
}
void Document::notifyViewInfo()
{
// Get the list of view ids from the core
const int viewCount = getLOKitDocument()->getViewsCount();
std::vector<int> viewIds(viewCount);
getLOKitDocument()->getViewIds(viewIds.data(), viewCount);
const std::map<int, UserInfo> viewInfoMap = getViewInfo();
const std::map<std::string, int> viewColorsMap = getViewColors();
// Double check if list of viewids from core and our list matches,
// and create an array of JSON objects containing id and username
std::map<int, std::string> viewStrings; // viewId -> public data string
for (const auto& viewId : viewIds)
{
std::ostringstream oss;
oss << "\"id\":" << viewId << ',';
int color = 0;
const auto itView = viewInfoMap.find(viewId);
if (itView == viewInfoMap.end())
{
LOG_ERR("No username found for viewId [" << viewId << "].");
oss << "\"username\":\"Unknown\",";
}
else
{
oss << "\"userid\":\"" << JsonUtil::escapeJSONValue(itView->second.getUserId()) << "\",";
const std::string username = itView->second.getUserName();
oss << "\"username\":\"" << JsonUtil::escapeJSONValue(username) << "\",";
if (!itView->second.getUserExtraInfo().empty())
oss << "\"userextrainfo\":" << itView->second.getUserExtraInfo() << ',';
const bool readonly = itView->second.isReadOnly();
oss << "\"readonly\":\"" << readonly << "\",";
const auto it = viewColorsMap.find(username);
if (it != viewColorsMap.end())
{
color = it->second;
}
}
oss << "\"color\":" << color;
viewStrings[viewId] = oss.str();
}
// Broadcast updated viewinfo to all clients. Every view gets own userprivateinfo.
for (const auto& it : _sessions)
{
std::ostringstream oss;
oss << "viewinfo: [";
for (const auto& viewId : viewIds)
{
if (viewId == it.second->getViewId() && !it.second->getUserPrivateInfo().empty())
{
oss << "{" << viewStrings[viewId];
oss << ",\"userprivateinfo\":" << it.second->getUserPrivateInfo();
oss << "},";
}
else
oss << "{" << viewStrings[viewId] << "},";
}
if (viewCount > 0)
oss.seekp(-1, std::ios_base::cur); // Remove last comma.
oss << ']';
it.second->sendTextFrame(oss.str());
}
}
std::shared_ptr<ChildSession> Document::findSessionByViewId(int viewId)
{
for (const auto& it : _sessions)
{
if (it.second->getViewId() == viewId)
return it.second;
}
return nullptr;
}
void Document::invalidateCanonicalId(const std::string& sessionId)
{
auto it = _sessions.find(sessionId);
if (it == _sessions.end())
{
LOG_ERR("Session [" << sessionId << "] not found");
return;
}
std::shared_ptr<ChildSession> session = it->second;
int newCanonicalId = _sessions.createCanonicalId(getViewProps(session));
if (newCanonicalId == session->getCanonicalViewId())
return;
session->setCanonicalViewId(newCanonicalId);
const std::string viewRenderedState = session->getViewRenderState();
std::string stateName;
if (!viewRenderedState.empty())
{
stateName = viewRenderedState;
}
else
{
stateName = "Empty";
}
std::string message = "canonicalidchange: viewid=" + std::to_string(session->getViewId()) +
" canonicalid=" + std::to_string(newCanonicalId) +
" viewrenderedstate=" + stateName;
session->sendTextFrame(message);
}
std::string Document::getViewProps(const std::shared_ptr<ChildSession>& session)
{
return session->getWatermarkText() + "|" + session->getViewRenderState();
}
void Document::updateEditorSpeeds(int id, int speed)
{
int maxSpeed = -1, fastestUser = -1;
auto now = std::chrono::steady_clock::now();
_lastUpdatedAt[id] = now;
_speedCount[id] = speed;
for (const auto& it : _sessions)
{
const std::shared_ptr<ChildSession> session = it.second;
int sessionId = session->getViewId();
auto duration = (_lastUpdatedAt[id] - now);
std::chrono::milliseconds::rep durationInMs = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
if (_speedCount[sessionId] != 0 && durationInMs > 5000)
{
_speedCount[sessionId] = session->getSpeed();
_lastUpdatedAt[sessionId] = now;
}
if (_speedCount[sessionId] > maxSpeed)
{
maxSpeed = _speedCount[sessionId];
fastestUser = sessionId;
}
}
// 0 for preventing selection of the first always
// 1 for preventing new users from directly becoming editors
if (_editorId != fastestUser && (maxSpeed != 0 && maxSpeed != 1)) {
if (!_editorChangeWarning && _editorId != -1)
{
_editorChangeWarning = true;
}
else
{
_editorChangeWarning = false;
_editorId = fastestUser;
for (const auto& it : _sessions)
it.second->sendTextFrame("editor: " + std::to_string(_editorId));
}
}
else
_editorChangeWarning = false;
}
// Get the color value for all author names from the core
std::map<std::string, int> Document::getViewColors()
{
char* values = _loKitDocument->getCommandValues(".uno:TrackedChangeAuthors");
const std::string colorValues = std::string(values == nullptr ? "" : values);
std::free(values);
std::map<std::string, int> viewColors;
try
{
if (!colorValues.empty())
{
Poco::JSON::Parser parser;
Poco::JSON::Object::Ptr root = parser.parse(colorValues).extract<Poco::JSON::Object::Ptr>();
if (root->get("authors").type() == typeid(Poco::JSON::Array::Ptr))
{
Poco::JSON::Array::Ptr authorsArray = root->get("authors").extract<Poco::JSON::Array::Ptr>();
for (auto& authorVar: *authorsArray)
{
Poco::JSON::Object::Ptr authorObj = authorVar.extract<Poco::JSON::Object::Ptr>();
std::string authorName = authorObj->get("name").convert<std::string>();
int colorValue = authorObj->get("color").convert<int>();
viewColors[authorName] = colorValue;
}
}
}
}
catch(const Exception& exc)
{
LOG_ERR("Poco Exception: " << exc.displayText() <<
(exc.nested() ? " (" + exc.nested()->displayText() + ')' : ""));
}
return viewColors;
}
std::string Document::getDefaultTheme(const std::shared_ptr<ChildSession>& session) const
{
bool darkTheme = session->getDarkTheme() == "true";
return darkTheme ? "Dark" : "Light";
}
std::shared_ptr<lok::Document> Document::load(const std::shared_ptr<ChildSession>& session,
const std::string& renderOpts)
{
const std::string sessionId = session->getId();
const std::string& uri = session->getJailedFilePath();
const std::string& uriAnonym = session->getJailedFilePathAnonym();
const std::string& userName = session->getUserName();
const std::string& userNameAnonym = session->getUserNameAnonym();
const std::string& docPassword = session->getDocPassword();
const bool haveDocPassword = session->getHaveDocPassword();
const std::string& lang = session->getLang();
const std::string& deviceFormFactor = session->getDeviceFormFactor();
const std::string& batchMode = session->getBatchMode();
const std::string& enableMacrosExecution = session->getEnableMacrosExecution();
const std::string& macroSecurityLevel = session->getMacroSecurityLevel();
const bool accessibilityState = session->getAccessibilityState();
const std::string& userTimezone = session->getTimezone();
if (!Util::isMobileApp())
consistencyCheckFileExists(uri);
std::string options;
if (!lang.empty())
options = "Language=" + lang;
if (!deviceFormFactor.empty())
options += ",DeviceFormFactor=" + deviceFormFactor;
if (!batchMode.empty())
options += ",Batch=" + batchMode;
if (!enableMacrosExecution.empty())
options += ",EnableMacrosExecution=" + enableMacrosExecution;
if (!macroSecurityLevel.empty())
options += ",MacroSecurityLevel=" + macroSecurityLevel;
if (!userTimezone.empty())
options += ",Timezone=" + userTimezone;
const std::string wopiCertDir = pathFromFileURL(session->getJailedFilePath() + ".certs");
if (FileUtil::Stat(wopiCertDir).exists())
::setenv("LO_CERTIFICATE_AUTHORITY_PATH", wopiCertDir.c_str(), 1);
#if !MOBILEAPP
// if ssl client verification was disabled in online for the wopi server,
// and this is a https connection then also exempt that host from ssl host
// verification in 'core'
if (session->isDisableVerifyHost())
{
std::string scheme, host, port;
if (net::parseUri(session->getDocURL(), scheme, host, port) && scheme == "https://")
::setenv("LOK_EXEMPT_VERIFY_HOST", host.c_str(), 1);
}
#endif
std::string spellOnline = session->getSpellOnline();
if (!_loKitDocument)
{
// This is the first time we are loading the document
LOG_INF("Loading new document from URI: [" << uriAnonym << "] for session [" << sessionId << "].");
_loKit->registerCallback(GlobalCallback, this);
const int flags = LOK_FEATURE_DOCUMENT_PASSWORD
| LOK_FEATURE_DOCUMENT_PASSWORD_TO_MODIFY
| LOK_FEATURE_PART_IN_INVALIDATION_CALLBACK
| LOK_FEATURE_NO_TILED_ANNOTATIONS
| LOK_FEATURE_RANGE_HEADERS
| LOK_FEATURE_VIEWID_IN_VISCURSOR_INVALIDATION_CALLBACK;
_loKit->setOptionalFeatures(flags);
// Save the provided password with us and the jailed url
_haveDocPassword = haveDocPassword;
_docPassword = docPassword;
_jailedUrl = uri;
_isDocPasswordProtected = false;
const char *pURL = uri.c_str();
LOG_DBG("Calling lokit::documentLoad(" << FileUtil::anonymizeUrl(pURL) << ", \"" << options << "\").");
const auto start = std::chrono::steady_clock::now();
_loKitDocument.reset(_loKit->documentLoad(pURL, options.c_str()));
#ifdef __ANDROID__
_loKitDocumentForAndroidOnly = _loKitDocument;
#endif
const auto duration = std::chrono::steady_clock::now() - start;
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(duration);
LOG_DBG("Returned lokit::documentLoad(" << FileUtil::anonymizeUrl(pURL) << ") in "
<< elapsed);
#ifdef IOS
DocumentData::get(_mobileAppDocId).loKitDocument = _loKitDocument.get();
#endif
if (!_loKitDocument || !_loKitDocument->get())
{
LOG_ERR("Failed to load: " << uriAnonym << ", error: " << _loKit->getError());
// Checking if wrong password or no password was reason for failure.
if (_isDocPasswordProtected)
{
LOG_INF("Document [" << uriAnonym << "] is password protected.");
if (!_haveDocPassword)
{
LOG_INF("No password provided for password-protected document [" << uriAnonym << "].");
std::string passwordFrame = "passwordrequired:";
if (_docPasswordType == DocumentPasswordType::ToView)
passwordFrame += "to-view";
else if (_docPasswordType == DocumentPasswordType::ToModify)
passwordFrame += "to-modify";
session->sendTextFrameAndLogError("error: cmd=load kind=" + passwordFrame);
}
else
{
LOG_INF("Wrong password for password-protected document [" << uriAnonym << "].");
session->sendTextFrameAndLogError("error: cmd=load kind=wrongpassword");
}
return nullptr;
}
session->sendTextFrameAndLogError("error: cmd=load kind=faileddocloading");
session->shutdownNormal();
LOG_FTL("Failed to load the document. Setting TerminationFlag");
SigUtil::setTerminationFlag();
return nullptr;
}
// Only save the options on opening the document.
// No support for changing them after opening a document.
_renderOpts = renderOpts;
}
else
{
LOG_INF("Document with url [" << uriAnonym << "] already loaded. Need to create new view for session [" << sessionId << "].");
// Check if this document requires password
if (_isDocPasswordProtected)
{
if (!haveDocPassword)
{
std::string passwordFrame = "passwordrequired:";
if (_docPasswordType == DocumentPasswordType::ToView)
passwordFrame += "to-view";
else if (_docPasswordType == DocumentPasswordType::ToModify)
passwordFrame += "to-modify";
session->sendTextFrameAndLogError("error: cmd=load kind=" + passwordFrame);
return nullptr;
}
else if (docPassword != _docPassword)
{
session->sendTextFrameAndLogError("error: cmd=load kind=wrongpassword");
return nullptr;
}
}
LOG_INF("Creating view to url [" << uriAnonym << "] for session [" << sessionId << "] with " << options << '.');
_loKitDocument->createView(options.c_str());
LOG_TRC("View to url [" << uriAnonym << "] created.");
switch (_loKitDocument->getDocumentType())
{
case LOK_DOCTYPE_TEXT:
case LOK_DOCTYPE_SPREADSHEET:
// writer and calc can have different spell checking settings per view, so use this users
// preference
break;
case LOK_DOCTYPE_PRESENTATION:
case LOK_DOCTYPE_DRAWING:
default:
// impress/draw currently cannot, so use the current document state
// so simply joining doesn't toggle that shared spelling state
if (char* viewRenderState = _loKitDocument->getCommandValues(".uno:ViewRenderState"))
{
StringVector tokens(StringVector::tokenize(viewRenderState, strlen(viewRenderState), ';'));
spellOnline = tokens[0] == "S" ? "true" : "false";
free(viewRenderState);
}
break;
}
}
std::string theme = getDefaultTheme(session);
LOG_INF("Initializing for rendering session [" << sessionId << "] on document url [" <<
anonymizeUrl(_url) << "] with: [" << makeRenderParams(_renderOpts, userNameAnonym, spellOnline, theme) << "].");
// initializeForRendering() should be called before
// registerCallback(), as the previous creates a new view in Impress.
const std::string renderParams = makeRenderParams(_renderOpts, userName, spellOnline, theme);
_loKitDocument->initializeForRendering(renderParams.c_str());
const int viewId = _loKitDocument->getView();
session->setViewId(viewId);
_sessionUserInfo[viewId] = UserInfo(session->getViewUserId(), session->getViewUserName(),
session->getViewUserExtraInfo(), session->getViewUserPrivateInfo(),
session->isReadOnly());
_loKitDocument->setViewLanguage(viewId, lang.c_str());
_loKitDocument->setViewTimezone(viewId, userTimezone.c_str());
_loKitDocument->setAccessibilityState(viewId, accessibilityState);
if (session->isReadOnly())
{
_loKitDocument->setViewReadOnly(viewId, true);
if (session->isAllowChangeComments())
{
_loKitDocument->setAllowChangeComments(viewId, true);
}
}
// viewId's monotonically increase, and CallbackDescriptors are never freed.
_viewIdToCallbackDescr.emplace(viewId,
std::unique_ptr<CallbackDescriptor>(new CallbackDescriptor({ this, viewId })));
_loKitDocument->registerCallback(ViewCallback, _viewIdToCallbackDescr[viewId].get());
const int viewCount = _loKitDocument->getViewsCount();
LOG_INF("Document url [" << anonymizeUrl(_url) << "] for session [" <<
sessionId << "] loaded view [" << viewId << "]. Have " <<
viewCount << " view" << (viewCount != 1 ? "s." : "."));
session->initWatermark();
if (char* viewRenderState = _loKitDocument->getCommandValues(".uno:ViewRenderState"))
{
session->setViewRenderState(viewRenderState);
free(viewRenderState);
}
invalidateCanonicalId(session->getId());
return _loKitDocument;
}
bool Document::forwardToChild(const std::string& prefix, const std::vector<char>& payload)
{
assert(payload.size() > prefix.size());
// Remove the prefix and trim.
std::size_t index = prefix.size();
for ( ; index < payload.size(); ++index)
{
if (payload[index] != ' ')
{
break;
}
}
const char* data = payload.data() + index;
std::size_t size = payload.size() - index;
std::string name;
std::string sessionId;
if (COOLProtocol::parseNameValuePair(prefix, name, sessionId, '-') && name == "child")
{
const auto it = _sessions.find(sessionId);
if (it != _sessions.end())
{
std::shared_ptr<ChildSession> session = it->second;
static const std::string disconnect("disconnect");
if (size == disconnect.size() &&
strncmp(data, disconnect.data(), disconnect.size()) == 0)
{
if(session->getViewId() == _editorId) {
_editorId = -1;
}
LOG_INF("Removing ChildSession [" << sessionId << "].");
// Tell them we're going quietly.
session->sendTextFrame("disconnected:");
_sessions.erase(it);
const std::size_t count = _sessions.size();
LOG_DBG("Have " << count << " child" << (count == 1 ? "" : "ren") <<
" after removing ChildSession [" << sessionId << "].");
_deltaGen->setSessionCount(count);
_sessionUserInfo[session->getViewId()].setDisconnected();
// No longer needed, and allow session dtor to take it.
session.reset();
return true;
}
// No longer needed, and allow the handler to take it.
if (session)
{
std::vector<char> vect(size);
vect.assign(data, data + size);
// TODO this is probably wrong...
session->handleMessage(vect);
return true;
}
}
std::string abbrMessage;
#ifndef BUILDING_TESTS
if (AnonymizeUserData)
{
abbrMessage = "...";
}
else
#endif
{
abbrMessage = getAbbreviatedMessage(data, size);
}
LOG_ERR("Child session [" << sessionId << "] not found to forward message: " << abbrMessage);
}
else
{
LOG_ERR("Failed to parse prefix of forward-to-child message: " << prefix);
}
return false;
}
namespace {
template <typename T>
Object::Ptr makePropertyValue(const std::string& type, const T& val)
{
Object::Ptr obj = new Object();
obj->set("type", type);
obj->set("value", val);
return obj;
}
}
/* static */ std::string Document::makeRenderParams(const std::string& renderOpts, const std::string& userName,
const std::string& spellOnline, const std::string& theme)
{
Object::Ptr renderOptsObj;
// Fill the object with renderoptions, if any
if (!renderOpts.empty())
{
Parser parser;
Poco::Dynamic::Var var = parser.parse(renderOpts);
renderOptsObj = var.extract<Object::Ptr>();
}
else
{
renderOptsObj = new Object();
}
// Append name of the user, if any, who opened the document to rendering options
if (!userName.empty())
{
// userName must be decoded already.
renderOptsObj->set(".uno:Author", makePropertyValue("string", userName));
}
// By default we enable spell-checking, unless it's disabled explicitly.
if (!spellOnline.empty())
{
const bool bSet = (spellOnline != "false");
renderOptsObj->set(".uno:SpellOnline", makePropertyValue("boolean", bSet));
}
if (!theme.empty())
renderOptsObj->set(".uno:ChangeTheme", makePropertyValue("string", theme));
if (renderOptsObj)
{
std::ostringstream ossRenderOpts;
renderOptsObj->stringify(ossRenderOpts);
return ossRenderOpts.str();
}
return std::string();
}
bool Document::isTileRequestInsideVisibleArea(const TileCombined& tileCombined)
{
const auto session = _sessions.findByCanonicalId(tileCombined.getNormalizedViewId());
if (!session)
return false;
for (const auto& rTile : tileCombined.getTiles())
{
if (session->isTileInsideVisibleArea(rTile))
return true;
}
return false;
}
// poll is idle, are we ?
void Document::checkIdle()
{
// FIXME: can have Idle CallbackFlushHandler work in the core.
if (!processInputEnabled() || hasQueueItems())
{
LOG_TRC("Nearly idle - but have more queued items to process");
return; // more to do
}
sendTextFrame("idle");
// get rid of idle check for now.
ProcessToIdleDeadline = std::chrono::steady_clock::now() - std::chrono::milliseconds(10);
}
bool Document::processInputEnabled() const
{
bool enabled = !_websocketHandler || _websocketHandler->processInputEnabled();
if (!enabled)
LOG_TRC("Document - not processing input");
return enabled;
}
void Document::drainCallbacks()
{
KitQueue::Callback cb;
LOG_TRC("drainCallbacks with " << _queue->callbackSize() << " items");
while (_queue && _queue->getCallback(cb))
{
if (_stop || SigUtil::getTerminationFlag())
{
LOG_INF("_stop or TerminationFlag is set, breaking Document::drainCallbacks");
break;
}
LOG_TRC("Kit handling callback " << cb);
int viewId = cb._view;
bool broadcast = cb._view == -1;
const int type = cb._type;
const std::string &payload = cb._payload;
// Forward the callback to the same view, demultiplexing is done by the LibreOffice core.
bool isFound = false;
for (const auto& it : _sessions)
{
ChildSession& session = *it.second;
if (broadcast || (!broadcast && (session.getViewId() == viewId)))
{
if (!session.isCloseFrame())
{
isFound = true;
session.loKitCallback(type, payload);
}
else
{
LOG_ERR("Session-thread of session ["
<< session.getId() << "] for view [" << viewId
<< "] is not running. Dropping ["
<< lokCallbackTypeToString(type) << "] payload ["
<< COOLProtocol::getAbbreviatedMessage(payload)
<< ']');
}
if (!broadcast)
break;
}
}
if (!isFound)
LOG_ERR("Document::ViewCallback. Session [" << viewId <<
"] is no longer active to process [" << lokCallbackTypeToString(type) <<
"] [" << COOLProtocol::getAbbreviatedMessage(payload) <<
"] message to Master Session.");
}
if (_websocketHandler)
_websocketHandler->flush();
}
void Document::drainQueue()
{
if (UnitKit::get().filterDrainQueue())
{
LOG_TRC("Filter disabled drainQueue");
return;
}
try
{
if (hasCallbacks())
drainCallbacks();
if (hasQueueItems())
LOG_TRC("drainQueue with " << _queue->size() <<
" items: " << (processInputEnabled() ? "processing" : "blocked") );
// FIXME: do we really want to process all of these items ?
while (processInputEnabled() && hasQueueItems())
{
if (_stop || SigUtil::getTerminationFlag())
{
LOG_INF("_stop or TerminationFlag is set, breaking Document::drainQueue of loop");
_queue->clearTileQueue();
_deltaPool.stop();
break;
}
const KitQueue::Payload input = _queue->pop();
LOG_TRC("Kit handling queue message: " << COOLProtocol::getAbbreviatedMessage(input));
const StringVector tokens = StringVector::tokenize(input.data(), input.size());
if (tokens.equals(0, "eof"))
{
LOG_INF("Received EOF. Finishing.");
break;
}
else if (tokens.equals(0, "tile") || tokens.equals(0, "tilecombine"))
{
assert(false && "Should not have incoming tile requests in message queue");
}
else if (tokens.startsWith(0, "child-"))
{
forwardToChild(tokens[0], input);
}
else if (tokens.equals(0, "processtoidle"))
{
ProcessToIdleDeadline = std::chrono::steady_clock::now();
uint32_t timeoutUs = 0;
if (tokens.getUInt32(1, "timeout", timeoutUs))
ProcessToIdleDeadline += std::chrono::microseconds(timeoutUs);
}
else if (tokens.equals(0, "callback"))
{
assert(false && "callbacks cannot now appear on the incoming queue");
}
else
{
LOG_ERR("Unexpected request: [" << COOLProtocol::getAbbreviatedMessage(input) << "].");
}
}
if (processInputEnabled() && !isLoadOngoing() &&
!isBackgroundSaveProcess() && _queue->getTileQueueSize() > 0)
{
std::vector<TileCombined> tileRequests = _queue->popWholeTileQueue();
// Put requests that include tiles in the visible area to the front to handle those first
std::partition(tileRequests.begin(), tileRequests.end(), [this](const TileCombined& req) {
return isTileRequestInsideVisibleArea(req); });
for (auto& tileCombined : tileRequests)
renderTiles(tileCombined);
}
}
catch (const std::exception& exc)
{
LOG_FTL("drainQueue: Exception: " << exc.what());
if (!Util::isMobileApp())
flushAndExit(EX_SOFTWARE);
}
catch (...)
{
LOG_FTL("drainQueue: Unknown exception");
if (!Util::isMobileApp())
flushAndExit(EX_SOFTWARE);
}
}
/// Return access to the lok::Document instance.
std::shared_ptr<lok::Document> Document::getLOKitDocument()
{
if (!_loKitDocument)
{
LOG_ERR("Document [" << _docKey << "] is not loaded.");
throw std::runtime_error("Document " + _docKey + " is not loaded.");
}
return _loKitDocument;
}
void Document::postForceModifiedCommand(bool modified)
{
std::string args = "{ \"Modified\": { \"type\": \"boolean\", ";
args += "\"value\": \"";
args += (modified ? "true" : "false");
args += "\" } }";
LOG_TRC("post force modified command: .uno:Modified " << args);
// Interestingly this seems not to notify the modified state change.
getLOKitDocument()->postUnoCommand(
".uno:Modified", args.c_str(),
false /* avoid an un-necessary unocommandresult */);
}
void Document::forceDocUnmodifiedForBgSave(int viewId)
{
LOG_TRC("force document unmodified from state " << toString(_modified));
if (_modified == ModifiedState::Modified)
{
getLOKitDocument()->setView(viewId);
SigUtil::addActivity("Force clear modified");
_modified = ModifiedState::UnModifiedButSaving;
// but tell the core we are not modified to track real changes
postForceModifiedCommand(false);
}
}
void Document::updateModifiedOnFailedBgSave()
{
if (_modified == ModifiedState::UnModifiedButSaving)
{
SigUtil::addActivity("Force re-modified");
_modified = ModifiedState::Modified;
postForceModifiedCommand(true);
}
}
void Document::notifySyntheticUnmodifiedState()
{
// no need to change core state that happened earlier
if (_modified == ModifiedState::UnModifiedButSaving)
{
LOG_TRC("document was not modified while background saving");
_modified = ModifiedState::UnModified;
notifyAll("statechanged: .uno:ModifiedStatus=false");
}
}
bool Document::trackDocModifiedState(const std::string &stateChanged)
{
bool filter = false;
StringVector tokens(StringVector::tokenize(stateChanged, '='));
bool modified = tokens.size() > 1 && tokens.equals(1, "true");
ModifiedState newState = _modified;
// NB. since 'modified' state is (oddly) notified per view we get
// several duplicate transitions from state A -> A again.
switch (_modified) {
case ModifiedState::Modified:
if (!modified)
newState = ModifiedState::UnModified;
// else duplicate
break;
// Only present in background save mode
case ModifiedState::UnModifiedButSaving:
if (modified)
{
// now we're really modified
newState = ModifiedState::Modified;
}
else // ignore being notified of our own force unmodification.
{
LOG_TRC("Ignore self generated unmodified notification");
filter = true;
}
break;
case ModifiedState::UnModified:
if (modified)
newState = ModifiedState::Modified;
// else duplicate
break;
}
if (_modified != newState)
LOG_TRC("Transition modified state from " <<
toString(_modified) << " to " << toString(newState));
_modified = newState;
return filter;
}
void Document::disableBgSave(const std::string &reason)
{
LOG_WRN("Disabled background save " + reason);
_isBgSaveDisabled = true;
}
/// Stops theads, flushes buffers, and exits the process.
void Document::flushAndExit(int code)
{
flushTraceEventRecordings();
_deltaPool.stop();
if (!Util::isKitInProcess())
Util::forcedExit(code);
else
SigUtil::setTerminationFlag();
}
void Document::dumpState(std::ostream& oss)
{
oss << "Kit Document:\n"
<< std::boolalpha
<< "\n\tpid: " << getpid()
<< "\n\tstop: " << _stop
<< "\n\tjailId: " << _jailId
<< "\n\tdocKey: " << _docKey
<< "\n\tdocId: " << _docId
<< "\n\turl: " << _url
<< "\n\tobfuscatedFileId: " << _obfuscatedFileId
<< "\n\tjailedUrl: " << _jailedUrl
<< "\n\trenderOpts: " << _renderOpts
<< "\n\thaveDocPassword: " << _haveDocPassword // not the pwd itself
<< "\n\tisDocPasswordProtected: " << _isDocPasswordProtected
<< "\n\tdocPasswordType: " << (int)_docPasswordType
<< "\n\teditorId: " << _editorId
<< "\n\teditorChangeWarning: " << _editorChangeWarning
<< "\n\tmobileAppDocId: " << _mobileAppDocId
<< "\n\tinputProcessingEnabled: " << processInputEnabled()
<< "\n\tduringLoad: " << _duringLoad
<< "\n\tmodified: " << toString(_modified)
<< "\n\tbgSaveProc: " << _isBgSaveProcess
<< "\n\tbgSaveDisabled: "<< _isBgSaveDisabled
<< "\n";
// dumpState:
// TODO: _websocketHandler - but this is an odd one.
_queue->dumpState(oss);
oss << "\tviewIdToCallbackDescr:";
for (const auto &it : _viewIdToCallbackDescr)
{
oss << "\n\t\tviewId: " << it.first
<< " editorId: " << it.second->getDoc()->getEditorId()
<< " mobileAppDocId: " << it.second->getDoc()->getMobileAppDocId();
}
oss << "\n";
_deltaPool.dumpState(oss);
_sessions.dumpState(oss);
_deltaGen->dumpState(oss);
oss << "\tlastUpdatedAt:";
for (const auto &it : _lastUpdatedAt)
{
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
it.second.time_since_epoch()).count();
oss << "\n\t\tviewId: " << it.first
<< " last update time(ms): " << ms;
}
oss << "\n";
oss << "\tspeedCount:";
for (const auto &it : _speedCount)
{
oss << "\n\t\tviewId: " << it.first
<< " speed: " << it.second;
}
oss << "\n";
/// For showing disconnected user info in the doc repair dialog.
oss << "\tsessionUserInfo:";
for (const auto &it : _sessionUserInfo)
{
oss << "\n\t\tviewId: " << it.first
<< " userId: " << it.second.getUserId()
<< " userName: " << it.second.getUserName()
<< " userExtraInfo: " << it.second.getUserExtraInfo()
<< " readOnly: " << it.second.isReadOnly()
<< " connected: " << it.second.isConnected();
}
oss << "\n";
char *pState = nullptr;
_loKit->dumpState("", &pState);
oss << "lok state:\n";
if (pState)
oss << pState;
oss << "\n";
}
#if !defined BUILDING_TESTS && !MOBILEAPP && !LIBFUZZER
// When building the fuzzer we link COOLWSD.cpp into the same executable so the
// Protected::emitOneRecording() there gets used. When building the unit tests the one in
// TraceEvent.cpp gets used.
static std::mutex traceEventLock;
static std::vector<std::string> traceEventRecords[2];
void flushTraceEventRecordings()
{
std::unique_lock<std::mutex> lock(traceEventLock);
for (size_t n = 0; n < 2; ++n)
{
std::vector<std::string> &r = traceEventRecords[n];
if (r.empty())
continue;
std::size_t totalLength = 32; // Provision for the command name.
for (const auto& i: r)
totalLength += i.length();
std::string recordings;
recordings.reserve(totalLength);
recordings.append(n == 0 ? "forcedtraceevent: \n" : "traceevent: \n");
for (const auto& i: r)
recordings += i;
singletonDocument->sendTextFrame(recordings);
r.clear();
}
}
static void addRecording(const std::string &recording, bool force)
{
// This can be called before the config system is initialized. Guard against that, as calling
// config::getBool() would cause an assertion failure.
static bool configChecked = false;
static bool traceEventsEnabled;
if (!configChecked && config::isInitialized())
{
traceEventsEnabled = config::getBool("trace_event[@enable]", false);
configChecked = true;
}
if (configChecked && !traceEventsEnabled)
return;
// catch if this gets called in the ForKit process & skip.
if (singletonDocument == nullptr)
return;
if (!TraceEvent::isRecordingOn() && !force)
return;
std::unique_lock<std::mutex> lock(traceEventLock);
traceEventRecords[force ? 0 : 1].push_back(recording + "\n");
}
void TraceEvent::emitOneRecordingIfEnabled(const std::string &recording)
{
addRecording(recording, true);
}
void TraceEvent::emitOneRecording(const std::string &recording)
{
addRecording(recording, false);
}
#else
void flushTraceEventRecordings()
{
}
#endif
#ifdef __ANDROID__
std::shared_ptr<lok::Document> Document::_loKitDocumentForAndroidOnly = std::shared_ptr<lok::Document>();
std::shared_ptr<lok::Document> getLOKDocumentForAndroidOnly()
{
return Document::_loKitDocumentForAndroidOnly;
}
#endif
KitSocketPoll::KitSocketPoll() : SocketPoll("kit")
{
#ifdef IOS
terminationFlag = false;
#endif
mainPoll = this;
}
KitSocketPoll::~KitSocketPoll()
{
// Just to make it easier to set a breakpoint
mainPoll = nullptr;
}
void KitSocketPoll::dumpGlobalState(std::ostream& oss) // static
{
if (mainPoll)
{
if (!mainPoll->_document)
oss << "KitSocketPoll: no doc\n";
else
{
mainPoll->_document->dumpState(oss);
mainPoll->dumpState(oss);
}
}
else
oss << "KitSocketPoll: none\n";
}
std::shared_ptr<KitSocketPoll> KitSocketPoll::create() // static
{
std::shared_ptr<KitSocketPoll> result(new KitSocketPoll());
#ifdef IOS
{
std::unique_lock<std::mutex> lock(KSPollsMutex);
KSPolls.push_back(result);
}
KitSocketPoll::KSPollsCV.notify_all();
#endif
return result;
}
/* static */ void KitSocketPoll::cleanupChildProcess()
{
mainPoll->closeAllSockets();
mainPoll->createWakeups();
}
// process pending message-queue events.
void KitSocketPoll::drainQueue()
{
SigUtil::checkDumpGlobalState(dump_kit_state);
if (_document)
_document->drainQueue();
}
// called from inside poll, inside a wakeup
void KitSocketPoll::wakeupHook() { _pollEnd = std::chrono::steady_clock::now(); }
// a LOK compatible poll function merging the functions.
// returns the number of events signalled
int KitSocketPoll::kitPoll(int timeoutMicroS)
{
ProfileZone profileZone("KitSocketPoll::kitPoll");
if (SigUtil::getTerminationFlag())
{
LOG_TRC("Termination of unipoll mainloop flagged");
return -1;
}
#if ENABLE_DEBUG
#if !MOBILEAPP
auto &unitKit = UnitKit::get();
if (unitKit.isFinished())
{
static bool sentResult = false;
if (!sentResult && singletonDocument)
{
LOG_TRC("Sending unit test result");
singletonDocument->sendTextFrame(unitKit.getResultMessage());
sentResult = true;
}
}
#endif
static std::atomic<int> reentries = 0;
static int lastWarned = 1;
ReEntrancyGuard guard(reentries);
if (reentries != lastWarned)
{
LOG_ERR("non-async dialog triggered");
#if !MOBILEAPP
if (singletonDocument && lastWarned < reentries)
singletonDocument->alertNotAsync();
#endif
lastWarned = reentries;
}
#endif
// The maximum number of extra events to process beyond the first.
int maxExtraEvents = 15;
int eventsSignalled = 0;
auto startTime = std::chrono::steady_clock::now();
// handle processtoidle waiting optimization
bool checkForIdle = ProcessToIdleDeadline >= startTime;
if (timeoutMicroS < 0)
{
// Flush at most 1 + maxExtraEvents, or return when nothing left.
while (poll(std::chrono::microseconds::zero()) > 0 && maxExtraEvents-- > 0)
++eventsSignalled;
}
else
{
if (checkForIdle)
timeoutMicroS = 0;
// Flush at most maxEvents+1, or return when nothing left.
_pollEnd = startTime + std::chrono::microseconds(timeoutMicroS);
do
{
int realTimeout = timeoutMicroS;
if (_document && _document->needsQuickPoll())
realTimeout = 0;
if (poll(std::chrono::microseconds(realTimeout)) <= 0)
break;
const auto now = std::chrono::steady_clock::now();
drainQueue();
timeoutMicroS =
std::chrono::duration_cast<std::chrono::microseconds>(_pollEnd - now).count();
++eventsSignalled;
} while (timeoutMicroS > 0 && !SigUtil::getTerminationFlag() && maxExtraEvents-- > 0);
}
if (_document && checkForIdle && eventsSignalled == 0 && timeoutMicroS > 0 &&
!hasCallbacks() && !hasBuffered())
{
auto remainingTime = ProcessToIdleDeadline - startTime;
LOG_TRC(
"Poll of "
<< timeoutMicroS << " vs. remaining time of: "
<< std::chrono::duration_cast<std::chrono::microseconds>(remainingTime).count());
// would we poll until then if we could ?
if (remainingTime < std::chrono::microseconds(timeoutMicroS))
_document->checkIdle();
else
LOG_TRC("Poll of would not close gap - continuing");
}
drainQueue();
if (_document)
_document->trimAfterInactivity();
if (!Util::isMobileApp())
{
flushTraceEventRecordings();
if (_document && _document->purgeSessions() == 0)
{
LOG_INF("Last session discarded. Setting TerminationFlag");
SigUtil::setTerminationFlag();
return -1;
}
}
// Report the number of events we processed.
return eventsSignalled;
}
// unusual LOK event from another thread, push into our loop to process.
bool KitSocketPoll::pushToMainThread(LibreOfficeKitCallback callback, int type,
const char* p, void* data) // static
{
if (mainPoll && mainPoll->getThreadOwner() != std::this_thread::get_id())
{
LOG_TRC("Unusual push callback to main thread");
std::shared_ptr<std::string> pCopy;
if (p)
pCopy = std::make_shared<std::string>(p, strlen(p));
mainPoll->addCallback([=] {
LOG_TRC("Unusual process callback in main thread");
callback(type, pCopy ? pCopy->c_str() : nullptr, data);
});
return true;
}
return false;
}
KitSocketPoll *KitSocketPoll::mainPoll = nullptr;
bool pushToMainThread(LibreOfficeKitCallback cb, int type, const char *p, void *data)
{
return KitSocketPoll::pushToMainThread(cb, type, p, data);
}
#ifdef IOS
std::mutex KitSocketPoll::KSPollsMutex;
std::condition_variable KitSocketPoll::KSPollsCV;
std::vector<std::weak_ptr<KitSocketPoll>> KitSocketPoll::KSPolls;
#endif
void documentViewCallback(const int type, const char* payload, void* data)
{
Document::ViewCallback(type, payload, data);
}
/// Called by LOK main-loop the central location for data processing.
int pollCallback(void* pData, int timeoutUs)
{
if (timeoutUs < 0)
timeoutUs = SocketPoll::DefaultPollTimeoutMicroS.count();
#ifndef IOS
if (!pData)
return 0;
else
return reinterpret_cast<KitSocketPoll*>(pData)->kitPoll(timeoutUs);
#else
std::unique_lock<std::mutex> lock(KitSocketPoll::KSPollsMutex);
std::vector<std::shared_ptr<KitSocketPoll>> v;
for (const auto &i : KitSocketPoll::KSPolls)
{
auto p = i.lock();
if (p)
v.push_back(p);
}
if (v.empty())
{
// Remove any stale elements from KitSocketPoll::KSPolls and
// block until an element is added to KitSocketPoll::KSPolls
KitSocketPoll::KSPolls.clear();
KitSocketPoll::KSPollsCV.wait(lock, []{ return KitSocketPoll::KSPolls.size(); });
}
else
{
lock.unlock();
for (const auto &p : v)
p->kitPoll(timeoutUs);
}
// We never want to exit the main loop
return 0;
#endif
}
/// Called by LOK main-loop
void wakeCallback(void* pData)
{
#ifndef IOS
if (!pData)
return;
else
return reinterpret_cast<KitSocketPoll*>(pData)->wakeup();
#else
std::unique_lock<std::mutex> lock(KitSocketPoll::KSPollsMutex);
if (KitSocketPoll::KSPolls.empty())
return;
std::vector<std::shared_ptr<KitSocketPoll>> v;
for (const auto &i : KitSocketPoll::KSPolls)
{
auto p = i.lock();
if (p)
v.push_back(p);
}
lock.unlock();
for (const auto &p : v)
p->wakeup();
#endif
}
#ifndef BUILDING_TESTS
namespace
{
#if !MOBILEAPP
void copyCertificateDatabaseToTmp(Poco::Path const& jailPath)
{
std::string aCertificatePathString = config::getString("certificates.database_path", "");
if (!aCertificatePathString.empty())
{
auto aFileStat = FileUtil::Stat(aCertificatePathString);
if (!aFileStat.exists() || !aFileStat.isDirectory())
{
LOG_WRN("Certificate database wasn't copied into the jail as path '" << aCertificatePathString << "' doesn't exist");
return;
}
Poco::Path aCertificatePath(aCertificatePathString);
Poco::Path aJailedCertDBPath(jailPath, "/tmp/certdb");
Poco::File(aJailedCertDBPath).createDirectories();
bool bCopied = false;
for (const char* pFilename : { "cert8.db", "cert9.db", "secmod.db", "key3.db", "key4.db" })
{
bool bResult = FileUtil::copy(Poco::Path(aCertificatePath, pFilename).toString(),
Poco::Path(aJailedCertDBPath, pFilename).toString(), false, false);
bCopied |= bResult;
}
if (bCopied)
{
LOG_INF("Certificate database files found in '" << aCertificatePathString << "' and were copied to the jail");
::setenv("LO_CERTIFICATE_DATABASE_PATH", "/tmp/certdb", 1);
}
else
{
LOG_WRN("No Certificate database files could be found in path '" << aCertificatePathString << "'");
}
}
}
#endif
}
void lokit_main(
#if !MOBILEAPP
const std::string& childRoot,
const std::string& jailId,
const std::string& sysTemplate,
const std::string& loTemplate,
bool noCapabilities,
bool noSeccomp,
bool useMountNamespaces,
bool queryVersion,
bool displayVersion,
#else
int docBrokerSocket,
const std::string& userInterface,
#endif
std::size_t numericIdentifier
)
{
#if !MOBILEAPP
if (!Util::isKitInProcess())
{
// Already set by COOLWSD.cpp
SigUtil::setFatalSignals("kit startup of " COOLWSD_VERSION " " COOLWSD_VERSION_HASH);
SigUtil::setUserSignals();
}
// Are we the first ever kit ? if so, we havn't tweaked our logging by
// the time we get here; FIXME: much of this is un-necessary duplication.
// Reinitialize logging when forked.
const bool logToFile = std::getenv("COOL_LOGFILE");
const char* logFilename = std::getenv("COOL_LOGFILENAME");
const char* logLevel = std::getenv("COOL_LOGLEVEL");
const char* logDisabledAreas = std::getenv("COOL_LOGDISABLED_AREAS");
const char* logLevelStartup = std::getenv("COOL_LOGLEVEL_STARTUP");
const bool logColor = config::getBool("logging.color", true) && isatty(fileno(stderr));
std::map<std::string, std::string> logProperties;
if (logToFile && logFilename)
{
logProperties["path"] = std::string(logFilename);
}
Util::rng::reseed();
const std::string LogLevel = logLevel ? logLevel : "trace";
const std::string LogLevelStartup = logLevelStartup ? logLevelStartup : "trace";
const bool bTraceStartup = (std::getenv("COOL_TRACE_STARTUP") != nullptr);
Log::initialize("kit", bTraceStartup ? LogLevelStartup : logLevel, logColor, logToFile, logProperties);
if (bTraceStartup && LogLevel != LogLevelStartup)
{
LOG_INF("Setting log-level to [" << LogLevelStartup << "] and delaying "
"setting to [" << LogLevel << "] until after Kit initialization.");
}
const std::string LogDisabledAreas = logDisabledAreas ? logDisabledAreas : "";
const char* pAnonymizationSalt = std::getenv("COOL_ANONYMIZATION_SALT");
if (pAnonymizationSalt)
{
AnonymizationSalt = std::stoull(std::string(pAnonymizationSalt));
AnonymizeUserData = true;
}
LOG_INF("User-data anonymization is " << (AnonymizeUserData ? "enabled." : "disabled."));
const char* pEnableWebsocketURP = std::getenv("ENABLE_WEBSOCKET_URP");
EnableWebsocketURP = pEnableWebsocketURP && std::string(pEnableWebsocketURP) == "true";
assert(!childRoot.empty());
assert(!sysTemplate.empty());
assert(!loTemplate.empty());
LOG_INF("Kit process for Jail [" << jailId << "] started.");
std::string userdir_url;
std::string instdir_path;
int ProcSMapsFile = -1;
// lokit's destroy typically throws from
// framework/source/services/modulemanager.cxx:198
// So we insure it lives until std::_Exit is called.
std::shared_ptr<lok::Office> loKit;
ChildSession::NoCapsForKit = noCapabilities;
#endif // MOBILEAPP
// Setup the OSL sandbox
std::string allowedPaths;
try
{
#if !MOBILEAPP
const Path jailPath = Path::forDirectory(childRoot + '/' + jailId);
const std::string jailPathStr = jailPath.toString();
JailUtil::createJailPath(jailPathStr);
// initialize while we have access to /proc/self/task
threadCounter.reset(new Util::ThreadCounter());
// initialize while we have access to /proc/self/fd
fdCounter.reset(new Util::FDCounter());
if (!ChildSession::NoCapsForKit)
{
std::chrono::time_point<std::chrono::steady_clock> jailSetupStartTime
= std::chrono::steady_clock::now();
userdir_url = "file:///tmp/user";
instdir_path = '/' + std::string(JailUtil::LO_JAIL_SUBPATH) + "/program";
allowedPaths += ":r:/" + std::string(JailUtil::LO_JAIL_SUBPATH);
Poco::Path jailLOInstallation(jailPath, JailUtil::LO_JAIL_SUBPATH);
jailLOInstallation.makeDirectory();
const std::string loJailDestPath = jailLOInstallation.toString();
const std::string tempRoot = Poco::Path(childRoot, "tmp").toString();
const std::string tmpSubDir = Poco::Path(tempRoot, "cool-" + jailId).toString();
const std::string jailTmpDir = Poco::Path(jailPath, "tmp").toString();
// The bind-mount implementation: inlined here to mirror
// the fallback link/copy version bellow.
const auto mountJail = [&]() -> bool {
// Mount sysTemplate for the jail directory.
LOG_INF("Mounting " << sysTemplate << " -> " << jailPathStr);
if (!JailUtil::bind(sysTemplate, jailPathStr)
|| !JailUtil::remountReadonly(sysTemplate, jailPathStr))
{
LOG_ERR("Failed to mount [" << sysTemplate << "] -> [" << jailPathStr
<< "], will link/copy contents.");
return false;
}
// Mount loTemplate inside it.
LOG_INF("Mounting " << loTemplate << " -> " << loJailDestPath);
if (!FileUtil::Stat(loJailDestPath).exists())
{
LOG_DBG("The mount-point [" << loJailDestPath
<< "] doesn't exist. Binding will likely fail");
}
if (!JailUtil::bind(loTemplate, loJailDestPath)
|| !JailUtil::remountReadonly(loTemplate, loJailDestPath))
{
LOG_WRN("Failed to mount [" << loTemplate << "] -> [" << loJailDestPath
<< "], will link/copy contents");
return false;
}
// tmpdir inside the jail for added sercurity.
Poco::File(tmpSubDir).createDirectories();
LOG_INF("Mounting random temp dir " << tmpSubDir << " -> " << jailTmpDir);
if (!JailUtil::bind(tmpSubDir, jailTmpDir))
{
LOG_ERR("Failed to mount [" << tmpSubDir << "] -> [" << jailTmpDir
<< "], will link/copy contents.");
return false;
}
return true;
};
bool usingMountNamespace = false;
#ifndef __FreeBSD__
const uid_t origuid = geteuid();
const gid_t origgid = getegid();
// create a namespace and map to root uid/gid
if (useMountNamespaces)
{
LOG_DBG("Move into user namespace as uid 0");
if (!JailUtil::enterMountingNS(origuid, origgid))
LOG_ERR("Linux mount namespace for kit failed: " << strerror(errno));
else
usingMountNamespace = true;
}
#endif
// Copy (link) LO installation and other necessary files into it from the template.
bool bindMount = JailUtil::isBindMountingEnabled();
if (bindMount)
{
#if CODE_COVERAGE
// Code coverage is not supported with bind-mounting.
LOG_ERR("Mounting is not compatible with code-coverage.");
assert(!"Mounting is not compatible with code-coverage.");
#endif // CODE_COVERAGE
if (!mountJail())
{
LOG_INF("Cleaning up jail before linking/copying.");
JailUtil::tryRemoveJail(jailPathStr);
bindMount = false;
JailUtil::disableBindMounting();
}
}
#ifndef __FreeBSD__
if (usingMountNamespace)
{
// create another namespace, map back to original uid/gid after chroot
LOG_DBG("Move into user namespace as uid " << origuid);
if (!JailUtil::enterUserNS(origuid, origgid))
LOG_ERR("Linux user namespace for kit failed: " << strerror(errno));
}
assert(origuid == geteuid());
assert(origgid == getegid());
#endif
if (!bindMount)
{
LOG_INF("Mounting is disabled, will link/copy " << sysTemplate << " -> "
<< jailPathStr);
// Make sure we have the jail directory.
JailUtil::createJailPath(jailPathStr);
// Create a file to mark this a copied jail.
JailUtil::markJailCopied(jailPathStr);
const std::string linkablePath = childRoot + "/linkable";
linkOrCopy(sysTemplate, jailPath, linkablePath, LinkOrCopyType::All);
linkOrCopy(loTemplate, loJailDestPath, linkablePath, LinkOrCopyType::LO);
#if CODE_COVERAGE
// Link the .gcda files.
linkGCDAFiles(jailPathStr);
#endif
// Update the dynamic files inside the jail.
if (!JailUtil::SysTemplate::updateDynamicFiles(jailPathStr))
{
LOG_ERR(
"Failed to update the dynamic files in the jail ["
<< jailPathStr
<< "]. If the systemplate directory is owned by a superuser or is "
"read-only, running the installation scripts with the owner's account "
"should update these files. Some functionality may be missing.");
}
if (usingMountNamespace)
{
Poco::File(tempRoot).createDirectories();
if (symlink(jailTmpDir.c_str(), tmpSubDir.c_str()) == -1)
{
LOG_SFL("Failed to create symlink [" << tmpSubDir << " -> " << jailTmpDir << "] " << strerror(errno));
Util::forcedExit(EX_SOFTWARE);
}
}
}
// Setup /tmp and set TMPDIR.
::setenv("TMPDIR", "/tmp", 1);
allowedPaths += ":w:/tmp";
copyCertificateDatabaseToTmp(jailPath);
// HOME must be writable, so create it in /tmp.
constexpr const char* HomePathInJail = "/tmp/home";
Poco::File(Poco::Path(jailPath, HomePathInJail)).createDirectories();
::setenv("HOME", HomePathInJail, 1);
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - jailSetupStartTime);
LOG_DBG("Initialized jail files in " << ms);
// The bug is that rewinding and rereading /proc/self/smaps_rollup doubles the previous
// values, so it only affects the case where we reuse the fd from opening smaps_rollup
const bool brokenSmapsRollup = (std::getenv("COOL_DISABLE_SMAPS_ROLLUP") != nullptr);
ProcSMapsFile = !brokenSmapsRollup ? open("/proc/self/smaps_rollup", O_RDONLY) : -1;
if (ProcSMapsFile < 0)
{
if (!brokenSmapsRollup)
LOG_WRN("Failed to open /proc/self/smaps_rollup. Memory stats will be slower");
ProcSMapsFile = open("/proc/self/smaps", O_RDONLY);
if (ProcSMapsFile < 0)
LOG_SYS("Failed to open /proc/self/smaps. Memory stats will be missing.");
}
LOG_INF("chroot(\"" << jailPathStr << "\")");
if (chroot(jailPathStr.c_str()) == -1)
{
LOG_SFL("chroot(\"" << jailPathStr << "\") failed");
Util::forcedExit(EX_SOFTWARE);
}
if (chdir("/") == -1)
{
LOG_SFL("chdir(\"/\") in jail failed");
Util::forcedExit(EX_SOFTWARE);
}
#ifndef __FreeBSD__
if (usingMountNamespace)
{
// We have a full set of capabilities in the namespace so drop
// them all
if (dropAllCapabilities() == -1)
LOG_ERR("Failed to drop all capabilities");
}
else
{
dropCapability(CAP_SYS_CHROOT);
dropCapability(CAP_FOWNER);
dropCapability(CAP_CHOWN);
}
#endif
char *capText = cap_to_text(cap_get_proc(), nullptr);
LOG_DBG("Initialized jail nodes, dropped caps. Final caps are: " << capText);
cap_free(capText);
}
else // noCapabilities set
{
LOG_WRN("Security warning: running without chroot jails is insecure.");
LOG_INF("Using template ["
<< loTemplate << "] as install subpath directly, without chroot jail setup.");
userdir_url = "file://" + jailPathStr + "tmp/user";
instdir_path = '/' + loTemplate + "/program";
allowedPaths += ":r:" + loTemplate;
JailRoot = jailPathStr;
std::string tmpPath = jailPathStr + "tmp";
::setenv("TMPDIR", tmpPath.c_str(), 1);
allowedPaths += ":w:" + tmpPath;
LOG_DBG("Using tmpdir [" << tmpPath << "]");
// used by LO Migration::migrateSettingsIfNecessary() in startup code as config dir
::setenv("XDG_CONFIG_HOME", (tmpPath + "/.config").c_str(), 1);
::setenv("HOME", tmpPath.c_str(), 1);
// overwrite coolkitconfig.xcu setting to fit into allowed paths
::setenv("LOK_WORKDIR", ("file://" + tmpPath).c_str(), 1);
// Setup the OSL sandbox
allowedPaths += ":r:" + pathFromFileURL(userdir_url);
::setenv("SAL_ALLOWED_PATHS", allowedPaths.c_str(), 1);
#if ENABLE_DEBUG
::setenv("SAL_ABORT_ON_FORBIDDEN", "1", 1);
#endif
}
LOG_DBG("Initializing LOK with instdir [" << instdir_path << "] and userdir ["
<< userdir_url << "].");
UserDirPath = pathFromFileURL(userdir_url);
InstDirPath = instdir_path;
LibreOfficeKit* kit = nullptr;
{
const char *instdir = instdir_path.c_str();
const char *userdir = userdir_url.c_str();
if (!initFunction)
initFunction = lok_init_2;
if (!Util::isKitInProcess())
kit = UnitKit::get().lok_init(instdir, userdir, initFunction);
if (!kit)
kit = initFunction(instdir, userdir);
loKit = std::make_shared<lok::Office>(kit);
if (!loKit)
{
LOG_FTL("LibreOfficeKit initialization failed. Exiting.");
Util::forcedExit(EX_SOFTWARE);
}
}
// Lock down the syscalls that can be used
if (!Seccomp::lockdown(Seccomp::Type::KIT))
{
if (!noSeccomp)
{
LOG_FTL("LibreOfficeKit seccomp security lockdown failed. Exiting.");
Util::forcedExit(EX_SOFTWARE);
}
LOG_ERR("LibreOfficeKit seccomp security lockdown failed, but configured to continue. "
"You are running in a significantly less secure mode.");
}
rlimit rlim = { 0, 0 };
if (getrlimit(RLIMIT_AS, &rlim) == 0)
LOG_INF("RLIMIT_AS is " << Util::getHumanizedBytes(rlim.rlim_max) << " (" << rlim.rlim_max << " bytes)");
else
LOG_SYS("Failed to get RLIMIT_AS");
if (getrlimit(RLIMIT_STACK, &rlim) == 0)
LOG_INF("RLIMIT_STACK is " << Util::getHumanizedBytes(rlim.rlim_max) << " (" << rlim.rlim_max << " bytes)");
else
LOG_SYS("Failed to get RLIMIT_STACK");
if (getrlimit(RLIMIT_FSIZE, &rlim) == 0)
LOG_INF("RLIMIT_FSIZE is " << Util::getHumanizedBytes(rlim.rlim_max) << " (" << rlim.rlim_max << " bytes)");
else
LOG_SYS("Failed to get RLIMIT_FSIZE");
if (getrlimit(RLIMIT_NOFILE, &rlim) == 0)
LOG_INF("RLIMIT_NOFILE is " << rlim.rlim_max << " files.");
else
LOG_SYS("Failed to get RLIMIT_NOFILE");
LOG_INF("Kit process for Jail [" << jailId << "] is ready.");
std::string pathAndQuery(NEW_CHILD_URI);
pathAndQuery.append("?jailid=");
pathAndQuery.append(jailId);
if (queryVersion)
{
char* versionInfo = loKit->getVersionInfo();
std::string versionString(versionInfo);
if (displayVersion)
std::cout << "office version details: " << versionString << std::endl;
SigUtil::setVersionInfo(versionString);
// Add some parameters we want to pass to the client. Could not figure out how to get
// the configuration parameters from COOLWSD.cpp's initialize() or coolwsd.xml here, so
// oh well, just have the value hardcoded in KitHelper.hpp. It isn't really useful to
// "tune" it at end-user installations anyway, I think.
auto versionJSON = Poco::JSON::Parser().parse(versionString).extract<Poco::JSON::Object::Ptr>();
versionJSON->set("tunnelled_dialog_image_cache_size", std::to_string(LOKitHelper::tunnelledDialogImageCacheSize));
std::stringstream ss;
versionJSON->stringify(ss);
versionString = ss.str();
std::string encodedVersion;
Poco::URI::encode(versionString, "?#/", encodedVersion);
pathAndQuery.append("&version=");
pathAndQuery.append(encodedVersion);
free(versionInfo);
}
#else // MOBILEAPP
#ifndef IOS
// Was not done by the preload.
// For iOS we call it in -[AppDelegate application: didFinishLaunchingWithOptions:]
setupKitEnvironment(userInterface);
#endif
#if (defined(__linux__) && !defined(__ANDROID__)) || defined(__FreeBSD__)
Poco::URI userInstallationURI("file", LO_PATH);
LibreOfficeKit *kit = lok_init_2(LO_PATH "/program", userInstallationURI.toString().c_str());
#else
#ifdef IOS // In the iOS app we call lok_init_2() just once, when the app starts
static LibreOfficeKit *kit = lo_kit;
#else
static LibreOfficeKit *kit = lok_init_2(nullptr, nullptr);
#endif
#endif
assert(kit);
static std::shared_ptr<lok::Office> loKit = std::make_shared<lok::Office>(kit);
assert(loKit);
COOLWSD::LOKitVersion = loKit->getVersionInfo();
// Dummies
const std::string jailId = "jailid";
#endif // MOBILEAPP
auto mainKit = KitSocketPoll::create();
mainKit->runOnClientThread(); // We will do the polling on this thread.
std::shared_ptr<KitWebSocketHandler> websocketHandler =
std::make_shared<KitWebSocketHandler>("child_ws", loKit, jailId, mainKit, numericIdentifier);
#if !MOBILEAPP
std::vector<int> shareFDs;
if (ProcSMapsFile >= 0)
shareFDs.push_back(ProcSMapsFile);
if (isURPEnabled())
{
if (pipe2(URPtoLoFDs, O_CLOEXEC) != 0 || pipe2(URPfromLoFDs, O_CLOEXEC | O_NONBLOCK) != 0)
LOG_ERR("Failed to create urp pipe " << strerror(errno));
else
{
shareFDs.push_back(URPtoLoFDs[1]);
shareFDs.push_back(URPfromLoFDs[0]);
}
}
if (!mainKit->insertNewUnixSocket(MasterLocation, pathAndQuery, websocketHandler,
&shareFDs))
{
LOG_SFL("Failed to connect to WSD. Will exit.");
Util::forcedExit(EX_SOFTWARE);
}
#else
mainKit->insertNewFakeSocket(docBrokerSocket, websocketHandler);
#endif
LOG_INF("New kit client websocket inserted.");
#if !MOBILEAPP
if (bTraceStartup && LogLevel != LogLevelStartup)
{
LOG_INF("Kit initialization complete: setting log-level to [" << LogLevel << "] as configured.");
Log::setLevel(LogLevel);
}
Log::setDisabledAreas(LogDisabledAreas);
#endif
#ifndef IOS
if (!LIBREOFFICEKIT_HAS(kit, runLoop))
{
LOG_FTL("Kit is missing Unipoll API");
std::cout << "Fatal: out of date LibreOfficeKit - no Unipoll API\n";
Util::forcedExit(EX_SOFTWARE);
}
LOG_INF("Kit unipoll loop run");
loKit->runLoop(pollCallback, wakeCallback, mainKit.get());
LOG_INF("Kit unipoll loop run terminated.");
#if MOBILEAPP
SocketPoll::wakeupWorld();
#else
// Trap the signal handler, if invoked,
// to prevent exiting.
LOG_INF("Kit process for Jail [" << jailId << "] finished.");
// Let forkit handle the jail cleanup.
#endif
#else // IOS
std::unique_lock<std::mutex> lock(mainKit->terminationMutex);
mainKit->terminationCV.wait(lock,[&]{ return mainKit->terminationFlag; } );
#endif // !IOS
}
catch (const Exception& exc)
{
LOG_ERR("Poco Exception: " << exc.displayText() <<
(exc.nested() ? " (" + exc.nested()->displayText() + ')' : ""));
}
catch (const std::exception& exc)
{
LOG_ERR("Exception: " << exc.what());
}
#if !MOBILEAPP
LOG_INF("Kit process for Jail [" << jailId << "] finished.");
flushTraceEventRecordings();
// Wait for the signal handler, if invoked, to prevent exiting until done.
SigUtil::waitSigHandlerTrap();
if (!Util::isKitInProcess())
Util::forcedExit(EX_OK);
#endif
}
#ifdef IOS
// In the iOS app we can have several documents open in the app process at the same time, thus
// several lokit_main() functions running at the same time. We want just one LO main loop, though,
// so we start it separately in its own thread.
void runKitLoopInAThread()
{
std::thread([&]
{
Util::setThreadName("lokit_runloop");
std::shared_ptr<lok::Office> loKit = std::make_shared<lok::Office>(lo_kit);
int dummy;
loKit->runLoop(pollCallback, wakeCallback, &dummy);
// Should never return
assert(false);
NSLog(@"loKit->runLoop() unexpectedly returned");
std::abort();
}).detach();
}
#endif // IOS
#endif // !BUILDING_TESTS
void consistencyCheckJail()
{
static bool warned = false;
if (!warned)
{
bool failedTmp, failedLo, failedUser;
FileUtil::Stat tmp("/tmp");
if ((failedTmp = (!tmp.good() || !tmp.isDirectory())))
LOG_ERR("Fatal system error: Kit jail is missing its /tmp directory");
FileUtil::Stat lo(InstDirPath + "/unorc");
if ((failedLo = (!lo.good() || !lo.isFile())))
LOG_ERR("Fatal system error: Kit jail is missing its LibreOfficeKit directory at '" << InstDirPath << "'");
FileUtil::Stat user(UserDirPath);
if ((failedUser = (!user.good() || !user.isDirectory())))
LOG_ERR("Fatal system error: Kit jail is missing its user directory at '" << UserDirPath << "'");
if (failedTmp || failedLo || failedUser)
{
LOG_ERR("A fatal system error indicates that, outside the control of COOL "
"major structural changes have occured in our filesystem. These are "
"potentially indicative of an operator damaging the system, and will "
"inevitably cause document data-loss and/or malfunction.");
warned = true;
SigUtil::addActivity("Fatal, inconsistent jail detected.");
assert(!"Fatal system error with jail setup.");
}
else
LOG_TRC("Passed system consistency check");
}
}
/// Fetch the latest montonically incrementing wire-id
TileWireId getCurrentWireId(bool increment)
{
return RenderTiles::getCurrentWireId(increment);
}
std::string anonymizeUrl(const std::string& url)
{
#ifndef BUILDING_TESTS
return AnonymizeUserData ? Util::anonymizeUrl(url, AnonymizationSalt) : url;
#else
return url;
#endif
}
static int receiveURPFromLO(void* pContext, const signed char* pBuffer, int bytesToWrite)
{
int bytesWritten = 0;
while (bytesToWrite > 0)
{
int bytes = ::write(reinterpret_cast<intptr_t>(pContext), pBuffer + bytesWritten, bytesToWrite);
if (bytes <= 0)
break;
bytesToWrite -= bytes;
bytesWritten += bytes;
}
return bytesWritten;
}
static int sendURPToLO(void* pContext, signed char* pBuffer, int bytesToRead)
{
int bytesRead = 0;
while (bytesToRead > 0)
{
int bytes = ::read(reinterpret_cast<intptr_t>(pContext), pBuffer + bytesRead, bytesToRead);
if (bytes <= 0)
break;
bytesToRead -= bytes;
bytesRead += bytes;
}
return bytesRead;
}
// temp workaround of changed signature of startURP. Compile detect
// old signature and if so return nullptr
extern "C" int (*ObsoleteStartURPSignature)(LibreOfficeKit*, void*, void**,
int (*)(void* pContext, const signed char* pBuffer, int nLen),
int (**)(void* pContext, const signed char* pBuffer, int nLen));
template<class T> void* doStartURP(T&, std::true_type)
{
(void)receiveURPFromLO;
(void)sendURPToLO;
return nullptr;
}
template<class T> void* doStartURP(T& LOKit, std::false_type)
{
return LOKit->startURP(reinterpret_cast<void*>(URPfromLoFDs[1]),
reinterpret_cast<void*>(URPtoLoFDs[0]),
receiveURPFromLO, sendURPToLO);
}
bool startURP(std::shared_ptr<lok::Office> LOKit, void** ppURPContext)
{
if (!isURPEnabled())
{
LOG_ERR("URP/WS: Attempted to start a URP session but URP is disabled");
return false;
}
if (URPStartCount > 0)
{
LOG_WRN("URP/WS: Not starting another URP session as one has already been opened for this "
"kit instance");
return false;
}
*ppURPContext = doStartURP(LOKit, std::is_same<decltype(LibreOfficeKitClass::startURP), decltype(ObsoleteStartURPSignature)>() );
if (!*ppURPContext)
{
LOG_ERR("URP/WS: tried to start a URP session but core did not let us");
return false;
}
URPStartCount++;
return true;
}
#if !MOBILEAPP
/// Initializes LibreOfficeKit for cross-fork re-use.
bool globalPreinit(const std::string &loTemplate)
{
const std::string libSofficeapp = loTemplate + "/program/" LIB_SOFFICEAPP;
const std::string libMerged = loTemplate + "/program/" LIB_MERGED;
std::string loadedLibrary;
// we deliberately don't dlclose handle on success, make it
// static so static analysis doesn't see this as a leak
static void *handle;
if (File(libMerged).exists())
{
LOG_TRC("dlopen(" << libMerged << ", RTLD_GLOBAL|RTLD_NOW)");
handle = dlopen(libMerged.c_str(), RTLD_GLOBAL|RTLD_NOW);
if (!handle)
{
LOG_FTL("Failed to load " << libMerged << ": " << dlerror());
return false;
}
loadedLibrary = libMerged;
}
else
{
if (File(libSofficeapp).exists())
{
LOG_TRC("dlopen(" << libSofficeapp << ", RTLD_GLOBAL|RTLD_NOW)");
handle = dlopen(libSofficeapp.c_str(), RTLD_GLOBAL|RTLD_NOW);
if (!handle)
{
LOG_FTL("Failed to load " << libSofficeapp << ": " << dlerror());
return false;
}
loadedLibrary = libSofficeapp;
}
else
{
LOG_FTL("Neither " << libSofficeapp << " or " << libMerged << " exist.");
return false;
}
}
LokHookPreInit2* preInit = reinterpret_cast<LokHookPreInit2 *>(dlsym(handle, "lok_preinit_2"));
if (!preInit)
{
LOG_FTL("No lok_preinit_2 symbol in " << loadedLibrary << ": " << dlerror());
dlclose(handle);
return false;
}
initFunction = reinterpret_cast<LokHookFunction2 *>(dlsym(handle, "libreofficekit_hook_2"));
if (!initFunction)
{
LOG_FTL("No libreofficekit_hook_2 symbol in " << loadedLibrary << ": " << dlerror());
}
// Disable problematic components that may be present from a
// desktop or developer's install if env. var not set.
::setenv("UNODISABLELIBRARY",
"abp avmediagst avmediavlc cmdmail losessioninstall OGLTrans PresenterScreen "
"syssh ucpftp1 ucpgio1 ucphier1 ucpimage updatecheckui updatefeed updchk"
// Database
"dbaxml dbmm dbp dbu deployment firebird_sdbc mork "
"mysql mysqlc odbc postgresql-sdbc postgresql-sdbc-impl sdbc2 sdbt"
// Java
"javaloader javavm jdbc rpt rptui rptxml ",
0 /* no overwrite */);
LOG_TRC("Invoking lok_preinit_2(" << loTemplate << "/program\", \"file:///tmp/user\")");
const auto start = std::chrono::steady_clock::now();
if (preInit((loTemplate + "/program").c_str(), "file:///tmp/user", &loKitPtr) != 0)
{
LOG_FTL("lok_preinit() in " << loadedLibrary << " failed");
dlclose(handle);
return false;
}
LOG_DBG("After lok_preinit_2: loKitPtr=" << loKitPtr);
LOG_TRC("Finished lok_preinit(" << loTemplate << "/program\", \"file:///tmp/user\") in "
<< std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start));
return true;
}
/// Anonymize usernames.
std::string anonymizeUsername(const std::string& username)
{
#ifndef BUILDING_TESTS
return AnonymizeUserData ? Util::anonymize(username, AnonymizationSalt) : username;
#else
return username;
#endif
}
#endif // !MOBILEAPP
void dump_kit_state()
{
std::ostringstream oss;
KitSocketPoll::dumpGlobalState(oss);
const std::string msg = oss.str();
fprintf(stderr, "%s", msg.c_str());
LOG_TRC(msg);
}
#if defined __GLIBC__
# include <malloc.h>
void dump_malloc_state()
{
malloc_info(0, stderr);
fflush(stderr);
}
#endif
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */