Add support for URP messages in COOLWSD

- Allow COOLWSD client sessions to forward messages with the prefix
  'urp' to the child, and return messages with 'urp:' to the client,
  communicating with binary
- Make COOLWSD child sessions use the FunctionBasedURPConnection from
  https://gerrit.libreoffice.org/c/core/+/155100
  (core change ID I2bda3d0b988bef7883f9b6829eeb5b7ae8075f27) to start a
  new URP session
- Make COOLWSD child sessions submit messages to this URP session,
  stripping and adding the 'urp' and 'urp:' prefixes so the Java client
  from https://gerrit.libreoffice.org/c/core/+/154680
  (core change ID I91ee52922a24688a6b94512cb7e7bc760bf25ec9) can
  use the connection (and to avoid interference with any other websocket
  messages)
- Add a COOLWSD option for enabling/disabling URP given the security
  implications around allowing anyone to write URP (e.g. URP lets you
  run shell commands so a mallicious actor can take over the child
  session)

Signed-off-by: Skyler Grey <skyler.grey@collabora.com>
Change-Id: Idadfe288a78cfd72b01253dfdade150d506e3f05
pull/7356/head
Skyler Grey 2023-07-31 14:28:27 +00:00 committed by Caolán McNamara
parent e591b1e08b
commit e6d9c1f87c
8 changed files with 177 additions and 40 deletions

View File

@ -166,7 +166,8 @@ private:
_tokens.equals(0, "delta:") ||
_tokens.equals(0, "renderfont:") ||
_tokens.equals(0, "rendersearchresult:") ||
_tokens.equals(0, "windowpaint:"))
_tokens.equals(0, "windowpaint:") ||
_tokens.equals(0, "urp:") )
{
return Type::Binary;
}

View File

@ -196,6 +196,7 @@
<jwt_expiry_secs desc="Time in seconds before the Admin Console's JWT token expires" type="int" default="1800">1800</jwt_expiry_secs>
<enable_macros_execution desc="Specifies whether the macro execution is enabled in general. This will enable Basic and Python scripts to execute both installed and from documents. If it is set to false, the macro_security_level is ignored. If it is set to true, the mentioned entry specified the level of macro security." type="bool" default="false">false</enable_macros_execution>
<macro_security_level desc="Level of Macro security. 1 (Medium) Confirmation required before executing macros from untrusted sources. 0 (Low, not recommended) All macros will be executed without confirmation." type="int" default="1">1</macro_security_level>
<enable_websocket_urp desc="Should we enable URP (UNO remote protocol) communication over the websocket. This allows full control of the Kit child server to anyone with access to the websocket including executing macros without confirmation or running arbitrary shell commands in the jail." type="bool" default="false">false</enable_websocket_urp>
<enable_metrics_unauthenticated desc="When enabled, the /cool/getMetrics endpoint will not require authentication." type="bool" default="false">false</enable_metrics_unauthenticated>
</security>

View File

