diff options
author | Pranav Kant <pranavk@collabora.co.uk> | 2016-11-08 19:07:28 +0530 |
---|---|---|
committer | Pranav Kant <pranavk@collabora.co.uk> | 2016-11-10 15:04:21 +0530 |
commit | b0933b063e8b0e8e0de7eeeb9706fb291af72b5c (patch) | |
tree | 2464485bea4081378ff54751b6a9758539ae2185 | |
parent | 410936b60012c5cbbd1032c2590582821ba22e62 (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.js | 15 | ||||
-rw-r--r-- | loleaflet/src/map/handler/Map.WOPI.js | 3 | ||||
-rw-r--r-- | loolwsd/ClientSession.cpp | 13 | ||||
-rw-r--r-- | loolwsd/ClientSession.hpp | 7 | ||||
-rw-r--r-- | loolwsd/DocumentBroker.cpp | 26 | ||||
-rw-r--r-- | loolwsd/DocumentBroker.hpp | 4 | ||||
-rw-r--r-- | loolwsd/LOOLSession.cpp | 6 | ||||
-rw-r--r-- | loolwsd/LOOLSession.hpp | 2 | ||||
-rw-r--r-- | loolwsd/Storage.cpp | 7 | ||||
-rw-r--r-- | loolwsd/Storage.hpp | 6 | ||||
-rw-r--r-- | loolwsd/protocol.txt | 21 |
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 |