/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ /* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "Util.hpp" #include "config.h" #include "helpers.hpp" #include "Log.hpp" #include "Unit.hpp" #include "UnitHTTP.hpp" #include #include #include #include #include #include #include #include #include #include class WopiTestServer : public UnitWSD { enum class LOOLStatusCode { DocChanged = 1010 }; private: /// The WOPISrc URL. std::string _wopiSrc; /// Websocket to communicate. std::unique_ptr _ws; /// Content of the file. std::string _fileContent; /// Last modified time of the file std::chrono::system_clock::time_point _fileLastModifiedTime; protected: const std::string& getWopiSrc() const { return _wopiSrc; } const std::unique_ptr& getWs() const { return _ws; } const std::string& getFileContent() const { return _fileContent; } /// Sets the file content to a given value and update the last file modified time void setFileContent(const std::string& fileContent) { LOG_TST("setFileContent: [" << fileContent << ']'); _fileContent = fileContent; _fileLastModifiedTime = std::chrono::system_clock::now(); } const std::chrono::system_clock::time_point& getFileLastModifiedTime() const { return _fileLastModifiedTime; } public: WopiTestServer(std::string testname, const std::string& fileContent = "Hello, world") : UnitWSD(std::move(testname)) { LOG_TST("WopiTestServer created for [" << testname << ']'); setFileContent(fileContent); } void initWebsocket(const std::string& wopiName) { Poco::URI wopiURL(helpers::getTestServerURI() + wopiName); _wopiSrc.clear(); Poco::URI::encode(wopiURL.toString(), ":/?", _wopiSrc); LOG_TST("Connecting to the fake WOPI server: /lool/" << _wopiSrc << "/ws"); _ws.reset(new UnitWebSocket("/lool/" + _wopiSrc + "/ws")); assert(_ws.get()); } virtual void assertCheckFileInfoRequest(const Poco::Net::HTTPRequest& /*request*/) { } virtual void assertGetFileRequest(const Poco::Net::HTTPRequest& /*request*/) { } virtual void assertPutFileRequest(const Poco::Net::HTTPRequest& /*request*/) { } virtual void assertPutRelativeFileRequest(const Poco::Net::HTTPRequest& /*request*/) { } virtual void assertRenameFileRequest(const Poco::Net::HTTPRequest& /*request*/) { } void configure(Poco::Util::LayeredConfiguration& config) override { UnitWSD::configure(config); // we're still internally confused as to https vs. http in places. config.setBool("storage.ssl.as_scheme", false); } protected: /// Here we act as a WOPI server, so that we have a server that responds to /// the wopi requests without additional expensive setup. virtual bool handleHttpRequest(const Poco::Net::HTTPRequest& request, Poco::MemoryInputStream& message, std::shared_ptr& socket) override { Poco::URI uriReq(request.getURI()); { std::ostringstream oss; oss << "Fake wopi host request URI [" << uriReq.toString() << "]:\n"; for (const auto& pair : request) { oss << '\t' << pair.first << ": " << pair.second << " / "; } LOG_TST(oss.str()); } static const Poco::RegularExpression regInfo("/wopi/files/[0-9]"); static const Poco::RegularExpression regContent("/wopi/files/[0-9]/contents"); // CheckFileInfo if (request.getMethod() == "GET" && regInfo.match(uriReq.getPath())) { LOG_TST("Fake wopi host request, handling CheckFileInfo: " << uriReq.getPath()); assertCheckFileInfoRequest(request); const std::string fileName(uriReq.getPath() == "/wopi/files/3" ? "he%llo.txt" : "hello.txt"); Poco::JSON::Object::Ptr fileInfo = new Poco::JSON::Object(); fileInfo->set("BaseFileName", fileName); fileInfo->set("Size", _fileContent.size()); fileInfo->set("Version", "1.0"); fileInfo->set("OwnerId", "test"); fileInfo->set("UserId", "test"); fileInfo->set("UserFriendlyName", "test"); fileInfo->set("UserCanWrite", "true"); fileInfo->set("PostMessageOrigin", "localhost"); fileInfo->set("LastModifiedTime", Util::getIso8601FracformatTime(_fileLastModifiedTime)); fileInfo->set("EnableOwnerTermination", "true"); std::ostringstream jsonStream; fileInfo->stringify(jsonStream); std::string responseString = jsonStream.str(); const std::string mimeType = "application/json; charset=utf-8"; std::ostringstream oss; oss << "HTTP/1.1 200 OK\r\n" "Last-Modified: " << Util::getHttpTime(_fileLastModifiedTime) << "\r\n" "User-Agent: " WOPI_AGENT_STRING "\r\n" "Content-Length: " << responseString.size() << "\r\n" "Content-Type: " << mimeType << "\r\n" "\r\n" << responseString; socket->send(oss.str()); socket->shutdown(); return true; } // GetFile else if (request.getMethod() == "GET" && regContent.match(uriReq.getPath())) { LOG_TST("Fake wopi host request, handling GetFile: " << uriReq.getPath()); assertGetFileRequest(request); const std::string mimeType = "text/plain; charset=utf-8"; std::ostringstream oss; oss << "HTTP/1.1 200 OK\r\n" "Last-Modified: " << Util::getHttpTime(_fileLastModifiedTime) << "\r\n" "User-Agent: " WOPI_AGENT_STRING "\r\n" "Content-Length: " << _fileContent.size() << "\r\n" "Content-Type: " << mimeType << "\r\n" "\r\n" << _fileContent; socket->send(oss.str()); socket->shutdown(); return true; } else if (request.getMethod() == "POST" && regInfo.match(uriReq.getPath())) { LOG_TST("Fake wopi host request, handling PutRelativeFile: " << uriReq.getPath()); std::string wopiURL = helpers::getTestServerURI() + "/something wopi/files/1?access_token=anything&reuse_cookies=cook=well"; std::string content; if(request.get("X-WOPI-Override") == std::string("PUT_RELATIVE")) { LOK_ASSERT_EQUAL(std::string("PUT_RELATIVE"), request.get("X-WOPI-Override")); assertPutRelativeFileRequest(request); content = "{ \"Name\":\"hello world%1.pdf\", \"Url\":\"" + wopiURL + "\" }"; } else { // rename file; response should be the file name without the url and the extension LOK_ASSERT_EQUAL(std::string("RENAME_FILE"), request.get("X-WOPI-Override")); assertRenameFileRequest(request); content = "{ \"Name\":\"hello\", \"Url\":\"" + wopiURL + "\" }"; } std::ostringstream oss; oss << "HTTP/1.1 200 OK\r\n" "Last-Modified: " << Util::getHttpTime(_fileLastModifiedTime) << "\r\n" "User-Agent: " WOPI_AGENT_STRING "\r\n" "Content-Length: " << content.size() << "\r\n" "Content-Type: application/json\r\n" "\r\n" << content; socket->send(oss.str()); socket->shutdown(); return true; } else if (request.getMethod() == "POST" && regContent.match(uriReq.getPath())) { LOG_TST("Fake wopi host request, handling PutFile: " << uriReq.getPath()); std::string wopiTimestamp = request.get("X-LOOL-WOPI-Timestamp"); if (!wopiTimestamp.empty()) { const std::string fileModifiedTime = Util::getIso8601FracformatTime(_fileLastModifiedTime); if (wopiTimestamp != fileModifiedTime) { std::ostringstream oss; oss << "HTTP/1.1 409 Conflict\r\n" "User-Agent: " WOPI_AGENT_STRING "\r\n" "\r\n" "{\"LOOLStatusCode\":" << static_cast(LOOLStatusCode::DocChanged) << '}'; socket->send(oss.str()); socket->shutdown(); return true; } } std::streamsize size = request.getContentLength(); char buffer[size]; message.read(buffer, size); setFileContent(std::string(buffer, size)); assertPutFileRequest(request); std::ostringstream oss; oss << "HTTP/1.1 200 OK\r\n" "User-Agent: " WOPI_AGENT_STRING "\r\n" "\r\n" "{\"LastModifiedTime\": \"" << Util::getIso8601FracformatTime(_fileLastModifiedTime) << "\" }"; socket->send(oss.str()); socket->shutdown(); return true; } else if (!Util::startsWith(uriReq.getPath(), "/lool/")) // Skip requests to the websrv. { // Complain if we are expected to handle something that we don't. LOG_TST("ERROR: Fake wopi host request, cannot handle request: " << uriReq.getPath()); } return false; } }; /// Send a command message to WSD from a WopiTestServer. #define WSD_CMD(MSG) \ do \ { \ LOG_TST(": Sending: " << MSG); \ helpers::sendTextFrame(*getWs()->getLOOLWebSocket(), MSG, getTestname()); \ } while (false) /* vim:set shiftwidth=4 softtabstop=4 expandtab: */