diff options
author | Michael Meeks <michael.meeks@collabora.com> | 2019-07-04 10:50:33 +0100 |
---|---|---|
committer | Michael Meeks <michael.meeks@collabora.com> | 2019-08-05 22:21:54 -0400 |
commit | 9e791fb0d4d4c701c50b8b50091b5665b832c3f1 (patch) | |
tree | 3588dfda4e66e48caa1e74dbad1da743c8fea7d5 | |
parent | wsd: add 'meta origin' to clipboardcontent payloads too (diff) | |
download | online-9e791fb0d4d4c701c50b8b50091b5665b832c3f1.tar.gz online-9e791fb0d4d4c701c50b8b50091b5665b832c3f1.zip |
clipboard: persist selections for a while after a view closes.
re-factor ClientSession state to be a simpler state machine.
Have a nice disconnect / disconnected handshake on view close.
Change-Id: Ie933cc5c7dfab46c66f4d38a4d75c459aa1cff87
-rw-r--r-- | common/Clipboard.hpp | 64 | ||||
-rw-r--r-- | kit/ChildSession.cpp | 2 | ||||
-rw-r--r-- | kit/ChildSession.hpp | 3 | ||||
-rw-r--r-- | kit/Kit.cpp | 6 | ||||
-rw-r--r-- | test/UnitCopyPaste.cpp | 18 | ||||
-rw-r--r-- | wsd/ClientSession.cpp | 121 | ||||
-rw-r--r-- | wsd/ClientSession.hpp | 33 | ||||
-rw-r--r-- | wsd/DocumentBroker.cpp | 158 | ||||
-rw-r--r-- | wsd/DocumentBroker.hpp | 9 | ||||
-rw-r--r-- | wsd/LOOLWSD.cpp | 14 | ||||
-rw-r--r-- | wsd/LOOLWSD.hpp | 2 |
11 files changed, 373 insertions, 57 deletions
diff --git a/common/Clipboard.hpp b/common/Clipboard.hpp index fbbcb557c8..0b4bee4389 100644 --- a/common/Clipboard.hpp +++ b/common/Clipboard.hpp @@ -13,9 +13,13 @@ #include <string> #include <vector> +#include <unordered_map> +#include <mutex> + #include <stdlib.h> #include <Log.hpp> #include <Exceptions.hpp> +#include <Poco/MemoryStream.h> struct ClipboardData { @@ -78,6 +82,66 @@ struct ClipboardData } }; +/// Used to store expired view's clipboards +class ClipboardCache +{ + std::mutex _mutex; + struct Entry { + std::chrono::steady_clock::time_point _inserted; + std::shared_ptr<std::string> _rawData; // big. + }; + // clipboard key -> data + std::unordered_map<std::string, Entry> _cache; +public: + ClipboardCache() + { + } + + void insertClipboard(const std::string key[2], + const char *data, size_t size) + { + if (size == 0) + { + LOG_TRC("clipboard cache - ignores empty clipboard data"); + return; + } + Entry ent; + ent._inserted = std::chrono::steady_clock::now(); + ent._rawData = std::make_shared<std::string>(data, size); + LOG_TRC("insert cached clipboard: " + key[0] + " and " + key[1]); + std::lock_guard<std::mutex> lock(_mutex); + _cache[key[0]] = ent; + _cache[key[1]] = ent; + } + + std::shared_ptr<std::string> getClipboard(const std::string &key) + { + std::lock_guard<std::mutex> lock(_mutex); + std::shared_ptr<std::string> data; + auto it = _cache.find(key); + if (it != _cache.end()) + data = it->second._rawData; + return data; + } + + void checkexpiry() + { + std::lock_guard<std::mutex> lock(_mutex); + auto now = std::chrono::steady_clock::now(); + LOG_TRC("check expiry of cached clipboards"); + for (auto it = _cache.begin(); it != _cache.end();) + { + if (std::chrono::duration_cast<std::chrono::minutes>(now - it->second._inserted).count() >= 10) + { + LOG_TRC("expiring expiry of cached clipboard: " + it->first); + it = _cache.erase(it); + } + else + ++it; + } + } +}; + #endif /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp index 56700852ec..b3a3fd8b8c 100644 --- a/kit/ChildSession.cpp +++ b/kit/ChildSession.cpp @@ -946,7 +946,7 @@ bool ChildSession::getTextSelection(const char* /*buffer*/, int /*length*/, cons return true; } -bool ChildSession::getClipboard(const char* /*buffer*/, int /*length*/, const std::vector<std::string>& tokens ) +bool ChildSession::getClipboard(const char* /*buffer*/, int /*length*/, const std::vector<std::string>& tokens) { const char **pMimeTypes = nullptr; // fetch all for now. const char *pOneType[2]; diff --git a/kit/ChildSession.hpp b/kit/ChildSession.hpp index b15c8b33aa..a1a4532bb4 100644 --- a/kit/ChildSession.hpp +++ b/kit/ChildSession.hpp @@ -233,6 +233,8 @@ public: using Session::sendTextFrame; + bool getClipboard(const char* buffer, int length, const std::vector<std::string>& tokens); + private: bool loadDocument(const char* buffer, int length, const std::vector<std::string>& tokens); @@ -245,7 +247,6 @@ private: bool downloadAs(const char* buffer, int length, const std::vector<std::string>& tokens); bool getChildId(); bool getTextSelection(const char* buffer, int length, const std::vector<std::string>& tokens); - bool getClipboard(const char* buffer, int length, const std::vector<std::string>& tokens); bool setClipboard(const char* buffer, int length, const std::vector<std::string>& tokens); std::string getTextSelectionInternal(const std::string& mimeType); bool paste(const char* buffer, int length, const std::vector<std::string>& tokens); diff --git a/kit/Kit.cpp b/kit/Kit.cpp index 832eb64235..a76b6d1f02 100644 --- a/kit/Kit.cpp +++ b/kit/Kit.cpp @@ -983,7 +983,7 @@ public: // session is being removed. for (auto it = _sessions.cbegin(); it != _sessions.cend(); ) { - if (it->second->isCloseFrame()) + if (it->second->isCloseFrame()) { deadSessions.push_back(it->second); it = _sessions.erase(it); @@ -1901,6 +1901,10 @@ private: _editorId = -1; } LOG_DBG("Removing ChildSession [" << sessionId << "]."); + + // Tell them we're going quietly. + session->sendTextFrame("disconnected:"); + _sessions.erase(it); const size_t count = _sessions.size(); LOG_DBG("Have " << count << " child" << (count == 1 ? "" : "ren") << diff --git a/test/UnitCopyPaste.cpp b/test/UnitCopyPaste.cpp index 23697778c5..018c60ea76 100644 --- a/test/UnitCopyPaste.cpp +++ b/test/UnitCopyPaste.cpp @@ -109,7 +109,7 @@ public: std::string value; // allow empty clipboards - if (clipboard && mimeType =="" && clipboard->size() == 0) + if (clipboard && mimeType == "" && content == "") return true; if (!clipboard || !clipboard->findType(mimeType, value)) @@ -205,7 +205,9 @@ public: assert(sessions.size() > 0 && session < sessions.size()); clientSession = sessions[session]; - return clientSession->getClipboardURI(false); // nominally thread unsafe + std::string tag = clientSession->getClipboardURI(false); // nominally thread unsafe + std::cerr << "Got tag '" << tag << "' for session " << session << "\n"; + return tag; } std::string buildClipboardText(const std::string &text) @@ -294,6 +296,18 @@ public: if (!fetchClipboardAssert(clipURI, "text/plain;charset=utf-8", "herring")) return; + std::cerr << "Close sockets:\n"; + socket->shutdown(); + socket2->shutdown(); + + sleep(1); // paranoia. + + std::cerr << "Fetch clipboards after shutdown:\n"; + if (!fetchClipboardAssert(clipURI2, "text/plain;charset=utf-8", "kippers")) + return; + if (!fetchClipboardAssert(clipURI, "text/plain;charset=utf-8", "herring")) + return; + std::cerr << "Clipboard tests succeeded" << std::endl; exitTest(TestResult::Ok); diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp index e02d9dadf9..29a72a9d5b 100644 --- a/wsd/ClientSession.cpp +++ b/wsd/ClientSession.cpp @@ -27,6 +27,7 @@ #include <common/Common.hpp> #include <common/Log.hpp> #include <common/Protocol.hpp> +#include <common/Clipboard.hpp> #include <common/Session.hpp> #include <common/Unit.hpp> #include <common/Util.hpp> @@ -48,8 +49,7 @@ ClientSession::ClientSession(const std::string& id, _docBroker(docBroker), _uriPublic(uriPublic), _isDocumentOwner(false), - _isAttached(false), - _isViewLoaded(false), + _state(SessionState::DETACHED), _keyEvents(1), _clientVisibleArea(0, 0, 0, 0), _clientSelectedPart(-1), @@ -67,6 +67,9 @@ ClientSession::ClientSession(const std::string& id, // populate with random values. for (auto it : _clipboardKeys) rotateClipboardKey(false); + + // get timestamp set + setState(SessionState::DETACHED); } // Can't take a reference in the constructor. @@ -85,11 +88,81 @@ ClientSession::~ClientSession() SessionMap.erase(getId()); } +static const char *stateToString(ClientSession::SessionState s) +{ + switch (s) + { + case ClientSession::SessionState::DETACHED: return "detached"; + case ClientSession::SessionState::LOADING: return "loading"; + case ClientSession::SessionState::LIVE: return "live"; + case ClientSession::SessionState::WAIT_DISCONNECT: return "wait_disconnect"; + } + return "invalid"; +} + +void ClientSession::setState(SessionState newState) +{ + LOG_TRC("ClientSession: transition from " << stateToString(_state) << + " to " << stateToString(newState)); + switch (newState) + { + case SessionState::DETACHED: + assert(_state == SessionState::DETACHED); + break; + case SessionState::LOADING: + assert(_state == SessionState::DETACHED); + break; + case SessionState::LIVE: + assert(_state == SessionState::LIVE || + _state == SessionState::LOADING); + break; + case SessionState::WAIT_DISCONNECT: + assert(_state == SessionState::LOADING || + _state == SessionState::LIVE); + break; + } + _state = newState; + _lastStateTime = std::chrono::steady_clock::now(); +} + +bool ClientSession::disconnectFromKit() +{ + assert(_state != SessionState::WAIT_DISCONNECT); + auto docBroker = getDocumentBroker(); + if (_state == SessionState::LIVE && docBroker) + { + setState(SessionState::WAIT_DISCONNECT); + + LOG_TRC("request/rescue clipboard on disconnect for " << getId()); + // rescue clipboard before shutdown. + docBroker->forwardToChild(getId(), "getclipboard"); + + // handshake nicely; so wait for 'disconnected' + docBroker->forwardToChild(getId(), "disconnect"); + + return false; + } + + return true; // just get on with it +} + +// Allow 20secs for the clipboard and disconection to come. +bool ClientSession::staleWaitDisconnect(const std::chrono::steady_clock::time_point &now) +{ + if (_state != SessionState::WAIT_DISCONNECT) + return false; + return std::chrono::duration_cast<std::chrono::seconds>(now - _lastStateTime).count() >= 20; +} + void ClientSession::rotateClipboardKey(bool notifyClient) { if (_wopiFileInfo && _wopiFileInfo->getDisableCopy()) return; + if (_state != SessionState::LIVE && // editing + _state != SessionState::DETACHED) // constructor + return; + _clipboardKeys[1] = _clipboardKeys[0]; _clipboardKeys[0] = Util::rng::getHardRandomHexString(16); LOG_TRC("Clipboard key on [" << getId() << "] set to " << _clipboardKeys[0] << @@ -135,14 +208,39 @@ bool ClientSession::matchesClipboardKeys(const std::string &/*viewId*/, const st return false; } + void ClientSession::handleClipboardRequest(DocumentBroker::ClipboardRequest type, const std::shared_ptr<StreamSocket> &socket, + const std::string &tag, const std::shared_ptr<std::string> &data) { // Move the socket into our DocBroker. auto docBroker = getDocumentBroker(); docBroker->addSocketToPoll(socket); + if (_state == SessionState::WAIT_DISCONNECT) + { + LOG_TRC("Clipboard request " << tag << " for disconnecting session"); + if (docBroker->lookupSendClipboardTag(socket, tag, false)) + return; // the getclipboard already completed. + if (type == DocumentBroker::CLIP_REQUEST_SET) + { + std::ostringstream oss; + oss << "HTTP/1.1 400 Bad Request\r\n" + << "Date: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n" + << "User-Agent: " << WOPI_AGENT_STRING << "\r\n" + << "Content-Length: 0\r\n" + << "\r\n"; + socket->send(oss.str()); + socket->shutdown(); + } + else // will be handled during shutdown + { + LOG_TRC("Clipboard request " << tag << " queued for shutdown"); + _clipSockets.push_back(socket); + } + } + std::string specific; if (type == DocumentBroker::CLIP_REQUEST_GET_RICH_HTML_ONLY) specific = " text/html"; @@ -1107,7 +1205,8 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt // 'download' and/or providing our helpful / user page. // for now just for remote sockets. - LOG_TRC("Got clipboard content to send to " << _clipSockets.size() << "sockets"); + LOG_TRC("Got clipboard content of size " << payload->size() << " to send to " << + _clipSockets.size() << " sockets in state " << stateToString(_state)); postProcessCopyPayload(payload); @@ -1116,6 +1215,13 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt if (payload->data()[header++] == '\n') break; const bool empty = header >= payload->size(); + + // final cleanup ... + if (!empty && _state == SessionState::WAIT_DISCONNECT && + (!_wopiFileInfo || !_wopiFileInfo->getDisableCopy())) + LOOLWSD::SavedClipboards->insertClipboard( + _clipboardKeys, &payload->data()[header], payload->size() - header); + for (auto it : _clipSockets) { std::ostringstream oss; @@ -1139,6 +1245,11 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt } _clipSockets.clear(); return true; + } else if (tokens[0] == "disconnected:") { + + LOG_INF("End of disconnection handshake for " << getId()); + docBroker->finalRemoveSession(getId()); + return true; } if (!isDocPasswordProtected()) @@ -1149,7 +1260,7 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt } else if (tokens[0] == "status:") { - setViewLoaded(); + setState(ClientSession::SessionState::LIVE); docBroker->setLoaded(); // Wopi post load actions if (_wopiFileInfo && !_wopiFileInfo->getTemplateSource().empty()) @@ -1454,7 +1565,7 @@ void ClientSession::dumpState(std::ostream& os) os << "\t\tisReadOnly: " << isReadOnly() << "\n\t\tisDocumentOwner: " << _isDocumentOwner - << "\n\t\tisAttached: " << _isAttached + << "\n\t\tstate: " << stateToString(_state) << "\n\t\tkeyEvents: " << _keyEvents // << "\n\t\tvisibleArea: " << _clientVisibleArea << "\n\t\tclientSelectedPart: " << _clientSelectedPart diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp index 5d5d68249e..d4b31dbeac 100644 --- a/wsd/ClientSession.hpp +++ b/wsd/ClientSession.hpp @@ -44,13 +44,21 @@ public: void setReadOnly() override; - /// Returns true if this session is added to a DocBroker. - bool isAttached() const { return _isAttached; } - void setAttached() { _isAttached = true; } + enum SessionState { + DETACHED, // initial + LOADING, // attached to a DocBroker & waiting for load + LIVE, // Document is loaded & editable or viewable. + WAIT_DISCONNECT // closed and waiting for Kit's disconnected message + }; /// Returns true if this session has loaded a view (i.e. we got status message). - bool isViewLoaded() const { return _isViewLoaded; } - void setViewLoaded() { _isViewLoaded = true; } + bool isViewLoaded() const { return _state == SessionState::LIVE; } + + /// returns true if we're waiting for the kit to acknowledge disconnect. + bool inWaitDisconnected() const { return _state == SessionState::WAIT_DISCONNECT; } + + /// transition to a new state + void setState(SessionState newState); void setDocumentOwner(const bool documentOwner) { _isDocumentOwner = documentOwner; } bool isDocumentOwner() const { return _isDocumentOwner; } @@ -61,6 +69,9 @@ public: /// Integer id of the view in the kit process, or -1 if unknown int getKitViewId() const { return _kitViewId; } + /// Disconnect the session and do final cleanup, @returns true if we should not wait. + bool disconnectFromKit(); + // sendTextFrame that takes std::string and string literal. using Session::sendTextFrame; @@ -147,6 +158,7 @@ public: /// Handle a clipboard fetch / put request. void handleClipboardRequest(DocumentBroker::ClipboardRequest type, const std::shared_ptr<StreamSocket> &socket, + const std::string &tag, const std::shared_ptr<std::string> &data); /// Create URI for transient clipboard content. @@ -155,6 +167,9 @@ public: /// Adds and/or modified the copied payload before sending on to the client. void postProcessCopyPayload(std::shared_ptr<Message> payload); + /// Returns true if we're expired waiting for a clipboard and should be removed + bool staleWaitDisconnect(const std::chrono::steady_clock::time_point &now); + /// Generate and rotate a new clipboard hash, sending it if appropriate void rotateClipboardKey(bool notifyClient); @@ -211,11 +226,11 @@ private: /// The socket to which the converted (saveas) doc is sent. std::shared_ptr<StreamSocket> _saveAsSocket; - /// If we are added to a DocBroker. - bool _isAttached; + /// The phase of our lifecycle that we're in. + SessionState _state; - /// If we have loaded a view. - bool _isViewLoaded; + /// Time of last state transition + std::chrono::steady_clock::time_point _lastStateTime; /// Wopi FileInfo object std::unique_ptr<WopiStorage::WOPIFileInfo> _wopiFileInfo; diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp index 2d46457479..be1794eb7a 100644 --- a/wsd/DocumentBroker.cpp +++ b/wsd/DocumentBroker.cpp @@ -35,6 +35,7 @@ #include "TileCache.hpp" #include <common/Log.hpp> #include <common/Message.hpp> +#include <common/Clipboard.hpp> #include <common/Protocol.hpp> #include <common/Unit.hpp> #include <common/FileUtil.hpp> @@ -343,10 +344,23 @@ void DocumentBroker::pollThread() #if !MOBILEAPP if (std::chrono::duration_cast<std::chrono::minutes>(now - lastClipboardHashUpdateTime).count() >= 2) + for (auto &it : _sessions) + { + if (it.second->staleWaitDisconnect(now)) + { + std::string id = it.second->getId(); + LOG_WRN("Unusual, Kit session " + id + " failed its disconnect handshake, killing"); + finalRemoveSession(id); + break; // it invalid. + } + } + + if (std::chrono::duration_cast<std::chrono::minutes>(now - lastClipboardHashUpdateTime).count() >= 5) { LOG_TRC("Rotating clipboard keys"); - for (auto& it : _sessions) + for (auto &it : _sessions) it.second->rotateClipboardKey(true); + lastClipboardHashUpdateTime = now; } @@ -810,7 +824,7 @@ bool DocumentBroker::saveToStorage(const std::string& sessionId, // If marked to destroy, or session is disconnected, remove. const auto it = _sessions.find(sessionId); if (_markToDestroy || (it != _sessions.end() && it->second->isCloseFrame())) - removeSessionInternal(sessionId); + disconnectSessionInternal(sessionId); // If marked to destroy, then this was the last session. if (_markToDestroy || _sessions.empty()) @@ -1025,7 +1039,8 @@ bool DocumentBroker::autoSave(const bool force, const bool dontSaveIfUnmodified) for (auto& sessionIt : _sessions) { // Save the document using an editable session, or first ... - if (savingSessionId.empty() || !sessionIt.second->isReadOnly()) + if (savingSessionId.empty() || + (!sessionIt.second->isReadOnly() && !sessionIt.second->inWaitDisconnected())) { savingSessionId = sessionIt.second->getId(); } @@ -1211,7 +1226,7 @@ size_t DocumentBroker::addSessionInternal(const std::shared_ptr<ClientSession>& // Add and attach the session. _sessions.emplace(session->getId(), session); - session->setAttached(); + session->setState(ClientSession::SessionState::LOADING); const size_t count = _sessions.size(); LOG_TRC("Added " << (session->isReadOnly() ? "readonly" : "non-readonly") << @@ -1247,7 +1262,7 @@ size_t DocumentBroker::removeSession(const std::string& id) // If last editable, save and don't remove until after uploading to storage. if (!lastEditableSession || !autoSave(isPossiblyModified(), dontSaveIfUnmodified)) - removeSessionInternal(id); + disconnectSessionInternal(id); } catch (const std::exception& ex) { @@ -1257,7 +1272,7 @@ size_t DocumentBroker::removeSession(const std::string& id) return _sessions.size(); } -size_t DocumentBroker::removeSessionInternal(const std::string& id) +void DocumentBroker::disconnectSessionInternal(const std::string& id) { assertCorrectThread(); try @@ -1272,10 +1287,51 @@ size_t DocumentBroker::removeSessionInternal(const std::string& id) LOOLWSD::dumpEndSessionTrace(getJailId(), id, _uriOrig); #endif + LOG_TRC("Disconnect session internal " << id); + + bool hardDisconnect; + if (it->second->inWaitDisconnected()) + { + LOG_TRC("hard disconnecting while waiting for disconnected handshake."); + hardDisconnect = true; + } + else + { + hardDisconnect = it->second->disconnectFromKit(); + + // Let the child know the client has disconnected. + const std::string msg("child-" + id + " disconnect"); + _childProcess->sendTextFrame(msg); + } + + if (hardDisconnect) + finalRemoveSession(id); + // else wait for disconnected. + } + else + { + LOG_TRC("Session [" << id << "] not found to disconnect from docKey [" << + _docKey << "]. Have " << _sessions.size() << " sessions."); + } + } + catch (const std::exception& ex) + { + LOG_ERR("Error while disconnecting session [" << id << "]: " << ex.what()); + } +} + +void DocumentBroker::finalRemoveSession(const std::string& id) +{ + assertCorrectThread(); + try + { + auto it = _sessions.find(id); + if (it != _sessions.end()) + { const bool readonly = (it->second ? it->second->isReadOnly() : false); // Remove. The caller must have a reference to the session - // in question, lest we destroy from underneith them. + // in question, lest we destroy from underneath them. _sessions.erase(it); const size_t count = _sessions.size(); @@ -1291,11 +1347,7 @@ size_t DocumentBroker::removeSessionInternal(const std::string& id) LOG_END(logger, true); } - // Let the child know the client has disconnected. - const std::string msg("child-" + id + " disconnect"); - _childProcess->sendTextFrame(msg); - - return count; + return; } else { @@ -1307,8 +1359,6 @@ size_t DocumentBroker::removeSessionInternal(const std::string& id) { LOG_ERR("Error while removing session [" << id << "]: " << ex.what()); } - - return _sessions.size(); } void DocumentBroker::addCallback(const SocketPoll::CallbackFn& fn) @@ -1333,7 +1383,8 @@ void DocumentBroker::alertAllUsers(const std::string& msg) LOG_DBG("Alerting all users of [" << _docKey << "]: " << msg); for (auto& it : _sessions) { - it.second->enqueueSendMessage(payload); + if (!it.second->inWaitDisconnected()) + it.second->enqueueSendMessage(payload); } } @@ -1421,7 +1472,8 @@ void DocumentBroker::handleTileRequest(TileDesc& tile, { for (auto& it: _sessions) { - tileCache().subscribeToTileRendering(tile, it.second); + if (!it.second->inWaitDisconnected()) + tileCache().subscribeToTileRendering(tile, it.second); } } else @@ -1515,19 +1567,34 @@ void DocumentBroker::handleTileCombinedRequest(TileCombined& tileCombined, sendRequestedTiles(session); } -void DocumentBroker::handleClipboardRequest(ClipboardRequest type, const std::shared_ptr<StreamSocket> &socket, - const std::string &viewId, const std::string &tag, - const std::shared_ptr<std::string> &data) +/// lookup in global clipboard cache and send response, send error if missing if @sendError +bool DocumentBroker::lookupSendClipboardTag(const std::shared_ptr<StreamSocket> &socket, + const std::string &tag, bool sendError) { - for (auto& it : _sessions) + LOG_TRC("Clipboard request " << tag << " not for a live session - check cache."); + std::shared_ptr<std::string> saved = + LOOLWSD::SavedClipboards->getClipboard(tag); + if (saved) { - if (it.second->matchesClipboardKeys(viewId, tag)) - { - it.second->handleClipboardRequest(type, socket, data); - return; - } + std::ostringstream oss; + oss << "HTTP/1.1 200 OK\r\n" + << "Last-Modified: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n" + << "User-Agent: " << WOPI_AGENT_STRING << "\r\n" + << "Content-Length: " << saved->length() << "\r\n" + << "Content-Type: application/octet-stream\r\n" + << "X-Content-Type-Options: nosniff\r\n" + << "\r\n"; + oss.write(saved->c_str(), saved->length()); + socket->setSocketBufferSize(std::min(saved->length() + 256, + size_t(Socket::MaximumSendBufferSize))); + socket->send(oss.str()); + socket->shutdown(); + LOG_INF("Found and queued clipboard response for send of size " << saved->length()); + return true; } - LOG_ERR("Could not find matching session to handle clipboard request for " << viewId << " tag: " << tag); + + if (!sendError) + return false; // Bad request. std::ostringstream oss; @@ -1539,6 +1606,24 @@ void DocumentBroker::handleClipboardRequest(ClipboardRequest type, const std::s << "Failed to find this clipboard"; socket->send(oss.str()); socket->shutdown(); + + return false; +} + +void DocumentBroker::handleClipboardRequest(ClipboardRequest type, const std::shared_ptr<StreamSocket> &socket, + const std::string &viewId, const std::string &tag, + const std::shared_ptr<std::string> &data) +{ + for (auto& it : _sessions) + { + if (it.second->matchesClipboardKeys(viewId, tag)) + { + it.second->handleClipboardRequest(type, socket, tag, data); + return; + } + } + if (!lookupSendClipboardTag(socket, tag, true)) + LOG_ERR("Could not find matching session to handle clipboard request for " << viewId << " tag: " << tag); } void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& session) @@ -1724,7 +1809,8 @@ bool DocumentBroker::haveAnotherEditableSession(const std::string& id) const { if (it.second->getId() != id && it.second->isViewLoaded() && - !it.second->isReadOnly()) + !it.second->isReadOnly() && + !it.second->inWaitDisconnected()) { // This is a loaded session that is non-readonly. return true; @@ -1820,9 +1906,10 @@ bool DocumentBroker::forwardToClient(const std::shared_ptr<Message>& payload) // Broadcast to all. // Events could cause the removal of sessions. std::map<std::string, std::shared_ptr<ClientSession>> sessions(_sessions); - for (const auto& pair : sessions) + for (const auto& it : _sessions) { - pair.second->handleKitToClientMessage(data, size); + if (!it.second->inWaitDisconnected()) + it.second->handleKitToClientMessage(data, size); } } else @@ -1862,11 +1949,16 @@ void DocumentBroker::shutdownClients(const std::string& closeReason) std::shared_ptr<ClientSession> session = pair.second; try { - // Notify the client and disconnect. - session->shutdown(WebSocketHandler::StatusCodes::ENDPOINT_GOING_AWAY, closeReason); + if (session->inWaitDisconnected()) + finalRemoveSession(session->getId()); + else + { + // Notify the client and disconnect. + session->shutdown(WebSocketHandler::StatusCodes::ENDPOINT_GOING_AWAY, closeReason); - // Remove session, save, and mark to destroy. - removeSession(session->getId()); + // Remove session, save, and mark to destroy. + removeSession(session->getId()); + } } catch (const std::exception& exc) { diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp index 719f748568..b50f6d706d 100644 --- a/wsd/DocumentBroker.hpp +++ b/wsd/DocumentBroker.hpp @@ -231,6 +231,9 @@ public: /// Flag for termination. Note that this doesn't save any unsaved changes in the document void stop(const std::string& reason); + /// Hard removes a session by ID, only for ClientSession. + void finalRemoveSession(const std::string& id); + /// Thread safe termination of this broker if it has a lingering thread void joinThread(); @@ -322,6 +325,8 @@ public: void handleClipboardRequest(ClipboardRequest type, const std::shared_ptr<StreamSocket> &socket, const std::string &viewId, const std::string &tag, const std::shared_ptr<std::string> &data); + static bool lookupSendClipboardTag(const std::shared_ptr<StreamSocket> &socket, + const std::string &tag, bool sendError = false); bool isMarkedToDestroy() const { return _markToDestroy || _stop; } @@ -401,8 +406,8 @@ private: /// Loads a new session and adds to the sessions container. size_t addSessionInternal(const std::shared_ptr<ClientSession>& session); - /// Removes a session by ID. Returns the new number of sessions. - size_t removeSessionInternal(const std::string& id); + /// Starts the Kit <-> DocumentBroker shutdown handshake + void disconnectSessionInternal(const std::string& id); /// Forward a message from child session to its respective client session. bool forwardToClient(const std::shared_ptr<Message>& payload); diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp index a7f5b89715..bbc3fa8fb3 100644 --- a/wsd/LOOLWSD.cpp +++ b/wsd/LOOLWSD.cpp @@ -104,6 +104,7 @@ using Poco::Net::PartHandler; #include "Auth.hpp" #include "ClientSession.hpp" #include <Common.hpp> +#include <Clipboard.hpp> #include <Crypto.hpp> #include <DelaySocket.hpp> #include "DocumentBroker.hpp" @@ -721,6 +722,7 @@ static std::string UnitTestLibrary; unsigned int LOOLWSD::NumPreSpawnedChildren = 0; std::unique_ptr<TraceFileWriter> LOOLWSD::TraceDumper; +std::unique_ptr<ClipboardCache> LOOLWSD::SavedClipboards; /// This thread polls basic web serving, and handling of /// websockets before upgrade: when upgraded they go to the @@ -1154,6 +1156,8 @@ void LOOLWSD::initialize(Application& self) } #if !MOBILEAPP + SavedClipboards.reset(new ClipboardCache()); + FileServerRequestHandler::initialize(); #endif @@ -2152,7 +2156,7 @@ private: StringTokenizer reqPathTokens(request.getURI(), "/?", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); if (reqPathTokens.count() > 1 && reqPathTokens[0] == "lool" && reqPathTokens[1] == "clipboard") { -// Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ... + Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ... handleClipboardRequest(request, message, disposition); } else if (!(request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0) && @@ -2325,7 +2329,8 @@ private: Poco::MemoryInputStream& message, SocketDisposition &disposition) { - LOG_DBG("Clipboard request: " << request.getURI()); + LOG_DBG("Clipboard " << ((request.getMethod() == HTTPRequest::HTTP_GET) ? "GET" : "POST") << + " request: " << request.getURI()); Poco::URI requestUri(request.getURI()); Poco::URI::QueryParameters params = requestUri.getQueryParameters(); @@ -2391,7 +2396,10 @@ private: }); }); LOG_TRC("queued clipboard command " << type << " on docBroker fetch"); - } else { + } + // fallback to persistent clipboards if we can + else if (!DocumentBroker::lookupSendClipboardTag(_socket.lock(), tag, false)) + { LOG_ERR("Invalid clipboard request: " << serverId << " with tag " << tag << " and broker: " << (docBroker ? "not" : "") << "found"); diff --git a/wsd/LOOLWSD.hpp b/wsd/LOOLWSD.hpp index a23a4f714a..0af2caa284 100644 --- a/wsd/LOOLWSD.hpp +++ b/wsd/LOOLWSD.hpp @@ -28,6 +28,7 @@ class ChildProcess; class TraceFileWriter; class DocumentBroker; +class ClipboardCache; std::shared_ptr<ChildProcess> getNewChild_Blocks( #if MOBILEAPP @@ -69,6 +70,7 @@ public: static bool AnonymizeUsernames; static std::atomic<unsigned> NumConnections; static std::unique_ptr<TraceFileWriter> TraceDumper; + static std::unique_ptr<ClipboardCache> SavedClipboards; static std::set<std::string> EditFileExtensions; static unsigned MaxConnections; static unsigned MaxDocuments; |