summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPranav Kant <pranavk@collabora.co.uk>2016-11-08 19:07:28 +0530
committerPranav Kant <pranavk@collabora.co.uk>2016-11-10 15:04:21 +0530
commitb0933b063e8b0e8e0de7eeeb9706fb291af72b5c (patch)
tree2464485bea4081378ff54751b6a9758539ae2185
parent410936b60012c5cbbd1032c2590582821ba22e62 (diff)
tdf#103640: Implement OwnerTermination; send application-level close frame
This implements a new feature 'OwnerTermination' for WOPI based hosts. WOPI hosts now have to enable this feature by mentioning 'EnableOwnerTermination' as 'true' in their CheckFileInfo response. If the OwnerId of the file matches that of the UserId of the session, this session would be able to terminate all other sessions currently editing the same document. The reason for this kind of document termination is sent to all sessions in a new application-level 'close:' message. This new message is similar to the CLOSE frame of WebSocket protocol which doesn't seem to work across all browsers as of now. Eg: Chrome - https://bugs.chromium.org/p/chromium/issues/detail?id=426798 After receiving this 'close: ' message, loleaflet acts accordingly and tells the WOPI host why the websocket was closed via post message API. Change-Id: I997aa2e7805157ed599a3946a877fd32477cee1b
-rw-r--r--loleaflet/src/core/Socket.js15
-rw-r--r--loleaflet/src/map/handler/Map.WOPI.js3
-rw-r--r--loolwsd/ClientSession.cpp13
-rw-r--r--loolwsd/ClientSession.hpp7
-rw-r--r--loolwsd/DocumentBroker.cpp26
-rw-r--r--loolwsd/DocumentBroker.hpp4
-rw-r--r--loolwsd/LOOLSession.cpp6
-rw-r--r--loolwsd/LOOLSession.hpp2
-rw-r--r--loolwsd/Storage.cpp7
-rw-r--r--loolwsd/Storage.hpp6
-rw-r--r--loolwsd/protocol.txt21
11 files changed, 99 insertions, 11 deletions
diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js
index 21cc00e3b..87ffe91cb 100644
--- a/loleaflet/src/core/Socket.js
+++ b/loleaflet/src/core/Socket.js
@@ -190,6 +190,21 @@ L.Socket = L.Class.extend({
return;
}
+ else if (textMsg.startsWith('close: ')) {
+ textMsg = textMsg.substring('close: '.length);
+
+ // This is due to document owner terminating the session
+ if (textMsg === 'ownertermination') {
+ // Disconnect the websocket manually
+ this.close();
+ // Tell WOPI host about it which should handle this situation
+ this._map.fire('postMessage', {msgId: 'Session_Closed'});
+ }
+
+ this._map.remove();
+
+ return;
+ }
else if (textMsg.startsWith('error:') && command.errorCmd === 'internal') {
this._map._fatal = true;
if (command.errorKind === 'diskfull') {
diff --git a/loleaflet/src/map/handler/Map.WOPI.js b/loleaflet/src/map/handler/Map.WOPI.js
index 4e12dd323..d96630630 100644
--- a/loleaflet/src/map/handler/Map.WOPI.js
+++ b/loleaflet/src/map/handler/Map.WOPI.js
@@ -40,6 +40,9 @@ L.Map.WOPI = L.Handler.extend({
this._postMessage('Get_Views_Resp', getMembersRespVal);
}
+ else if (msg.MessageId === 'Close_Session') {
+ this._map._socket.sendMessage('closedocument');
+ }
},
_postMessage: function(e) {
diff --git a/loolwsd/ClientSession.cpp b/loolwsd/ClientSession.cpp
index 69654dad6..c186619fc 100644
--- a/loolwsd/ClientSession.cpp
+++ b/loolwsd/ClientSession.cpp
@@ -43,6 +43,7 @@ ClientSession::ClientSession(const std::string& id,
_docBroker(docBroker),
_uriPublic(uriPublic),
_isReadOnly(readOnly),
+ _isDocumentOwner(false),
_loadPart(-1)
{
Log::info("ClientSession ctor [" + getName() + "].");
@@ -115,6 +116,7 @@ bool ClientSession::_handleInput(const char *buffer, int length)
tokens[0] != "clientzoom" &&
tokens[0] != "clientvisiblearea" &&
tokens[0] != "commandvalues" &&
+ tokens[0] != "closedocument" &&
tokens[0] != "downloadas" &&
tokens[0] != "getchildid" &&
tokens[0] != "gettextselection" &&
@@ -156,6 +158,17 @@ bool ClientSession::_handleInput(const char *buffer, int length)
{
return getCommandValues(buffer, length, tokens, docBroker);
}
+ else if (tokens[0] == "closedocument")
+ {
+ // If this session is the owner of the file, let it close all sessions
+ if (_isDocumentOwner)
+ {
+ LOG_DBG("Session [" + getId() + "] requested owner termination");
+ docBroker->closeDocument("ownertermination");
+ }
+
+ return true;
+ }
else if (tokens[0] == "partpagerectangles")
{
return getPartPageRectangles(buffer, length, docBroker);
diff --git a/loolwsd/ClientSession.hpp b/loolwsd/ClientSession.hpp
index 8713026e7..77ddcfa9a 100644
--- a/loolwsd/ClientSession.hpp
+++ b/loolwsd/ClientSession.hpp
@@ -37,8 +37,11 @@ public:
std::shared_ptr<PrisonerSession> getPeer() const { return _peer; }
bool shutdownPeer(Poco::UInt16 statusCode);
+ const std::string getUserId() const { return _userId; }
+
void setUserId(const std::string& userId) { _userId = userId; }
void setUserName(const std::string& userName) { _userName = userName; }
+ void setDocumentOwner(const bool isDocumentOwner) { _isDocumentOwner = isDocumentOwner; }
/**
* Return the URL of the saved-as document when it's ready. If called
@@ -66,7 +69,6 @@ private:
bool loadDocument(const char* buffer, int length, Poco::StringTokenizer& tokens,
const std::shared_ptr<DocumentBroker>& docBroker);
-
bool getStatus(const char* buffer, int length,
const std::shared_ptr<DocumentBroker>& docBroker);
bool getCommandValues(const char* buffer, int length, Poco::StringTokenizer& tokens,
@@ -94,6 +96,9 @@ private:
/// Whether the session is opened as readonly
bool _isReadOnly;
+ /// Whether this session is the owner of currently opened document
+ bool _isDocumentOwner;
+
/// Our peer that connects us to the child.
std::shared_ptr<PrisonerSession> _peer;
diff --git a/loolwsd/DocumentBroker.cpp b/loolwsd/DocumentBroker.cpp
index 723b73b18..295e07cd9 100644
--- a/loolwsd/DocumentBroker.cpp
+++ b/loolwsd/DocumentBroker.cpp
@@ -241,6 +241,13 @@ bool DocumentBroker::load(std::shared_ptr<ClientSession>& session, const std::st
session->sendTextFrame("wopi: postmessageorigin " + wopifileinfo._postMessageOrigin);
}
+ // Mark the session as 'Document owner' if WOPI hosts supports it
+ if (wopifileinfo._enableOwnerTermination && userid == _storage->getFileInfo()._ownerId)
+ {
+ LOG_DBG("Session [" + sessionId + "] is the document owner");
+ session->setDocumentOwner(true);
+ }
+
getInfoCallDuration = wopifileinfo._callDuration;
}
else if (dynamic_cast<LocalStorage*>(_storage.get()) != nullptr)
@@ -895,14 +902,22 @@ void DocumentBroker::childSocketTerminated()
}
}
-void DocumentBroker::terminateChild(std::unique_lock<std::mutex>& lock)
+void DocumentBroker::terminateChild(std::unique_lock<std::mutex>& lock, const std::string& closeReason)
{
Util::assertIsLocked(_mutex);
Util::assertIsLocked(lock);
LOG_INF("Terminating child [" << getPid() << "] of doc [" << _docKey << "].");
- assert(_sessions.empty() && "DocumentBroker still has unremoved sessions!");
+ // Close all running sessions
+ for (auto& pair : _sessions)
+ {
+ // See protocol.txt for this application-level close frame
+ pair.second->sendTextFrame("close: " + closeReason);
+ pair.second->shutdown(Poco::Net::WebSocket::WS_ENDPOINT_GOING_AWAY, closeReason);
+ }
+
+ std::this_thread::sleep_for (std::chrono::seconds(5));
// First flag to stop as it might be waiting on our lock
// to process some incoming message.
@@ -914,4 +929,11 @@ void DocumentBroker::terminateChild(std::unique_lock<std::mutex>& lock)
_childProcess->close(false);
}
+void DocumentBroker::closeDocument(const std::string& reason)
+{
+ auto lock = getLock();
+
+ terminateChild(lock, reason);
+}
+
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/loolwsd/DocumentBroker.hpp b/loolwsd/DocumentBroker.hpp
index 7280fb6e9..321271505 100644
--- a/loolwsd/DocumentBroker.hpp
+++ b/loolwsd/DocumentBroker.hpp
@@ -275,6 +275,8 @@ public:
int getRenderedTileCount() { return _debugRenderedTileCount; }
+ void closeDocument(const std::string& reason);
+
/// Called by the ChildProcess object to notify
/// that it has terminated on its own.
/// This happens either when the child exists
@@ -286,7 +288,7 @@ public:
/// We must be called under lock and it must be
/// passed to us so we unlock before waiting on
/// the ChildProcess thread, which can take our lock.
- void terminateChild(std::unique_lock<std::mutex>& lock);
+ void terminateChild(std::unique_lock<std::mutex>& lock, const std::string& closeReason = "");
/// Get the PID of the associated child process
Poco::Process::PID getPid() const { return _childProcess->getPid(); }
diff --git a/loolwsd/LOOLSession.cpp b/loolwsd/LOOLSession.cpp
index 25bc40859..db091f5ca 100644
--- a/loolwsd/LOOLSession.cpp
+++ b/loolwsd/LOOLSession.cpp
@@ -202,14 +202,14 @@ bool LOOLSession::handleDisconnect()
return false;
}
-void LOOLSession::shutdown(Poco::UInt16 statusCode)
+void LOOLSession::shutdown(Poco::UInt16 statusCode, const std::string& statusMessage)
{
if (_ws)
{
try
{
- LOG_TRC("Shutting down WS [" << getName() << "].");
- _ws->shutdown(statusCode);
+ LOG_TRC("Shutting down WS [" << getName() << "] with statusCode [" << statusCode << "] and reason [" << statusMessage << "].");
+ _ws->shutdown(statusCode, statusMessage);
}
catch (const Poco::Exception &exc)
{
diff --git a/loolwsd/LOOLSession.hpp b/loolwsd/LOOLSession.hpp
index 6ef8d228a..cd6b9fed5 100644
--- a/loolwsd/LOOLSession.hpp
+++ b/loolwsd/LOOLSession.hpp
@@ -62,7 +62,7 @@ public:
/// Called to handle disconnection command from socket.
virtual bool handleDisconnect();
- void shutdown(Poco::UInt16 statusCode);
+ void shutdown(Poco::UInt16 statusCode, const std::string& statusMessage = "");
bool isActive() const { return _isActive; }
void setIsActive(bool active) { _isActive = active; }
diff --git a/loolwsd/Storage.cpp b/loolwsd/Storage.cpp
index 86ccdba38..0a451ae3e 100644
--- a/loolwsd/Storage.cpp
+++ b/loolwsd/Storage.cpp
@@ -195,7 +195,7 @@ LocalStorage::LocalFileInfo LocalStorage::getLocalFileInfo(const Poco::URI& uriP
const auto lastModified = file.getLastModified();
const auto size = file.getSize();
- _fileInfo = FileInfo({filename, "lool", lastModified, size});
+ _fileInfo = FileInfo({filename, "localhost", lastModified, size});
}
// Set automatic userid and username
@@ -318,6 +318,7 @@ WopiStorage::WOPIFileInfo WopiStorage::getWOPIFileInfo(const Poco::URI& uriPubli
std::string userId;
std::string userName;
bool canWrite = false;
+ bool enableOwnerTermination = false;
std::string postMessageOrigin;
std::string resMsg;
Poco::StreamCopier::copyToString(rs, resMsg);
@@ -345,6 +346,8 @@ WopiStorage::WOPIFileInfo WopiStorage::getWOPIFileInfo(const Poco::URI& uriPubli
canWrite = canWriteVar.isString() ? (canWriteVar.toString() == "true") : false;
const auto postMessageOriginVar = getOrWarn(object, "PostMessageOrigin");
postMessageOrigin = postMessageOriginVar.isString() ? postMessageOriginVar.toString() : "";
+ const auto enableOwnerTerminationVar = getOrWarn(object, "EnableOwnerTermination");
+ enableOwnerTermination = enableOwnerTerminationVar.isString() ? (enableOwnerTerminationVar.toString() == "true") : false;
}
else
Log::error("WOPI::CheckFileInfo is missing JSON payload");
@@ -355,7 +358,7 @@ WopiStorage::WOPIFileInfo WopiStorage::getWOPIFileInfo(const Poco::URI& uriPubli
_fileInfo = FileInfo({filename, ownerId, Poco::Timestamp(), size});
}
- return WOPIFileInfo({userId, userName, canWrite, postMessageOrigin, callDuration});
+ return WOPIFileInfo({userId, userName, canWrite, postMessageOrigin, enableOwnerTermination, callDuration});
}
/// uri format: http://server/<...>/wopi*/files/<id>/content
diff --git a/loolwsd/Storage.hpp b/loolwsd/Storage.hpp
index 7912230f4..a1eb2cf96 100644
--- a/loolwsd/Storage.hpp
+++ b/loolwsd/Storage.hpp
@@ -60,7 +60,7 @@ public:
_uri(uri),
_localStorePath(localStorePath),
_jailPath(jailPath),
- _fileInfo("", Poco::Timestamp(), 0),
+ _fileInfo("", "lool", Poco::Timestamp(), 0),
_isLoaded(false)
{
Log::debug("Storage ctor: " + uri.toString());
@@ -169,11 +169,13 @@ public:
const std::string& username,
const bool userCanWrite,
const std::string& postMessageOrigin,
+ const bool enableOwnerTermination,
const std::chrono::duration<double> callDuration)
: _userid(userid),
_username(username),
_userCanWrite(userCanWrite),
_postMessageOrigin(postMessageOrigin),
+ _enableOwnerTermination(enableOwnerTermination),
_callDuration(callDuration)
{
}
@@ -186,6 +188,8 @@ public:
bool _userCanWrite;
/// WOPI Post message property
std::string _postMessageOrigin;
+ /// If WOPI host has enabled owner termination feature on
+ bool _enableOwnerTermination;
/// Time it took to call WOPI's CheckFileInfo
std::chrono::duration<double> _callDuration;
};
diff --git a/loolwsd/protocol.txt b/loolwsd/protocol.txt
index f6035b9a6..7fea7869e 100644
--- a/loolwsd/protocol.txt
+++ b/loolwsd/protocol.txt
@@ -164,6 +164,12 @@ userinactive
See 'useractive'.
+closedocument
+
+ This gives document owners the ability to terminate all sessions currently
+ having that document opened. This functionality is enabled only in case WOPI
+ host mentions 'EnableOwnerTermination' flag in its CheckFileInfo response
+
server -> client
================
@@ -220,6 +226,21 @@ error: cmd=<command> kind=<kind> [code=<error_code>] [params=1,2,3,...,N]
<code> (when provided) further specifies the error as forwarded from
LibreOffice
+close: <reason>
+
+ Ask a client to close the websocket connection with <reason>.
+ Exactly similar fields are also available in WebSocket protocol's
+ CLOSE frame, but some browser implementation (google-chrome) doesn't seem to
+ handle that well. This is a temporary application-level close websocket
+ to circumvent the same.
+
+ <reason> can have following values:
+ * ownertermination - If the session close is due to 'Document owner'
+ terminating the session.
+ (Document owner is the one who has the file ownership and hence have the
+ ability to kill all other sessions if EnableOwnerTermination flag in WOPI
+ CheckFileInfo is 'true' (assumed to be 'false' by default).
+
getchildid: id=<id>
Returns the child id