@ -10,6 +10,7 @@
#include "Kit.hpp"
#include "ChildSession.hpp"
#include "MobileApp.hpp"
#include "COOLWSD.hpp"
#include <climits>
#include <fstream>
@ -97,23 +98,48 @@ std::string formatUnoCommandInfo(const std::string& sessionId, const std::string
}
ChildSession::ChildSession(
const std::shared_ptr<ProtocolHandlerInterface> &protocol,
const std::string& id,
const std::string& jailId,
const std::string& jailRoot,
DocumentManagerInterface& docManager) :
Session(protocol, "ToMaster-" + id, id, false),
_jailId(jailId),
_jailRoot(jailRoot),
_docManager(&docManager),
_viewId(-1),
_isDocLoaded(false),
_copyToClipboard(false),
_canonicalViewId(0),
_isDumpingTiles(false),
_clientVisibleArea(0, 0, 0, 0)
int sendURPToClient(void* pContext /* std::function* of sendBinaryFrame */,
const signed char* pBuffer, int nLen)
{
static const std::string header = "urp:\n";
size_t responseSize = header.size() + nLen;
char* response = new char[responseSize];
std::memcpy(response, header.data(), header.size());
std::memcpy(response + header.size(), pBuffer, nLen);
pushToMainThread([pContext, response, responseSize]() {
static_cast<ChildSession*>(pContext)->sendBinaryFrame(response, responseSize);
});
return 0;
}
ChildSession::ChildSession(const std::shared_ptr<ProtocolHandlerInterface>& protocol,
const std::string& id, const std::string& jailId,
const std::string& jailRoot, DocumentManagerInterface& docManager)
: Session(protocol, "ToMaster-" + id, id, false)
, _jailId(jailId)
, _jailRoot(jailRoot)
, _docManager(&docManager)
, _viewId(-1)
, _isDocLoaded(false)
, _copyToClipboard(false)
, _canonicalViewId(-1)
, _isDumpingTiles(false)
, _clientVisibleArea(0, 0, 0, 0)
, m_hasURP(false)
{
if (isURPEnabled())
{
LOG_WRN("URP is enabled in the config: Starting a URP tunnel for this session ["
<< getName() << "]");
m_hasURP = startURP(docManager.getLOKit(), this, &m_sendURPToLOContext, sendURPToClient,
&m_sendURPToLO);
if (!m_hasURP)
LOG_INF("Failed to start a URP bridge for this session [" << getName()
<< "], disabling URP");
}
LOG_INF("ChildSession ctor [" << getName() << "]. JailRoot: [" << _jailRoot << ']');
}
@ -121,6 +147,11 @@ ChildSession::~ChildSession()
{
LOG_INF("~ChildSession dtor [" << getName() << ']');
disconnect();
if (m_hasURP)
{
_docManager->getLOKit()->stopURP(m_sendURPToLOContext);
}
}
void ChildSession::disconnect()
@ -347,6 +378,23 @@ bool ChildSession::_handleInput(const char *buffer, int length)
return success;
}
else if (tokens.equals(0, "urp"))
{
if (length < 4)
return false;
if (m_hasURP)
{
m_sendURPToLO(m_sendURPToLOContext, (signed char*)(buffer + 4),
length - 4); // HACK: not portable as char may be unsigned
return true;
}
else
{
sendTextFrameAndLogError("error: cmd=" + tokens[0] + " kind=urpnotenabled");
return false;
}
}
else if (!_isDocLoaded)
{
sendTextFrameAndLogError("error: cmd=" + tokens[0] + " kind=nodocloaded");

View File

@ -416,6 +416,12 @@ private:
bool _isDumpingTiles;
Util::Rectangle _clientVisibleArea;
void* m_sendURPToLOContext;
int (*m_sendURPToLO)(void* pContext, const signed char*, int);
/// whether there is a URP session created for this ChildSession
bool m_hasURP;
};
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */

View File

@ -127,6 +127,9 @@ static bool AnonymizeUserData = false;
static uint64_t AnonymizationSalt = 82589933;
#endif
static bool EnableWebsocketURP = false;
static int URPStartCount = 0;
/// When chroot is enabled, this is blank as all
/// the paths inside the jail, relative to it's jail.
/// E.g. /tmp/user/docs/...
@ -2295,10 +2298,10 @@ class KitSocketPoll final : public SocketPoll
std::chrono::steady_clock::time_point _pollEnd;
std::shared_ptr<Document> _document;
static KitSocketPoll *mainPoll;
static KitSocketPoll* mainPoll;
KitSocketPoll() :
SocketPoll("kit")
KitSocketPoll()
: SocketPoll("kit")
{
#ifdef IOS
terminationFlag = false;
@ -2313,7 +2316,7 @@ public:
mainPoll = nullptr;
}
static void dumpGlobalState(std::ostream &oss)
static void dumpGlobalState(std::ostream& oss)
{
if (mainPoll)
{
@ -2350,10 +2353,7 @@ public:
}
// called from inside poll, inside a wakeup
void wakeupHook()
{
_pollEnd = std::chrono::steady_clock::now();
}
void wakeupHook() { _pollEnd = std::chrono::steady_clock::now(); }
// a LOK compatible poll function merging the functions.
// returns the number of events signalled
@ -2401,18 +2401,20 @@ public:
const auto now = std::chrono::steady_clock::now();
drainQueue();
timeoutMicroS = std::chrono::duration_cast<std::chrono::microseconds>(_pollEnd - now).count();
timeoutMicroS =
std::chrono::duration_cast<std::chrono::microseconds>(_pollEnd - now).count();
++eventsSignalled;
}
while (timeoutMicroS > 0 && !SigUtil::getTerminationFlag() && maxExtraEvents-- > 0);
} while (timeoutMicroS > 0 && !SigUtil::getTerminationFlag() && maxExtraEvents-- > 0);
}
if (_document && checkForIdle && eventsSignalled == 0 &&
timeoutMicroS > 0 && !hasCallbacks() && !hasBuffered())
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());
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();
@ -2439,13 +2441,11 @@ public:
return eventsSignalled;
}
void setDocument(std::shared_ptr<Document> document)
{
_document = std::move(document);
}
void setDocument(std::shared_ptr<Document> document) { _document = std::move(document); }
// unusual LOK event from another thread, push into our loop to process.
static bool pushToMainThread(LibreOfficeKitCallback callback, int type, const char *p, void *data)
static bool pushToMainThread(LibreOfficeKitCallback callback, int type, const char* p,
void* data)
{
if (mainPoll && mainPoll->getThreadOwner() != std::this_thread::get_id())
{
@ -2453,7 +2453,7 @@ public:
std::shared_ptr<std::string> pCopy;
if (p)
pCopy = std::make_shared<std::string>(p, strlen(p));
mainPoll->addCallback([=]{
mainPoll->addCallback([=] {
LOG_TRC("Unusual process callback in main thread");
callback(type, pCopy ? pCopy->c_str() : nullptr, data);
});
@ -2462,6 +2462,18 @@ public:
return false;
}
// we can also directly push callbacks into the main thread when needed...
static bool pushToMainThread(const SocketPoll::CallbackFn& cb)
{
if (mainPoll && mainPoll->getThreadOwner() != std::this_thread::get_id())
{
LOG_TRC("Unusual directly push callback to main thread");
mainPoll->addCallback(cb);
return true;
}
return false;
}
#ifdef IOS
static std::mutex KSPollsMutex;
// static std::condition_variable KSPollsCV;
@ -2480,6 +2492,11 @@ bool pushToMainThread(LibreOfficeKitCallback cb, int type, const char *p, void *
return KitSocketPoll::pushToMainThread(cb, type, p, data);
}
bool pushToMainThread(const SocketPoll::CallbackFn& cb)
{
return KitSocketPoll::pushToMainThread(cb);
}
#ifdef IOS
std::mutex KitSocketPoll::KSPollsMutex;
@ -2843,6 +2860,9 @@ void lokit_main(
LOG_INF("User-data anonymization is " << (AnonymizeUserData ? "enabled." : "disabled."));
const char* pEnableWebsocketURP = std::getenv("ENABLE_WEBSOCKET_URP");
EnableWebsocketURP = std::string(pEnableWebsocketURP) == "true";
assert(!childRoot.empty());
assert(!sysTemplate.empty());
assert(!loTemplate.empty());
@ -3307,6 +3327,38 @@ std::string anonymizeUrl(const std::string& url)
#endif
}
bool isURPEnabled() { return EnableWebsocketURP; }
bool startURP(std::shared_ptr<lok::Office> LOKit, void* pReceiveURPFromLOContext,
void** ppSendURPToLOContext,
int (*fnReceiveURPFromLO)(void* pContext, const signed char* pBuffer, int nLen),
int (**pfnSendURPToLO)(void* pContext, const signed char* pBuffer, int nLen))
{
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;
}
bool coreURPSuccess = LOKit->startURP(pReceiveURPFromLOContext, ppSendURPToLOContext,
fnReceiveURPFromLO, pfnSendURPToLO);
if (!coreURPSuccess)
{
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.

View File

@ -13,6 +13,7 @@
#include <common/Util.hpp>
#include <wsd/TileDesc.hpp>
#include "Socket.hpp"
#define LOK_USE_UNSTABLE_API
#include <LibreOfficeKit/LibreOfficeKit.hxx>
@ -21,7 +22,6 @@
#include "ClientSession.hpp"
#include "DocumentBroker.hpp"
#include "Socket.hpp"
#endif
@ -166,4 +166,17 @@ std::shared_ptr<lok::Document> getLOKDocumentForAndroidOnly();
extern _LibreOfficeKit* loKitPtr;
// Abnormally we sometimes have functions to directly push
// into the main thread
bool pushToMainThread(const SocketPoll::CallbackFn& cb);
/// Check if URP is enabled
bool isURPEnabled();
/// Start a URP connection, checking if URP is enabled and there is not already an active URP session
bool startURP(std::shared_ptr<lok::Office> LOKit, void* pReceiveURPFromLOContext,
void** ppSendURPToLOContext,
int (*fnReceiveURPFromLO)(void* pContext, const signed char* pBuffer, int nLen),
int (**pfnSendURPToLO)(void* pContext, const signed char* pBuffer, int nLen));
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */

View File

@ -2064,6 +2064,7 @@ void COOLWSD::innerInitialize(Application& self)
{ "admin_console.enable_pam", "false" },
{ "child_root_path", "jails" },
{ "file_server_root_path", "browser/.." },
{ "enable_websocket_urp", "false" },
{ "hexify_embedded_urls", "false" },
{ "experimental_features", "false" },
{ "logging.protocol", "false" },
@ -2432,6 +2433,12 @@ void COOLWSD::innerInitialize(Application& self)
}
FileUtil::setUrlAnonymization(AnonymizeUserData, anonymizationSalt);
{
bool enableWebsocketURP =
COOLWSD::getConfigValue<bool>("security.enable_websocket_urp", false);
setenv("ENABLE_WEBSOCKET_URP", enableWebsocketURP ? "true" : "false", 1);
}
{
std::string proto = getConfigValue<std::string>(conf, "net.proto", "");
if (Util::iequal(proto, "ipv4"))

View File

@ -488,6 +488,14 @@ bool ClientSession::_handleInput(const char *buffer, int length)
}
}
if (tokens.equals(0, "urp"))
{
// This can't be pushed down into the long list of tokens that are
// forwarded to the child later as we need it to be able to run before
// documents are loaded
LOG_TRC("UNO remote protocol message (from client): " << firstLine);
return forwardToChild(std::string(buffer, length), docBroker);
}
if (tokens.equals(0, "coolclient"))
{
if (tokens.size() < 2)
@ -1058,6 +1066,7 @@ bool ClientSession::_handleInput(const char *buffer, int length)
tokens.equals(0, "windowselecttext") ||
tokens.equals(0, "setpage") ||
tokens.equals(0, "uno") ||
tokens.equals(0, "urp") ||
tokens.equals(0, "useractive") ||
tokens.equals(0, "userinactive") ||
tokens.equals(0, "paintwindow") ||
@ -1365,7 +1374,7 @@ bool ClientSession::sendCombinedTiles(const char* /*buffer*/, int /*length*/, co
bool ClientSession::forwardToChild(const std::string& message,
const std::shared_ptr<DocumentBroker>& docBroker)
{
const bool binary = Util::startsWith(message, "paste") ? true : false;
const bool binary = Util::startsWith(message, "paste") || Util::startsWith(message, "urp");
return docBroker->forwardToChild(client_from_this(), message, binary);
}