loolwsd/ChildProcessSession.cpp | 560 +++++++++++++++++++++ loolwsd/ChildProcessSession.hpp | 57 ++ loolwsd/LOOLBroker.cpp | 451 +++++++++++++++++ loolwsd/LOOLKit.cpp | 164 ++++++ loolwsd/LOOLSession.cpp | 975 -------------------------------------- loolwsd/LOOLSession.hpp | 102 --- loolwsd/LOOLWSD.cpp | 613 +++-------------------- loolwsd/LOOLWSD.hpp | 9 loolwsd/Makefile.am | 21 loolwsd/MasterProcessSession.cpp | 571 ++++++++++++++++++++++ loolwsd/MasterProcessSession.hpp | 73 ++ loolwsd/loolwsd-systemplate-setup | 42 + 12 files changed, 2030 insertions(+), 1608 deletions(-)
New commits: commit b9a04ecac1f618e770cab227db2996ad5e1135a6 Author: Henry Castro <[email protected]> Date: Thu Jul 23 09:23:24 2015 -0400 loolwsd: Initial setup Separate classes, MasterProcessSession, ChildProcessSession config loolwsd-systemplate-setup, adjust code, create process LOOBroker and LOOLKit diff --git a/loolwsd/ChildProcessSession.cpp b/loolwsd/ChildProcessSession.cpp new file mode 100644 index 0000000..49e6a43 --- /dev/null +++ b/loolwsd/ChildProcessSession.cpp @@ -0,0 +1,560 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <sys/stat.h> +#include <sys/types.h> + +#include <ftw.h> +#include <utime.h> + +#include <cassert> +#include <condition_variable> +#include <cstring> +#include <fstream> +#include <iostream> +#include <iterator> +#include <map> +#include <memory> +#include <mutex> +#include <set> + +/*#define LOK_USE_UNSTABLE_API +#include <LibreOfficeKit/LibreOfficeKit.h> +#include <LibreOfficeKit/LibreOfficeKitEnums.h>*/ + +#include <Poco/Net/WebSocket.h> +#include <Poco/StringTokenizer.h> +#include <Poco/Util/Application.h> +#include <Poco/URI.h> +#include <Poco/Path.h> + +#include "ChildProcessSession.hpp" +#include "Util.hpp" +#include "LOOLProtocol.hpp" +#include "LOKitHelper.hpp" + +using namespace LOOLProtocol; +using Poco::Net::WebSocket; +using Poco::StringTokenizer; +using Poco::Util::Application; +using Poco::URI; +using Poco::Path; + +/* + +#include <Poco/Exception.h> +#include <Poco/File.h> +#include <Poco/Net/HTTPStreamFactory.h> +#include <Poco/Process.h> +#include <Poco/Random.h> +#include <Poco/StreamCopier.h> +#include <Poco/String.h> +#include <Poco/ThreadLocal.h> +#include <Poco/URIStreamOpener.h> +#include <Poco/Exception.h> +#include <Poco/Net/NetException.h> +#include <Poco/Net/DialogSocket.h> +#include <Poco/Net/SocketAddress.h> + +#include "LOOLSession.hpp" +#include "LOOLWSD.hpp" +#include "TileCache.hpp" + + +using Poco::File; +using Poco::IOException; +using Poco::Net::HTTPStreamFactory; +using Poco::Process; +using Poco::ProcessHandle; +using Poco::Random; +using Poco::StreamCopier; +using Poco::Thread; +using Poco::ThreadLocal; +using Poco::UInt64; +using Poco::URIStreamOpener; +using Poco::Util::Application; +using Poco::Exception; +using Poco::Net::DialogSocket; +using Poco::Net::SocketAddress; +using Poco::Net::WebSocketException;*/ + + +ChildProcessSession::ChildProcessSession(std::shared_ptr<WebSocket> ws, LibreOfficeKit *loKit) : + LOOLSession(ws, Kind::ToMaster), + _loKitDocument(NULL), + _loKit(loKit), + _clientPart(0) +{ + std::cout << Util::logPrefix() << "ChildProcessSession ctor this=" << this << " ws=" << _ws.get() << std::endl; +} + +ChildProcessSession::~ChildProcessSession() +{ + std::cout << Util::logPrefix() << "ChildProcessSession dtor this=" << this << std::endl; + if (LIBREOFFICEKIT_HAS(_loKit, registerCallback)) + _loKit->pClass->registerCallback(_loKit, 0, 0); + Util::shutdownWebSocket(*_ws); +} + +bool ChildProcessSession::handleInput(const char *buffer, int length) +{ + std::string firstLine = getFirstLine(buffer, length); + StringTokenizer tokens(firstLine, " ", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); + + Application::instance().logger().information(Util::logPrefix() + _kindString + ",Input," + getAbbreviatedMessage(buffer, length)); + + if (tokens[0] == "load") + { + if (_docURL != "") + { + sendTextFrame("error: cmd=load kind=docalreadyloaded"); + return false; + } + return loadDocument(buffer, length, tokens); + } + else if (_docURL == "") + { + sendTextFrame("error: cmd=" + tokens[0] + " kind=nodocloaded"); + return false; + } + else if (tokens[0] == "setclientpart") + { + return setClientPart(buffer, length, tokens); + } + else if (tokens[0] == "status") + { + return getStatus(buffer, length); + } + else if (tokens[0] == "tile") + { + sendTile(buffer, length, tokens); + } + else + { + // All other commands are such that they always require a LibreOfficeKitDocument session, + // i.e. need to be handled in a child process. + + assert(tokens[0] == "gettextselection" || + tokens[0] == "key" || + tokens[0] == "mouse" || + tokens[0] == "uno" || + tokens[0] == "selecttext" || + tokens[0] == "selectgraphic" || + tokens[0] == "resetselection" || + tokens[0] == "saveas"); + + if (_loKitDocument->pClass->getPart(_loKitDocument) != _clientPart) + { + _loKitDocument->pClass->setPart(_loKitDocument, _clientPart); + } + if (tokens[0] == "gettextselection") + { + return getTextSelection(buffer, length, tokens); + } + else if (tokens[0] == "key") + { + return keyEvent(buffer, length, tokens); + } + else if (tokens[0] == "mouse") + { + return mouseEvent(buffer, length, tokens); + } + else if (tokens[0] == "uno") + { + return unoCommand(buffer, length, tokens); + } + else if (tokens[0] == "selecttext") + { + return selectText(buffer, length, tokens); + } + else if (tokens[0] == "selectgraphic") + { + return selectGraphic(buffer, length, tokens); + } + else if (tokens[0] == "resetselection") + { + return resetSelection(buffer, length, tokens); + } + else if (tokens[0] == "saveas") + { + return saveAs(buffer, length, tokens); + } + else + { + assert(false); + } + } + return true; +} + +extern "C" +{ + static void myCallback(int nType, const char* pPayload, void* pData) + { + ChildProcessSession *srv = reinterpret_cast<ChildProcessSession *>(pData); + + switch ((LibreOfficeKitCallbackType) nType) + { + case LOK_CALLBACK_INVALIDATE_TILES: + { + int curPart = srv->_loKitDocument->pClass->getPart(srv->_loKitDocument); + srv->sendTextFrame("curpart: part=" + std::to_string(curPart)); + StringTokenizer tokens(std::string(pPayload), " ", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); + if (tokens.count() == 4) + { + int x(std::stoi(tokens[0])); + int y(std::stoi(tokens[1])); + int width(std::stoi(tokens[2])); + int height(std::stoi(tokens[3])); + srv->sendTextFrame("invalidatetiles:" + " part=" + std::to_string(curPart) + + " x=" + std::to_string(x) + + " y=" + std::to_string(y) + + " width=" + std::to_string(width) + + " height=" + std::to_string(height)); + } + else { + srv->sendTextFrame("invalidatetiles: " + std::string(pPayload)); + } + } + break; + case LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR: + srv->sendTextFrame("invalidatecursor: " + std::string(pPayload)); + break; + case LOK_CALLBACK_TEXT_SELECTION: + srv->sendTextFrame("textselection: " + std::string(pPayload)); + break; + case LOK_CALLBACK_TEXT_SELECTION_START: + srv->sendTextFrame("textselectionstart: " + std::string(pPayload)); + break; + case LOK_CALLBACK_TEXT_SELECTION_END: + srv->sendTextFrame("textselectionend: " + std::string(pPayload)); + break; + case LOK_CALLBACK_CURSOR_VISIBLE: + srv->sendTextFrame("cursorvisible: " + std::string(pPayload)); + break; + case LOK_CALLBACK_GRAPHIC_SELECTION: + srv->sendTextFrame("graphicselection: " + std::string(pPayload)); + break; + case LOK_CALLBACK_HYPERLINK_CLICKED: + srv->sendTextFrame("hyperlinkclicked: " + std::string(pPayload)); + break; + case LOK_CALLBACK_STATE_CHANGED: + srv->sendTextFrame("statechanged: " + std::string(pPayload)); + break; + case LOK_CALLBACK_STATUS_INDICATOR_START: + srv->sendTextFrame("statusindicatorstart:"); + break; + case LOK_CALLBACK_STATUS_INDICATOR_SET_VALUE: + srv->sendTextFrame("statusindicatorsetvalue: " + std::string(pPayload)); + break; + case LOK_CALLBACK_STATUS_INDICATOR_FINISH: + srv->sendTextFrame("statusindicatorfinish:"); + break; + case LOK_CALLBACK_SEARCH_NOT_FOUND: + srv->sendTextFrame("searchnotfound: " + std::string(pPayload)); + break; + case LOK_CALLBACK_DOCUMENT_SIZE_CHANGED: + srv->getStatus("", 0); + break; + case LOK_CALLBACK_SET_PART: + srv->sendTextFrame("setpart: " + std::string(pPayload)); + break; + } + } +} + +bool ChildProcessSession::loadDocument(const char *buffer, int length, StringTokenizer& tokens) +{ + if (tokens.count() != 2) + { + sendTextFrame("error: cmd=load kind=syntax"); + return false; + } + + if (tokens[1].find("url=") == 0) + _docURL = tokens[1].substr(strlen("url=")); + else + _docURL = tokens[1]; + + URI aUri; + try + { + aUri = URI(_docURL); + } + catch(Poco::SyntaxException&) + { + sendTextFrame("error: cmd=load kind=URI invalid syntax"); + return false; + } + + if (aUri.empty()) + { + sendTextFrame("error: cmd=load kind=URI empty"); + return false; + } + + // The URL in the request is the original one, not visible in the chroot jail. + // The child process uses the fixed name jailDocumentURL. + + if (LIBREOFFICEKIT_HAS(_loKit, registerCallback)) + _loKit->pClass->registerCallback(_loKit, myCallback, this); + + if (aUri.isRelative() || aUri.getScheme() == "file") + aUri = URI( URI("file://"), Path(jailDocumentURL, Path(aUri.getPath()).getFileName()).toString() ); + + if ((_loKitDocument = _loKit->pClass->documentLoad(_loKit, aUri.toString().c_str())) == NULL) + { + sendTextFrame("error: cmd=load kind=failed"); + return false; + } + + _loKitDocument->pClass->initializeForRendering(_loKitDocument); + + if (!getStatus(buffer, length)) + return false; + _loKitDocument->pClass->registerCallback(_loKitDocument, myCallback, this); + + return true; +} + +bool ChildProcessSession::getStatus(const char *buffer, int length) +{ + std::string status = "status: " + LOKitHelper::documentStatus(_loKitDocument); + + sendTextFrame(status); + + return true; +} + +void ChildProcessSession::sendTile(const char *buffer, int length, StringTokenizer& tokens) +{ + int part, width, height, tilePosX, tilePosY, tileWidth, tileHeight; + + if (tokens.count() < 8 || + !getTokenInteger(tokens[1], "part", part) || + !getTokenInteger(tokens[2], "width", width) || + !getTokenInteger(tokens[3], "height", height) || + !getTokenInteger(tokens[4], "tileposx", tilePosX) || + !getTokenInteger(tokens[5], "tileposy", tilePosY) || + !getTokenInteger(tokens[6], "tilewidth", tileWidth) || + !getTokenInteger(tokens[7], "tileheight", tileHeight)) + { + sendTextFrame("error: cmd=tile kind=syntax"); + return; + } + + if (part < 0 || + width <= 0 || + height <= 0 || + tilePosX < 0 || + tilePosY < 0 || + tileWidth <= 0 || + tileHeight <= 0) + { + sendTextFrame("error: cmd=tile kind=invalid"); + return; + } + + std::string response = "tile: " + Poco::cat(std::string(" "), tokens.begin() + 1, tokens.end()) + "\n"; + + std::vector<char> output; + output.reserve(4 * width * height); + output.resize(response.size()); + std::memcpy(output.data(), response.data(), response.size()); + + unsigned char *pixmap = new unsigned char[4 * width * height]; + _loKitDocument->pClass->setPart(_loKitDocument, part); + _loKitDocument->pClass->paintTile(_loKitDocument, pixmap, width, height, tilePosX, tilePosY, tileWidth, tileHeight); + + if (!Util::encodePNGAndAppendToBuffer(pixmap, width, height, output)) + { + sendTextFrame("error: cmd=tile kind=failure"); + return; + } + + delete[] pixmap; + + sendBinaryFrame(output.data(), output.size()); +} + +bool ChildProcessSession::getTextSelection(const char *buffer, int length, StringTokenizer& tokens) +{ + std::string mimeType; + + if (tokens.count() != 2 || + !getTokenString(tokens[1], "mimetype", mimeType)) + { + sendTextFrame("error: cmd=gettextselection kind=syntax"); + return false; + } + + char *textSelection = _loKitDocument->pClass->getTextSelection(_loKitDocument, mimeType.c_str(), NULL); + + sendTextFrame("textselectioncontent: " + std::string(textSelection)); + return true; +} + +bool ChildProcessSession::keyEvent(const char *buffer, int length, StringTokenizer& tokens) +{ + int type, charcode, keycode; + + if (tokens.count() != 4 || + !getTokenKeyword(tokens[1], "type", + {{"input", LOK_KEYEVENT_KEYINPUT}, {"up", LOK_KEYEVENT_KEYUP}}, + type) || + !getTokenInteger(tokens[2], "char", charcode) || + !getTokenInteger(tokens[3], "key", keycode)) + { + sendTextFrame("error: cmd=key kind=syntax"); + return false; + } + + _loKitDocument->pClass->postKeyEvent(_loKitDocument, type, charcode, keycode); + + return true; +} + +bool ChildProcessSession::mouseEvent(const char *buffer, int length, StringTokenizer& tokens) +{ + int type, x, y, count; + + if (tokens.count() != 5 || + !getTokenKeyword(tokens[1], "type", + {{"buttondown", LOK_MOUSEEVENT_MOUSEBUTTONDOWN}, + {"buttonup", LOK_MOUSEEVENT_MOUSEBUTTONUP}, + {"move", LOK_MOUSEEVENT_MOUSEMOVE}}, + type) || + !getTokenInteger(tokens[2], "x", x) || + !getTokenInteger(tokens[3], "y", y) || + !getTokenInteger(tokens[4], "count", count)) + { + sendTextFrame("error: cmd=mouse kind=syntax"); + return false; + } + + _loKitDocument->pClass->postMouseEvent(_loKitDocument, type, x, y, count); + + return true; +} + +bool ChildProcessSession::unoCommand(const char *buffer, int length, StringTokenizer& tokens) +{ + if (tokens.count() == 1) + { + sendTextFrame("error: cmd=uno kind=syntax"); + return false; + } + + if (tokens.count() == 2) + { + _loKitDocument->pClass->postUnoCommand(_loKitDocument, tokens[1].c_str(), 0); + } + else + { + _loKitDocument->pClass->postUnoCommand(_loKitDocument, tokens[1].c_str(), Poco::cat(std::string(" "), tokens.begin() + 2, tokens.end()).c_str()); + } + + return true; +} + +bool ChildProcessSession::selectText(const char *buffer, int length, StringTokenizer& tokens) +{ + int type, x, y; + + if (tokens.count() != 4 || + !getTokenKeyword(tokens[1], "type", + {{"start", LOK_SETTEXTSELECTION_START}, + {"end", LOK_SETTEXTSELECTION_END}, + {"reset", LOK_SETTEXTSELECTION_RESET}}, + type) || + !getTokenInteger(tokens[2], "x", x) || + !getTokenInteger(tokens[3], "y", y)) + { + sendTextFrame("error: cmd=selecttext kind=syntax"); + return false; + } + + _loKitDocument->pClass->setTextSelection(_loKitDocument, type, x, y); + + return true; +} + +bool ChildProcessSession::selectGraphic(const char *buffer, int length, StringTokenizer& tokens) +{ + int type, x, y; + + if (tokens.count() != 4 || + !getTokenKeyword(tokens[1], "type", + {{"start", LOK_SETGRAPHICSELECTION_START}, + {"end", LOK_SETGRAPHICSELECTION_END}}, + type) || + !getTokenInteger(tokens[2], "x", x) || + !getTokenInteger(tokens[3], "y", y)) + { + sendTextFrame("error: cmd=selectghraphic kind=syntax"); + return false; + } + + _loKitDocument->pClass->setGraphicSelection(_loKitDocument, type, x, y); + + return true; +} + +bool ChildProcessSession::resetSelection(const char *buffer, int length, StringTokenizer& tokens) +{ + if (tokens.count() != 1) + { + sendTextFrame("error: cmd=resetselection kind=syntax"); + return false; + } + + _loKitDocument->pClass->resetSelection(_loKitDocument); + + return true; +} + +bool ChildProcessSession::saveAs(const char *buffer, int length, StringTokenizer& tokens) +{ + std::string url, format, filterOptions; + + if (tokens.count() < 4 || + !getTokenString(tokens[1], "url", url)) + { + sendTextFrame("error: cmd=saveas kind=syntax"); + return false; + } + + URI::decode(url, url, true); + if (getTokenString(tokens[2], "format", format)) { + URI::decode(format, format, true); + } + + if (getTokenString(tokens[3], "options", filterOptions)) { + if (tokens.count() > 4) { + filterOptions += Poco::cat(std::string(" "), tokens.begin() + 4, tokens.end()); + } + } + + _loKitDocument->pClass->saveAs(_loKitDocument, url.c_str(), format.c_str(), filterOptions.c_str()); + + return true; +} + +bool ChildProcessSession::setClientPart(const char *buffer, int length, StringTokenizer& tokens) +{ + if (tokens.count() < 2 || + !getTokenInteger(tokens[1], "part", _clientPart)) + { + return false; + } + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/loolwsd/ChildProcessSession.hpp b/loolwsd/ChildProcessSession.hpp new file mode 100644 index 0000000..7bf1f6d --- /dev/null +++ b/loolwsd/ChildProcessSession.hpp @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * 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/. + */ + +#ifndef INCLUDED_LOOLCHILDPROCESSSESSION_HPP +#define INCLUDED_LOOLCHILDPROCESSSESSION_HPP + +#define LOK_USE_UNSTABLE_API +#include <LibreOfficeKit/LibreOfficeKit.h> + +#include <Poco/StringTokenizer.h> + +#include "LOOLSession.hpp" + +class ChildProcessSession final : public LOOLSession +{ +public: + ChildProcessSession(std::shared_ptr<Poco::Net::WebSocket> ws, LibreOfficeKit *loKit); + virtual ~ChildProcessSession(); + + virtual bool handleInput(const char *buffer, int length) override; + + virtual bool getStatus(const char *buffer, int length); + + LibreOfficeKitDocument *_loKitDocument; + + protected: + virtual bool loadDocument(const char *buffer, int length, Poco::StringTokenizer& tokens) override; + + virtual void sendTile(const char *buffer, int length, Poco::StringTokenizer& tokens); + + bool getTextSelection(const char *buffer, int length, Poco::StringTokenizer& tokens); + bool keyEvent(const char *buffer, int length, Poco::StringTokenizer& tokens); + bool mouseEvent(const char *buffer, int length, Poco::StringTokenizer& tokens); + bool unoCommand(const char *buffer, int length, Poco::StringTokenizer& tokens); + bool selectText(const char *buffer, int length, Poco::StringTokenizer& tokens); + bool selectGraphic(const char *buffer, int length, Poco::StringTokenizer& tokens); + bool resetSelection(const char *buffer, int length, Poco::StringTokenizer& tokens); + bool saveAs(const char *buffer, int length, Poco::StringTokenizer& tokens); + bool setClientPart(const char *buffer, int length, Poco::StringTokenizer& tokens); + + std::string _jail; + std::string _loSubPath; + LibreOfficeKit *_loKit; + + private: + int _clientPart; +}; + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/loolwsd/LOOLBroker.cpp b/loolwsd/LOOLBroker.cpp new file mode 100644 index 0000000..3f50572 --- /dev/null +++ b/loolwsd/LOOLBroker.cpp @@ -0,0 +1,451 @@ +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/capability.h> + +#include <utime.h> +#include <ftw.h> +#include <unistd.h> + +#include <mutex> +#include <cstring> +#include <cassert> +#include <iostream> +#include <fstream> + +#include <Poco/Types.h> +#include <Poco/Random.h> +#include <Poco/Path.h> +#include <Poco/File.h> +#include <Poco/ThreadLocal.h> +#include <Poco/Process.h> +#include <Poco/Thread.h> +#include <Poco/SharedMemory.h> + +#include "Util.hpp" + +using Poco::Path; +using Poco::File; +using Poco::ThreadLocal; +using Poco::Process; +using Poco::Thread; +using Poco::ProcessHandle; + +namespace +{ + ThreadLocal<std::string> sourceForLinkOrCopy; + ThreadLocal<Path> destinationForLinkOrCopy; + + int linkOrCopyFunction(const char *fpath, + const struct stat *sb, + int typeflag, + struct FTW *ftwbuf) + { + if (strcmp(fpath, sourceForLinkOrCopy->c_str()) == 0) + return 0; + + assert(fpath[strlen(sourceForLinkOrCopy->c_str())] == '/'); + const char *relativeOldPath = fpath + strlen(sourceForLinkOrCopy->c_str()) + 1; + +#ifdef __APPLE__ + if (strcmp(relativeOldPath, "PkgInfo") == 0) + return 0; +#endif + + Path newPath(*destinationForLinkOrCopy, Path(relativeOldPath)); + + switch (typeflag) + { + case FTW_F: + File(newPath.parent()).createDirectories(); + if (link(fpath, newPath.toString().c_str()) == -1) + { + std::cout << Util::logPrefix() + + "link(\"" + fpath + "\",\"" + newPath.toString() + "\") failed: " + + strerror(errno) << std::endl; + exit(1); + } + break; + case FTW_DP: + { + struct stat st; + if (stat(fpath, &st) == -1) + { + std::cout << Util::logPrefix() + + "stat(\"" + fpath + "\") failed: " + + strerror(errno) << std::endl; + return 1; + } + File(newPath).createDirectories(); + struct utimbuf ut; + ut.actime = st.st_atime; + ut.modtime = st.st_mtime; + if (utime(newPath.toString().c_str(), &ut) == -1) + { + std::cout << Util::logPrefix() + + "utime(\"" + newPath.toString() + "\", &ut) failed: " + + strerror(errno) << std::endl; + return 1; + } + } + break; + case FTW_DNR: + std::cout <<Util::logPrefix() + + "Cannot read directory '" + fpath + "'" << std::endl; + return 1; + case FTW_NS: + std::cout <<Util::logPrefix() + + "nftw: stat failed for '" + fpath + "'" << std::endl; + return 1; + case FTW_SLN: + std::cout <<Util::logPrefix() + + "nftw: symlink to nonexistent file: '" + fpath + "', ignored" << std::endl; + break; + default: + assert(false); + } + return 0; + } + + void linkOrCopy(const std::string& source, const Path& destination) + { + *sourceForLinkOrCopy = source; + if (sourceForLinkOrCopy->back() == '/') + sourceForLinkOrCopy->pop_back(); + *destinationForLinkOrCopy = destination; + if (nftw(source.c_str(), linkOrCopyFunction, 10, FTW_DEPTH) == -1) + std::cout << Util::logPrefix() + + "linkOrCopy: nftw() failed for '" + source + "'" << std::endl; + } + + void dropCapability( +#ifdef __linux + cap_value_t capability +#endif + ) + { +#ifdef __linux + cap_t caps; + cap_value_t cap_list[] = { capability }; + + caps = cap_get_proc(); + if (caps == NULL) + { + std::cout << Util::logPrefix() + "cap_get_proc() failed: " + strerror(errno) << std::endl; + exit(1); + } + + if (cap_set_flag(caps, CAP_EFFECTIVE, sizeof(cap_list)/sizeof(cap_list[0]), cap_list, CAP_CLEAR) == -1 || + cap_set_flag(caps, CAP_PERMITTED, sizeof(cap_list)/sizeof(cap_list[0]), cap_list, CAP_CLEAR) == -1) + { + std::cout << Util::logPrefix() + "cap_set_flag() failed: " + strerror(errno) << std::endl; + exit(1); + } + + if (cap_set_proc(caps) == -1) + { + std::cout << std::string("cap_set_proc() failed: ") + strerror(errno) << std::endl; + exit(1); + } + + char *capText = cap_to_text(caps, NULL); + std::cout <<Util::logPrefix() + "Capabilities now: " + capText << std::endl; + cap_free(capText); + + cap_free(caps); +#endif + // We assume that on non-Linux we don't need to be root to be able to hardlink to files we + // don't own, so drop root. + if (geteuid() == 0 && getuid() != 0) + { + // The program is setuid root. Not normal on Linux where we use setcap, but if this + // needs to run on non-Linux Unixes, setuid root is what it will bneed to be to be able + // to do chroot(). + if (setuid(getuid()) != 0) { + std::cout << std::string("setuid() failed: ") + strerror(errno) << std::endl; + } + } +#if ENABLE_DEBUG + if (geteuid() == 0 && getuid() == 0) + { +#ifdef __linux + // Argh, awful hack + if (capability == CAP_FOWNER) + return; +#endif + + // Running under sudo, probably because being debugged? Let's drop super-user rights. + LOOLWSD::runningAsRoot = true; + if (LOOLWSD::uid == 0) + { + struct passwd *nobody = getpwnam("nobody"); + if (nobody) + LOOLWSD::uid = nobody->pw_uid; + else + LOOLWSD::uid = 65534; + } + if (setuid(LOOLWSD::uid) != 0) { + std::cout << std::string("setuid() failed: ") + strerror(errno) << std::endl; + } + } +#endif + } +} + +static std::map<Poco::Process::PID, Poco::UInt64> _childProcesses; + + +static int prefixcmp(const char *str, const char *prefix) +{ + for (; ; str++, prefix++) + if (!*prefix) + return 0; + else if (*str != *prefix) + return (unsigned char)*prefix - (unsigned char)*str; +} + + +static int createLibreOfficeKit() +{ + Process::Args args; + //args.push_back("--losubpath=" + LOOLWSD::loSubPath); + //args.push_back("--systemplate=" + sysTemplate); + //args.push_back("--lotemplate=" + loTemplate); + //args.push_back("--childroot=" + childRoot); + //args.push_back("--numprespawns=" + std::to_string(_numPreSpawnedChildren)); + + std::string executable = "loolkit"; + + /*if (!File(executable).exists()) + { + std::cout << Util::logPrefix() + "Error loolkit does not exists" << std::endl; + return -1; + }*/ + + //Process::Env env; + //env["LD_LIBRARY_PATH"] = "/usr/local/lib"; + //env["LD_DEBUG"] = "libs"; + + std::cout << Util::logPrefix() + "Launching LibreOfficeKit: " + executable + " " + Poco::cat(std::string(" "), args.begin(), args.end()) << std::endl; + + //ProcessHandle child = Process::launch(executable, args, "/usr/bin/", NULL, NULL, NULL, env); + ProcessHandle child = Process::launch(executable, args); + + _childProcesses[child.id()] = child.id(); + return 0; +} + +static void startupLibreOfficeKit(int nLOKits) +{ + for (int nCntr = nLOKits; nCntr; nCntr--) + { + if (createLibreOfficeKit() < 0) + break; + } +} + +// Broker process +int main(int argc, char** argv) +{ + // Initialization + std::mutex _rngMutex; + Poco::Random _rng; + std::string childRoot; + std::string loSubPath; + std::string sysTemplate; + std::string loTemplate; + int _numPreSpawnedChildren = 0; + Poco::SharedMemory _sharedForkChild("loolwsd", sizeof(bool), Poco::SharedMemory::AM_WRITE); + + while (argc > 0) + { + char *cmd = argv[0]; + char *eq = NULL; + if (strstr(cmd, "loolbroker")) + { + + } + if (!prefixcmp(cmd, "--losubpath=")) + { + eq = strchrnul(cmd, '='); + if (*eq) + loSubPath = std::string(++eq); + } + else if (!prefixcmp(cmd, "--systemplate=")) + { + eq = strchrnul(cmd, '='); + if (*eq) + sysTemplate = std::string(++eq); + } + else if (!prefixcmp(cmd, "--lotemplate=")) + { + eq = strchrnul(cmd, '='); + if (*eq) + loTemplate = std::string(++eq); + } + else if (!prefixcmp(cmd, "--childroot=")) + { + eq = strchrnul(cmd, '='); + if (*eq) + childRoot = std::string(++eq); + } + else if (!prefixcmp(cmd, "--numprespawns=")) + { + eq = strchrnul(cmd, '='); + if (*eq) + _numPreSpawnedChildren = std::stoi(std::string(++eq)); + } + + argv++; + argc--; + } + + if (loSubPath.empty()) + { + std::cout << "--losubpath is empty" << std::endl; + exit(1); + } + + if (sysTemplate.empty()) + { + std::cout << "--systemplate is empty" << std::endl; + exit(1); + } + + if (loTemplate.empty()) + { + std::cout << "--lotemplate is empty" << std::endl; + exit(1); + } + + if (childRoot.empty()) + { + std::cout << "--childroot is empty" << std::endl; + exit(1); + } + + if ( !_numPreSpawnedChildren ) + { + std::cout << "--numprespawns is 0" << std::endl; + exit(1); + } + + std::unique_lock<std::mutex> rngLock(_rngMutex); + Poco::UInt64 _childId = (((Poco::UInt64)_rng.next()) << 32) | _rng.next() | 1; + rngLock.unlock(); + + + Path jail = Path::forDirectory(childRoot + Path::separator() + std::to_string(_childId)); + File(jail).createDirectories(); + + Path jailLOInstallation(jail, loSubPath); + jailLOInstallation.makeDirectory(); + File(jailLOInstallation).createDirectory(); + + // Copy (link) LO installation and other necessary files into it from the template + + linkOrCopy(sysTemplate, jail); + linkOrCopy(loTemplate, jailLOInstallation); + +#ifdef __linux + // Create the urandom and random devices + File(Path(jail, "/dev")).createDirectory(); + if (mknod((jail.toString() + "/dev/random").c_str(), + S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH, + makedev(1, 8)) != 0) + { + std::cout << Util::logPrefix() + + "mknod(" + jail.toString() + "/dev/random) failed: " + + strerror(errno) << std::endl; + + } + if (mknod((jail.toString() + "/dev/urandom").c_str(), + S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH, + makedev(1, 9)) != 0) + { + std::cout << Util::logPrefix() + + "mknod(" + jail.toString() + "/dev/urandom) failed: " + + strerror(errno) << std::endl; + } +#endif + + std::cout << "desktopMain -> chroot(\"" + jail.toString() + "\")" << std::endl; + if (chroot(jail.toString().c_str()) == -1) + { + std::cout << "chroot(\"" + jail.toString() + "\") failed: " + strerror(errno) << std::endl; + exit(-1); + } + + if (chdir("/") == -1) + { + std::cout << std::string("chdir(\"/\") in jail failed: ") + strerror(errno) << std::endl; + exit(-1); + } + +#ifdef __linux + dropCapability(CAP_SYS_CHROOT); +#else + dropCapability(); +#endif + + if (std::getenv("SLEEPFORDEBUGGER")) + { + std::cout << "Sleeping " << std::getenv("SLEEPFORDEBUGGER") << " seconds, " << + "attach process " << Process::id() << " in debugger now." << std::endl; + Thread::sleep(std::stoul(std::getenv("SLEEPFORDEBUGGER")) * 1000); + } + + startupLibreOfficeKit(_numPreSpawnedChildren); + + while (_childProcesses.size() > 0) + { + int status; + pid_t pid = waitpid(-1, &status, WUNTRACED | WNOHANG); + if (pid > 0) + { + if ( _childProcesses.find(pid) != _childProcesses.end() ) + { + if ((WIFEXITED(status) || WIFSIGNALED(status) || WTERMSIG(status) ) ) + { + std::cout << Util::logPrefix() << "One of our known child processes died :" << std::to_string(pid) << std::endl; + _childProcesses.erase(pid); + } + + if ( WCOREDUMP(status) ) + std::cout << Util::logPrefix() << "The child produced a core dump." << std::endl; + + if ( WIFSTOPPED(status) ) + std::cout << Util::logPrefix() << "The child process was stopped by delivery of a signal." << std::endl; + + if ( WSTOPSIG(status) ) + std::cout << Util::logPrefix() << "The child process was stopped." << std::endl; + + if ( WIFCONTINUED(status) ) + std::cout << Util::logPrefix() << "The child process was resumed." << std::endl; + } + else + { + std::cout << Util::logPrefix() << "None of our known child processes died :" << std::to_string(pid) << std::endl; + } + } + else if (pid < 0) + std::cout << Util::logPrefix() << "Child error: " << strerror(errno) << std::endl; + + if ( _sharedForkChild.begin()[0] ) + { + _sharedForkChild.begin()[0] = 0; + std::cout << Util::logPrefix() << "No availabe child session, fork new one" << std::endl; + if (createLibreOfficeKit() < 0 ) + break; + } + } + + // Terminate child processes + for (auto i : _childProcesses) + { + std::cout << Util::logPrefix() + "Requesting child process " + std::to_string(i.first) + " to terminate" << std::endl; + Process::requestTermination(i.first); + } + + std::cout << Util::logPrefix() << "loolbroker finished OK!" << std::endl; + return 0; +} diff --git a/loolwsd/LOOLKit.cpp b/loolwsd/LOOLKit.cpp new file mode 100644 index 0000000..06f5dcf --- /dev/null +++ b/loolwsd/LOOLKit.cpp @@ -0,0 +1,164 @@ + +#include <memory> +#include <iostream> + +#include <Poco/NamedMutex.h> +#include <Poco/Util/Application.h> +#include <Poco/Net/WebSocket.h> +#include <Poco/Net/HTTPClientSession.h> +#include <Poco/Net/HTTPRequest.h> +#include <Poco/Net/HTTPResponse.h> +#include <Poco/Thread.h> +#include <Poco/Runnable.h> +#include <Poco/StringTokenizer.h> +#include <Poco/Exception.h> +#include <Poco/Process.h> + +#define LOK_USE_UNSTABLE_API +#include <LibreOfficeKit/LibreOfficeKitInit.h> + +#include "tsqueue.h" +#include "Util.hpp" +#include "ChildProcessSession.hpp" +#include "LOOLProtocol.hpp" + +using namespace LOOLProtocol; +using Poco::Util::Application; +using Poco::Net::WebSocket; +using Poco::Net::HTTPClientSession; +using Poco::Net::HTTPRequest; +using Poco::Net::HTTPResponse; +using Poco::Thread; +using Poco::Runnable; +using Poco::StringTokenizer; +using Poco::Exception; +using Poco::Process; + +class QueueHandler: public Runnable +{ +public: + QueueHandler(tsqueue<std::string>& queue): + _queue(queue) + { + } + + void setSession(std::shared_ptr<LOOLSession> session) + { + _session = session; + } + + void run() override + { + while (true) + { + std::string input = _queue.get(); + if (input == "eof") + break; + if (!_session->handleInput(input.c_str(), input.size())) + break; + } + } + +private: + std::shared_ptr<LOOLSession> _session; + tsqueue<std::string>& _queue; +}; + +const int MASTER_PORT_NUMBER = 9981; +const std::string CHILD_URI = "/loolws/child/"; + +Poco::NamedMutex _namedMutexLOOL("loolwsd"); + +int main(int argc, char** argv) +{ + std::string loSubPath = "lo"; + Poco::UInt64 _childId = Process::id(); + + try + { + _namedMutexLOOL.lock(); + +#ifdef __APPLE__ + LibreOfficeKit *loKit(lok_init_2(("/" + loSubPath + "/Frameworks").c_str(), "file:///user")); +#else + LibreOfficeKit *loKit(lok_init_2(("/" + loSubPath + "/program").c_str(), "file:///user")); +#endif + + if (!loKit) + { + Application::instance().logger().fatal(Util::logPrefix() + "LibreOfficeKit initialisation failed"); + exit(Application::EXIT_UNAVAILABLE); + } + + _namedMutexLOOL.unlock(); + + // Open websocket connection between the child process and the + // parent. The parent forwards us requests that it can't handle. + + HTTPClientSession cs("127.0.0.1", MASTER_PORT_NUMBER); + cs.setTimeout(0); + HTTPRequest request(HTTPRequest::HTTP_GET, CHILD_URI); + HTTPResponse response; + std::shared_ptr<WebSocket> ws(new WebSocket(cs, request, response)); + + std::shared_ptr<ChildProcessSession> session(new ChildProcessSession(ws, loKit)); + + ws->setReceiveTimeout(0); + + std::string hello("child " + std::to_string(_childId)); + session->sendTextFrame(hello); + + tsqueue<std::string> queue; + Thread queueHandlerThread; + QueueHandler handler(queue); + + handler.setSession(session); + queueHandlerThread.start(handler); + + int flags; + int n; + do + { + char buffer[1024]; + n = ws->receiveFrame(buffer, sizeof(buffer), flags); + + if (n > 0 && (flags & WebSocket::FRAME_OP_BITMASK) != WebSocket::FRAME_OP_CLOSE) + { + std::string firstLine = getFirstLine(buffer, n); + StringTokenizer tokens(firstLine, " ", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); + + // The only kind of messages a child process receives are the single-line ones (?) + assert(firstLine.size() == static_cast<std::string::size_type>(n)); + + // Check if it is a "canceltiles" and in that case remove outstanding + // "tile" messages from the queue. + if (tokens.count() == 1 && tokens[0] == "canceltiles") + { + queue.remove_if([](std::string& x) { + return (x.find("tile ") == 0 && x.find("id=") == std::string::npos); + }); + } + else + { + queue.put(firstLine); + } + } + } + while (n > 0 && (flags & WebSocket::FRAME_OP_BITMASK) != WebSocket::FRAME_OP_CLOSE); + + queue.clear(); + queue.put("eof"); + queueHandlerThread.join(); + } + catch (Exception& exc) + { + Application::instance().logger().log(Util::logPrefix() + "Exception: " + exc.what()); + } + catch (std::exception& exc) + { + Application::instance().logger().error(Util::logPrefix() + "Exception: " + exc.what()); + } + + std::cout << Util::logPrefix() << "loolkit finished OK!" << std::endl; + return 0; +} diff --git a/loolwsd/LOOLSession.cpp b/loolwsd/LOOLSession.cpp index 3294806..b0a7a05 100644 --- a/loolwsd/LOOLSession.cpp +++ b/loolwsd/LOOLSession.cpp @@ -24,58 +24,10 @@ #include <mutex> #include <set> -#define LOK_USE_UNSTABLE_API -#include <LibreOfficeKit/LibreOfficeKit.h> -#include <LibreOfficeKit/LibreOfficeKitEnums.h> - -#include <Poco/Exception.h> -#include <Poco/File.h> -#include <Poco/Net/HTTPStreamFactory.h> -#include <Poco/Net/WebSocket.h> -#include <Poco/Path.h> -#include <Poco/Process.h> -#include <Poco/Random.h> -#include <Poco/StreamCopier.h> -#include <Poco/String.h> -#include <Poco/StringTokenizer.h> -#include <Poco/ThreadLocal.h> -#include <Poco/URI.h> -#include <Poco/URIStreamOpener.h> -#include <Poco/Util/Application.h> -#include <Poco/Exception.h> -#include <Poco/Net/NetException.h> -#include <Poco/Net/DialogSocket.h> -#include <Poco/Net/SocketAddress.h> - -#include "LOKitHelper.hpp" -#include "LOOLProtocol.hpp" #include "LOOLSession.hpp" -#include "LOOLWSD.hpp" -#include "TileCache.hpp" #include "Util.hpp" -using namespace LOOLProtocol; - -using Poco::File; -using Poco::IOException; -using Poco::Net::HTTPStreamFactory; using Poco::Net::WebSocket; -using Poco::Path; -using Poco::Process; -using Poco::ProcessHandle; -using Poco::Random; -using Poco::StreamCopier; -using Poco::StringTokenizer; -using Poco::Thread; -using Poco::ThreadLocal; -using Poco::UInt64; -using Poco::URI; -using Poco::URIStreamOpener; -using Poco::Util::Application; -using Poco::Exception; -using Poco::Net::DialogSocket; -using Poco::Net::SocketAddress; -using Poco::Net::WebSocketException; const std::string LOOLSession::jailDocumentURL = "/user/thedocument"; @@ -84,7 +36,7 @@ LOOLSession::LOOLSession(std::shared_ptr<WebSocket> ws, Kind kind) : _ws(ws), _docURL("") { - std::cout << Util::logPrefix() << "LOOLSession ctor this=" << this << " " << _kind << " ws=" << _ws.get() << std::endl; + //std::cout << Util::logPrefix() << "LOOLSession ctor this=" << this << " " << _kind << " ws=" << _ws.get() << std::endl; if (kind == Kind::ToClient) { _kindString = "ToClient"; } @@ -98,7 +50,7 @@ LOOLSession::LOOLSession(std::shared_ptr<WebSocket> ws, Kind kind) : LOOLSession::~LOOLSession() { - std::cout << Util::logPrefix() << "LOOLSession dtor this=" << this << " " << _kind << std::endl; + //std::cout << Util::logPrefix() << "LOOLSession dtor this=" << this << " " << _kind << std::endl; Util::shutdownWebSocket(*_ws); } @@ -121,927 +73,4 @@ void LOOLSession::sendBinaryFrame(const char *buffer, int length) _ws->sendFrame(buffer, length, WebSocket::FRAME_BINARY); } - -std::map<Process::PID, UInt64> MasterProcessSession::_childProcesses; - -std::set<UInt64> MasterProcessSession::_pendingPreSpawnedChildren; -std::set<std::shared_ptr<MasterProcessSession>> MasterProcessSession::_availableChildSessions; -std::mutex MasterProcessSession::_availableChildSessionMutex; -std::condition_variable MasterProcessSession::_availableChildSessionCV; -Random MasterProcessSession::_rng; -std::mutex MasterProcessSession::_rngMutex; - -MasterProcessSession::MasterProcessSession(std::shared_ptr<WebSocket> ws, Kind kind) : - LOOLSession(ws, kind), - _childId(0), - _curPart(0) -{ - std::cout << Util::logPrefix() << "MasterProcessSession ctor this=" << this << " ws=" << _ws.get() << std::endl; -} - -MasterProcessSession::~MasterProcessSession() -{ - std::cout << Util::logPrefix() << "MasterProcessSession dtor this=" << this << " _peer=" << _peer.lock().get() << std::endl; - Util::shutdownWebSocket(*_ws); - auto peer = _peer.lock(); - if (_kind == Kind::ToClient && peer) - { - Util::shutdownWebSocket(*(peer->_ws)); - } -} - -bool MasterProcessSession::handleInput(const char *buffer, int length) -{ - Application::instance().logger().information(Util::logPrefix() + _kindString + ",Input," + getAbbreviatedMessage(buffer, length)); - - std::string firstLine = getFirstLine(buffer, length); - StringTokenizer tokens(firstLine, " ", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); - - if (haveSeparateProcess()) - { - // Note that this handles both forwarding requests from the client to the child process, and - // forwarding replies from the child process to the client. Or does it? - - // Snoop at some messages and manipulate tile cache information as needed - auto peer = _peer.lock(); - - if (_kind == Kind::ToPrisoner) - { - if (tokens[0] == "curpart:" && - tokens.count() == 2 && - getTokenInteger(tokens[1], "part", _curPart)) - { - return true; - } - } - - if (_kind == Kind::ToPrisoner && peer && peer->_tileCache) - { - if (tokens[0] == "tile:") - { - int part, width, height, tilePosX, tilePosY, tileWidth, tileHeight; - if (tokens.count() < 8 || - !getTokenInteger(tokens[1], "part", part) || - !getTokenInteger(tokens[2], "width", width) || - !getTokenInteger(tokens[3], "height", height) || - !getTokenInteger(tokens[4], "tileposx", tilePosX) || - !getTokenInteger(tokens[5], "tileposy", tilePosY) || - !getTokenInteger(tokens[6], "tilewidth", tileWidth) || - !getTokenInteger(tokens[7], "tileheight", tileHeight)) - assert(false); - - assert(firstLine.size() < static_cast<std::string::size_type>(length)); - peer->_tileCache->saveTile(part, width, height, tilePosX, tilePosY, tileWidth, tileHeight, buffer + firstLine.size() + 1, length - firstLine.size() - 1); - } - else if (tokens[0] == "status:") - { - peer->_tileCache->saveStatus(firstLine); - } - else if (tokens[0] == "invalidatetiles:") - { - // FIXME temporarily, set the editing on the 1st invalidate, TODO extend - // the protocol so that the client can set the editing or view only. - peer->_tileCache->setEditing(true); - - assert(firstLine.size() == static_cast<std::string::size_type>(length)); - peer->_tileCache->invalidateTiles(firstLine); - } - } - - forwardToPeer(buffer, length); - return true; - } - - if (tokens[0] == "child") - { - if (_kind != Kind::ToPrisoner) - { - sendTextFrame("error: cmd=child kind=invalid"); - return false; - } - if (!_peer.expired()) - { - sendTextFrame("error: cmd=child kind=invalid"); - return false; - } - if (tokens.count() != 2) - { - sendTextFrame("error: cmd=child kind=syntax"); - return false; - } - - UInt64 childId = std::stoull(tokens[1]); - // TODO. rework, the desktop and its childrem is jail root same folder - /*if (_pendingPreSpawnedChildren.find(childId) == _pendingPreSpawnedChildren.end()) - { - std::cout << Util::logPrefix() << "Error _pendingPreSpawnedChildren.find(childId)" << this << " id=" << childId << std::endl; - - sendTextFrame("error: cmd=child kind=notfound"); - return false; - }*/ - - if (_pendingPreSpawnedChildren.size() > 0) - { - std::set<UInt64>::iterator it = _pendingPreSpawnedChildren.begin(); - _pendingPreSpawnedChildren.erase(it); - } - std::unique_lock<std::mutex> lock(_availableChildSessionMutex); - _availableChildSessions.insert(shared_from_this()); - std::cout << Util::logPrefix() << "Inserted " << this << " id=" << childId << " into _availableChildSessions, size=" << _availableChildSessions.size() << std::endl; - _childId = childId; - lock.unlock(); - _availableChildSessionCV.notify_one(); - } - else if (_kind == Kind::ToPrisoner) - { - // Message from child process to be forwarded to client. - - // I think we should never get here - assert(false); - } - else if (tokens[0] == "load") - { - if (_docURL != "") - { - sendTextFrame("error: cmd=load kind=docalreadyloaded"); - return false; - } - return loadDocument(buffer, length, tokens); - } - else if (tokens[0] != "canceltiles" && - tokens[0] != "gettextselection" && - tokens[0] != "invalidatetiles" && - tokens[0] != "key" && - tokens[0] != "mouse" && - tokens[0] != "resetselection" && - tokens[0] != "saveas" && - tokens[0] != "selectgraphic" && - tokens[0] != "selecttext" && - tokens[0] != "setclientpart" && - tokens[0] != "status" && - tokens[0] != "tile" && - tokens[0] != "uno") - { - sendTextFrame("error: cmd=" + tokens[0] + " kind=unknown"); - return false; - } - else if (_docURL == "") - { - sendTextFrame("error: cmd=" + tokens[0] + " kind=nodocloaded"); - return false; - } - else if (tokens[0] == "canceltiles") - { - if (!_peer.expired()) - forwardToPeer(buffer, length); - } - else if (tokens[0] == "invalidatetiles") - { - return invalidateTiles(buffer, length, tokens); - } - else if (tokens[0] == "status") - { - return getStatus(buffer, length); - } - else if (tokens[0] == "tile") - { - sendTile(buffer, length, tokens); - } - else - { - // All other commands are such that they always require a - // LibreOfficeKitDocument session, i.e. need to be handled in - // a child process. - - if (_peer.expired()) - dispatchChild(); - forwardToPeer(buffer, length); - - if ((tokens.count() > 1 && tokens[0] == "uno" && tokens[1] == ".uno:Save") || - tokens[0] == "saveas") { - _tileCache->documentSaved(); - } - } - return true; -} - -bool MasterProcessSession::haveSeparateProcess() -{ - return _childId != 0; -} - -Path MasterProcessSession::getJailPath(UInt64 childId) -{ - return Path::forDirectory(LOOLWSD::childRoot + Path::separator() + std::to_string(childId)); -} - -void MasterProcessSession::addPendingChildrem(UInt64 childId) -{ - _pendingPreSpawnedChildren.insert(childId); -} - -int MasterProcessSession::getAvailableChildSessions() -{ - return _availableChildSessions.size(); -} - -int MasterProcessSession::getPendingPreSpawnedChildren() -{ - return _pendingPreSpawnedChildren.size(); -} - - -bool MasterProcessSession::invalidateTiles(const char *buffer, int length, StringTokenizer& tokens) -{ - int part, tilePosX, tilePosY, tileWidth, tileHeight; - - if (tokens.count() != 6 || - !getTokenInteger(tokens[1], "part", part) || - !getTokenInteger(tokens[2], "tileposx", tilePosX) || - !getTokenInteger(tokens[3], "tileposy", tilePosY) || - !getTokenInteger(tokens[4], "tilewidth", tileWidth) || - !getTokenInteger(tokens[5], "tileheight", tileHeight)) - { - sendTextFrame("error: cmd=invalidatetiles kind=syntax"); - return false; - } - - // FIXME temporarily, set the editing on the 1st invalidate, TODO extend - // the protocol so that the client can set the editing or view only. - _tileCache->setEditing(true); - - _tileCache->invalidateTiles(_curPart, tilePosX, tilePosY, tileWidth, tileHeight); - return true; -} - -bool MasterProcessSession::loadDocument(const char *buffer, int length, StringTokenizer& tokens) -{ - if (tokens.count() != 2) - { - sendTextFrame("error: cmd=load kind=syntax"); - return false; - } - - if (tokens[1].find("url=") == 0) - _docURL = tokens[1].substr(strlen("url=")); - else - _docURL = tokens[1]; - - try - { - URI aUri(_docURL); - } - catch(Poco::SyntaxException&) - { - sendTextFrame("error: cmd=load kind=URI invalid syntax"); - return false; - } - - _tileCache.reset(new TileCache(_docURL)); - - return true; -} - -bool MasterProcessSession::getStatus(const char *buffer, int length) -{ - std::string status; - - status = _tileCache->getStatus(); - if (status.size() > 0) - { - sendTextFrame(status); - return true; - } - - if (_peer.expired()) - dispatchChild(); - forwardToPeer(buffer, length); - return true; -} - -void MasterProcessSession::sendTile(const char *buffer, int length, StringTokenizer& tokens) -{ - int part, width, height, tilePosX, tilePosY, tileWidth, tileHeight; - - if (tokens.count() < 8 || - !getTokenInteger(tokens[1], "part", part) || - !getTokenInteger(tokens[2], "width", width) || - !getTokenInteger(tokens[3], "height", height) || - !getTokenInteger(tokens[4], "tileposx", tilePosX) || - !getTokenInteger(tokens[5], "tileposy", tilePosY) || - !getTokenInteger(tokens[6], "tilewidth", tileWidth) || - !getTokenInteger(tokens[7], "tileheight", tileHeight)) - { - sendTextFrame("error: cmd=tile kind=syntax"); - return; - } - - if (part < 0 || - width <= 0 || - height <= 0 || - tilePosX < 0 || - tilePosY < 0 || - tileWidth <= 0 || - tileHeight <= 0) - { - sendTextFrame("error: cmd=tile kind=invalid"); - return; - } - - std::string response = "tile: " + Poco::cat(std::string(" "), tokens.begin() + 1, tokens.end()) + "\n"; - - std::vector<char> output; - output.reserve(4 * width * height); - output.resize(response.size()); - std::memcpy(output.data(), response.data(), response.size()); - - std::unique_ptr<std::fstream> cachedTile = _tileCache->lookupTile(part, width, height, tilePosX, tilePosY, tileWidth, tileHeight); - if (cachedTile && cachedTile->is_open()) - { - cachedTile->seekg(0, std::ios_base::end); - size_t pos = output.size(); - std::streamsize size = cachedTile->tellg(); - output.resize(pos + size); - cachedTile->seekg(0, std::ios_base::beg); - cachedTile->read(output.data() + pos, size); - cachedTile->close(); - - sendBinaryFrame(output.data(), output.size()); - - return; - } - - if (_peer.expired()) - dispatchChild(); - forwardToPeer(buffer, length); -} - -void MasterProcessSession::dispatchChild() -{ - // Copy document into jail using the fixed name - - std::shared_ptr<MasterProcessSession> childSession; - std::unique_lock<std::mutex> lock(_availableChildSessionMutex); - - std::cout << Util::logPrefix() << "_availableChildSessions size=" << _availableChildSessions.size() << " _pendingChildSessions size=" << _pendingPreSpawnedChildren.size() << std::endl; - - if (_availableChildSessions.size() == 0) - { - if (_pendingPreSpawnedChildren.size() == 0) - { - // Running out of pre-spawned children, so spawn one more - Application::instance().logger().information(Util::logPrefix() + "Running out of pre-spawned childred, adding one more"); - } - - std::cout << Util::logPrefix() << "waiting for a child session to become available" << std::endl; - _availableChildSessionCV.wait(lock, [] { return _availableChildSessions.size() > 0; }); - std::cout << Util::logPrefix() << "waiting done" << std::endl; - } - - childSession = *(_availableChildSessions.begin()); - - _availableChildSessions.erase(childSession); - std::cout << Util::logPrefix() << "_availableChildSessions size=" << _availableChildSessions.size() << std::endl; - if (_availableChildSessions.size() == 0) - LOOLWSD::_sharedForkChild.begin()[0] = 1; - lock.unlock(); - - // Assume a valid URI - URI aUri(_docURL); - - if (aUri.isRelative()) - aUri = URI( URI("file://"), aUri.toString() ); - - if (!aUri.empty() && aUri.getScheme() == "file") - { - Path aSrcFile(aUri.getPath()); - Path aDstFile(Path(getJailPath(childSession->_childId), jailDocumentURL.substr(1)), aSrcFile.getFileName()); - Path aDstPath(getJailPath(childSession->_childId), jailDocumentURL.substr(1)); - Path aJailFile(jailDocumentURL, aSrcFile.getFileName()); - - try - { - File(aDstPath).createDirectories(); - } - catch (Exception& exc) - { - Application::instance().logger().error( Util::logPrefix() + - "createDirectories(\"" + aDstPath.toString() + "\") failed: " + exc.displayText() ); - - } - -#ifdef __linux - Application::instance().logger().information(Util::logPrefix() + "Linking " + aSrcFile.toString() + " to " + aDstFile.toString()); - if (link(aSrcFile.toString().c_str(), aDstFile.toString().c_str()) == -1) - { - // Failed - Application::instance().logger().error( Util::logPrefix() + - "link(\"" + aSrcFile.toString() + "\",\"" + aDstFile.toString() + "\") failed: " + strerror(errno) ); - } -#endif - - try - { - //fallback - if (!File(aDstFile).exists()) - { - Application::instance().logger().information(Util::logPrefix() + "Copying " + aSrcFile.toString() + " to " + aDstFile.toString()); - File(aSrcFile).copyTo(aDstFile.toString()); - } - } - catch (Exception& exc) - { - Application::instance().logger().error( Util::logPrefix() + - "copyTo(\"" + aSrcFile.toString() + "\",\"" + aDstFile.toString() + "\") failed: " + exc.displayText()); - } - } - - _peer = childSession; - childSession->_peer = shared_from_this(); - - std::string loadRequest = "load url=" + _docURL; - forwardToPeer(loadRequest.c_str(), loadRequest.size()); -} - -void MasterProcessSession::forwardToPeer(const char *buffer, int length) -{ - Application::instance().logger().information(Util::logPrefix() + _kindString + ",forwardToPeer," + getAbbreviatedMessage(buffer, length)); - auto peer = _peer.lock(); - if (!peer) - return; - peer->sendBinaryFrame(buffer, length); -} - -ChildProcessSession::ChildProcessSession(std::shared_ptr<WebSocket> ws, LibreOfficeKit *loKit) : - LOOLSession(ws, Kind::ToMaster), - _loKitDocument(NULL), - _loKit(loKit), - _clientPart(0) -{ - std::cout << Util::logPrefix() << "ChildProcessSession ctor this=" << this << " ws=" << _ws.get() << std::endl; -} - -ChildProcessSession::~ChildProcessSession() -{ - std::cout << Util::logPrefix() << "ChildProcessSession dtor this=" << this << std::endl; - if (LIBREOFFICEKIT_HAS(_loKit, registerCallback)) - _loKit->pClass->registerCallback(_loKit, 0, 0); - Util::shutdownWebSocket(*_ws); -} - -bool ChildProcessSession::handleInput(const char *buffer, int length) -{ - std::string firstLine = getFirstLine(buffer, length); - StringTokenizer tokens(firstLine, " ", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); - - Application::instance().logger().information(Util::logPrefix() + _kindString + ",Input," + getAbbreviatedMessage(buffer, length)); - - if (tokens[0] == "load") - { - if (_docURL != "") - { - sendTextFrame("error: cmd=load kind=docalreadyloaded"); - return false; - } - return loadDocument(buffer, length, tokens); - } - else if (_docURL == "") - { - sendTextFrame("error: cmd=" + tokens[0] + " kind=nodocloaded"); - return false; - } - else if (tokens[0] == "setclientpart") - { - return setClientPart(buffer, length, tokens); - } - else if (tokens[0] == "status") - { - return getStatus(buffer, length); - } - else if (tokens[0] == "tile") - { - sendTile(buffer, length, tokens); - } - else - { - // All other commands are such that they always require a LibreOfficeKitDocument session, - // i.e. need to be handled in a child process. - - assert(tokens[0] == "gettextselection" || - tokens[0] == "key" || - tokens[0] == "mouse" || - tokens[0] == "uno" || - tokens[0] == "selecttext" || - tokens[0] == "selectgraphic" || - tokens[0] == "resetselection" || - tokens[0] == "saveas"); - - if (_loKitDocument->pClass->getPart(_loKitDocument) != _clientPart) - { - _loKitDocument->pClass->setPart(_loKitDocument, _clientPart); - } - if (tokens[0] == "gettextselection") - { - return getTextSelection(buffer, length, tokens); - } - else if (tokens[0] == "key") - { - return keyEvent(buffer, length, tokens); - } - else if (tokens[0] == "mouse") - { - return mouseEvent(buffer, length, tokens); - } - else if (tokens[0] == "uno") - { - return unoCommand(buffer, length, tokens); - } - else if (tokens[0] == "selecttext") - { - return selectText(buffer, length, tokens); - } - else if (tokens[0] == "selectgraphic") - { - return selectGraphic(buffer, length, tokens); - } - else if (tokens[0] == "resetselection") - { - return resetSelection(buffer, length, tokens); - } - else if (tokens[0] == "saveas") - { - return saveAs(buffer, length, tokens); - } - else - { - assert(false); - } - } - return true; -} - -extern "C" -{ - static void myCallback(int nType, const char* pPayload, void* pData) - { - ChildProcessSession *srv = reinterpret_cast<ChildProcessSession *>(pData); - - switch ((LibreOfficeKitCallbackType) nType) - { - case LOK_CALLBACK_INVALIDATE_TILES: - { - int curPart = srv->_loKitDocument->pClass->getPart(srv->_loKitDocument); - srv->sendTextFrame("curpart: part=" + std::to_string(curPart)); - StringTokenizer tokens(std::string(pPayload), " ", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); - if (tokens.count() == 4) - { - int x(std::stoi(tokens[0])); - int y(std::stoi(tokens[1])); - int width(std::stoi(tokens[2])); - int height(std::stoi(tokens[3])); - srv->sendTextFrame("invalidatetiles:" - " part=" + std::to_string(curPart) + - " x=" + std::to_string(x) + - " y=" + std::to_string(y) + - " width=" + std::to_string(width) + - " height=" + std::to_string(height)); - } - else { - srv->sendTextFrame("invalidatetiles: " + std::string(pPayload)); - } - } - break; - case LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR: - srv->sendTextFrame("invalidatecursor: " + std::string(pPayload)); - break; - case LOK_CALLBACK_TEXT_SELECTION: - srv->sendTextFrame("textselection: " + std::string(pPayload)); - break; - case LOK_CALLBACK_TEXT_SELECTION_START: - srv->sendTextFrame("textselectionstart: " + std::string(pPayload)); - break; - case LOK_CALLBACK_TEXT_SELECTION_END: - srv->sendTextFrame("textselectionend: " + std::string(pPayload)); - break; - case LOK_CALLBACK_CURSOR_VISIBLE: - srv->sendTextFrame("cursorvisible: " + std::string(pPayload)); - break; - case LOK_CALLBACK_GRAPHIC_SELECTION: - srv->sendTextFrame("graphicselection: " + std::string(pPayload)); - break; - case LOK_CALLBACK_HYPERLINK_CLICKED: - srv->sendTextFrame("hyperlinkclicked: " + std::string(pPayload)); - break; - case LOK_CALLBACK_STATE_CHANGED: - srv->sendTextFrame("statechanged: " + std::string(pPayload)); - break; - case LOK_CALLBACK_STATUS_INDICATOR_START: - srv->sendTextFrame("statusindicatorstart:"); - break; - case LOK_CALLBACK_STATUS_INDICATOR_SET_VALUE: - srv->sendTextFrame("statusindicatorsetvalue: " + std::string(pPayload)); - break; - case LOK_CALLBACK_STATUS_INDICATOR_FINISH: - srv->sendTextFrame("statusindicatorfinish:"); - break; - case LOK_CALLBACK_SEARCH_NOT_FOUND: - srv->sendTextFrame("searchnotfound: " + std::string(pPayload)); - break; - case LOK_CALLBACK_DOCUMENT_SIZE_CHANGED: - srv->getStatus("", 0); - break; - case LOK_CALLBACK_SET_PART: - srv->sendTextFrame("setpart: " + std::string(pPayload)); - break; - } - } -} - -bool ChildProcessSession::loadDocument(const char *buffer, int length, StringTokenizer& tokens) -{ - if (tokens.count() != 2) - { - sendTextFrame("error: cmd=load kind=syntax"); - return false; - } - - if (tokens[1].find("url=") == 0) - _docURL = tokens[1].substr(strlen("url=")); - else - _docURL = tokens[1]; - - URI aUri; - try - { - aUri = URI(_docURL); - } - catch(Poco::SyntaxException&) - { - sendTextFrame("error: cmd=load kind=URI invalid syntax"); - return false; - } - - if (aUri.empty()) - { - sendTextFrame("error: cmd=load kind=URI empty"); - return false; - } - - // The URL in the request is the original one, not visible in the chroot jail. - // The child process uses the fixed name jailDocumentURL. - - if (LIBREOFFICEKIT_HAS(_loKit, registerCallback)) - _loKit->pClass->registerCallback(_loKit, myCallback, this); - - if (aUri.isRelative() || aUri.getScheme() == "file") - aUri = URI( URI("file://"), Path(jailDocumentURL, Path(aUri.getPath()).getFileName()).toString() ); - - if ((_loKitDocument = _loKit->pClass->documentLoad(_loKit, aUri.toString().c_str())) == NULL) - { - sendTextFrame("error: cmd=load kind=failed"); - return false; - } - - _loKitDocument->pClass->initializeForRendering(_loKitDocument); - - if (!getStatus(buffer, length)) - return false; - _loKitDocument->pClass->registerCallback(_loKitDocument, myCallback, this); - - return true; -} - -bool ChildProcessSession::getStatus(const char *buffer, int length) -{ - std::string status = "status: " + LOKitHelper::documentStatus(_loKitDocument); - - sendTextFrame(status); - - return true; -} - -void ChildProcessSession::sendTile(const char *buffer, int length, StringTokenizer& tokens) -{ - int part, width, height, tilePosX, tilePosY, tileWidth, tileHeight; - - if (tokens.count() < 8 || - !getTokenInteger(tokens[1], "part", part) || - !getTokenInteger(tokens[2], "width", width) || - !getTokenInteger(tokens[3], "height", height) || - !getTokenInteger(tokens[4], "tileposx", tilePosX) || - !getTokenInteger(tokens[5], "tileposy", tilePosY) || - !getTokenInteger(tokens[6], "tilewidth", tileWidth) || - !getTokenInteger(tokens[7], "tileheight", tileHeight)) - { - sendTextFrame("error: cmd=tile kind=syntax"); - return; - } - - if (part < 0 || - width <= 0 || - height <= 0 || - tilePosX < 0 || - tilePosY < 0 || - tileWidth <= 0 || - tileHeight <= 0) - { - sendTextFrame("error: cmd=tile kind=invalid"); - return; - } - - std::string response = "tile: " + Poco::cat(std::string(" "), tokens.begin() + 1, tokens.end()) + "\n"; - - std::vector<char> output; - output.reserve(4 * width * height); - output.resize(response.size()); - std::memcpy(output.data(), response.data(), response.size()); - - unsigned char *pixmap = new unsigned char[4 * width * height]; - _loKitDocument->pClass->setPart(_loKitDocument, part); - _loKitDocument->pClass->paintTile(_loKitDocument, pixmap, width, height, tilePosX, tilePosY, tileWidth, tileHeight); - - if (!Util::encodePNGAndAppendToBuffer(pixmap, width, height, output)) - { - sendTextFrame("error: cmd=tile kind=failure"); - return; - } - - delete[] pixmap; - - sendBinaryFrame(output.data(), output.size()); -} - -bool ChildProcessSession::getTextSelection(const char *buffer, int length, StringTokenizer& tokens) -{ - std::string mimeType; - - if (tokens.count() != 2 || - !getTokenString(tokens[1], "mimetype", mimeType)) - { - sendTextFrame("error: cmd=gettextselection kind=syntax"); - return false; - } - - char *textSelection = _loKitDocument->pClass->getTextSelection(_loKitDocument, mimeType.c_str(), NULL); - - sendTextFrame("textselectioncontent: " + std::string(textSelection)); - return true; -} - -bool ChildProcessSession::keyEvent(const char *buffer, int length, StringTokenizer& tokens) -{ - int type, charcode, keycode; - - if (tokens.count() != 4 || - !getTokenKeyword(tokens[1], "type", - {{"input", LOK_KEYEVENT_KEYINPUT}, {"up", LOK_KEYEVENT_KEYUP}}, - type) || - !getTokenInteger(tokens[2], "char", charcode) || - !getTokenInteger(tokens[3], "key", keycode)) - { - sendTextFrame("error: cmd=key kind=syntax"); - return false; - } - - _loKitDocument->pClass->postKeyEvent(_loKitDocument, type, charcode, keycode); - - return true; -} - -bool ChildProcessSession::mouseEvent(const char *buffer, int length, StringTokenizer& tokens) -{ - int type, x, y, count; - - if (tokens.count() != 5 || - !getTokenKeyword(tokens[1], "type", - {{"buttondown", LOK_MOUSEEVENT_MOUSEBUTTONDOWN}, - {"buttonup", LOK_MOUSEEVENT_MOUSEBUTTONUP}, - {"move", LOK_MOUSEEVENT_MOUSEMOVE}}, - type) || - !getTokenInteger(tokens[2], "x", x) || - !getTokenInteger(tokens[3], "y", y) || - !getTokenInteger(tokens[4], "count", count)) - { - sendTextFrame("error: cmd=mouse kind=syntax"); - return false; - } - - _loKitDocument->pClass->postMouseEvent(_loKitDocument, type, x, y, count); - - return true; -} - -bool ChildProcessSession::unoCommand(const char *buffer, int length, StringTokenizer& tokens) -{ - if (tokens.count() == 1) - { - sendTextFrame("error: cmd=uno kind=syntax"); - return false; - } - - if (tokens.count() == 2) - { - _loKitDocument->pClass->postUnoCommand(_loKitDocument, tokens[1].c_str(), 0); - } - else - { - _loKitDocument->pClass->postUnoCommand(_loKitDocument, tokens[1].c_str(), Poco::cat(std::string(" "), tokens.begin() + 2, tokens.end()).c_str()); - } - - return true; -} - -bool ChildProcessSession::selectText(const char *buffer, int length, StringTokenizer& tokens) -{ - int type, x, y; - - if (tokens.count() != 4 || - !getTokenKeyword(tokens[1], "type", - {{"start", LOK_SETTEXTSELECTION_START}, - {"end", LOK_SETTEXTSELECTION_END}, - {"reset", LOK_SETTEXTSELECTION_RESET}}, - type) || - !getTokenInteger(tokens[2], "x", x) || - !getTokenInteger(tokens[3], "y", y)) - { - sendTextFrame("error: cmd=selecttext kind=syntax"); - return false; - } - - _loKitDocument->pClass->setTextSelection(_loKitDocument, type, x, y); - - return true; -} - -bool ChildProcessSession::selectGraphic(const char *buffer, int length, StringTokenizer& tokens) -{ - int type, x, y; - - if (tokens.count() != 4 || - !getTokenKeyword(tokens[1], "type", - {{"start", LOK_SETGRAPHICSELECTION_START}, - {"end", LOK_SETGRAPHICSELECTION_END}}, - type) || - !getTokenInteger(tokens[2], "x", x) || - !getTokenInteger(tokens[3], "y", y)) - { - sendTextFrame("error: cmd=selectghraphic kind=syntax"); - return false; - } - - _loKitDocument->pClass->setGraphicSelection(_loKitDocument, type, x, y); - - return true; -} - -bool ChildProcessSession::resetSelection(const char *buffer, int length, StringTokenizer& tokens) -{ - if (tokens.count() != 1) - { - sendTextFrame("error: cmd=resetselection kind=syntax"); - return false; - } - - _loKitDocument->pClass->resetSelection(_loKitDocument); - - return true; -} - -bool ChildProcessSession::saveAs(const char *buffer, int length, StringTokenizer& tokens) -{ - std::string url, format, filterOptions; - - if (tokens.count() < 4 || - !getTokenString(tokens[1], "url", url)) - { - sendTextFrame("error: cmd=saveas kind=syntax"); - return false; - } - - URI::decode(url, url, true); - if (getTokenString(tokens[2], "format", format)) { - URI::decode(format, format, true); - } - - if (getTokenString(tokens[3], "options", filterOptions)) { - if (tokens.count() > 4) { - filterOptions += Poco::cat(std::string(" "), tokens.begin() + 4, tokens.end()); - } - } - - _loKitDocument->pClass->saveAs(_loKitDocument, url.c_str(), format.c_str(), filterOptions.c_str()); - - return true; -} - -bool ChildProcessSession::setClientPart(const char *buffer, int length, StringTokenizer& tokens) -{ - if (tokens.count() < 2 || - !getTokenInteger(tokens[1], "part", _clientPart)) - { - return false; - } - return true; -} - /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/loolwsd/LOOLSession.hpp b/loolwsd/LOOLSession.hpp index 361f6fb..bd770c1 100644 --- a/loolwsd/LOOLSession.hpp +++ b/loolwsd/LOOLSession.hpp @@ -18,18 +18,8 @@ #include <ostream> #include <set> -#define LOK_USE_UNSTABLE_API -#include <LibreOfficeKit/LibreOfficeKit.h> - #include <Poco/Net/WebSocket.h> -#include <Poco/Buffer.h> -#include <Poco/Path.h> -#include <Poco/Process.h> -#include <Poco/Random.h> #include <Poco/StringTokenizer.h> -#include <Poco/Types.h> - -#include "TileCache.hpp" // We have three kinds of Websocket sessions // 1) Between the master loolwsd server to the end-user LOOL client @@ -93,98 +83,6 @@ inline std::basic_ostream<charT, traits> & operator <<(std::basic_ostream<charT, } } -class MasterProcessSession final : public LOOLSession, public std::enable_shared_from_this<MasterProcessSession> -{ -public: - MasterProcessSession(std::shared_ptr<Poco::Net::WebSocket> ws, Kind kind); - virtual ~MasterProcessSession(); - - virtual bool handleInput(const char *buffer, int length) override; - - bool haveSeparateProcess(); - - static Poco::Path getJailPath(Poco::UInt64 childId); - static void addPendingChildrem(Poco::UInt64 childId); - static int getAvailableChildSessions(); - static int getPendingPreSpawnedChildren(); - - static std::map<Poco::Process::PID, Poco::UInt64> _childProcesses; - - virtual bool getStatus(const char *buffer, int length); - - protected: - bool invalidateTiles(const char *buffer, int length, Poco::StringTokenizer& tokens); - - virtual bool loadDocument(const char *buffer, int length, Poco::StringTokenizer& tokens) override; - - virtual void sendTile(const char *buffer, int length, Poco::StringTokenizer& tokens); - - void dispatchChild(); - void forwardToPeer(const char *buffer, int length); - - // If _kind==ToPrisoner and the child process has started and completed its handshake with the - // parent process: Points to the WebSocketSession for the child process handling the document in - // question, if any. - - // In the session to the child process, points to the LOOLSession for the LOOL client. This will - // obvious have to be rethought when we add collaboration and there can be several LOOL clients - // per document being edited (i.e., per child process). - std::weak_ptr<MasterProcessSession> _peer; - - // Pre-spawned child processes that haven't yet connected. - static std::set<Poco::UInt64> _pendingPreSpawnedChildren; - - // Sessions to pre-spawned child processes that have connected but are not yet assigned a - // document to work on. - static std::set<std::shared_ptr<MasterProcessSession>> _availableChildSessions; - static std::mutex _availableChildSessionMutex; - static std::condition_variable _availableChildSessionCV; - - std::unique_ptr<TileCache> _tileCache; - -private: - // The id of the child process - Poco::UInt64 _childId; - static Poco::Random _rng; - static std::mutex _rngMutex; - int _curPart; -}; - -class ChildProcessSession final : public LOOLSession -{ -public: - ChildProcessSession(std::shared_ptr<Poco::Net::WebSocket> ws, LibreOfficeKit *loKit); - virtual ~ChildProcessSession(); - - virtual bool handleInput(const char *buffer, int length) override; - - virtual bool getStatus(const char *buffer, int length); - - LibreOfficeKitDocument *_loKitDocument; - - protected: - virtual bool loadDocument(const char *buffer, int length, Poco::StringTokenizer& tokens) override; - - virtual void sendTile(const char *buffer, int length, Poco::StringTokenizer& tokens); - - bool getTextSelection(const char *buffer, int length, Poco::StringTokenizer& tokens); - bool keyEvent(const char *buffer, int length, Poco::StringTokenizer& tokens); - bool mouseEvent(const char *buffer, int length, Poco::StringTokenizer& tokens); - bool unoCommand(const char *buffer, int length, Poco::StringTokenizer& tokens); - bool selectText(const char *buffer, int length, Poco::StringTokenizer& tokens); - bool selectGraphic(const char *buffer, int length, Poco::StringTokenizer& tokens); - bool resetSelection(const char *buffer, int length, Poco::StringTokenizer& tokens); - bool saveAs(const char *buffer, int length, Poco::StringTokenizer& tokens); - bool setClientPart(const char *buffer, int length, Poco::StringTokenizer& tokens); - - std::string _jail; - std::string _loSubPath; - LibreOfficeKit *_loKit; - - private: - int _clientPart; -}; - #endif /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/loolwsd/LOOLWSD.cpp b/loolwsd/LOOLWSD.cpp index 4bccf72..ee36e0b 100644 --- a/loolwsd/LOOLWSD.cpp +++ b/loolwsd/LOOLWSD.cpp @@ -56,6 +56,7 @@ DEALINGS IN THE SOFTWARE. #include <cassert> #include <cstdlib> #include <cstring> +#include <string> #include <iostream> #include <sstream> #include <mutex> @@ -96,7 +97,7 @@ DEALINGS IN THE SOFTWARE. #include "LOOLProtocol.hpp" -#include "LOOLSession.hpp" +#include "MasterProcessSession.hpp" #include "LOOLWSD.hpp" #include "tsqueue.h" #include "Util.hpp" @@ -138,6 +139,84 @@ using Poco::Net::Socket; using Poco::ThreadLocal; using Poco::Random; using Poco::NamedMutex; +using Poco::ProcessHandle; + + +namespace +{ + void dropCapability( +#ifdef __linux + cap_value_t capability +#endif + ) + { +#ifdef __linux + cap_t caps; + cap_value_t cap_list[] = { capability }; + + caps = cap_get_proc(); + if (caps == NULL) + { + Application::instance().logger().error(Util::logPrefix() + "cap_get_proc() failed: " + strerror(errno)); + exit(1); + } + + if (cap_set_flag(caps, CAP_EFFECTIVE, sizeof(cap_list)/sizeof(cap_list[0]), cap_list, CAP_CLEAR) == -1 || + cap_set_flag(caps, CAP_PERMITTED, sizeof(cap_list)/sizeof(cap_list[0]), cap_list, CAP_CLEAR) == -1) + { + Application::instance().logger().error(Util::logPrefix() + "cap_set_flag() failed: " + strerror(errno)); + exit(1); + } + + if (cap_set_proc(caps) == -1) + { + Application::instance().logger().error(std::string("cap_set_proc() failed: ") + strerror(errno)); + exit(1); + } + + char *capText = cap_to_text(caps, NULL); + Application::instance().logger().information(Util::logPrefix() + "Capabilities now: " + capText); + cap_free(capText); + + cap_free(caps); +#endif + // We assume that on non-Linux we don't need to be root to be able to hardlink to files we + // don't own, so drop root. + if (geteuid() == 0 && getuid() != 0) + { + // The program is setuid root. Not normal on Linux where we use setcap, but if this + // needs to run on non-Linux Unixes, setuid root is what it will bneed to be to be able + // to do chroot(). + if (setuid(getuid()) != 0) { + Application::instance().logger().error(std::string("setuid() failed: ") + strerror(errno)); + } + } +#if ENABLE_DEBUG + if (geteuid() == 0 && getuid() == 0) + { +#ifdef __linux + // Argh, awful hack + if (capability == CAP_FOWNER) + return; +#endif + + // Running under sudo, probably because being debugged? Let's drop super-user rights. + LOOLWSD::runningAsRoot = true; + if (LOOLWSD::uid == 0) + { + struct passwd *nobody = getpwnam("nobody"); + if (nobody) + LOOLWSD::uid = nobody->pw_uid; + else + LOOLWSD::uid = 65534; + } + if (setuid(LOOLWSD::uid) != 0) { + Application::instance().logger().error(std::string("setuid() failed: ") + strerror(errno)); + } + } +#endif + } +} class QueueHandler: public Runnable { @@ -327,91 +406,6 @@ public: } }; -class TestOutput : public Runnable -{ -public: - TestOutput(WebSocket& ws) : - _ws(ws) - { - } - - void run() override - { - int flags; - int n; - _ws.setReceiveTimeout(0); - try - { - do - { - char buffer[100000]; - n = _ws.receiveFrame(buffer, sizeof(buffer), flags); - - if (n > 0 && (flags & WebSocket::FRAME_OP_BITMASK) != WebSocket::FRAME_OP_CLOSE) - { - std::cout << - Util::logPrefix() << - "Client got " << n << " bytes: " << getAbbreviatedMessage(buffer, n) << - std::endl; - } - } - while (n > 0 && (flags & WebSocket::FRAME_OP_BITMASK) != WebSocket::FRAME_OP_CLOSE); - } - catch (WebSocketException& exc) - { - Application::instance().logger().error(Util::logPrefix() + "WebSocketException: " + exc.message()); - _ws.close(); - } - } - -private: - WebSocket& _ws; -}; - -class TestInput : public Runnable -{ -public: - TestInput(ServerApplication& main, ServerSocket& svs, HTTPServer& srv) : - _main(main), - _svs(svs), - _srv(srv) - { - } - - void run() override - { - HTTPClientSession cs("127.0.0.1", _svs.address().port()); - HTTPRequest request(HTTPRequest::HTTP_GET, "/ws"); - HTTPResponse response; - WebSocket ws(cs, request, response); - - Thread thread; - TestOutput output(ws); - thread.start(output); - - if (isatty(0)) - { - std::cout << std::endl; - std::cout << "Enter LOOL WS requests, one per line. Enter EOF to finish." << std::endl; - } - - while (!std::cin.eof()) - { - std::string line; - std::getline(std::cin, line); - ws.sendFrame(line.c_str(), line.size()); - } - thread.join(); - _srv.stopAll(); - _main.terminate(); - } - -private: - ServerApplication& _main; - ServerSocket& _svs; - HTTPServer& _srv; -}; - int LOOLWSD::portNumber = DEFAULT_CLIENT_PORT_NUMBER; std::string LOOLWSD::cache = LOOLWSD_CACHEDIR; std::string LOOLWSD::sysTemplate; @@ -562,436 +556,35 @@ void LOOLWSD::displayHelp() helpFormatter.format(std::cout); } -namespace -{ - ThreadLocal<std::string> sourceForLinkOrCopy; - ThreadLocal<Path> destinationForLinkOrCopy; - - int linkOrCopyFunction(const char *fpath, - const struct stat *sb, - int typeflag, - struct FTW *ftwbuf) - { - if (strcmp(fpath, sourceForLinkOrCopy->c_str()) == 0) - return 0; - - assert(fpath[strlen(sourceForLinkOrCopy->c_str())] == '/'); - const char *relativeOldPath = fpath + strlen(sourceForLinkOrCopy->c_str()) + 1; - -#ifdef __APPLE__ - if (strcmp(relativeOldPath, "PkgInfo") == 0) - return 0; -#endif - - Path newPath(*destinationForLinkOrCopy, Path(relativeOldPath)); - - switch (typeflag) - { - case FTW_F: - File(newPath.parent()).createDirectories(); - if (link(fpath, newPath.toString().c_str()) == -1) - { - Application::instance().logger().error(Util::logPrefix() + - "link(\"" + fpath + "\",\"" + newPath.toString() + "\") failed: " + - strerror(errno)); - exit(1); - } - break; - case FTW_DP: - { - struct stat st; - if (stat(fpath, &st) == -1) - { - Application::instance().logger().error(Util::logPrefix() + - "stat(\"" + fpath + "\") failed: " + - strerror(errno)); - return 1; - } - File(newPath).createDirectories(); - struct utimbuf ut; - ut.actime = st.st_atime; - ut.modtime = st.st_mtime; - if (utime(newPath.toString().c_str(), &ut) == -1) - { - Application::instance().logger().error(Util::logPrefix() + - "utime(\"" + newPath.toString() + "\", &ut) failed: " + - strerror(errno)); - return 1; - } - } - break; - case FTW_DNR: - Application::instance().logger().error(Util::logPrefix() + - "Cannot read directory '" + fpath + "'"); - return 1; - case FTW_NS: - Application::instance().logger().error(Util::logPrefix() + - "nftw: stat failed for '" + fpath + "'"); - return 1; - case FTW_SLN: - Application::instance().logger().information(Util::logPrefix() + - "nftw: symlink to nonexistent file: '" + fpath + "', ignored"); - break; - default: - assert(false); - } - return 0; - } - - void linkOrCopy(const std::string& source, const Path& destination) - { - *sourceForLinkOrCopy = source; - if (sourceForLinkOrCopy->back() == '/') - sourceForLinkOrCopy->pop_back(); - *destinationForLinkOrCopy = destination; - if (nftw(source.c_str(), linkOrCopyFunction, 10, FTW_DEPTH) == -1) - Application::instance().logger().error(Util::logPrefix() + - "linkOrCopy: nftw() failed for '" + source + "'"); - } - - void dropCapability( -#ifdef __linux - cap_value_t capability -#endif - ) - { -#ifdef __linux - cap_t caps; - cap_value_t cap_list[] = { capability }; - - caps = cap_get_proc(); - if (caps == NULL) - { - Application::instance().logger().error(Util::logPrefix() + "cap_get_proc() failed: " + strerror(errno)); - exit(1); - } - - if (cap_set_flag(caps, CAP_EFFECTIVE, sizeof(cap_list)/sizeof(cap_list[0]), cap_list, CAP_CLEAR) == -1 || - cap_set_flag(caps, CAP_PERMITTED, sizeof(cap_list)/sizeof(cap_list[0]), cap_list, CAP_CLEAR) == -1) - { - Application::instance().logger().error(Util::logPrefix() + "cap_set_flag() failed: " + strerror(errno)); - exit(1); - } - - if (cap_set_proc(caps) == -1) - { - Application::instance().logger().error(std::string("cap_set_proc() failed: ") + strerror(errno)); - exit(1); - } - - char *capText = cap_to_text(caps, NULL); - Application::instance().logger().information(Util::logPrefix() + "Capabilities now: " + capText); - cap_free(capText); - - cap_free(caps); -#endif - // We assume that on non-Linux we don't need to be root to be able to hardlink to files we - // don't own, so drop root. - if (geteuid() == 0 && getuid() != 0) - { - // The program is setuid root. Not normal on Linux where we use setcap, but if this - // needs to run on non-Linux Unixes, setuid root is what it will bneed to be to be able - // to do chroot(). - if (setuid(getuid()) != 0) { - Application::instance().logger().error(std::string("setuid() failed: ") + strerror(errno)); - } - } -#if ENABLE_DEBUG - if (geteuid() == 0 && getuid() == 0) - { -#ifdef __linux - // Argh, awful hack - if (capability == CAP_FOWNER) - return; -#endif - - // Running under sudo, probably because being debugged? Let's drop super-user rights. - LOOLWSD::runningAsRoot = true; - if (LOOLWSD::uid == 0) - { - struct passwd *nobody = getpwnam("nobody"); - if (nobody) - LOOLWSD::uid = nobody->pw_uid; - else - LOOLWSD::uid = 65534; - } - if (setuid(LOOLWSD::uid) != 0) { - Application::instance().logger().error(std::string("setuid() failed: ") + strerror(errno)); - } - } -#endif - } -} - -// Writer, Impress or Calc -void LOOLWSD::componentMain() -{ - try - { - _namedMutexLOOL.lock(); - -#ifdef __APPLE__ - LibreOfficeKit *loKit(lok_init_2(("/" + loSubPath + "/Frameworks").c_str(), "file:///user")); -#else - LibreOfficeKit *loKit(lok_init_2(("/" + loSubPath + "/program").c_str(), "file:///user")); -#endif - - if (!loKit) - { - logger().fatal(Util::logPrefix() + "LibreOfficeKit initialisation failed"); - exit(Application::EXIT_UNAVAILABLE); - } - - _namedMutexLOOL.unlock(); - - // Open websocket connection between the child process and the - // parent. The parent forwards us requests that it can't handle. - - HTTPClientSession cs("127.0.0.1", MASTER_PORT_NUMBER); - cs.setTimeout(0); - HTTPRequest request(HTTPRequest::HTTP_GET, LOOLWSD::CHILD_URI); - HTTPResponse response; - std::shared_ptr<WebSocket> ws(new WebSocket(cs, request, response)); - - std::shared_ptr<ChildProcessSession> session(new ChildProcessSession(ws, loKit)); - - ws->setReceiveTimeout(0); - - std::string hello("child " + std::to_string(_childId)); - session->sendTextFrame(hello); - - tsqueue<std::string> queue; - Thread queueHandlerThread; - QueueHandler handler(queue); - - handler.setSession(session); - queueHandlerThread.start(handler); - - int flags; - int n; - do - { - char buffer[1024]; - n = ws->receiveFrame(buffer, sizeof(buffer), flags); - - if (n > 0 && (flags & WebSocket::FRAME_OP_BITMASK) != WebSocket::FRAME_OP_CLOSE) - { - std::string firstLine = getFirstLine(buffer, n); - StringTokenizer tokens(firstLine, " ", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); - - // The only kind of messages a child process receives are the single-line ones (?) - assert(firstLine.size() == static_cast<std::string::size_type>(n)); - - // Check if it is a "canceltiles" and in that case remove outstanding - // "tile" messages from the queue. - if (tokens.count() == 1 && tokens[0] == "canceltiles") - { - queue.remove_if([](std::string& x) { - return (x.find("tile ") == 0 && x.find("id=") == std::string::npos); - }); - } - else - { - queue.put(firstLine); - } - } - } - while (n > 0 && (flags & WebSocket::FRAME_OP_BITMASK) != WebSocket::FRAME_OP_CLOSE); - - queue.clear(); - queue.put("eof"); - queueHandlerThread.join(); - } - catch (Exception& exc) - { - logger().log(Util::logPrefix() + "Exception: " + exc.what()); - } - catch (std::exception& exc) - { - logger().error(Util::logPrefix() + "Exception: " + exc.what()); - } - - exit(Application::EXIT_OK); -} - -int LOOLWSD::createComponent() -{ - int pid; - - if ((pid = fork()) == -1) - { - std::cout << "Fork failed." << std::endl; - return Application::EXIT_UNAVAILABLE; - } - - if (!pid) - { - componentMain(); - } - - MasterProcessSession::addPendingChildrem(pid); - MasterProcessSession::_childProcesses[pid] = pid; - - return Application::EXIT_OK; -} - -void LOOLWSD::startupComponent(int nComponents) -{ - for (int nCntr = nComponents; nCntr; nCntr--) - { - if (createComponent() < 0) - break; - } -} - -void LOOLWSD::desktopMain() +int LOOLWSD::createBroker() { - // Initialization - std::unique_lock<std::mutex> rngLock(_rngMutex); - _childId = (((Poco::UInt64)_rng.next()) << 32) | _rng.next() | 1; - rngLock.unlock(); - - Path jail = Path::forDirectory(LOOLWSD::childRoot + Path::separator() + std::to_string(_childId)); - File(jail).createDirectory(); - - Path jailLOInstallation(jail, LOOLWSD::loSubPath); - jailLOInstallation.makeDirectory(); - File(jailLOInstallation).createDirectory(); - - // Copy (link) LO installation and other necessary files into it from the template - - linkOrCopy(LOOLWSD::sysTemplate, jail); - linkOrCopy(LOOLWSD::loTemplate, jailLOInstallation); - -#ifdef __linux - // Create the urandom and random devices - File(Path(jail, "/dev")).createDirectory(); - if (mknod((jail.toString() + "/dev/random").c_str(), - S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH, - makedev(1, 8)) != 0) - { - Application::instance().logger().error(Util::logPrefix() + - "mknod(" + jail.toString() + "/dev/random) failed: " + - strerror(errno)); + Process::Args args; - } - if (mknod((jail.toString() + "/dev/urandom").c_str(), - S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH, - makedev(1, 9)) != 0) - { - Application::instance().logger().error(Util::logPrefix() + - "mknod(" + jail.toString() + "/dev/urandom) failed: " + - strerror(errno)); - } -#endif + //args.push_back("--child=" + std::to_string(_childId)); + //args.push_back("--port=" + std::to_string(LOOLWSD::portNumber)); + //args.push_back("--jail=" + LOOLWSD::jail); + args.push_back("--losubpath=" + LOOLWSD::loSubPath); + args.push_back("--systemplate=" + sysTemplate); + args.push_back("--lotemplate=" + loTemplate); + args.push_back("--childroot=" + childRoot); + args.push_back("--numprespawns=" + std::to_string(_numPreSpawnedChildren)); - Application::instance().logger().information("desktopMain -> chroot(\"" + jail.toString() + "\")"); - if (chroot(jail.toString().c_str()) == -1) - { - logger().error("chroot(\"" + jail.toString() + "\") failed: " + strerror(errno)); - exit(Application::EXIT_UNAVAILABLE); - } + std::string executable = Path(Application::instance().commandPath()).parent().toString() + "loolbroker"; + + Application::instance().logger().information(Util::logPrefix() + "Launching broker: " + executable + " " + Poco::cat(std::string(" "), args.begin(), args.end())); + + ProcessHandle child = Process::launch(executable, args); - if (chdir("/") == -1) - { - logger().error(std::string("chdir(\"/\") in jail failed: ") + strerror(errno)); - exit(Application::EXIT_UNAVAILABLE); - } - -#ifdef __linux - dropCapability(CAP_SYS_CHROOT); -#else - dropCapability(); -#endif - - if (std::getenv("SLEEPFORDEBUGGER")) - { - std::cout << "Sleeping " << std::getenv("SLEEPFORDEBUGGER") << " seconds, " << - "attach process " << Process::id() << " in debugger now." << std::endl; - Thread::sleep(std::stoul(std::getenv("SLEEPFORDEBUGGER")) * 1000); - } - - startupComponent(_numPreSpawnedChildren); - - while (MasterProcessSession::_childProcesses.size() > 0) - { - int status; - pid_t pid = waitpid(-1, &status, WUNTRACED | WNOHANG); - if (pid > 0) - { - if ( MasterProcessSession::_childProcesses.find(pid) != MasterProcessSession::_childProcesses.end() ) - { - if ((WIFEXITED(status) || WIFSIGNALED(status) || WTERMSIG(status) ) ) - { - std::cout << Util::logPrefix() << "One of our known child processes died :" << std::to_string(pid) << std::endl; - MasterProcessSession::_childProcesses.erase(pid); - } - - if ( WCOREDUMP(status) ) - std::cout << Util::logPrefix() << "The child produced a core dump." << std::endl; - - if ( WIFSTOPPED(status) ) - std::cout << Util::logPrefix() << "The child process was stopped by delivery of a signal." << std::endl; - - if ( WSTOPSIG(status) ) ... etc. - the rest is truncated _______________________________________________ Libreoffice-commits mailing list [email protected] http://lists.freedesktop.org/mailman/listinfo/libreoffice-commits
