loleaflet/dist/errormessages.js | 1 loleaflet/src/control/Control.Dialog.js | 3 loleaflet/src/core/Socket.js | 49 +++++---- loleaflet/src/map/Map.js | 8 - loolwsd/LOOLWSD.cpp | 50 +++++++++- loolwsd/UserMessages.hpp | 7 + loolwsd/protocol.txt | 2 loolwsd/test/Makefile.am | 2 loolwsd/test/data/empty.odt |binary loolwsd/test/helpers.hpp | 28 +++++ loolwsd/test/httpwserror.cpp | 158 ++++++++++++++++++++++++++++++++ 11 files changed, 277 insertions(+), 31 deletions(-)
New commits: commit 21ab135ec4b96a948913c67d13e0f8d7f84895b7 Author: Henry Castro <hcas...@collabora.com> Date: Mon Oct 3 18:30:18 2016 -0400 loleaflet: fix show dialog error diff --git a/loleaflet/dist/errormessages.js b/loleaflet/dist/errormessages.js index 6fa628b..0ea91ff 100644 --- a/loleaflet/dist/errormessages.js +++ b/loleaflet/dist/errormessages.js @@ -1,2 +1,3 @@ var wrongwopisrc = _('Wrong WOPISrc, usage: WOPISrc=valid encoded URI, or file_path, usage: file_path=/path/to/doc/'); var emptyhosturl = _('The host URL is empty. The loolwsd server is probably misconfigured, please contact the administrator.'); +var limitreached = _('This development build is limited to %0 documents, and %1 connections - to avoid the impression that it is suitable for deployment in large enterprises. To find out more about deploy ing and scaling %2 checkout: <br/><a href=\"%3\">%3</a>.'); diff --git a/loleaflet/src/control/Control.Dialog.js b/loleaflet/src/control/Control.Dialog.js index 9417a8b..41836ae 100644 --- a/loleaflet/src/control/Control.Dialog.js +++ b/loleaflet/src/control/Control.Dialog.js @@ -10,7 +10,8 @@ L.Control.Dialog = L.Control.extend({ }, _onError: function (e) { - if (vex.dialogID > 0) { + if (vex.dialogID > 0 && !this._map._fatal) { + // TODO. queue message errors and pop-up dialogs // Close other dialogs before presenting a new one. vex.close(vex.dialogID); } diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js index abf9165..59ceb10 100644 --- a/loleaflet/src/core/Socket.js +++ b/loleaflet/src/core/Socket.js @@ -10,16 +10,16 @@ L.Socket = L.Class.extend({ this._map = map; try { this.socket = new WebSocket(map.options.server + '/lool/' + encodeURIComponent(map.options.doc) + '/ws'); + this.socket.onerror = L.bind(this._onSocketError, this); + this.socket.onclose = L.bind(this._onSocketClose, this); + this.socket.onopen = L.bind(this._onSocketOpen, this); + this.socket.onmessage = L.bind(this._onMessage, this); + this.socket.binaryType = 'arraybuffer'; } catch (e) { - this.fire('error', {msg: _('Oops, there is a problem connecting to LibreOffice Online : ' + e), cmd: 'socket', kind: 'failed', id: 3}); + this._map.fire('error', {msg: _('Oops, there is a problem connecting to LibreOffice Online : ' + e), cmd: 'socket', kind: 'failed', id: 3}); return null; } this._msgQueue = []; - this.socket.onerror = L.bind(this._onSocketError, map); - this.socket.onclose = L.bind(this._onSocketClose, map); - this.socket.onopen = L.bind(this._onOpen, this); - this.socket.onmessage = L.bind(this._onMessage, this); - this.socket.binaryType = 'arraybuffer'; }, close: function () { @@ -66,7 +66,7 @@ L.Socket = L.Class.extend({ this.socket.send(msg); }, - _onOpen: function () { + _onSocketOpen: function () { // Always send the protocol version number. // TODO: Move the version number somewhere sensible. this._doSend('loolclient ' + this.ProtocolVersionNumber); @@ -132,7 +132,7 @@ L.Socket = L.Class.extend({ // TODO: For now we expect perfect match in protocol versions if (loolwsdVersionObj.Protocol !== this.ProtocolVersionNumber) { - this.fire('error', {msg: _('Unsupported server version.')}); + this._map.fire('error', {msg: _('Unsupported server version.')}); } } else if (textMsg.startsWith('lokitversion ')) { @@ -184,11 +184,20 @@ L.Socket = L.Class.extend({ } } else if (textMsg.startsWith('error:') && !this._map._docLayer) { - this.fail = true; + textMsg = textMsg.substring(6); + if (command.errorKind === 'limitreached') { + this._map._fatal = true; + textMsg = limitreached; + textMsg = textMsg.replace(/%0/g, command.params[0]); + textMsg = textMsg.replace(/%1/g, command.params[1]); + textMsg = textMsg.replace(/%2/g, (typeof brandProductName !== 'undefined' ? brandProductName : 'LibreOffice Online')); + textMsg = textMsg.replace(/%3/g, (typeof brandProductURL !== 'undefined' ? brandProductURL : 'https://wiki.documentfoundation.org/Development/LibreOffice_Online')); + } + this._map.fire('error', {msg: textMsg}); } else if (textMsg.startsWith('statusindicator:')) { //FIXME: We should get statusindicator when saving too, no? - this._map.showBusy('Connecting...', false); + this._map.showBusy(_('Connecting...'), false); } else if (!textMsg.startsWith('tile:') && !textMsg.startsWith('renderfont:')) { // log the tile msg separately as we need the tile coordinates @@ -291,23 +300,20 @@ L.Socket = L.Class.extend({ }, _onSocketError: function () { - this.hideBusy(); + this._map.hideBusy(); // Let onclose (_onSocketClose) report errors. }, - _onSocketClose: function () { - this.hideBusy(); - if (this._map) { - this._map._active = false; - } + _onSocketClose: function (e) { + this._map.hideBusy(); + this._map._active = false; - if (this.fail) { - this.fire('error', {msg: _('Well, this is embarrassing, we cannot connect to your document. Please try again.'), cmd: 'socket', kind: 'closed', id: 4}); + if (e.code && e.reason) { + this._map.fire('error', {msg: e.reason}); } else { - this.fire('error', {msg: _('We are sorry, this is an unexpected connection error. Please try again.'), cmd: 'socket', kind: 'closed', id: 4}); + this._map.fire('error', {msg: _('Well, this is embarrassing, we cannot connect to your document. Please try again.'), cmd: 'socket', kind: 'closed', id: 4}); } - this.fail = false; }, parseServerCmd: function (msg) { @@ -382,6 +388,9 @@ L.Socket = L.Class.extend({ else if (tokens[i].substring(0, 5) === 'font=') { command.font = window.decodeURIComponent(tokens[i].substring(5)); } + else if (tokens[i].substring(0, 7) === 'params=') { + command.params = tokens[i].substring(7).split(','); + } } if (command.tileWidth && command.tileHeight && this._map._docLayer) { var defaultZoom = this._map.options.zoom; commit 8cd78ef851840e74e266a8e5f8c17c7cd645d171 Author: Henry Castro <hcas...@collabora.com> Date: Thu Sep 29 13:48:08 2016 -0400 loleaflet: fix undefined variables diff --git a/loleaflet/src/map/Map.js b/loleaflet/src/map/Map.js index ea3f06f..3bcc9a2 100644 --- a/loleaflet/src/map/Map.js +++ b/loleaflet/src/map/Map.js @@ -736,7 +736,7 @@ L.Map = L.Evented.extend({ // A dialog is already dimming the screen and probably // shows an error message. Leave it alone. this._active = false; - this._docLayer._onMessage('textselection:', null); + this._docLayer && this._docLayer._onMessage('textselection:', null); if (this._socket.connected()) { this._socket.sendMessage('userinactive'); } @@ -776,7 +776,7 @@ L.Map = L.Evented.extend({ $(options.appendLocation).append(options.$vex); vex.setupBodyClassName(options.$vex); - map._docLayer._onMessage('textselection:', null); + map._doclayer && map._docLayer._onMessage('textselection:', null); map._socket.sendMessage('userinactive'); }, 30 * 1000); // Dim in 30 seconds. @@ -784,7 +784,7 @@ L.Map = L.Evented.extend({ _onLostFocus: function () { var doclayer = this._docLayer; - if (doclayer._isCursorVisible && doclayer._isCursorOverlayVisible) { + if (doclayer && doclayer._isCursorVisible && doclayer._isCursorOverlayVisible) { doclayer._visibleCursorOnLostFocus = doclayer._visibleCursor; doclayer._isCursorOverlayVisibleOnLostFocus = doclayer._isCursorVisibleOnLostFocus = true; doclayer._isCursorOverlayVisible = false; @@ -796,7 +796,7 @@ L.Map = L.Evented.extend({ _onGotFocus: function () { var doclayer = this._docLayer; - if (doclayer._isCursorVisibleOnLostFocus && doclayer._isCursorOverlayVisibleOnLostFocus) { + if (doclayer && doclayer._isCursorVisibleOnLostFocus && doclayer._isCursorOverlayVisibleOnLostFocus) { // we restore the old cursor position by a small delay, so that if the user clicks // inside the document we skip to restore it, so that the user does not see the cursor // jumping from the old position to the new one commit 41605abcd28ff4abaca81ad64ce1071c0420d08c Author: Henry Castro <hcas...@collabora.com> Date: Mon Oct 3 16:57:59 2016 -0400 loolwsd: bccu#2022, User warning on hitting limit diff --git a/loolwsd/LOOLWSD.cpp b/loolwsd/LOOLWSD.cpp index 00e9471..94ae192 100644 --- a/loolwsd/LOOLWSD.cpp +++ b/loolwsd/LOOLWSD.cpp @@ -174,6 +174,48 @@ static std::map<std::string, std::shared_ptr<MasterProcessSession>> AvailableChi static int careerSpanSeconds = 0; #endif +namespace { + +static inline +void lcl_shutdownLimitReached(WebSocket& ws) +{ + const std::string error = Poco::format(PAYLOAD_UNAVALABLE_LIMIT_REACHED, MAX_DOCUMENTS, MAX_CONNECTIONS); + const std::string close = Poco::format(SERVICE_UNAVALABLE_LIMIT_REACHED, static_cast<int>(WebSocket::WS_POLICY_VIOLATION)); + + /* loleaflet sends loolclient, load and partrectangles message immediately + after web socket handshake, so closing web socket fails loading page in + some sensible browsers. Ignore handshake messages and gracefully + close in order to send error messages. + */ + try + { + int flags = 0; + int retries = 7; + std::vector<char> buffer(READ_BUFFER_SIZE * 100); + + // 5 seconds timeout + ws.setReceiveTimeout(5000000); + do + { + // ignore loolclient, load and partpagerectangles + ws.receiveFrame(buffer.data(), buffer.capacity(), flags); + if (--retries == 4) + { + ws.sendFrame(error.data(), error.size()); + ws.shutdown(WebSocket::WS_POLICY_VIOLATION, close); + } + } + while (retries > 0 && (flags & WebSocket::FRAME_OP_BITMASK) != WebSocket::FRAME_OP_CLOSE); + } + catch (Exception&) + { + ws.sendFrame(error.data(), error.size()); + ws.shutdown(WebSocket::WS_POLICY_VIOLATION, close); + } +} + +} + static void forkChildren(const int number) { Util::assertIsLocked(newChildrenMutex); @@ -620,7 +662,8 @@ private: { --LOOLWSD::NumDocBrokers; Log::error("Maximum number of open documents reached."); - throw WebSocketErrorMessageException(SERVICE_UNAVALABLE_LIMIT_REACHED); + lcl_shutdownLimitReached(*ws); + return; } #endif @@ -841,7 +884,10 @@ public: { --LOOLWSD::NumConnections; Log::error("Maximum number of connections reached."); - throw WebSocketErrorMessageException(SERVICE_UNAVALABLE_LIMIT_REACHED); + // accept hand shake + WebSocket ws(request, response); + lcl_shutdownLimitReached(ws); + return; } #endif diff --git a/loolwsd/UserMessages.hpp b/loolwsd/UserMessages.hpp index 5208b09..36e6ad3 100644 --- a/loolwsd/UserMessages.hpp +++ b/loolwsd/UserMessages.hpp @@ -12,8 +12,11 @@ #ifndef INCLUDED_USERMESSAGES_HPP #define INCLUDED_USERMESSAGES_HPP -constexpr auto SERVICE_UNAVALABLE_INTERNAL_ERROR = "Service is unavailable. Please try again later and report to your administrator if the issue persists."; -constexpr auto SERVICE_UNAVALABLE_LIMIT_REACHED = "This server has reached the number of connections or documents it supports at a given time."; +//NOTE: For whatever reason Poco seems to trim the first character. + +constexpr auto SERVICE_UNAVALABLE_INTERNAL_ERROR = " Service is unavailable. Please try again later and report to your administrator if the issue persists."; +constexpr auto SERVICE_UNAVALABLE_LIMIT_REACHED = "error: cmd=socket kind=close code=%d"; +constexpr auto PAYLOAD_UNAVALABLE_LIMIT_REACHED = "error: cmd=socket kind=limitreached params=%d,%d"; #endif diff --git a/loolwsd/protocol.txt b/loolwsd/protocol.txt index 82819ea..a3b1c82 100644 --- a/loolwsd/protocol.txt +++ b/loolwsd/protocol.txt @@ -220,7 +220,7 @@ editlock: <1 or 0> Note that only one client can have the editlock at a time and others can only view. -error: cmd=<command> kind=<kind> [code=<error_code>] +error: cmd=<command> kind=<kind> [code=<error_code>] [params=1,2,3,...,N] <freeErrorText> <command> is the command part of the corresponding client->server diff --git a/loolwsd/test/Makefile.am b/loolwsd/test/Makefile.am index 9ce7c17..4f28533 100644 --- a/loolwsd/test/Makefile.am +++ b/loolwsd/test/Makefile.am @@ -28,7 +28,7 @@ wsd_sources = \ test_CPPFLAGS = -DTDOC=\"$(top_srcdir)/test/data\" -I$(top_srcdir) test_SOURCES = TileCacheTests.cpp WhiteBoxTests.cpp integration-http-server.cpp \ - httpwstest.cpp httpcrashtest.cpp test.cpp $(wsd_sources) + httpwstest.cpp httpcrashtest.cpp httpwserror.cpp test.cpp $(wsd_sources) test_LDADD = $(CPPUNIT_LIBS) # unit test modules: diff --git a/loolwsd/test/data/empty.odt b/loolwsd/test/data/empty.odt new file mode 100755 index 0000000..6b07475 Binary files /dev/null and b/loolwsd/test/data/empty.odt differ diff --git a/loolwsd/test/helpers.hpp b/loolwsd/test/helpers.hpp index e676ff1..7e570d3 100644 --- a/loolwsd/test/helpers.hpp +++ b/loolwsd/test/helpers.hpp @@ -15,6 +15,7 @@ #include <thread> #include <regex> +#include <Poco/BinaryReader.h> #include <Poco/DirectoryIterator.h> #include <Poco/Dynamic/Var.h> #include <Poco/FileStream.h> @@ -156,6 +157,33 @@ std::string getTestServerURI() } inline +int getErrorCode(Poco::Net::WebSocket& ws, std::string& message) +{ + int flags = 0; + int bytes = 0; + Poco::UInt16 statusCode = -1; + Poco::Buffer<char> buffer(READ_BUFFER_SIZE); + + message.clear(); + Poco::Timespan timeout(5000000); + ws.setReceiveTimeout(timeout); + do + { + bytes = ws.receiveFrame(buffer.begin(), READ_BUFFER_SIZE, flags); + } + while ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) != Poco::Net::WebSocket::FRAME_OP_CLOSE); + + if (bytes > 0) + { + Poco::MemoryBinaryReader reader(buffer, Poco::BinaryReader::NETWORK_BYTE_ORDER); + reader >> statusCode; + message.append(buffer.begin() + 2, bytes - 2); + } + + return statusCode; +} + +inline void getResponseMessage(Poco::Net::WebSocket& ws, const std::string& prefix, std::string& response, const bool isLine) { try diff --git a/loolwsd/test/httpwserror.cpp b/loolwsd/test/httpwserror.cpp new file mode 100644 index 0000000..8e7769d --- /dev/null +++ b/loolwsd/test/httpwserror.cpp @@ -0,0 +1,158 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * 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 "config.h" + +#include <vector> +#include <string> + +#include <Poco/Net/HTTPClientSession.h> +#include <Poco/Net/HTTPRequest.h> +#include <Poco/Net/HTTPResponse.h> +#include <Poco/Net/HTTPSClientSession.h> +#include <Poco/Net/NetException.h> +#include <Poco/Net/WebSocket.h> +#include <Poco/URI.h> + +#include <cppunit/extensions/HelperMacros.h> + +#include "Common.hpp" +#include "LOOLProtocol.hpp" +#include "helpers.hpp" + +using namespace helpers; + +class HTTPWSError : public CPPUNIT_NS::TestFixture +{ + const Poco::URI _uri; + Poco::Net::HTTPResponse _response; + + CPPUNIT_TEST_SUITE(HTTPWSError); + + CPPUNIT_TEST(testMaxDocuments); + CPPUNIT_TEST(testMaxConnections); + + CPPUNIT_TEST_SUITE_END(); + + void testMaxDocuments(); + void testMaxConnections(); + +public: + HTTPWSError() + : _uri(helpers::getTestServerURI()) + { +#if ENABLE_SSL + Poco::Net::initializeSSL(); + // Just accept the certificate anyway for testing purposes + Poco::SharedPtr<Poco::Net::InvalidCertificateHandler> invalidCertHandler = new Poco::Net::AcceptCertificateHandler(false); + Poco::Net::Context::Params sslParams; + Poco::Net::Context::Ptr sslContext = new Poco::Net::Context(Poco::Net::Context::CLIENT_USE, sslParams); + Poco::Net::SSLManager::instance().initializeClient(0, invalidCertHandler, sslContext); +#endif + } + +#if ENABLE_SSL + ~HTTPWSError() + { + Poco::Net::uninitializeSSL(); + } +#endif + + void setUp() + { + } + + void tearDown() + { + } +}; + +void HTTPWSError::testMaxDocuments() +{ +#if MAX_DOCUMENTS > 0 + try + { + // Load a document. + std::string docPath; + std::string docURL; + std::string message; + Poco::UInt16 statusCode; + std::vector<std::shared_ptr<Poco::Net::WebSocket>> docs; + + for(int it = 1; it <= MAX_DOCUMENTS; it++) + { + getDocumentPathAndURL("empty.odt", docPath, docURL); + Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, docURL); + docs.emplace_back(connectLOKit(_uri, request, _response)); + } + + // try to open MAX_DOCUMENTS + 1 + getDocumentPathAndURL("empty.odt", docPath, docURL); + Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, docURL); + std::unique_ptr<Poco::Net::HTTPClientSession> session(helpers::createSession(_uri)); + Poco::Net::WebSocket socket(*session, request, _response); + // send loolclient, load and partpagerectangles + sendTextFrame(socket, "loolclient "); + sendTextFrame(socket, "load "); + sendTextFrame(socket, "partpagerectangles "); + statusCode = getErrorCode(socket, message); + CPPUNIT_ASSERT_EQUAL(static_cast<Poco::UInt16>(Poco::Net::WebSocket::WS_POLICY_VIOLATION), statusCode); + CPPUNIT_ASSERT_MESSAGE("Wrong error message ", message.find("error: cmd=socket kind=close") != std::string::npos); + } + catch (const Poco::Exception& exc) + { + CPPUNIT_FAIL(exc.displayText()); + } +#endif +} + +void HTTPWSError::testMaxConnections() +{ +#if MAX_CONNECTIONS > 0 + try + { + // Load a document. + std::string docPath; + std::string docURL; + std::string message; + Poco::UInt16 statusCode; + std::vector<std::shared_ptr<Poco::Net::WebSocket>> views; + + getDocumentPathAndURL("empty.odt", docPath, docURL); + Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, docURL); + auto socket = loadDocAndGetSocket(_uri, docURL, "testMaxConnections "); + + for(int it = 1; it < MAX_CONNECTIONS; it++) + { + std::unique_ptr<Poco::Net::HTTPClientSession> session(createSession(_uri)); + auto ws = std::make_shared<Poco::Net::WebSocket>(*session, request, _response); + views.emplace_back(ws); + } + + // try to connect MAX_CONNECTIONS + 1 + std::unique_ptr<Poco::Net::HTTPClientSession> session(createSession(_uri)); + auto socketN = std::make_shared<Poco::Net::WebSocket>(*session, request, _response); + // send loolclient, load and partpagerectangles + sendTextFrame(socketN, "loolclient "); + sendTextFrame(socketN, "load "); + sendTextFrame(socketN, "partpagerectangles "); + statusCode = getErrorCode(*socketN, message); + CPPUNIT_ASSERT_EQUAL(static_cast<Poco::UInt16>(Poco::Net::WebSocket::WS_POLICY_VIOLATION), statusCode); + CPPUNIT_ASSERT_MESSAGE("Wrong error message ", message.find("error: cmd=socket kind=close") != std::string::npos); + } + catch (const Poco::Exception& exc) + { + CPPUNIT_FAIL(exc.displayText()); + } +#endif +} + +CPPUNIT_TEST_SUITE_REGISTRATION(HTTPWSError); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ _______________________________________________ Libreoffice-commits mailing list libreoffice-comm...@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits