diff options
author | Michael Meeks <michael.meeks@collabora.com> | 2019-11-19 22:51:45 +0000 |
---|---|---|
committer | Michael Meeks <michael.meeks@collabora.com> | 2019-11-20 15:42:05 +0000 |
commit | 0613e44c71c5ea7213ad6a0af332e26c4c7f1c90 (patch) | |
tree | 53f8b49eb4785138b6c903a7f6aa2e891c29e239 | |
parent | d4b71cb73d4f9ca081f62f1a137d9b0cec6ed6f6 (diff) |
Initial wopi-like locking implementation.feature/locking
Change-Id: I3e16cfc40dbd29a24016b09e62afc9ec488300c7
-rw-r--r-- | wsd/DocumentBroker.cpp | 33 | ||||
-rw-r--r-- | wsd/DocumentBroker.hpp | 2 | ||||
-rw-r--r-- | wsd/Storage.cpp | 96 | ||||
-rw-r--r-- | wsd/Storage.hpp | 48 |
4 files changed, 154 insertions, 25 deletions
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp index 6b393d277..ef1ad7ed6 100644 --- a/wsd/DocumentBroker.cpp +++ b/wsd/DocumentBroker.cpp @@ -189,6 +189,7 @@ DocumentBroker::DocumentBroker(const std::string& uri, _poll(new DocumentBrokerPoll("docbroker_" + _docId, *this)), _stop(false), _closeReason("stopped"), + _lockCtx(new LockContext()), _tileVersion(0), _debugRenderedTileCount(0) { @@ -563,7 +564,8 @@ bool DocumentBroker::load(const std::shared_ptr<ClientSession>& session, const s WopiStorage* wopiStorage = dynamic_cast<WopiStorage*>(_storage.get()); if (wopiStorage != nullptr) { - std::unique_ptr<WopiStorage::WOPIFileInfo> wopifileinfo = wopiStorage->getWOPIFileInfo(session->getAuthorization()); + std::unique_ptr<WopiStorage::WOPIFileInfo> wopifileinfo = + wopiStorage->getWOPIFileInfo(session->getAuthorization(), *_lockCtx); userId = wopifileinfo->getUserId(); username = wopifileinfo->getUsername(); userExtraInfo = wopifileinfo->getUserExtraInfo(); @@ -722,7 +724,11 @@ bool DocumentBroker::load(const std::shared_ptr<ClientSession>& session, const s // Let's load the document now, if not loaded. if (!_storage->isLoaded()) { - std::string localPath = _storage->loadStorageFileToLocal(session->getAuthorization(), templateSource); + std::string localPath = _storage->loadStorageFileToLocal( + session->getAuthorization(), *_lockCtx, templateSource); + + if (!_storage->updateLockState(session->getAuthorization(), *_lockCtx, true)) + LOG_ERR("Failed to lock!"); #if !MOBILEAPP // Check if we have a prefilter "plugin" for this document format @@ -955,7 +961,8 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId, bool su LOG_DBG("Persisting [" << _docKey << "] after saving to URI [" << uriAnonym << "]."); assert(_storage && _tileCache); - StorageBase::SaveResult storageSaveResult = _storage->saveLocalFileToStorage(auth, saveAsPath, saveAsFilename, isRename); + StorageBase::SaveResult storageSaveResult = _storage->saveLocalFileToStorage( + auth, *_lockCtx, saveAsPath, saveAsFilename, isRename); if (storageSaveResult.getResult() == StorageBase::SaveResult::OK) { if (!isSaveAs && !isRename) @@ -1299,18 +1306,19 @@ size_t DocumentBroker::removeSession(const std::string& id) LOG_ERR("Invalid or unknown session [" << id << "] to remove."); return _sessions.size(); } + std::shared_ptr<ClientSession> session = it->second; // Last view going away, can destroy. _markToDestroy = (_sessions.size() <= 1); - const bool lastEditableSession = !it->second->isReadOnly() && !haveAnotherEditableSession(id); + const bool lastEditableSession = !session->isReadOnly() && !haveAnotherEditableSession(id); static const bool dontSaveIfUnmodified = !LOOLWSD::getConfigValue<bool>("per_document.always_save_on_exit", false); LOG_INF("Removing session [" << id << "] on docKey [" << _docKey << "]. Have " << _sessions.size() - << " sessions. IsReadOnly: " << it->second->isReadOnly() - << ", IsViewLoaded: " << it->second->isViewLoaded() << ", IsWaitDisconnected: " - << it->second->inWaitDisconnected() << ", MarkToDestroy: " << _markToDestroy + << " sessions. IsReadOnly: " << session->isReadOnly() + << ", IsViewLoaded: " << session->isViewLoaded() << ", IsWaitDisconnected: " + << session->inWaitDisconnected() << ", MarkToDestroy: " << _markToDestroy << ", LastEditableSession: " << lastEditableSession << ", dontSaveIfUnmodified: " << dontSaveIfUnmodified); @@ -1341,7 +1349,16 @@ void DocumentBroker::disconnectSessionInternal(const std::string& id) LOOLWSD::dumpEndSessionTrace(getJailId(), id, _uriOrig); #endif - LOG_TRC("Disconnect session internal " << id); + LOG_TRC("Disconnect session internal " << id << + " destroy? " << _markToDestroy << + " locked? " << _lockCtx->_isLocked); + + if (_markToDestroy && // last session to remove; FIXME: Editable? + _lockCtx->_isLocked && _storage) + { + if (!_storage->updateLockState(it->second->getAuthorization(), *_lockCtx, false)) + LOG_ERR("Failed to unlock!"); + } bool hardDisconnect; if (it->second->inWaitDisconnected()) diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp index 944120ca8..8641fe123 100644 --- a/wsd/DocumentBroker.hpp +++ b/wsd/DocumentBroker.hpp @@ -36,6 +36,7 @@ // Forwards. class PrisonerRequestDispatcher; class DocumentBroker; +class LockContext; class StorageBase; class TileCache; class Message; @@ -488,6 +489,7 @@ private: std::unique_ptr<DocumentBrokerPoll> _poll; std::atomic<bool> _stop; std::string _closeReason; + std::unique_ptr<LockContext> _lockCtx; /// Versioning is used to prevent races between /// painting and invalidation. diff --git a/wsd/Storage.cpp b/wsd/Storage.cpp index 5569763ee..0825c4ef3 100644 --- a/wsd/Storage.cpp +++ b/wsd/Storage.cpp @@ -299,7 +299,7 @@ std::unique_ptr<LocalStorage::LocalFileInfo> LocalStorage::getLocalFileInfo() return std::unique_ptr<LocalStorage::LocalFileInfo>(new LocalFileInfo({"localhost" + std::to_string(LastLocalStorageId), "LocalHost#" + std::to_string(LastLocalStorageId++)})); } -std::string LocalStorage::loadStorageFileToLocal(const Authorization& /*auth*/, const std::string& /*templateUri*/) +std::string LocalStorage::loadStorageFileToLocal(const Authorization& /*auth*/, LockContext & /*lockCtx*/, const std::string& /*templateUri*/) { #if !MOBILEAPP // /chroot/jailId/user/doc/childId/file.ext @@ -363,7 +363,7 @@ std::string LocalStorage::loadStorageFileToLocal(const Authorization& /*auth*/, } -StorageBase::SaveResult LocalStorage::saveLocalFileToStorage(const Authorization& /*auth*/, const std::string& /*saveAsPath*/, const std::string& /*saveAsFilename*/, bool /*isRename*/) +StorageBase::SaveResult LocalStorage::saveLocalFileToStorage(const Authorization& /*auth*/, LockContext &/*lockCtx*/, const std::string& /*saveAsPath*/, const std::string& /*saveAsFilename*/, bool /*isRename*/) { try { @@ -461,7 +461,17 @@ std::string getReuseCookies(const Poco::URI &uriObject) } // anonymous namespace -std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Authorization& auth) +void LockContext::initSupportsLocks() +{ + if (_supportsLocks) + return; + + // first time token setup + _supportsLocks = true; + _lockToken = "lool-lock" + Util::rng::getHexString(8); +} + +std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Authorization& auth, LockContext &lockCtx) { // update the access_token to the one matching to the session Poco::URI uriObject(getUri()); @@ -657,6 +667,9 @@ std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Au const std::chrono::system_clock::time_point modifiedTime = Util::iso8601ToTimestamp(lastModifiedTime, "LastModifiedTime"); setFileInfo(FileInfo({filename, ownerId, modifiedTime, size})); + if (supportsLocks) + lockCtx.initSupportsLocks(); + return std::unique_ptr<WopiStorage::WOPIFileInfo>(new WOPIFileInfo( {userId, obfuscatedUserId, userName, userExtraInfo, watermarkText, templateSaveAs, templateSource, canWrite, postMessageOrigin, hidePrintOption, hideSaveOption, hideExportOption, @@ -667,8 +680,71 @@ std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Au userCanRename, callDuration})); } +bool WopiStorage::updateLockState(const Authorization &auth, LockContext &lockCtx, bool lock) +{ + if (!lockCtx._supportsLocks) + return true; + + Poco::URI uriObject(getUri()); + auth.authorizeURI(uriObject); + + std::string reuseStorageCookies = getReuseCookies(uriObject); + + Poco::URI uriObjectAnonym(getUri()); + uriObjectAnonym.setPath(LOOLWSD::anonymizeUrl(uriObjectAnonym.getPath())); + const std::string uriAnonym = uriObjectAnonym.toString(); + + const std::string wopiLog(lock ? "WOPI::Lock" : "WOPI::Unlock"); + LOG_DBG(wopiLog << " requesting: " << uriAnonym); + + try + { + std::unique_ptr<Poco::Net::HTTPClientSession> psession(getHTTPClientSession(uriObject)); + + Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_POST, uriObject.getPathAndQuery(), Poco::Net::HTTPMessage::HTTP_1_1); + request.set("User-Agent", WOPI_AGENT_STRING); + auth.authorizeRequest(request); + + request.set("X-WOPI-Override", lock ? "LOCK" : "UNLOCK"); + request.set("X-WOPI-Lock", lockCtx._lockToken); + if (!getExtendedData().empty()) + request.set("X-LOOL-WOPI-ExtendedData", getExtendedData()); + addStorageDebugCookie(request); + if (_reuseCookies) + addStorageReuseCookie(request, reuseStorageCookies); + + psession->sendRequest(request); + Poco::Net::HTTPResponse response; + std::istream& rs = psession->receiveResponse(response); + + std::ostringstream oss; + Poco::StreamCopier::copyStream(rs, oss); + std::string responseString = oss.str(); + + LOG_INF(wopiLog << " response: " << responseString << + " status " << response.getStatus()); + + if (response.getStatus() == Poco::Net::HTTPResponse::HTTP_OK) + { + lockCtx._isLocked = lock; + return true; + } + else + { + LOG_WRN("Un-successfull " << wopiLog << " with status " << response.getStatus() << + " and response: " << responseString); + } + } + catch (const Poco::Exception& pexc) + { + LOG_ERR("Cannot " << wopiLog << " uri [" << uriAnonym << "]. Error: " << + pexc.displayText() << (pexc.nested() ? " (" + pexc.nested()->displayText() + ")" : "")); + } + return false; +} + /// uri format: http://server/<...>/wopi*/files/<id>/content -std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth, const std::string& templateUri) +std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth, LockContext &/* lockCtx */, const std::string& templateUri) { // WOPI URI to download files ends in '/contents'. // Add it here to get the payload instead of file info. @@ -741,7 +817,6 @@ std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth, const ofs.close(); LOG_INF("WOPI::GetFile downloaded " << getFileSize(getRootFilePath()) << " bytes from [" << uriAnonym << "] -> " << getRootFilePathAnonym() << " in " << diff.count() << "s"); - setLoaded(true); // Now return the jailed path. @@ -758,7 +833,7 @@ std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth, const return ""; } -StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) +StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization& auth, LockContext &lockCtx, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) { // TODO: Check if this URI has write permission (canWrite = true) @@ -791,6 +866,8 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization& { // normal save request.set("X-WOPI-Override", "PUT"); + if (lockCtx._supportsLocks) + request.set("X-WOPI-Lock", lockCtx._lockToken); request.set("X-LOOL-WOPI-IsModifiedByUser", isUserModified()? "true": "false"); request.set("X-LOOL-WOPI-IsAutosave", getIsAutosave()? "true": "false"); request.set("X-LOOL-WOPI-IsExitSave", isExitSave()? "true": "false"); @@ -970,14 +1047,17 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization& return saveResult; } -std::string WebDAVStorage::loadStorageFileToLocal(const Authorization& /*auth*/, const std::string& /*templateUri*/) +std::string WebDAVStorage::loadStorageFileToLocal( + const Authorization& /*auth*/, LockContext &/*lockCtx*/, const std::string& /*templateUri*/) { // TODO: implement webdav GET. setLoaded(true); return getUri().toString(); } -StorageBase::SaveResult WebDAVStorage::saveLocalFileToStorage(const Authorization& /*auth*/, const std::string& /*saveAsPath*/, const std::string& /*saveAsFilename*/, bool /*isRename*/) +StorageBase::SaveResult WebDAVStorage::saveLocalFileToStorage( + const Authorization& /*auth*/, LockContext &/*lockCtx*/, const std::string& /*saveAsPath*/, + const std::string& /*saveAsFilename*/, bool /*isRename*/) { // TODO: implement webdav PUT. return StorageBase::SaveResult(StorageBase::SaveResult::OK); diff --git a/wsd/Storage.hpp b/wsd/Storage.hpp index 3632cd6f3..655e089b4 100644 --- a/wsd/Storage.hpp +++ b/wsd/Storage.hpp @@ -24,10 +24,28 @@ #include "Util.hpp" #include <common/Authorization.hpp> +/// Represents whether the underlying file is locked +/// and with what token. +struct LockContext +{ + /// Do we have support for locking for a storage. + bool _supportsLocks; + /// Do we own the (leased) lock currently + bool _isLocked; + /// Name if we need it to use consistently for locking + std::string _lockToken; + + LockContext() : _supportsLocks(false), _isLocked(false) { } + + /// one-time setup for supporting locks & create token + void initSupportsLocks(); +}; + /// Base class of all Storage abstractions. class StorageBase { public: + /// Represents basic file's attributes. /// Used for local and network files. class FileInfo @@ -190,13 +208,16 @@ public: std::string getFileExtension() const { return Poco::Path(_fileInfo.getFilename()).getExtension(); } + /// Update the locking state (check-in/out) of the associated file + virtual bool updateLockState(const Authorization &auth, LockContext &lockCtx, bool lock) = 0; + /// Returns a local file path for the given URI. /// If necessary copies the file locally first. - virtual std::string loadStorageFileToLocal(const Authorization& auth, const std::string& templateUri) = 0; + virtual std::string loadStorageFileToLocal(const Authorization& auth, LockContext &lockCtx, const std::string& templateUri) = 0; /// Writes the contents of the file back to the source. /// @param savedFile When the operation was saveAs, this is the path to the file that was saved. - virtual SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) = 0; + virtual SaveResult saveLocalFileToStorage(const Authorization& auth, LockContext &lockCtx, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) = 0; static size_t getFileSize(const std::string& filename); @@ -283,9 +304,11 @@ public: /// obtained using getFileInfo method std::unique_ptr<LocalFileInfo> getLocalFileInfo(); - std::string loadStorageFileToLocal(const Authorization& auth, const std::string& templateUri) override; + bool updateLockState(const Authorization &, LockContext &, bool) override { return true; } + + std::string loadStorageFileToLocal(const Authorization& auth, LockContext &lockCtx, const std::string& templateUri) override; - SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) override; + SaveResult saveLocalFileToStorage(const Authorization& auth, LockContext &lockCtx, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) override; private: /// True if the jailed file is not linked but copied. @@ -511,12 +534,17 @@ public: /// provided during the initial creation of the WOPI storage. /// Also extracts the basic file information from the response /// which can then be obtained using getFileInfo() - std::unique_ptr<WOPIFileInfo> getWOPIFileInfo(const Authorization& auth); + /// Also sets up the locking context for future operations. + std::unique_ptr<WOPIFileInfo> getWOPIFileInfo(const Authorization& auth, + LockContext &lockCtx); + + /// Update the locking state (check-in/out) of the associated file + bool updateLockState(const Authorization &auth, LockContext &lockCtx, bool lock) override; /// uri format: http://server/<...>/wopi*/files/<id>/content - std::string loadStorageFileToLocal(const Authorization& auth, const std::string& templateUri) override; + std::string loadStorageFileToLocal(const Authorization& auth, LockContext &lockCtx, const std::string& templateUri) override; - SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) override; + SaveResult saveLocalFileToStorage(const Authorization& auth, LockContext &lockCtx, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) override; /// Total time taken for making WOPI calls during load std::chrono::duration<double> getWopiLoadDuration() const { return _wopiLoadDuration; } @@ -546,9 +574,11 @@ public: // Implement me // WebDAVFileInfo getWebDAVFileInfo(const Poco::URI& uriPublic); - std::string loadStorageFileToLocal(const Authorization& auth, const std::string& templateUri) override; + bool updateLockState(const Authorization &, LockContext &, bool) override { return true; } + + std::string loadStorageFileToLocal(const Authorization& auth, LockContext &lockCtx, const std::string& templateUri) override; - SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) override; + SaveResult saveLocalFileToStorage(const Authorization& auth, LockContext &lockCtx, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) override; private: std::unique_ptr<AuthBase> _authAgent; |