[Libreoffice-commits] online.git: Branch 'feature/proxyhack' - 85 commits - android/app android/build.gradle android/build.gradle.in android/.gitignore android/gradle android/lib android/Makefile.am android/README common/FileUtil.cpp common/FileUtil.hpp configure.ac cypress_test/data cypress_test/integration_tests fuzzer/admin-data .gitignore ios/config.h.in kit/ChildSession.cpp kit/ChildSession.hpp kit/Kit.cpp kit/Kit.hpp loleaflet/css loleaflet/html loleaflet/images loleaflet/js loleaflet/Makefile.am loleaflet/po loleaflet/src loleaflet/util loolwsd.xml.in Makefile.am net/Socket.cpp net/Socket.hpp net/SslSocket.hpp test/WhiteBoxTests.cpp wsd/Admin.cpp wsd/AdminModel.cpp wsd/ClientSession.hpp wsd/DocumentBroker.hpp wsd/FileServer.cpp wsd/LOOLWSD.cpp wsd/LOOLWSD.hpp wsd/ProofKey.cpp wsd/ProxyProtocol.cpp wsd/ProxyProtocol.hpp wsd/Storage.cpp

Michael Meeks (via logerrit) logerrit at kemper.freedesktop.org
Thu Apr 23 12:44:24 UTC 2020


Rebased ref, commits from common ancestor:
commit 931fd9241cc45f6650bdad1ae3234ae370faef2a
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Thu Apr 23 13:39:33 2020 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proof: inject ProxyPrefix into discovery xml.
    
    Change-Id: Ief79372d17e830c10883805e430a8b8cfb6d4f31

diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 78bbf62d7..6e06b87d7 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2465,8 +2465,17 @@ private:
         LOG_DBG("Wopi discovery request: " << request.getURI());
 
         std::string xml = getFileContent("discovery.xml");
-        const std::string hostname = (LOOLWSD::ServerName.empty() ? request.getHost() : LOOLWSD::ServerName);
-        Poco::replaceInPlace(xml, std::string("%SERVER_HOST%"), hostname);
+        std::string srvUrl =
+#if ENABLE_SSL
+            ((LOOLWSD::isSSLEnabled() || LOOLWSD::isSSLTermination()) ? "https://" : "http://")
+#else
+            "http://"
+#endif
+            + (LOOLWSD::ServerName.empty() ? request.getHost() : LOOLWSD::ServerName)
+            + LOOLWSD::ServiceRoot;
+        if (request.has("ProxyPrefix"))
+            srvUrl += request["ProxyPrefix"];
+        Poco::replaceInPlace(xml, std::string("%SRV_URI%"), srvUrl);
 
         // TODO: Refactor this to some common handler.
         std::ostringstream oss;
@@ -3146,20 +3155,14 @@ private:
             discoveryPath = LOOLWSD::FileServerRoot + "/discovery.xml";
         }
 
-        const std::string action = "action";
-        const std::string urlsrc = "urlsrc";
         const auto& config = Application::instance().config();
         const std::string loleafletHtml = config.getString("loleaflet_html", "loleaflet.html");
-        const std::string rootUriValue =
-#if ENABLE_SSL
-            ((LOOLWSD::isSSLEnabled() || LOOLWSD::isSSLTermination()) ? "https://" : "http://")
-#else
-            "http://"
-#endif
-            + std::string("%SERVER_HOST%")
-            + LOOLWSD::ServiceRoot;
-        const std::string uriValue = rootUriValue
-                                   + "/loleaflet/" LOOLWSD_VERSION_HASH "/" + loleafletHtml + '?';
+
+        const std::string action = "action";
+        const std::string urlsrc = "urlsrc";
+
+        const std::string rootUriValue = "%SRV_URI%";
+        const std::string uriValue = rootUriValue + "/loleaflet/" LOOLWSD_VERSION_HASH "/" + loleafletHtml + '?';
 
         InputSource inputSrc(discoveryPath);
         DOMParser parser;
commit 5566eb8ac5d9349e4f12afbee24f54332831b3ca
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Apr 22 21:07:10 2020 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: detect isWaiting properly now we have requests serials.
    
    Change-Id: I76b2602192f1a49df73f4354bb26209fe2ed6d5d

diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 8235d9dc0..78bbf62d7 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2917,8 +2917,7 @@ private:
 
         std::string fullURL = request.getURI();
         std::string ending = "/ws/wait";
-        bool isWaiting = (fullURL.size() > ending.size() &&
-                          std::equal(ending.rbegin(), ending.rend(), fullURL.rbegin()));
+        bool isWaiting = fullURL.find(ending) != std::string::npos;
         if (docBroker)
         {
             // need to move into the DocumentBroker context before doing session lookup / creation etc.
commit 4af363510e83a438d6ac758d29c320ef1e37eb20
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Mon Apr 20 15:23:45 2020 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: mend JS quoting.
    
    Change-Id: I9da2b2e58891acb9fb5947cc4d5dde79bda394da

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 77cf7c7aa..1e09a86e0 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -273,7 +273,7 @@
 					data = arr.slice(i, i + size);
 
 				if (serial !== that.inSerial + 1) {
-					console.debug("Error: serial mismatch " + serial + " vs. " + (that.inSerial + 1));
+					console.debug('Error: serial mismatch ' + serial + ' vs. ' + (that.inSerial + 1));
 				}
 				that.inSerial = serial;
 				this.onmessage({ data: data });
commit 8fd8685869e2de315df55027d95c7a5a5f38f9ab
Author:     Jan Holesovsky <kendy at collabora.com>
AuthorDate: Mon Apr 20 16:22:37 2020 +0200
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Fix build with clang.
    
    Change-Id: I64126238c64069000a11ad9b219d3f72ab7b3719

diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
index 30ef2dacf..7bca66600 100644
--- a/wsd/ProxyProtocol.hpp
+++ b/wsd/ProxyProtocol.hpp
@@ -53,13 +53,13 @@ public:
 
 public:
     /// Clear all external references
-    virtual void dispose() { _msgHandler.reset(); }
+    void dispose() override { _msgHandler.reset(); }
 
     int sendTextMessage(const char *msg, const size_t len, bool flush = false) const override;
     int sendBinaryMessage(const char *data, const size_t len, bool flush = false) const override;
     void shutdown(bool goingAway = false, const std::string &statusMessage = "") override;
     void getIOStats(uint64_t &sent, uint64_t &recv) override;
-    void dumpState(std::ostream& os);
+    void dumpState(std::ostream& os) override;
     bool parseEmitIncoming(const std::shared_ptr<StreamSocket> &socket);
     void handleRequest(bool isWaiting, const std::shared_ptr<Socket> &socket);
 
commit 7e885ab47cb57967b1fd85708542ada6858d5274
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Apr 18 21:24:41 2020 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: adapt the path with a serial for each request.
    
    Helps to debug, and avoids the webserver getting unhappy.
    
    Change-Id: I632550f2ad26eef973cd036422f9f1a73d0f0764

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 4580b4740..77cf7c7aa 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -334,7 +334,7 @@
 		};
 		this.getEndPoint = function(type) {
 			var base = this.uri;
-			return base.replace(/^ws/, 'http') + '/' + type;
+			return base.replace(/^ws/, 'http') + '/' + type + '/' + this.outSerial;
 		};
 		console.debug('proxy: new socket ' + this.id + ' ' + this.uri);
 
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index cb3c020c2..8235d9dc0 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2334,8 +2334,8 @@ private:
 //                    Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ...
                     handleClipboardRequest(request, message, disposition);
                 }
-                else if (request.has("ProxyPrefix") && reqPathTokens.count() > 2 &&
-                         (reqPathTokens[reqPathTokens.count()-2] == "ws"))
+                else if (request.has("ProxyPrefix") && reqPathTokens.count() > 3 &&
+                         (reqPathTokens[reqPathTokens.count()-3] == "ws"))
                 {
                     std::string decodedUri; // WOPISrc
                     Poco::URI::decode(reqPathTokens[1], decodedUri);
commit 7334aea9cba52754f7fd8f3b081a16a89848c7ae
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Apr 18 18:40:59 2020 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: marshal message serial too.
    
    Change-Id: I23a28fe052062a0b98bbb2828b71ab8de6f1459c

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index cc1ab2090..4580b4740 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -208,6 +208,8 @@
 		this.id = window.proxySocketCounter++;
 		this.sendCounter = 0;
 		this.readWaiting = 0;
+		this.inSerial = 0;
+		this.outSerial = 0;
 		this.onclose = function() {
 		};
 		this.onerror = function() {
@@ -231,17 +233,35 @@
 					console.debug('wrong data type: ' + type);
 					break;
 				}
-				if (arr[i+1] !== 48 && arr[i+2] !== 120) // '0x'
+				i++;
+
+				// Serial
+				if (arr[i] !== 48 && arr[i+1] !== 120) // '0x'
 				{
 					console.debug('missing hex preamble');
 					break;
 				}
-				i += 3;
+				i += 2;
 				var numStr = '';
 				var start = i;
 				while (arr[i] != 10) // '\n'
 					i++;
 				numStr = decoder.decode(arr.slice(start, i)); // FIXME: IE11
+				var serial = parseInt(numStr, 16);
+
+				i++; // skip \n
+
+				// Size:
+				if (arr[i] !== 48 && arr[i+1] !== 120) // '0x'
+				{
+					console.debug('missing hex preamble');
+					break;
+				}
+				i += 2;
+				start = i;
+				while (arr[i] != 10) // '\n'
+					i++;
+				numStr = decoder.decode(arr.slice(start, i)); // FIXME: IE11
 				var size = parseInt(numStr, 16);
 
 				i++; // skip \n
@@ -252,6 +272,10 @@
 				else
 					data = arr.slice(i, i + size);
 
+				if (serial !== that.inSerial + 1) {
+					console.debug("Error: serial mismatch " + serial + " vs. " + (that.inSerial + 1));
+				}
+				that.inSerial = serial;
 				this.onmessage({ data: data });
 
 				i += size; // skip trailing '\n' in loop-increment
@@ -295,7 +319,9 @@
 		};
 		this.send = function(msg) {
 			this.sendQueue = this.sendQueue.concat(
-				'B0x' + msg.length.toString(16) + '\n' + msg + '\n');
+				'B0x' + this.outSerial.toString(16) + '\n' +
+				'0x' + msg.length.toString(16) + '\n' + msg + '\n');
+			this.outSerial++;
 			if (this.sessionId !== 'fetchsession' && this.sendTimeout === undefined)
 				this.sendTimeout = setTimeout(this.doSend, 2 /* ms */);
 		};
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index c8a259abe..973d9f3c5 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -97,21 +97,37 @@ bool ProxyProtocolHandler::parseEmitIncoming(
 
     while (in.size() > 0)
     {
-        if (in[0] != 'T' && in[0] != 'B')
+        // Type
+        if ((in[0] != 'T' && in[0] != 'B') || in.size() < 2)
         {
             LOG_ERR("Invalid message type " << in[0]);
             return false;
         }
         auto it = in.begin() + 1;
+
+        // Serial
         for (; it != in.end() && *it != '\n'; ++it);
         *it = '\0';
-        uint64_t len = strtoll( &in[1], nullptr, 16 );
+        uint64_t serial = strtoll( &in[1], nullptr, 16 );
+        in.erase(in.begin(), it + 1);
+        if (in.size() < 2)
+        {
+            LOG_ERR("Invalid message framing size " << in.size());
+            return false;
+        }
+
+        // Length
+        it = in.begin();
+        for (; it != in.end() && *it != '\n'; ++it);
+        *it = '\0';
+        uint64_t len = strtoll( &in[0], nullptr, 16 );
         in.erase(in.begin(), it + 1);
         if (len > in.size())
         {
             LOG_ERR("Invalid message length " << len << " vs " << in.size());
             return false;
         }
+
         // far from efficient:
         std::vector<char> data;
         data.insert(data.begin(), in.begin(), in.begin() + len + 1);
@@ -124,6 +140,9 @@ bool ProxyProtocolHandler::parseEmitIncoming(
         }
         in.erase(in.begin(), in.begin() + 1);
 
+        if (serial != _inSerial + 1)
+            LOG_ERR("Serial mismatch " << serial << " vs. " << (_inSerial + 1));
+        _inSerial = serial;
         _msgHandler->handleMessage(data);
     }
     return true;
@@ -180,7 +199,7 @@ void ProxyProtocolHandler::handleIncomingMessage(SocketDisposition &disposition)
 
 int ProxyProtocolHandler::sendMessage(const char *msg, const size_t len, bool text, bool flush)
 {
-    _writeQueue.push_back(std::make_shared<Message>(msg, len, text));
+    _writeQueue.push_back(std::make_shared<Message>(msg, len, text, _outSerial++));
     if (flush)
     {
         auto sock = popOutSocket();
diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
index 692fd903c..30ef2dacf 100644
--- a/wsd/ProxyProtocol.hpp
+++ b/wsd/ProxyProtocol.hpp
@@ -16,12 +16,16 @@
  * Implementation that builds a websocket like protocol from many
  * individual proxied HTTP requests back to back.
  *
- * we use a trivial framing: <hex-length>\r\n<content>\r\n
+ * we use a trivial framing: [T(ext)|B(inary)]<hex-serial->\n<hex-length>\n<content>\n
  */
 class ProxyProtocolHandler : public ProtocolHandlerInterface
 {
 public:
-    ProxyProtocolHandler() { }
+    ProxyProtocolHandler() :
+        _inSerial(0),
+        _outSerial(0)
+    {
+    }
 
     virtual ~ProxyProtocolHandler() { }
 
@@ -67,12 +71,12 @@ private:
 
     struct Message : public std::vector<char>
     {
-        Message(const char *msg, const size_t len, bool text)
+        Message(const char *msg, const size_t len, bool text, uint64_t serial)
         {
             const char *type = text ? "T" : "B";
             insert(end(), type, type + 1);
             std::ostringstream os;
-            os << std::hex << "0x" << len << "\n";
+            os << std::hex << "0x" << serial << "\n" << "0x" << len << "\n";
             std::string str = os.str();
             insert(end(), str.c_str(), str.c_str() + str.size());
             insert(end(), msg, msg + len);
@@ -83,6 +87,8 @@ private:
     /// queue things when we have no socket to hand.
     std::vector<std::shared_ptr<Message>> _writeQueue;
     std::vector<std::weak_ptr<StreamSocket>> _outSockets;
+    uint64_t _inSerial;
+    uint64_t _outSerial;
 };
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
commit 96645c86fe05a136688548850e02b4cbeb9dc650
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Tue Apr 14 17:01:41 2020 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: improve debugging and connection handling.
    
    Change-Id: I1d48c4ec7fb80eaab1aabc83b0c210b7cf138ef2

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index aacf59496..cc1ab2090 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -260,7 +260,6 @@
 		this.sendQueue = '';
 		this.sendTimeout = undefined;
 		this.doSend = function () {
-			that.sendTimeout = undefined;
 			console.debug('send msg "' + that.sendQueue + '"');
 			var req = new XMLHttpRequest();
 			req.open('POST', that.getEndPoint('write'));
@@ -279,6 +278,7 @@
 			}
 			req.send(that.sendQueue);
 			that.sendQueue = '';
+			that.sendTimeout = undefined;
 		};
 		this.getSessionId = function() {
 			var req = new XMLHttpRequest();
@@ -318,7 +318,7 @@
 		// horrors ...
 		this.waitConnect = function() {
 			console.debug('proxy: waiting - ' + that.readWaiting + ' on session ' + that.sessionId);
-			if (that.readWaiting > 4) // max 4 waiting connections concurrently.
+			if (that.readWaiting >= 4) // max 4 waiting connections concurrently.
 				return;
 			if (that.sessionId == 'fetchsession')
 				return; // waiting for our session id.
diff --git a/net/Socket.cpp b/net/Socket.cpp
index 24041bc4b..0b7a2f2ff 100644
--- a/net/Socket.cpp
+++ b/net/Socket.cpp
@@ -219,7 +219,6 @@ int SocketPoll::poll(int64_t timeoutMaxMicroS)
         timeout.tv_sec = timeoutMaxMicroS / (1000 * 1000);
         timeout.tv_nsec = (timeoutMaxMicroS % (1000 * 1000)) * 1000;
         rc = ::ppoll(&_pollFds[0], size + 1, &timeout, nullptr);
-        LOG_TRC("ppoll result " << rc << " errno " << strerror(errno));
 #  else
         int timeoutMaxMs = (timeoutMaxMicroS + 9999) / 1000;
         LOG_TRC("Legacy Poll start, timeoutMs: " << timeoutMaxMs);
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 3ed43e979..c8a259abe 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -219,6 +219,13 @@ void ProxyProtocolHandler::getIOStats(uint64_t &sent, uint64_t &recv)
 void ProxyProtocolHandler::dumpState(std::ostream& os)
 {
     os << "proxy protocol sockets: " << _outSockets.size() << " writeQueue: " << _writeQueue.size() << ":\n";
+    os << "\t";
+    for (auto &it : _outSockets)
+    {
+        auto sock = it.lock();
+        os << "#" << (sock ? sock->getFD() : -2) << " ";
+    }
+    os << "\n";
     for (auto it : _writeQueue)
         Util::dumpHex(os, "\twrite queue entry:", "\t\t", *it);
     if (_msgHandler)
commit 5371c303230fd5092a053b8ba7805b45e7b2d66d
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Mar 21 20:03:37 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: don't leave out sockets lingering around for the !flush case.
    
    Change-Id: I13ad123a6c3a068a676eae5e509367e727e9ac06

diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 89b3d9ba9..3ed43e979 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -181,11 +181,14 @@ void ProxyProtocolHandler::handleIncomingMessage(SocketDisposition &disposition)
 int ProxyProtocolHandler::sendMessage(const char *msg, const size_t len, bool text, bool flush)
 {
     _writeQueue.push_back(std::make_shared<Message>(msg, len, text));
-    auto sock = popOutSocket();
-    if (sock && flush)
+    if (flush)
     {
-        flushQueueTo(sock);
-        sock->shutdown();
+        auto sock = popOutSocket();
+        if (sock)
+        {
+            flushQueueTo(sock);
+            sock->shutdown();
+        }
     }
 
     return len;
commit 589d691dac2171bf768a1edb141f80da3d30f5f5
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Mar 21 15:07:10 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: improve debugging & naming.
    
    Change-Id: Ifba669a33855a67c9a4e968db42ef1a2cb301d63

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index c711c84b0..aacf59496 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -216,7 +216,7 @@
 		};
 		this.parseIncomingArray = function(arr) {
 			var decoder = new TextDecoder();
-			console.debug('Parse incoming array of length ' + arr.length);
+			console.debug('proxy: parse incoming array of length ' + arr.length);
 			for (var i = 0; i < arr.length; ++i)
 			{
 				var left = arr.length - i;
@@ -274,7 +274,7 @@
 					if (this.status == 200)
 						that.parseIncomingArray(new Uint8Array(this.response));
 					else
-						console.debug('Error on incoming response');
+						console.debug('proxy: error on incoming response');
 				});
 			}
 			req.send(that.sendQueue);
@@ -300,21 +300,24 @@
 				this.sendTimeout = setTimeout(this.doSend, 2 /* ms */);
 		};
 		this.close = function() {
-			console.debug('close socket');
+			console.debug('proxy: close socket');
 			this.readyState = 3;
 			this.onclose();
+			clearInterval(this.waitInterval);
+			this.waitInterval = undefined;
 		};
 		this.getEndPoint = function(type) {
 			var base = this.uri;
 			return base.replace(/^ws/, 'http') + '/' + type;
 		};
-		console.debug('New proxy socket ' + this.id + ' ' + this.uri);
+		console.debug('proxy: new socket ' + this.id + ' ' + this.uri);
 
 		// queue fetch of session id.
 		this.getSessionId();
 
 		// horrors ...
-		this.readInterval = setInterval(function() {
+		this.waitConnect = function() {
+			console.debug('proxy: waiting - ' + that.readWaiting + ' on session ' + that.sessionId);
 			if (that.readWaiting > 4) // max 4 waiting connections concurrently.
 				return;
 			if (that.sessionId == 'fetchsession')
@@ -329,13 +332,16 @@
 			});
 			req.addEventListener('loadend', function() {
 				that.readWaiting--;
+				console.debug('proxy: wait ended, re-issue');
+				that.waitConnect();
 			});
-			req.open('GET', that.getEndPoint('read'));
+			req.open('GET', that.getEndPoint('wait'));
 			req.setRequestHeader('SessionId', that.sessionId);
 			req.responseType = 'arraybuffer';
 			req.send('');
 			that.readWaiting++;
-		}, 250);
+		};
+		this.waitInterval = setInterval(this.waitConnect, 250);
 	};
 
 	if (global.socketProxy)
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 3c5d22677..cb3c020c2 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2916,7 +2916,7 @@ private:
             none, url, docKey, _id, uriPublic);
 
         std::string fullURL = request.getURI();
-        std::string ending = "/ws/read";
+        std::string ending = "/ws/wait";
         bool isWaiting = (fullURL.size() > ending.size() &&
                           std::equal(ending.rbegin(), ending.rend(), fullURL.rbegin()));
         if (docBroker)
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index c8f578559..89b3d9ba9 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -31,7 +31,7 @@ void DocumentBroker::handleProxyRequest(
     std::shared_ptr<ClientSession> clientSession;
     if (sessionId == "fetchsession")
     {
-        LOG_TRC("Create session for " << _docKey);
+        LOG_TRC("proxy: Create session for " << _docKey);
         clientSession = createNewClientSession(
                 std::make_shared<ProxyProtocolHandler>(),
                 id, uriPublic, isReadOnly, hostNoTrust);
@@ -39,7 +39,7 @@ void DocumentBroker::handleProxyRequest(
         LOOLWSD::checkDiskSpaceAndWarnClients(true);
         LOOLWSD::checkSessionLimitsAndWarnClients();
 
-        LOG_TRC("Returning id " << clientSession->getId());
+        LOG_TRC("proxy: Returning sessionId " << clientSession->getId());
 
         std::ostringstream oss;
         oss << "HTTP/1.1 200 OK\r\n"
@@ -57,7 +57,7 @@ void DocumentBroker::handleProxyRequest(
     }
     else
     {
-        LOG_TRC("Find session for " << _docKey << " with id " << sessionId);
+        LOG_TRC("proxy: find session for " << _docKey << " with id " << sessionId);
         for (const auto &it : _sessions)
         {
             if (it.second->getId() == sessionId)
@@ -133,28 +133,29 @@ void ProxyProtocolHandler::handleRequest(bool isWaiting, const std::shared_ptr<S
 {
     auto streamSocket = std::static_pointer_cast<StreamSocket>(socket);
 
-    LOG_INF("Proxy handle request type: " << (isWaiting ? "wait" : "respond"));
+    LOG_INF("proxy: handle request type: " << (isWaiting ? "wait" : "respond") <<
+            " on socket #" << socket->getFD());
 
     if (!isWaiting)
     {
         if (!_msgHandler)
-            LOG_WRN("unusual - incoming message with no-one to handle it");
+            LOG_WRN("proxy: unusual - incoming message with no-one to handle it");
         else if (!parseEmitIncoming(streamSocket))
         {
             std::stringstream oss;
             streamSocket->dumpState(oss);
-            LOG_ERR("bad socket structure " << oss.str());
+            LOG_ERR("proxy: bad socket structure " << oss.str());
         }
     }
 
     if (!flushQueueTo(streamSocket) && isWaiting)
     {
-        LOG_TRC("Queue a waiting socket");
+        LOG_TRC("proxy: queue a waiting out socket #" << streamSocket->getFD());
         // longer running 'write socket' (marked 'read' by the client)
         _outSockets.push_back(streamSocket);
         if (_outSockets.size() > 16)
         {
-            LOG_ERR("Unexpected - client opening many concurrent waiting connections " << _outSockets.size());
+            LOG_ERR("proxy: Unexpected - client opening many concurrent waiting connections " << _outSockets.size());
             // cleanup older waiting sockets.
             auto sockWeak = _outSockets.front();
             _outSockets.erase(_outSockets.begin());
@@ -180,7 +181,7 @@ void ProxyProtocolHandler::handleIncomingMessage(SocketDisposition &disposition)
 int ProxyProtocolHandler::sendMessage(const char *msg, const size_t len, bool text, bool flush)
 {
     _writeQueue.push_back(std::make_shared<Message>(msg, len, text));
-    auto sock = popWriteSocket();
+    auto sock = popOutSocket();
     if (sock && flush)
     {
         flushQueueTo(sock);
@@ -230,16 +231,24 @@ int ProxyProtocolHandler::getPollEvents(std::chrono::steady_clock::time_point /*
     return events;
 }
 
-void ProxyProtocolHandler::performWrites()
+/// slurp from the core to us, @returns true if there are messages to send
+bool ProxyProtocolHandler::slurpHasMessages()
 {
-    if (_msgHandler)
+    if (_msgHandler && _msgHandler->hasQueuedMessages())
         _msgHandler->writeQueuedMessages();
-    if (_writeQueue.size() <= 0)
+
+    return _writeQueue.size() > 0;
+}
+
+void ProxyProtocolHandler::performWrites()
+{
+    if (!slurpHasMessages())
         return;
 
-    auto sock = popWriteSocket();
+    auto sock = popOutSocket();
     if (sock)
     {
+        LOG_TRC("proxy: performWrites");
         flushQueueTo(sock);
         sock->shutdown();
     }
@@ -247,9 +256,8 @@ void ProxyProtocolHandler::performWrites()
 
 bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &socket)
 {
-    // slurp from the core to us.
-    if (_msgHandler && _msgHandler->hasQueuedMessages())
-        _msgHandler->writeQueuedMessages();
+    if (!slurpHasMessages())
+        return false;
 
     size_t totalSize = 0;
     for (auto it : _writeQueue)
@@ -258,6 +266,8 @@ bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &soc
     if (!totalSize)
         return false;
 
+    LOG_TRC("proxy: flushQueue of size " << totalSize << " to socket #" << socket->getFD() << " & close");
+
     std::ostringstream oss;
     oss << "HTTP/1.1 200 OK\r\n"
         "Last-Modified: " << Util::getHttpTimeNow() << "\r\n"
@@ -276,7 +286,7 @@ bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &soc
 }
 
 // LRU-ness ...
-std::shared_ptr<StreamSocket> ProxyProtocolHandler::popWriteSocket()
+std::shared_ptr<StreamSocket> ProxyProtocolHandler::popOutSocket()
 {
     std::weak_ptr<StreamSocket> sock;
     while (!_outSockets.empty())
@@ -285,8 +295,12 @@ std::shared_ptr<StreamSocket> ProxyProtocolHandler::popWriteSocket()
         _outSockets.erase(_outSockets.begin());
         auto realSock = sock.lock();
         if (realSock)
+        {
+            LOG_TRC("proxy: popped an out socket #" << realSock->getFD() << " leaving: " << _outSockets.size());
             return realSock;
+        }
     }
+    LOG_TRC("proxy: no out sockets to pop.");
     return std::shared_ptr<StreamSocket>();
 }
 
diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
index 090d4331e..692fd903c 100644
--- a/wsd/ProxyProtocol.hpp
+++ b/wsd/ProxyProtocol.hpp
@@ -60,7 +60,8 @@ public:
     void handleRequest(bool isWaiting, const std::shared_ptr<Socket> &socket);
 
 private:
-    std::shared_ptr<StreamSocket> popWriteSocket();
+    std::shared_ptr<StreamSocket> popOutSocket();
+    bool slurpHasMessages();
     int sendMessage(const char *msg, const size_t len, bool text, bool flush);
     bool flushQueueTo(const std::shared_ptr<StreamSocket> &socket);
 
commit ddeb40b3e79cbcdd6adfb9b24ca608b557eafa73
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Mar 21 14:27:15 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: ensure dumpState dumps via the MessageHandlerInterface too.
    
    Change-Id: If514e2359188d56bbf7ddef6e04f9d8bf5c50910

diff --git a/net/Socket.cpp b/net/Socket.cpp
index 372f9ea1a..24041bc4b 100644
--- a/net/Socket.cpp
+++ b/net/Socket.cpp
@@ -524,6 +524,8 @@ void WebSocketHandler::dumpState(std::ostream& os)
     if (_wsPayload.size() > 0)
         Util::dumpHex(os, "\t\tws queued payload:\n", "\t\t", _wsPayload);
     os << "\n";
+    if (_msgHandler)
+        _msgHandler->dumpState(os);
 }
 
 void StreamSocket::dumpState(std::ostream& os)
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 6279682f2..c8f578559 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -217,6 +217,8 @@ void ProxyProtocolHandler::dumpState(std::ostream& os)
     os << "proxy protocol sockets: " << _outSockets.size() << " writeQueue: " << _writeQueue.size() << ":\n";
     for (auto it : _writeQueue)
         Util::dumpHex(os, "\twrite queue entry:", "\t\t", *it);
+    if (_msgHandler)
+        _msgHandler->dumpState(os);
 }
 
 int ProxyProtocolHandler::getPollEvents(std::chrono::steady_clock::time_point /* now */,
commit 73f2aefe34e6d2375bf961c5a977b69a27bdbce2
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Mar 21 14:19:49 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: make eslint happier.
    
    Change-Id: I9ecec787a9a69633a015459eaf39d4b8bd5bb61d

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index b0b0c5fd5..c711c84b0 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -1,4 +1,5 @@
 /* -*- js-indent-level: 8 -*- */
+/* global Uint8Array */
 (function (global) {
 
 	var ua = navigator.userAgent.toLowerCase(),
@@ -312,8 +313,6 @@
 		// queue fetch of session id.
 		this.getSessionId();
 
-		var that = this;
-
 		// horrors ...
 		this.readInterval = setInterval(function() {
 			if (that.readWaiting > 4) // max 4 waiting connections concurrently.
commit c33d97c9e7e0d5b08782c0ce468573a879e63191
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Mar 20 20:45:38 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: poll for output space if we need waking.
    
    Change-Id: I18a5e71bd3342eea7992672d9be1f5518ea008e3

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index f19c4c300..b0b0c5fd5 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -215,6 +215,7 @@
 		};
 		this.parseIncomingArray = function(arr) {
 			var decoder = new TextDecoder();
+			console.debug('Parse incoming array of length ' + arr.length);
 			for (var i = 0; i < arr.length; ++i)
 			{
 				var left = arr.length - i;
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 25602f146..6279682f2 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -219,6 +219,15 @@ void ProxyProtocolHandler::dumpState(std::ostream& os)
         Util::dumpHex(os, "\twrite queue entry:", "\t\t", *it);
 }
 
+int ProxyProtocolHandler::getPollEvents(std::chrono::steady_clock::time_point /* now */,
+                                        int64_t &/* timeoutMaxMs */)
+{
+    int events = POLLIN;
+    if (_msgHandler && _msgHandler->hasQueuedMessages())
+        events |= POLLOUT;
+    return events;
+}
+
 void ProxyProtocolHandler::performWrites()
 {
     if (_msgHandler)
diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
index dda24328a..090d4331e 100644
--- a/wsd/ProxyProtocol.hpp
+++ b/wsd/ProxyProtocol.hpp
@@ -34,11 +34,7 @@ public:
     void handleIncomingMessage(SocketDisposition &/* disposition */) override;
 
     int getPollEvents(std::chrono::steady_clock::time_point /* now */,
-                      int &/* timeoutMaxMs */) override
-    {
-        // underlying buffer based polling is fine.
-        return POLLIN;
-    }
+                      int64_t &/* timeoutMaxMs */) override;
 
     void checkTimeout(std::chrono::steady_clock::time_point /* now */) override
     {
commit 4cf290006c9cf1cce5d240b6e87e30bf432885e7
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Mar 20 20:15:08 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: open four wait sockets concurrently.
    
    Change-Id: I08b85677be528b7aa77272a8527c9bacf3f7c336

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 9a19d9a59..f19c4c300 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -206,7 +206,7 @@
 		this.sessionId = 'fetchsession';
 		this.id = window.proxySocketCounter++;
 		this.sendCounter = 0;
-		this.readWaiting = false;
+		this.readWaiting = 0;
 		this.onclose = function() {
 		};
 		this.onerror = function() {
@@ -315,9 +315,9 @@
 
 		// horrors ...
 		this.readInterval = setInterval(function() {
-			if (this.readWaiting) // one at a time for now
+			if (that.readWaiting > 4) // max 4 waiting connections concurrently.
 				return;
-			if (this.sessionId == 'fetchsession')
+			if (that.sessionId == 'fetchsession')
 				return; // waiting for our session id.
 			var req = new XMLHttpRequest();
 			// fetch session id:
@@ -326,13 +326,15 @@
 					that.parseIncomingArray(new Uint8Array(this.response));
 				else
 					console.debug('Handle error ' + this.status);
-				that.readWaiting = false;
+			});
+			req.addEventListener('loadend', function() {
+				that.readWaiting--;
 			});
 			req.open('GET', that.getEndPoint('read'));
 			req.setRequestHeader('SessionId', that.sessionId);
 			req.responseType = 'arraybuffer';
 			req.send('');
-			that.readWaiting = true;
+			that.readWaiting++;
 		}, 250);
 	};
 
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index 4e854d3fb..6e265c3dc 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -147,7 +147,8 @@ public:
         const Poco::URI& uriPublic,
         const bool isReadOnly,
         const std::string& hostNoTrust,
-        const std::shared_ptr<StreamSocket> &socket);
+        const std::shared_ptr<StreamSocket> &socket,
+        bool isWaiting);
 
     /// Thread safe termination of this broker if it has a lingering thread
     void joinThread();
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index c58430509..3c5d22677 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2914,11 +2914,16 @@ private:
         // Request a kit process for this doc.
         std::shared_ptr<DocumentBroker> docBroker = findOrCreateDocBroker(
             none, url, docKey, _id, uriPublic);
+
+        std::string fullURL = request.getURI();
+        std::string ending = "/ws/read";
+        bool isWaiting = (fullURL.size() > ending.size() &&
+                          std::equal(ending.rbegin(), ending.rend(), fullURL.rbegin()));
         if (docBroker)
         {
             // need to move into the DocumentBroker context before doing session lookup / creation etc.
             std::string id = _id;
-            disposition.setMove([docBroker, id, uriPublic, isReadOnly, hostNoTrust, sessionId]
+            disposition.setMove([docBroker, id, uriPublic, isReadOnly, hostNoTrust, sessionId, isWaiting]
                                 (const std::shared_ptr<Socket> &moveSocket)
                 {
                     LOG_TRC("Setting up docbroker thread for " << docBroker->getDocKey());
@@ -2928,7 +2933,8 @@ private:
                     // We no longer own this socket.
                     moveSocket->setThreadOwner(std::thread::id());
 
-                    docBroker->addCallback([docBroker, id, uriPublic, isReadOnly, hostNoTrust, sessionId, moveSocket]()
+                    docBroker->addCallback([docBroker, id, uriPublic, isReadOnly, hostNoTrust,
+                                            sessionId, moveSocket, isWaiting]()
                         {
                             // Now inside the document broker thread ...
                             LOG_TRC("In the docbroker thread for " << docBroker->getDocKey());
@@ -2938,7 +2944,7 @@ private:
                             {
                                 docBroker->handleProxyRequest(
                                     sessionId, id, uriPublic, isReadOnly,
-                                    hostNoTrust, streamSocket);
+                                    hostNoTrust, streamSocket, isWaiting);
                                 return;
                             }
                             catch (const UnauthorizedRequestException& exc)
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 8aaff0131..25602f146 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -25,7 +25,8 @@ void DocumentBroker::handleProxyRequest(
     const Poco::URI& uriPublic,
     const bool isReadOnly,
     const std::string& hostNoTrust,
-    const std::shared_ptr<StreamSocket> &socket)
+    const std::shared_ptr<StreamSocket> &socket,
+    bool isWaiting)
 {
     std::shared_ptr<ClientSession> clientSession;
     if (sessionId == "fetchsession")
@@ -82,7 +83,7 @@ void DocumentBroker::handleProxyRequest(
     auto proxy = std::static_pointer_cast<ProxyProtocolHandler>(
         protocol);
 
-    proxy->handleRequest(uriPublic.toString(), socket);
+    proxy->handleRequest(isWaiting, socket);
 }
 
 bool ProxyProtocolHandler::parseEmitIncoming(
@@ -128,16 +129,13 @@ bool ProxyProtocolHandler::parseEmitIncoming(
     return true;
 }
 
-void ProxyProtocolHandler::handleRequest(const std::string &uriPublic,
-                                         const std::shared_ptr<Socket> &socket)
+void ProxyProtocolHandler::handleRequest(bool isWaiting, const std::shared_ptr<Socket> &socket)
 {
     auto streamSocket = std::static_pointer_cast<StreamSocket>(socket);
 
-    bool bRead = uriPublic.find("/write") == std::string::npos;
-    LOG_INF("Proxy handle request " << uriPublic << " type: " <<
-            (bRead ? "read" : "write"));
+    LOG_INF("Proxy handle request type: " << (isWaiting ? "wait" : "respond"));
 
-    if (bRead)
+    if (!isWaiting)
     {
         if (!_msgHandler)
             LOG_WRN("unusual - incoming message with no-one to handle it");
@@ -149,13 +147,27 @@ void ProxyProtocolHandler::handleRequest(const std::string &uriPublic,
         }
     }
 
-    if (!flushQueueTo(streamSocket) && !bRead)
+    if (!flushQueueTo(streamSocket) && isWaiting)
     {
-        // longer running 'write socket'
-        _writeSockets.push_back(streamSocket);
+        LOG_TRC("Queue a waiting socket");
+        // longer running 'write socket' (marked 'read' by the client)
+        _outSockets.push_back(streamSocket);
+        if (_outSockets.size() > 16)
+        {
+            LOG_ERR("Unexpected - client opening many concurrent waiting connections " << _outSockets.size());
+            // cleanup older waiting sockets.
+            auto sockWeak = _outSockets.front();
+            _outSockets.erase(_outSockets.begin());
+            auto sock = sockWeak.lock();
+            if (sock)
+                sock->shutdown();
+        }
     }
     else
+    {
+        LOG_TRC("Return a reply immediately");
         socket->shutdown();
+    }
 }
 
 void ProxyProtocolHandler::handleIncomingMessage(SocketDisposition &disposition)
@@ -202,7 +214,7 @@ void ProxyProtocolHandler::getIOStats(uint64_t &sent, uint64_t &recv)
 
 void ProxyProtocolHandler::dumpState(std::ostream& os)
 {
-    os << "proxy protocol sockets: " << _writeSockets.size() << " writeQueue: " << _writeQueue.size() << ":\n";
+    os << "proxy protocol sockets: " << _outSockets.size() << " writeQueue: " << _writeQueue.size() << ":\n";
     for (auto it : _writeQueue)
         Util::dumpHex(os, "\twrite queue entry:", "\t\t", *it);
 }
@@ -256,10 +268,10 @@ bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &soc
 std::shared_ptr<StreamSocket> ProxyProtocolHandler::popWriteSocket()
 {
     std::weak_ptr<StreamSocket> sock;
-    while (!_writeSockets.empty())
+    while (!_outSockets.empty())
     {
-        sock = _writeSockets.front();
-        _writeSockets.erase(_writeSockets.begin());
+        sock = _outSockets.front();
+        _outSockets.erase(_outSockets.begin());
         auto realSock = sock.lock();
         if (realSock)
             return realSock;
diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
index 0b20900b1..dda24328a 100644
--- a/wsd/ProxyProtocol.hpp
+++ b/wsd/ProxyProtocol.hpp
@@ -60,11 +60,8 @@ public:
     void shutdown(bool goingAway = false, const std::string &statusMessage = "") override;
     void getIOStats(uint64_t &sent, uint64_t &recv) override;
     void dumpState(std::ostream& os);
-
     bool parseEmitIncoming(const std::shared_ptr<StreamSocket> &socket);
-
-    void handleRequest(const std::string &uriPublic,
-                       const std::shared_ptr<Socket> &socket);
+    void handleRequest(bool isWaiting, const std::shared_ptr<Socket> &socket);
 
 private:
     std::shared_ptr<StreamSocket> popWriteSocket();
@@ -88,7 +85,7 @@ private:
     };
     /// queue things when we have no socket to hand.
     std::vector<std::shared_ptr<Message>> _writeQueue;
-    std::vector<std::weak_ptr<StreamSocket>> _writeSockets;
+    std::vector<std::weak_ptr<StreamSocket>> _outSockets;
 };
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
commit 91ffe8bc5829c041d0ad17e086596b862c3ba014
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Mar 20 19:05:48 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: re-write css image URLs to handle the proxy.
    
    Change-Id: I09f3dea2f5e3a51869d5b0aa3f473d8f3ba75f44

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 0e9179763..9a19d9a59 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -336,6 +336,33 @@
 		}, 250);
 	};
 
+	if (global.socketProxy)
+	{
+		// re-write relative URLs in CSS - somewhat grim.
+		window.addEventListener('load', function() {
+			var sheets = document.styleSheets;
+			for (var i = 0; i < sheets.length; ++i) {
+				var relBases = sheets[i].href.split('/');
+				relBases.pop(); // bin last - css name.
+				var replaceBase = 'url("' + relBases.join('/') + '/images/';
+
+				var rules = sheets[i].cssRules || sheets[i].rules;
+				for (var r = 0; r < rules.length; ++r) {
+					if (!rules[r] || !rules[r].style)
+						continue;
+					var img = rules[r].style.backgroundImage;
+					if (img === '' || img === undefined)
+						continue;
+					if (img.startsWith('url("images/'))
+					{
+						rules[r].style.backgroundImage =
+							img.replace('url("images/', replaceBase);
+					}
+				}
+			}
+		}, false);
+	}
+
 	global.createWebSocket = function(uri) {
 		if (global.socketProxy) {
 			return new global.ProxySocket(uri);
commit 4aed9c0a09b6da4c67087b31f0e7cffcf3807463
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Mar 20 16:38:14 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy: send multiple messages in a single request.
    
    Change-Id: Ic0a303979478801bd23941e8893ce5721cf3e732

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 0d1a40ef2..0e9179763 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -195,6 +195,7 @@
 
 	global.proxySocketCounter = 0;
 	global.ProxySocket = function (uri) {
+		var that = this;
 		this.uri = uri;
 		this.binaryType = 'arraybuffer';
 		this.bufferedAmount = 0;
@@ -254,56 +255,48 @@
 				i += size; // skip trailing '\n' in loop-increment
 			}
 		};
-		this.parseIncoming = function(type, msg) {
-			if (type === 'blob')
-			{
-				var fileReader = new FileReader();
-				var that = this;
-				fileReader.onload = function(event) {
-					that.parseIncomingArray(event.target.result);
-				};
-				fileReader.readAsArrayBuffer(msg);
-			}
-			else if (type === 'arraybuffer')
-			{
-				this.parseIncomingArray(new Uint8Array(msg));
-			}
-			else if (type === 'text' || type === '')
-			{
-				const encoder = new TextEncoder()
-				const arr = encoder.encode(msg);
-				this.parseIncomingArray(arr);
-			}
-			else
-				console.debug('Unknown encoding type: ' + type);
-		};
-		this.send = function(msg) {
-			console.debug('send msg "' + msg + '"');
+		this.sendQueue = '';
+		this.sendTimeout = undefined;
+		this.doSend = function () {
+			that.sendTimeout = undefined;
+			console.debug('send msg "' + that.sendQueue + '"');
 			var req = new XMLHttpRequest();
-			req.open('POST', this.getEndPoint('write'));
-			req.setRequestHeader('SessionId', this.sessionId);
-			if (this.sessionId === 'fetchsession')
-			{
-				req.responseType = 'text';
-				req.addEventListener('load', function() {
-					console.debug('got session: ' + this.responseText);
-					that.sessionId = this.responseText;
-					that.readyState = 1;
-					that.onopen();
-				});
-			}
+			req.open('POST', that.getEndPoint('write'));
+			req.setRequestHeader('SessionId', that.sessionId);
+			if (that.sessionId === 'fetchsession')
+				console.debug('session fetch not completed');
 			else
 			{
 				req.responseType = 'arraybuffer';
 				req.addEventListener('load', function() {
 					if (this.status == 200)
-						that.parseIncoming(this.responseType, this.response);
+						that.parseIncomingArray(new Uint8Array(this.response));
 					else
 						console.debug('Error on incoming response');
 				});
 			}
-			req.send('B0x' + msg.length.toString(16) + '\n' + msg + '\n');
-		},
+			req.send(that.sendQueue);
+			that.sendQueue = '';
+		};
+		this.getSessionId = function() {
+			var req = new XMLHttpRequest();
+			req.open('POST', that.getEndPoint('write'));
+			req.setRequestHeader('SessionId', that.sessionId);
+			req.responseType = 'text';
+			req.addEventListener('load', function() {
+				console.debug('got session: ' + this.responseText);
+				that.sessionId = this.responseText;
+				that.readyState = 1;
+				that.onopen();
+			});
+			req.send('');
+		};
+		this.send = function(msg) {
+			this.sendQueue = this.sendQueue.concat(
+				'B0x' + msg.length.toString(16) + '\n' + msg + '\n');
+			if (this.sessionId !== 'fetchsession' && this.sendTimeout === undefined)
+				this.sendTimeout = setTimeout(this.doSend, 2 /* ms */);
+		};
 		this.close = function() {
 			console.debug('close socket');
 			this.readyState = 3;
@@ -315,7 +308,9 @@
 		};
 		console.debug('New proxy socket ' + this.id + ' ' + this.uri);
 
-		this.send('fetchsession');
+		// queue fetch of session id.
+		this.getSessionId();
+
 		var that = this;
 
 		// horrors ...
@@ -328,7 +323,7 @@
 			// fetch session id:
 			req.addEventListener('load', function() {
 				if (this.status == 200)
-					that.parseIncoming(this.responseType, this.response);
+					that.parseIncomingArray(new Uint8Array(this.response));
 				else
 					console.debug('Handle error ' + this.status);
 				that.readWaiting = false;
commit c0cd0d1d5e00195506e30287fe1238880b73a7bb
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Thu Mar 19 15:54:28 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:41:56 2020 +0100

    Proxy protocol bits.
    
    For now very silly: [T|B] + hex length + \n + content + \n
    
    Change-Id: I256b834a23cca975a705da2c569887665ac6be02

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 477c66a5d..0d1a40ef2 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -212,19 +212,97 @@
 		};
 		this.onmessage = function() {
 		};
+		this.parseIncomingArray = function(arr) {
+			var decoder = new TextDecoder();
+			for (var i = 0; i < arr.length; ++i)
+			{
+				var left = arr.length - i;
+				if (left < 4)
+				{
+					console.debug('no data left');
+					break;
+				}
+				var type = String.fromCharCode(arr[i+0]);
+				if (type != 'T' && type != 'B')
+				{
+					console.debug('wrong data type: ' + type);
+					break;
+				}
+				if (arr[i+1] !== 48 && arr[i+2] !== 120) // '0x'
+				{
+					console.debug('missing hex preamble');
+					break;
+				}
+				i += 3;
+				var numStr = '';
+				var start = i;
+				while (arr[i] != 10) // '\n'
+					i++;
+				numStr = decoder.decode(arr.slice(start, i)); // FIXME: IE11
+				var size = parseInt(numStr, 16);
+
+				i++; // skip \n
+
+				var data;
+				if (type == 'T') // FIXME: IE11
+					data = decoder.decode(arr.slice(i, i + size));
+				else
+					data = arr.slice(i, i + size);
+
+				this.onmessage({ data: data });
+
+				i += size; // skip trailing '\n' in loop-increment
+			}
+		};
+		this.parseIncoming = function(type, msg) {
+			if (type === 'blob')
+			{
+				var fileReader = new FileReader();
+				var that = this;
+				fileReader.onload = function(event) {
+					that.parseIncomingArray(event.target.result);
+				};
+				fileReader.readAsArrayBuffer(msg);
+			}
+			else if (type === 'arraybuffer')
+			{
+				this.parseIncomingArray(new Uint8Array(msg));
+			}
+			else if (type === 'text' || type === '')
+			{
+				const encoder = new TextEncoder()
+				const arr = encoder.encode(msg);
+				this.parseIncomingArray(arr);
+			}
+			else
+				console.debug('Unknown encoding type: ' + type);
+		};
 		this.send = function(msg) {
 			console.debug('send msg "' + msg + '"');
 			var req = new XMLHttpRequest();
 			req.open('POST', this.getEndPoint('write'));
 			req.setRequestHeader('SessionId', this.sessionId);
 			if (this.sessionId === 'fetchsession')
+			{
+				req.responseType = 'text';
 				req.addEventListener('load', function() {
 					console.debug('got session: ' + this.responseText);
 					that.sessionId = this.responseText;
 					that.readyState = 1;
 					that.onopen();
 				});
-			req.send(msg);
+			}
+			else
+			{
+				req.responseType = 'arraybuffer';
+				req.addEventListener('load', function() {
+					if (this.status == 200)
+						that.parseIncoming(this.responseType, this.response);
+					else
+						console.debug('Error on incoming response');
+				});
+			}
+			req.send('B0x' + msg.length.toString(16) + '\n' + msg + '\n');
 		},
 		this.close = function() {
 			console.debug('close socket');
@@ -237,7 +315,6 @@
 		};
 		console.debug('New proxy socket ' + this.id + ' ' + this.uri);
 
-		// FIXME: perhaps a little risky.
 		this.send('fetchsession');
 		var that = this;
 
@@ -250,20 +327,16 @@
 			var req = new XMLHttpRequest();
 			// fetch session id:
 			req.addEventListener('load', function() {
-				console.debug('read: ' + this.responseText);
 				if (this.status == 200)
-				{
-					that.onmessage({ data: this.response });
-				}
+					that.parseIncoming(this.responseType, this.response);
 				else
-				{
 					console.debug('Handle error ' + this.status);
-				}
 				that.readWaiting = false;
 			});
 			req.open('GET', that.getEndPoint('read'));
-			req.setRequestHeader('SessionId', this.sessionId);
-			req.send(that.sessionId);
+			req.setRequestHeader('SessionId', that.sessionId);
+			req.responseType = 'arraybuffer';
+			req.send('');
 			that.readWaiting = true;
 		}, 250);
 	};
diff --git a/net/Socket.hpp b/net/Socket.hpp
index 7ae042b86..2c94601ee 100644
--- a/net/Socket.hpp
+++ b/net/Socket.hpp
@@ -85,6 +85,10 @@ public:
     {
         _disposition = Type::CLOSED;
     }
+    std::shared_ptr<Socket> getSocket() const
+    {
+        return _socket;
+    }
     bool isMove() { return _disposition == Type::MOVE; }
     bool isClosed() { return _disposition == Type::CLOSED; }
 
@@ -923,6 +927,12 @@ public:
         std::vector<std::pair<size_t, size_t>> _spans;
     };
 
+    /// remove all queued input bytes
+    void clearInput()
+    {
+        _inBuffer.clear();
+    }
+
     /// Remove the first @count bytes from input buffer
     void eraseFirstInputBytes(const MessageMap &map)
     {
@@ -1086,6 +1096,8 @@ public:
     /// Does it look like we have some TLS / SSL where we don't expect it ?
     bool sniffSSL() const;
 
+    void dumpState(std::ostream& os) override;
+
 protected:
     /// Override to handle reading of socket data differently.
     virtual int readData(char* buf, int len)
@@ -1109,8 +1121,6 @@ protected:
 #endif
     }
 
-    void dumpState(std::ostream& os) override;
-
     void setShutdownSignalled()
     {
         _shutdownSignalled = true;
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index 7d7b5e67d..4e854d3fb 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -147,7 +147,7 @@ public:
         const Poco::URI& uriPublic,
         const bool isReadOnly,
         const std::string& hostNoTrust,
-        const std::shared_ptr<Socket> &moveSocket);
+        const std::shared_ptr<StreamSocket> &socket);
 
     /// Thread safe termination of this broker if it has a lingering thread
     void joinThread();
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 7aaa79e0d..c58430509 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2938,7 +2938,7 @@ private:
                             {
                                 docBroker->handleProxyRequest(
                                     sessionId, id, uriPublic, isReadOnly,
-                                    hostNoTrust, moveSocket);
+                                    hostNoTrust, streamSocket);
                                 return;
                             }
                             catch (const UnauthorizedRequestException& exc)
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 41043a57a..8aaff0131 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -25,7 +25,7 @@ void DocumentBroker::handleProxyRequest(
     const Poco::URI& uriPublic,
     const bool isReadOnly,
     const std::string& hostNoTrust,
-    const std::shared_ptr<Socket> &socket)
+    const std::shared_ptr<StreamSocket> &socket)
 {
     std::shared_ptr<ClientSession> clientSession;
     if (sessionId == "fetchsession")
@@ -37,6 +37,22 @@ void DocumentBroker::handleProxyRequest(
         addSession(clientSession);
         LOOLWSD::checkDiskSpaceAndWarnClients(true);
         LOOLWSD::checkSessionLimitsAndWarnClients();
+
+        LOG_TRC("Returning id " << clientSession->getId());
+
+        std::ostringstream oss;
+        oss << "HTTP/1.1 200 OK\r\n"
+            "Last-Modified: " << Util::getHttpTimeNow() << "\r\n"
+            "User-Agent: " WOPI_AGENT_STRING "\r\n"
+            "Content-Length: " << clientSession->getId().size() << "\r\n"
+            "Content-Type: application/json\r\n"
+            "X-Content-Type-Options: nosniff\r\n"
+            "\r\n"
+            << clientSession->getId();
+
+        socket->send(oss.str());
+        socket->shutdown();
+        return;
     }
     else
     {
@@ -69,13 +85,186 @@ void DocumentBroker::handleProxyRequest(
     proxy->handleRequest(uriPublic.toString(), socket);
 }
 
+bool ProxyProtocolHandler::parseEmitIncoming(
+    const std::shared_ptr<StreamSocket> &socket)
+{
+    std::vector<char> &in = socket->getInBuffer();
+
+    std::stringstream oss;
+    socket->dumpState(oss);
+    LOG_TRC("Parse message:\n" << oss.str());
+
+    while (in.size() > 0)
+    {
+        if (in[0] != 'T' && in[0] != 'B')
+        {
+            LOG_ERR("Invalid message type " << in[0]);
+            return false;
+        }
+        auto it = in.begin() + 1;
+        for (; it != in.end() && *it != '\n'; ++it);
+        *it = '\0';
+        uint64_t len = strtoll( &in[1], nullptr, 16 );
+        in.erase(in.begin(), it + 1);
+        if (len > in.size())
+        {
+            LOG_ERR("Invalid message length " << len << " vs " << in.size());
+            return false;
+        }
+        // far from efficient:
+        std::vector<char> data;
+        data.insert(data.begin(), in.begin(), in.begin() + len + 1);
+        in.erase(in.begin(), in.begin() + len);
+
+        if (in.size() < 1 || in[0] != '\n')
+        {
+            LOG_ERR("Missing final newline");
+            return false;
+        }
+        in.erase(in.begin(), in.begin() + 1);
+
+        _msgHandler->handleMessage(data);
+    }
+    return true;
+}
+
 void ProxyProtocolHandler::handleRequest(const std::string &uriPublic,
                                          const std::shared_ptr<Socket> &socket)
 {
+    auto streamSocket = std::static_pointer_cast<StreamSocket>(socket);
+
     bool bRead = uriPublic.find("/write") == std::string::npos;
     LOG_INF("Proxy handle request " << uriPublic << " type: " <<
             (bRead ? "read" : "write"));
-    (void)socket;
+
+    if (bRead)
+    {
+        if (!_msgHandler)
+            LOG_WRN("unusual - incoming message with no-one to handle it");
+        else if (!parseEmitIncoming(streamSocket))
+        {
+            std::stringstream oss;
+            streamSocket->dumpState(oss);
+            LOG_ERR("bad socket structure " << oss.str());
+        }
+    }
+
+    if (!flushQueueTo(streamSocket) && !bRead)
+    {
+        // longer running 'write socket'
+        _writeSockets.push_back(streamSocket);
+    }
+    else
+        socket->shutdown();
+}
+
+void ProxyProtocolHandler::handleIncomingMessage(SocketDisposition &disposition)
+{
+    std::stringstream oss;
+    disposition.getSocket()->dumpState(oss);
+    LOG_ERR("If you got here, it means we failed to parse this properly in handleRequest: " << oss.str());
+}
+
+int ProxyProtocolHandler::sendMessage(const char *msg, const size_t len, bool text, bool flush)
+{
+    _writeQueue.push_back(std::make_shared<Message>(msg, len, text));
+    auto sock = popWriteSocket();
+    if (sock && flush)
+    {
+        flushQueueTo(sock);
+        sock->shutdown();
+    }
+
+    return len;
+}
+
+int ProxyProtocolHandler::sendTextMessage(const char *msg, const size_t len, bool flush) const
+{
+    LOG_TRC("ProxyHack - send text msg " + std::string(msg, len));
+    return const_cast<ProxyProtocolHandler *>(this)->sendMessage(msg, len, true, flush);
+}
+
+int ProxyProtocolHandler::sendBinaryMessage(const char *data, const size_t len, bool flush) const
+{
+    LOG_TRC("ProxyHack - send binary msg len " << len);
+    return const_cast<ProxyProtocolHandler *>(this)->sendMessage(data, len, false, flush);
+}
+
+void ProxyProtocolHandler::shutdown(bool goingAway, const std::string &statusMessage)
+{
+    LOG_TRC("ProxyHack - shutdown " << goingAway << ": " << statusMessage);
+}
+
+void ProxyProtocolHandler::getIOStats(uint64_t &sent, uint64_t &recv)
+{
+    sent = recv = 0;
+}
+
+void ProxyProtocolHandler::dumpState(std::ostream& os)
+{
+    os << "proxy protocol sockets: " << _writeSockets.size() << " writeQueue: " << _writeQueue.size() << ":\n";
+    for (auto it : _writeQueue)
+        Util::dumpHex(os, "\twrite queue entry:", "\t\t", *it);
+}
+
+void ProxyProtocolHandler::performWrites()
+{
+    if (_msgHandler)
+        _msgHandler->writeQueuedMessages();
+    if (_writeQueue.size() <= 0)
+        return;
+
+    auto sock = popWriteSocket();
+    if (sock)
+    {
+        flushQueueTo(sock);
+        sock->shutdown();
+    }
+}
+
+bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &socket)
+{
+    // slurp from the core to us.
+    if (_msgHandler && _msgHandler->hasQueuedMessages())
+        _msgHandler->writeQueuedMessages();
+
+    size_t totalSize = 0;
+    for (auto it : _writeQueue)
+        totalSize += it->size();
+
+    if (!totalSize)
+        return false;
+
+    std::ostringstream oss;
+    oss << "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: " << Util::getHttpTimeNow() << "\r\n"
+        "User-Agent: " WOPI_AGENT_STRING "\r\n"
+        "Content-Length: " << totalSize << "\r\n"
+        "Content-Type: application/json\r\n"
+        "X-Content-Type-Options: nosniff\r\n"
+        "\r\n";
+    socket->send(oss.str());
+
+    for (auto it : _writeQueue)
+        socket->send(it->data(), it->size(), false);
+    _writeQueue.clear();
+
+    return true;
+}
+
+// LRU-ness ...
+std::shared_ptr<StreamSocket> ProxyProtocolHandler::popWriteSocket()
+{
+    std::weak_ptr<StreamSocket> sock;
+    while (!_writeSockets.empty())
+    {
+        sock = _writeSockets.front();
+        _writeSockets.erase(_writeSockets.begin());
+        auto realSock = sock.lock();
+        if (realSock)
+            return realSock;
+    }
+    return std::shared_ptr<StreamSocket>();
 }
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
index bd6beac3d..0b20900b1 100644
--- a/wsd/ProxyProtocol.hpp
+++ b/wsd/ProxyProtocol.hpp
@@ -9,19 +9,21 @@
 
 #pragma once
 
+#include <memory>
 #include <net/Socket.hpp>
 
-/// Interface for building a websocket from this ...
+/**
+ * Implementation that builds a websocket like protocol from many
+ * individual proxied HTTP requests back to back.
+ *
+ * we use a trivial framing: <hex-length>\r\n<content>\r\n
+ */
 class ProxyProtocolHandler : public ProtocolHandlerInterface
 {
 public:
-    ProxyProtocolHandler()
-    {
-    }
+    ProxyProtocolHandler() { }
 
-    virtual ~ProxyProtocolHandler()
-    {
-    }
+    virtual ~ProxyProtocolHandler() { }
 
     /// Will be called exactly once by setHandler
     void onConnect(const std::shared_ptr<StreamSocket>& /* socket */) override
@@ -29,10 +31,7 @@ public:
     }
 
     /// Called after successful socket reads.
-    void handleIncomingMessage(SocketDisposition &/* disposition */) override
-    {
-        assert("we get our data a different way" && false);
-    }
+    void handleIncomingMessage(SocketDisposition &/* disposition */) override;
 
     int getPollEvents(std::chrono::steady_clock::time_point /* now */,
                       int &/* timeoutMaxMs */) override
@@ -45,9 +44,7 @@ public:
     {
     }
 
-    void performWrites() override
-    {
-    }
+    void performWrites() override;
 
     void onDisconnect() override
     {
@@ -58,40 +55,40 @@ public:
     /// Clear all external references
     virtual void dispose() { _msgHandler.reset(); }
 
-    int sendTextMessage(const char *msg, const size_t len, bool flush = false) const override
-    {
-        LOG_TRC("ProxyHack - send text msg " + std::string(msg, len));
-        (void) flush;
-        return len;
-    }
+    int sendTextMessage(const char *msg, const size_t len, bool flush = false) const override;
+    int sendBinaryMessage(const char *data, const size_t len, bool flush = false) const override;
+    void shutdown(bool goingAway = false, const std::string &statusMessage = "") override;
+    void getIOStats(uint64_t &sent, uint64_t &recv) override;
+    void dumpState(std::ostream& os);
 
-    int sendBinaryMessage(const char *data, const size_t len, bool flush = false) const override
-    {
-        (void) data; (void) flush;
-        LOG_TRC("ProxyHack - send binary msg len " << len);
-        return len;
-    }
-
-    void shutdown(bool goingAway = false, const std::string &statusMessage = "") override
-    {
-        LOG_TRC("ProxyHack - shutdown " << goingAway << ": " << statusMessage);
-    }
-
-    void getIOStats(uint64_t &sent, uint64_t &recv) override
-    {
-        sent = recv = 0;
-    }
-
-    void dumpState(std::ostream& os)
-    {
-        os << "proxy protocol\n";
-    }
+    bool parseEmitIncoming(const std::shared_ptr<StreamSocket> &socket);
 
     void handleRequest(const std::string &uriPublic,
                        const std::shared_ptr<Socket> &socket);
 
 private:
-    std::vector<std::weak_ptr<StreamSocket>> _sockets;
+    std::shared_ptr<StreamSocket> popWriteSocket();
+    int sendMessage(const char *msg, const size_t len, bool text, bool flush);
+    bool flushQueueTo(const std::shared_ptr<StreamSocket> &socket);
+
+    struct Message : public std::vector<char>
+    {
+        Message(const char *msg, const size_t len, bool text)
+        {
+            const char *type = text ? "T" : "B";
+            insert(end(), type, type + 1);
+            std::ostringstream os;
+            os << std::hex << "0x" << len << "\n";
+            std::string str = os.str();
+            insert(end(), str.c_str(), str.c_str() + str.size());
+            insert(end(), msg, msg + len);
+            const char *terminator = "\n";
+            insert(end(), terminator, terminator + 1);
+        }
+    };
+    /// queue things when we have no socket to hand.
+    std::vector<std::shared_ptr<Message>> _writeQueue;
+    std::vector<std::weak_ptr<StreamSocket>> _writeSockets;
 };
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
commit 71c7816c518d4bf6315cfcfea2e41e8898d9639a
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Mar 4 13:54:04 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:40:23 2020 +0100

    Proxy websocket prototype.
    
    Try to read/write avoiding a websocket.
    
    Change-Id: I382039fa88f1030a63df1e47f687df2ee5a6055b

diff --git a/Makefile.am b/Makefile.am
index dcbfbdcbb..d28300996 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -109,6 +109,7 @@ loolwsd_sources = common/Crypto.cpp \
                   wsd/AdminModel.cpp \
                   wsd/Auth.cpp \
                   wsd/DocumentBroker.cpp \
+                  wsd/ProxyProtocol.cpp \
                   wsd/LOOLWSD.cpp \
                   wsd/ClientSession.cpp \
                   wsd/FileServer.cpp \
@@ -215,6 +216,7 @@ wsd_headers = wsd/Admin.hpp \
               wsd/Auth.hpp \
               wsd/ClientSession.hpp \
               wsd/DocumentBroker.hpp \
+              wsd/ProxyProtocol.hpp \
               wsd/Exceptions.hpp \
               wsd/FileServer.hpp \
               wsd/LOOLWSD.hpp \
diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index a3b1fb49c..477c66a5d 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -185,16 +185,97 @@
 		};
 		this.onopen = function() {
 		};
+		this.close = function() {
+		};
 	};
-
-	global.FakeWebSocket.prototype.close = function() {
-	};
-
 	global.FakeWebSocket.prototype.send = function(data) {
 		this.sendCounter++;
 		window.postMobileMessage(data);
 	};
 
+	global.proxySocketCounter = 0;
+	global.ProxySocket = function (uri) {
+		this.uri = uri;
+		this.binaryType = 'arraybuffer';
+		this.bufferedAmount = 0;
+		this.extensions = '';
+		this.protocol = '';
+		this.connected = true;
+		this.readyState = 0; // connecting
+		this.sessionId = 'fetchsession';
+		this.id = window.proxySocketCounter++;
+		this.sendCounter = 0;
+		this.readWaiting = false;
+		this.onclose = function() {
+		};
+		this.onerror = function() {
+		};
+		this.onmessage = function() {
+		};
+		this.send = function(msg) {
+			console.debug('send msg "' + msg + '"');
+			var req = new XMLHttpRequest();
+			req.open('POST', this.getEndPoint('write'));
+			req.setRequestHeader('SessionId', this.sessionId);
+			if (this.sessionId === 'fetchsession')
+				req.addEventListener('load', function() {
+					console.debug('got session: ' + this.responseText);
+					that.sessionId = this.responseText;
+					that.readyState = 1;
+					that.onopen();
+				});
+			req.send(msg);
+		},
+		this.close = function() {
+			console.debug('close socket');
+			this.readyState = 3;
+			this.onclose();
+		};
+		this.getEndPoint = function(type) {
+			var base = this.uri;
+			return base.replace(/^ws/, 'http') + '/' + type;
+		};
+		console.debug('New proxy socket ' + this.id + ' ' + this.uri);
+
+		// FIXME: perhaps a little risky.
+		this.send('fetchsession');
+		var that = this;
+
+		// horrors ...
+		this.readInterval = setInterval(function() {
+			if (this.readWaiting) // one at a time for now
+				return;
+			if (this.sessionId == 'fetchsession')
+				return; // waiting for our session id.
+			var req = new XMLHttpRequest();
+			// fetch session id:
+			req.addEventListener('load', function() {
+				console.debug('read: ' + this.responseText);
+				if (this.status == 200)
+				{
+					that.onmessage({ data: this.response });
+				}
+				else
+				{
+					console.debug('Handle error ' + this.status);
+				}
+				that.readWaiting = false;
+			});
+			req.open('GET', that.getEndPoint('read'));
+			req.setRequestHeader('SessionId', this.sessionId);
+			req.send(that.sessionId);
+			that.readWaiting = true;
+		}, 250);
+	};
+
+	global.createWebSocket = function(uri) {
+		if (global.socketProxy) {
+			return new global.ProxySocket(uri);
+		} else {
+			return new WebSocket(uri);
+		}
+	};
+
 	// If not debug, don't print anything on the console
 	// except in tile debug mode (Ctrl-Shift-Alt-d)
 	console.log2 = console.log;
@@ -219,7 +300,8 @@
 				window.postMobileError(log);
 			} else if (global.socket && (global.socket instanceof WebSocket) && global.socket.readyState === 1) {
 				global.socket.send(log);
-			} else if (global.socket && (global.socket instanceof global.L.Socket) && global.socket.connected()) {
+			} else if (global.socket && global.L && global.L.Socket &&
+				   (global.socket instanceof global.L.Socket) && global.socket.connected()) {
 				global.socket.sendMessage(log);
 			} else {
 				var req = new XMLHttpRequest();
@@ -296,7 +378,7 @@
 		var websocketURI = global.host + global.serviceRoot + '/lool/' + encodeURIComponent(global.docURL + (docParams ? '?' + docParams : '')) + '/ws' + wopiSrc;
 
 		try {
-			global.socket = new WebSocket(websocketURI);
+			global.socket = global.createWebSocket(websocketURI);
 		} catch (err) {
 			console.log(err);
 		}
diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js
index a4aa948c5..fdd0b2db6 100644
--- a/loleaflet/src/core/Socket.js
+++ b/loleaflet/src/core/Socket.js
@@ -49,7 +49,7 @@ L.Socket = L.Class.extend({
 			}
 
 			try {
-				this.socket = new WebSocket(this.getWebSocketBaseURI(map) + wopiSrc);
+				this.socket = window.createWebSocket(this.getWebSocketBaseURI(map) + wopiSrc);
 			} catch (e) {
 				// On IE 11 there is a limitation on the number of WebSockets open to a single domain (6 by default and can go to 128).
 				// Detect this and hint the user.
diff --git a/net/Socket.hpp b/net/Socket.hpp
index bc44f208b..7ae042b86 100644
--- a/net/Socket.hpp
+++ b/net/Socket.hpp
@@ -445,6 +445,11 @@ public:
         }
     }
 
+    std::shared_ptr<ProtocolHandlerInterface> getProtocol() const
+    {
+        return _protocol;
+    }
+
     /// Do we have something to send ?
     virtual bool hasQueuedMessages() const = 0;
     /// Please send them to me then.
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index 0f425b117..f2bd63adc 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -37,9 +37,6 @@ public:
     void construct();
     virtual ~ClientSession();
 
-    /// Lookup any session by id.
-    static std::shared_ptr<ClientSession> getById(const std::string &id);
-
     void setReadOnly() override;
 
     enum SessionState {
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index 329bece7f..7d7b5e67d 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -140,6 +140,15 @@ public:
         const bool isReadOnly,
         const std::string& hostNoTrust);
 
+    /// Find or create a new client session for the PHP proxy
+    void handleProxyRequest(
+        const std::string& sessionId,
+        const std::string& id,
+        const Poco::URI& uriPublic,
+        const bool isReadOnly,
+        const std::string& hostNoTrust,
+        const std::shared_ptr<Socket> &moveSocket);
+
     /// Thread safe termination of this broker if it has a lingering thread
     void joinThread();
 
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 379e960fe..7aaa79e0d 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -236,6 +236,9 @@ namespace
 #if ENABLE_SUPPORT_KEY
 inline void shutdownLimitReached(const std::shared_ptr<ProtocolHandlerInterface>& proto)
 {
+    if (!proto)
+        return;
+
     const std::string error = Poco::format(PAYLOAD_UNAVAILABLE_LIMIT_REACHED, LOOLWSD::MaxDocuments, LOOLWSD::MaxConnections);
     LOG_INF("Sending client 'hardlimitreached' message: " << error);
 
@@ -1833,9 +1836,12 @@ static std::shared_ptr<DocumentBroker>
         if (docBroker->isMarkedToDestroy())
         {
             LOG_WRN("DocBroker with docKey [" << docKey << "] that is marked to be destroyed. Rejecting client request.");
-            std::string msg("error: cmd=load kind=docunloading");
-            proto->sendTextMessage(msg.data(), msg.size());
-            proto->shutdown(true, "error: cmd=load kind=docunloading");
+            if (proto)
+            {
+                std::string msg("error: cmd=load kind=docunloading");
+                proto->sendTextMessage(msg.data(), msg.size());
+                proto->shutdown(true, "error: cmd=load kind=docunloading");
+            }
             return nullptr;
         }
     }
@@ -1851,9 +1857,12 @@ static std::shared_ptr<DocumentBroker>
     }
 
     // Indicate to the client that we're connecting to the docbroker.
-    const std::string statusConnect = "statusindicator: connect";
-    LOG_TRC("Sending to Client [" << statusConnect << "].");
-    proto->sendTextMessage(statusConnect.data(), statusConnect.size());
+    if (proto)
+    {
+        const std::string statusConnect = "statusindicator: connect";
+        LOG_TRC("Sending to Client [" << statusConnect << "].");
+        proto->sendTextMessage(statusConnect.data(), statusConnect.size());
+    }
 
     if (!docBroker)
     {
@@ -2325,6 +2334,13 @@ private:
 //                    Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ...
                     handleClipboardRequest(request, message, disposition);
                 }
+                else if (request.has("ProxyPrefix") && reqPathTokens.count() > 2 &&
+                         (reqPathTokens[reqPathTokens.count()-2] == "ws"))
+                {
+                    std::string decodedUri; // WOPISrc
+                    Poco::URI::decode(reqPathTokens[1], decodedUri);
+                    handleClientProxyRequest(request, decodedUri, message, disposition);
+                }
                 else if (!(request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0) &&
                     reqPathTokens.count() > 0 && reqPathTokens[0] == "lool")
                 {
@@ -2858,6 +2874,99 @@ private:
     }
 #endif
 
+    void handleClientProxyRequest(const Poco::Net::HTTPRequest& request,
+                                  std::string url,
+                                  Poco::MemoryInputStream& message,
+                                  SocketDisposition &disposition)
+    {
+        if (!request.has("SessionId"))
+            throw BadRequestException("No session id header on proxied request");
+
+        std::string sessionId = request.get("SessionId");
+
+        LOG_INF("URL [" << url << "].");
+        const auto uriPublic = DocumentBroker::sanitizeURI(url);
+        LOG_INF("URI [" << uriPublic.getPath() << "].");
+        const auto docKey = DocumentBroker::getDocKey(uriPublic);
+        LOG_INF("DocKey [" << docKey << "].");
+        const std::string fileId = Util::getFilenameFromURL(docKey);
+        Util::mapAnonymized(fileId, fileId); // Identity mapping, since fileId is already obfuscated
+
+        LOG_INF("Starting Proxy request handler for session [" << _id << "] on url [" << LOOLWSD::anonymizeUrl(url) << "].");
+
+        // Check if readonly session is required
+        bool isReadOnly = false;
+        for (const auto& param : uriPublic.getQueryParameters())
+        {
+            LOG_DBG("Query param: " << param.first << ", value: " << param.second);
+            if (param.first == "permission" && param.second == "readonly")
+            {
+                isReadOnly = true;
+            }
+        }
+
+        const std::string hostNoTrust = (LOOLWSD::ServerName.empty() ? request.getHost() : LOOLWSD::ServerName);
+
+        LOG_INF("URL [" << LOOLWSD::anonymizeUrl(url) << "] is " << (isReadOnly ? "readonly" : "writable") << ".");
+        (void)request; (void)message; (void)disposition;
+
+        std::shared_ptr<ProtocolHandlerInterface> none;
+        // Request a kit process for this doc.
+        std::shared_ptr<DocumentBroker> docBroker = findOrCreateDocBroker(
+            none, url, docKey, _id, uriPublic);
+        if (docBroker)
+        {
+            // need to move into the DocumentBroker context before doing session lookup / creation etc.
+            std::string id = _id;
+            disposition.setMove([docBroker, id, uriPublic, isReadOnly, hostNoTrust, sessionId]
+                                (const std::shared_ptr<Socket> &moveSocket)
+                {
+                    LOG_TRC("Setting up docbroker thread for " << docBroker->getDocKey());
+                    // Make sure the thread is running before adding callback.
+                    docBroker->startThread();
+
+                    // We no longer own this socket.
+                    moveSocket->setThreadOwner(std::thread::id());
+
+                    docBroker->addCallback([docBroker, id, uriPublic, isReadOnly, hostNoTrust, sessionId, moveSocket]()
+                        {
+                            // Now inside the document broker thread ...
+                            LOG_TRC("In the docbroker thread for " << docBroker->getDocKey());
+
+                            auto streamSocket = std::static_pointer_cast<StreamSocket>(moveSocket);
+                            try
+                            {
+                                docBroker->handleProxyRequest(
+                                    sessionId, id, uriPublic, isReadOnly,
+                                    hostNoTrust, moveSocket);
+                                return;
+                            }
+                            catch (const UnauthorizedRequestException& exc)
+                            {
+                                LOG_ERR("Unauthorized Request while loading session for " << docBroker->getDocKey() << ": " << exc.what());
+                            }
+                            catch (const StorageConnectionException& exc)
+                            {
+                                LOG_ERR("Error while loading : " << exc.what());
+                            }
+                            catch (const std::exception& exc)
+                            {
+                                LOG_ERR("Error while loading : " << exc.what());
+                            }
+                            // badness occured:
+                            std::ostringstream oss;
+                            oss << "HTTP/1.1 400\r\n"
+                                << "Date: " << Util::getHttpTimeNow() << "\r\n"
+                                << "User-Agent: LOOLWSD WOPI Agent\r\n"
+                                << "Content-Length: 0\r\n"
+                                << "\r\n";
+                            streamSocket->send(oss.str());
+                            streamSocket->shutdown();
+                        });
+                });
+        }
+    }
+
     void handleClientWsUpgrade(const Poco::Net::HTTPRequest& request, const std::string& url,
                                SocketDisposition &disposition)
     {
@@ -2920,7 +3029,7 @@ private:
             // Request a kit process for this doc.
             std::shared_ptr<DocumentBroker> docBroker = findOrCreateDocBroker(
                 std::static_pointer_cast<ProtocolHandlerInterface>(ws), url, docKey, _id, uriPublic);
-             if (docBroker)
+            if (docBroker)
             {
 #if MOBILEAPP
                 const std::string hostNoTrust;
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
new file mode 100644
index 000000000..41043a57a
--- /dev/null
+++ b/wsd/ProxyProtocol.cpp
@@ -0,0 +1,81 @@
+/* -*- 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 "DocumentBroker.hpp"
+#include "ClientSession.hpp"
+#include "ProxyProtocol.hpp"
+#include "Exceptions.hpp"
+#include "LOOLWSD.hpp"
+#include <Socket.hpp>
+
+#include <atomic>
+#include <cassert>
+
+void DocumentBroker::handleProxyRequest(
+    const std::string& sessionId,
+    const std::string& id,
+    const Poco::URI& uriPublic,
+    const bool isReadOnly,
+    const std::string& hostNoTrust,
+    const std::shared_ptr<Socket> &socket)
+{
+    std::shared_ptr<ClientSession> clientSession;
+    if (sessionId == "fetchsession")
+    {
+        LOG_TRC("Create session for " << _docKey);
+        clientSession = createNewClientSession(
+                std::make_shared<ProxyProtocolHandler>(),
+                id, uriPublic, isReadOnly, hostNoTrust);
+        addSession(clientSession);
+        LOOLWSD::checkDiskSpaceAndWarnClients(true);
+        LOOLWSD::checkSessionLimitsAndWarnClients();
+    }
+    else
+    {
+        LOG_TRC("Find session for " << _docKey << " with id " << sessionId);
+        for (const auto &it : _sessions)
+        {
+            if (it.second->getId() == sessionId)
+            {
+                clientSession = it.second;
+                break;
+            }
+        }
+        if (!clientSession)
+        {
+            LOG_ERR("Invalid session id used " << sessionId);
+            throw BadRequestException("invalid session id");
+        }
+    }
+
+    auto protocol = clientSession->getProtocol();
+    auto streamSocket = std::static_pointer_cast<StreamSocket>(socket);
+    streamSocket->setHandler(protocol);
+
+    // this DocumentBroker's poll handles reading & writing
+    addSocketToPoll(socket);
+
+    auto proxy = std::static_pointer_cast<ProxyProtocolHandler>(
+        protocol);
+
+    proxy->handleRequest(uriPublic.toString(), socket);
+}
+
+void ProxyProtocolHandler::handleRequest(const std::string &uriPublic,
+                                         const std::shared_ptr<Socket> &socket)
+{
+    bool bRead = uriPublic.find("/write") == std::string::npos;
+    LOG_INF("Proxy handle request " << uriPublic << " type: " <<
+            (bRead ? "read" : "write"));
+    (void)socket;
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
new file mode 100644
index 000000000..bd6beac3d
--- /dev/null
+++ b/wsd/ProxyProtocol.hpp
@@ -0,0 +1,97 @@
+/* -*- 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/.
+ */
+
+#pragma once
+
+#include <net/Socket.hpp>
+
+/// Interface for building a websocket from this ...
+class ProxyProtocolHandler : public ProtocolHandlerInterface
+{
+public:
+    ProxyProtocolHandler()
+    {
+    }
+
+    virtual ~ProxyProtocolHandler()
+    {
+    }
+
+    /// Will be called exactly once by setHandler
+    void onConnect(const std::shared_ptr<StreamSocket>& /* socket */) override
+    {
+    }
+
+    /// Called after successful socket reads.
+    void handleIncomingMessage(SocketDisposition &/* disposition */) override
+    {
+        assert("we get our data a different way" && false);
+    }
+
+    int getPollEvents(std::chrono::steady_clock::time_point /* now */,
+                      int &/* timeoutMaxMs */) override
+    {
+        // underlying buffer based polling is fine.
+        return POLLIN;
+    }
+
+    void checkTimeout(std::chrono::steady_clock::time_point /* now */) override
+    {
+    }
+
+    void performWrites() override
+    {
+    }
+
+    void onDisconnect() override
+    {
+        // connections & sockets come and go a lot.
+    }
+
+public:
+    /// Clear all external references
+    virtual void dispose() { _msgHandler.reset(); }
+
+    int sendTextMessage(const char *msg, const size_t len, bool flush = false) const override
+    {
+        LOG_TRC("ProxyHack - send text msg " + std::string(msg, len));
+        (void) flush;
+        return len;
+    }
+
+    int sendBinaryMessage(const char *data, const size_t len, bool flush = false) const override
+    {
+        (void) data; (void) flush;
+        LOG_TRC("ProxyHack - send binary msg len " << len);
+        return len;
+    }
+
+    void shutdown(bool goingAway = false, const std::string &statusMessage = "") override
+    {
+        LOG_TRC("ProxyHack - shutdown " << goingAway << ": " << statusMessage);
+    }
+
+    void getIOStats(uint64_t &sent, uint64_t &recv) override
+    {
+        sent = recv = 0;
+    }
+
+    void dumpState(std::ostream& os)
+    {
+        os << "proxy protocol\n";
+    }
+
+    void handleRequest(const std::string &uriPublic,
+                       const std::shared_ptr<Socket> &socket);
+
+private:
+    std::vector<std::weak_ptr<StreamSocket>> _sockets;
+};
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
commit fd3af1db2bfc6650128fc7441da85ed42e8f680b
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Mar 4 13:52:51 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 23 13:40:23 2020 +0100

    ProxyPrefix: allow the user to specify a custom prefix.
    
    This allows us to re-direct web traffic via a proxy quite simply
    during fetch, instead of changing the service root.
    
    Change-Id: I28d348467e48394d581fca4da4c199348a2ca8e0

diff --git a/loleaflet/html/loleaflet.html.m4 b/loleaflet/html/loleaflet.html.m4
index 945449d9f..3b63c87ff 100644
--- a/loleaflet/html/loleaflet.html.m4
+++ b/loleaflet/html/loleaflet.html.m4
@@ -241,6 +241,7 @@ m4_ifelse(MOBILEAPP,[true],
       window.reuseCookies = '';
       window.protocolDebug = false;
       window.frameAncestors = '';
+      window.socketProxy = false;
       window.tileSize = 256;],
      [window.host = '%HOST%';
       window.serviceRoot = '%SERVICE_ROOT%';
@@ -255,6 +256,7 @@ m4_ifelse(MOBILEAPP,[true],
       window.reuseCookies = '%REUSE_COOKIES%';
       window.protocolDebug = %PROTOCOL_DEBUG%;
       window.frameAncestors = '%FRAME_ANCESTORS%';
+      window.socketProxy = %SOCKET_PROXY%;
       window.tileSize = 256;])
 m4_syscmd([cat ]GLOBAL_JS)m4_dnl
 
diff --git a/wsd/FileServer.cpp b/wsd/FileServer.cpp
index 432a09b1c..8073860c2 100644
--- a/wsd/FileServer.cpp
+++ b/wsd/FileServer.cpp
@@ -640,6 +640,17 @@ constexpr char BRANDING[] = "branding";
 constexpr char BRANDING_UNSUPPORTED[] = "branding-unsupported";
 #endif
 
+namespace {
+    // The user can override the ServerRoot with a new prefix.
+    std::string getResponseRoot(const HTTPRequest &request)
+    {
+        if (!request.has("ProxyPrefix"))
+            return LOOLWSD::ServiceRoot;
+        std::string proxyPrefix = request.get("ProxyPrefix", "");
+        return proxyPrefix;
+    }
+}
+
 void FileServerRequestHandler::preprocessFile(const HTTPRequest& request, Poco::MemoryInputStream& message,
                                               const std::shared_ptr<StreamSocket>& socket)
 {
@@ -686,15 +697,21 @@ void FileServerRequestHandler::preprocessFile(const HTTPRequest& request, Poco::
         }
     }
 
-    const auto& config = Application::instance().config();
+    std::string socketProxy = "false";
+    if (request.has("ProxyPrefix"))
+        socketProxy = "true";
+    Poco::replaceInPlace(preprocess, std::string("%SOCKET_PROXY%"), socketProxy);
+
+    std::string responseRoot = getResponseRoot(request);
 
     Poco::replaceInPlace(preprocess, std::string("%ACCESS_TOKEN%"), escapedAccessToken);
     Poco::replaceInPlace(preprocess, std::string("%ACCESS_TOKEN_TTL%"), std::to_string(tokenTtl));
     Poco::replaceInPlace(preprocess, std::string("%ACCESS_HEADER%"), escapedAccessHeader);
     Poco::replaceInPlace(preprocess, std::string("%HOST%"), host);
     Poco::replaceInPlace(preprocess, std::string("%VERSION%"), std::string(LOOLWSD_VERSION_HASH));
-    Poco::replaceInPlace(preprocess, std::string("%SERVICE_ROOT%"), LOOLWSD::ServiceRoot);
+    Poco::replaceInPlace(preprocess, std::string("%SERVICE_ROOT%"), responseRoot);
 
+    const auto& config = Application::instance().config();
     std::string protocolDebug = "false";
     if (config.getBool("logging.protocol"))
         protocolDebug = "true";
@@ -703,16 +720,16 @@ void FileServerRequestHandler::preprocessFile(const HTTPRequest& request, Poco::
     static const std::string linkCSS("<link rel=\"stylesheet\" href=\"%s/loleaflet/" LOOLWSD_VERSION_HASH "/%s.css\">");
     static const std::string scriptJS("<script src=\"%s/loleaflet/" LOOLWSD_VERSION_HASH "/%s.js\"></script>");
 
-    std::string brandCSS(Poco::format(linkCSS, LOOLWSD::ServiceRoot, std::string(BRANDING)));
-    std::string brandJS(Poco::format(scriptJS, LOOLWSD::ServiceRoot, std::string(BRANDING)));
+    std::string brandCSS(Poco::format(linkCSS, responseRoot, std::string(BRANDING)));
+    std::string brandJS(Poco::format(scriptJS, responseRoot, std::string(BRANDING)));
 
 #if ENABLE_SUPPORT_KEY
     const std::string keyString = config.getString("support_key", "");
     SupportKey key(keyString);
     if (!key.verify() || key.validDaysRemaining() <= 0)
     {
-        brandCSS = Poco::format(linkCSS, LOOLWSD::ServiceRoot, std::string(BRANDING_UNSUPPORTED));
-        brandJS = Poco::format(scriptJS, LOOLWSD::ServiceRoot, std::string(BRANDING_UNSUPPORTED));
+        brandCSS = Poco::format(linkCSS, responseRoot, std::string(BRANDING_UNSUPPORTED));
+        brandJS = Poco::format(scriptJS, responseRoot, std::string(BRANDING_UNSUPPORTED));
     }
 #endif
 
@@ -905,13 +922,15 @@ void FileServerRequestHandler::preprocessAdminFile(const HTTPRequest& request,co
     if (!FileServerRequestHandler::isAdminLoggedIn(request, response))
         throw Poco::Net::NotAuthenticatedException("Invalid admin login");
 
+    std::string responseRoot = getResponseRoot(request);
+
     static const std::string scriptJS("<script src=\"%s/loleaflet/" LOOLWSD_VERSION_HASH "/%s.js\"></script>");
     static const std::string footerPage("<div class=\"footer navbar-fixed-bottom text-info text-center\"><strong>Key:</strong> %s   <strong>Expiry Date:</strong> %s</div>");
 
     const std::string relPath = getRequestPathname(request);
     LOG_DBG("Preprocessing file: " << relPath);
     std::string adminFile = *getUncompressedFile(relPath);
-    std::string brandJS(Poco::format(scriptJS, LOOLWSD::ServiceRoot, std::string(BRANDING)));
+    std::string brandJS(Poco::format(scriptJS, responseRoot, std::string(BRANDING)));
     std::string brandFooter;
 
 #if ENABLE_SUPPORT_KEY
@@ -929,7 +948,7 @@ void FileServerRequestHandler::preprocessAdminFile(const HTTPRequest& request,co
     Poco::replaceInPlace(adminFile, std::string("<!--%BRANDING_JS%-->"), brandJS);
     Poco::replaceInPlace(adminFile, std::string("<!--%FOOTER%-->"), brandFooter);
     Poco::replaceInPlace(adminFile, std::string("%VERSION%"), std::string(LOOLWSD_VERSION_HASH));
-    Poco::replaceInPlace(adminFile, std::string("%SERVICE_ROOT%"), LOOLWSD::ServiceRoot);
+    Poco::replaceInPlace(adminFile, std::string("%SERVICE_ROOT%"), responseRoot);
 
     // Ask UAs to block if they detect any XSS attempt
     response.add("X-XSS-Protection", "1; mode=block");
commit 8f05c156563b882f3e843df54dbd46f142b804a3
Author:     Pedro Pinto Silva <pedro.silva at collabora.com>
AuthorDate: Thu Apr 23 14:11:42 2020 +0200
Commit:     Pedro Pinto da Silva <pedro.silva at collabora.com>
CommitDate: Thu Apr 23 14:31:31 2020 +0200

    Mobile: mWizard: disabled input fields: make sure both image and spinfield controls do not stay active
    
    And make sure the js listener is only added when the inputfield is enabled (fixing useless callbacks and changes on html)
    
    Change-Id: I4f4f2b4430b1c43143f95ab9d9fe075c8e1f49b8
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92763
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Tested-by: Pedro Pinto da Silva <pedro.silva at collabora.com>
    Reviewed-by: Pedro Pinto da Silva <pedro.silva at collabora.com>

diff --git a/loleaflet/css/mobilewizard.css b/loleaflet/css/mobilewizard.css
index 1a8919e4e..d2b7d73b6 100644
--- a/loleaflet/css/mobilewizard.css
+++ b/loleaflet/css/mobilewizard.css
@@ -540,8 +540,11 @@ input.spinfield:disabled ~ .spinfieldcontrols * {
 	background-color: #f9f9f9;
 }
 
-input.spinfield:disabled ~ .spinfieldcontrols *:active {
+input.spinfield:disabled ~ .spinfieldcontrols *:active, input.spinfield:disabled ~ .spinfieldcontrols *:hover, input.spinfield:disabled ~ .spinfieldcontrols *:focus {
 	background-color: #f9f9f9 !important;
+	filter: none;
+	font-size: 100%;
+	font-weight: normal;
 }
 
 /*insert table special layout*/
@@ -701,6 +704,9 @@ div#mobile-wizard-content .spinfieldcontainer .spinfieldimage {
 div#mobile-wizard-content .spinfieldcontainer:active .spinfieldimage, div#mobile-wizard-content .spinfieldcontainer:focus .spinfieldimage, div#mobile-wizard-content .spinfieldcontainer:hover .spinfieldimage {
 	opacity: 1 !important;
 }
+div#mobile-wizard-content .spinfieldcontainer .spinfieldimage.disabled {
+	opacity: 0.5 !important;
+}
 #buttonbefore, #buttonafter, #buttonoptimal, #buttonparallel, #buttonnone, #buttonthrough, #bottom, #top, #standard, #Bold, #Italic, #Underline, #Strikeout{
 	margin: 0;
 	padding: 0;
diff --git a/loleaflet/src/control/Control.JSDialogBuilder.js b/loleaflet/src/control/Control.JSDialogBuilder.js
index 323f69bd7..c9a410feb 100644
--- a/loleaflet/src/control/Control.JSDialogBuilder.js
+++ b/loleaflet/src/control/Control.JSDialogBuilder.js
@@ -1044,8 +1044,10 @@ L.Control.JSDialogBuilder = L.Control.extend({
 		if (data.max != undefined)
 			$(spinfield).attr('max', data.max);
 
-		if (data.enabled == 'false')
+		if (data.enabled == 'false') {
 			$(spinfield).attr('disabled', 'disabled');
+			$(image).addClass('disabled');
+		}
 
 		if (data.readOnly === true)
 			$(spinfield).attr('readOnly', 'true');
@@ -1081,17 +1083,23 @@ L.Control.JSDialogBuilder = L.Control.extend({
 		});
 
 		plus.addEventListener('click', function() {
-			if (customCallback)
-				customCallback('spinfield', 'plus', div, this.value, builder);
-			else
-				builder.callback('spinfield', 'plus', div, this.value, builder);
+			var attrdisabled = $(spinfield).attr('disabled');
+			if (attrdisabled !== 'disabled') {
+				if (customCallback)
+					customCallback('spinfield', 'plus', div, this.value, builder);
+				else
+					builder.callback('spinfield', 'plus', div, this.value, builder);
+			}
 		});
 
 		minus.addEventListener('click', function() {
-			if (customCallback)
-				customCallback('spinfield', 'minus', div, this.value, builder);
-			else
-				builder.callback('spinfield', 'minus', div, this.value, builder);
+			var attrdisabled = $(spinfield).attr('disabled');
+			if (attrdisabled !== 'disabled') {
+				if (customCallback)
+					customCallback('spinfield', 'minus', div, this.value, builder);
+				else
+					builder.callback('spinfield', 'minus', div, this.value, builder);
+			}
 		});
 
 		if (data.hidden)
commit 15c2235d160bdbbc163d0d370d46cb5a61a62acf
Author:     Pranam Lashkari <lpranam at collabora.com>
AuthorDate: Thu Apr 23 13:57:09 2020 +0530
Commit:     Szymon Kłos <szymon.klos at collabora.com>
CommitDate: Thu Apr 23 13:21:04 2020 +0200

    leaflet: changed ID of shape fill style
    
    Change-Id: Ib8ad95cf265afbee88f02c3d7746a030e44bbf7c
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92743
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Szymon Kłos <szymon.klos at collabora.com>

diff --git a/loleaflet/src/control/Control.JSDialogBuilder.js b/loleaflet/src/control/Control.JSDialogBuilder.js
index f2cb686c6..323f69bd7 100644
--- a/loleaflet/src/control/Control.JSDialogBuilder.js
+++ b/loleaflet/src/control/Control.JSDialogBuilder.js
@@ -862,7 +862,7 @@ L.Control.JSDialogBuilder = L.Control.extend({
 			}
 			break;
 
-		case 'fillstyle':
+		case 'fillstylearea':
 			state = items.getItemValue('.uno:FillStyle');
 			if (state) {
 				switch (state) {
commit 313db8a8cd33e2a9c98daf9d6b70cdfb5af5175b
Author:     Pranam Lashkari <lpranam at collabora.com>
AuthorDate: Tue Apr 21 14:49:13 2020 +0530
Commit:     Szymon Kłos <szymon.klos at collabora.com>
CommitDate: Thu Apr 23 13:19:48 2020 +0200

    leaflet: resolved impress slide background gradient not changing
    
    Change-Id: If71b380859c202327a533b121af45232682e52df
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92088
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Szymon Kłos <szymon.klos at collabora.com>

diff --git a/loleaflet/src/control/Control.JSDialogBuilder.js b/loleaflet/src/control/Control.JSDialogBuilder.js
index f5f92bb6f..f2cb686c6 100644
--- a/loleaflet/src/control/Control.JSDialogBuilder.js
+++ b/loleaflet/src/control/Control.JSDialogBuilder.js
@@ -886,10 +886,23 @@ L.Control.JSDialogBuilder = L.Control.extend({
 			break;
 
 		case 'fillattr':
-			var hatch = items.getItemValue('.uno:FillHatch');
-			var bitmap = items.getItemValue('.uno:FillBitmap');
-			if (hatch || bitmap) {
-				// TODO
+			state = items.getItemValue('.uno:FillPageColor');
+			if (state) {
+				return state;
+			}
+			break;
+
+		case 'fillattr2':
+			state = items.getItemValue('.uno:FillPageGradient');
+			if (state) {
+				return state.startcolor;
+			}
+			break;
+
+		case 'fillattr3':
+			state = items.getItemValue('.uno:FillPageGradient');
+			if (state) {
+				return state.endcolor;
 			}
 			break;
 
@@ -1397,6 +1410,16 @@ L.Control.JSDialogBuilder = L.Control.extend({
 		} else if (data.id === 'fillattr') {
 			builder.map.sendUnoCommand('.uno:FillPageColor?Color:string=' + color);
 			return;
+		} else if (data.id === 'fillattr2') {
+			gradientItem = builder.map['stateChangeHandler'].getItemValue('.uno:FillPageGradient');
+			gradientItem.startcolor = color;
+			builder.map.sendUnoCommand('.uno:FillPageGradient?FillPageGradientJSON:string=' + JSON.stringify(gradientItem));
+			return;
+		} else if (data.id === 'fillattr3') {
+			gradientItem = builder.map['stateChangeHandler'].getItemValue('.uno:FillPageGradient');
+			gradientItem.endcolor = color;
+			builder.map.sendUnoCommand('.uno:FillPageGradient?FillPageGradientJSON:string=' + JSON.stringify(gradientItem));
+			return;
 		}
 
 		var command = data.command + '?Color:string=' + color;
commit a2410c599cf4855fa1ed04194e76c291a134964b
Author:     Henry Castro <hcastro at collabora.com>
AuthorDate: Wed Apr 22 12:33:33 2020 -0400
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu Apr 23 09:09:11 2020 +0200

    android: convert remaining rules to gradle tasks
    
    It will give an Independence (at least) to the
    gradle build system to package the product
    
    Change-Id: I127c2f921b506ec280a244d609707f3480e0f92e
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92719
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Andras Timar <andras.timar at collabora.com>

diff --git a/Makefile.am b/Makefile.am
index ff7ad0b5f..dcbfbdcbb 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -5,12 +5,8 @@ if ENABLE_MOBILEAPP
 if ENABLE_GTKAPP
 SUBDIRS = gtk loleaflet
 else
-if ENABLE_ANDROIDAPP
-SUBDIRS = loleaflet android
-else
 SUBDIRS = loleaflet
 endif
-endif
 
 else
 
diff --git a/android/Makefile.am b/android/Makefile.am
deleted file mode 100644
index d717a8b6a..000000000
--- a/android/Makefile.am
+++ /dev/null
@@ -1,16 +0,0 @@
-clean-local:
-	rm -rf $(abs_top_srcdir)/android/lib/src/main/assets/*
-	rm -rf app/build
-
-all-local: lib/src/main/assets/templates/untitled.odp \
-	   lib/src/main/assets/templates/untitled.ods \
-	   lib/src/main/assets/templates/untitled.odt \
-	   lib/src/main/assets/etc/loolwsd/loolkitconfig.xcu
-
-lib/src/main/assets/templates/untitled.%: templates/untitled.%
-	@mkdir -p $(dir $@)
-	@cp -a $< $@
-
-lib/src/main/assets/etc/loolwsd/loolkitconfig.xcu: $(top_srcdir)/loolkitconfig-mobile.xcu
-	@mkdir -p $(dir $@)
-	@cp -a $< $@
diff --git a/android/lib/build.gradle b/android/lib/build.gradle
index 94c505d42..50cb2fc43 100644
--- a/android/lib/build.gradle
+++ b/android/lib/build.gradle
@@ -186,6 +186,19 @@ task copyBrandTheme(type: Copy) {
 	into "src/main/assets/share/theme_definitions/online"
 }
 
+task copyDocTemplates(type: Copy) {
+        from "${rootProject.getRootDir()}/templates/untitled.odp",
+             "${rootProject.getRootDir()}/templates/untitled.ods",
+             "${rootProject.getRootDir()}/templates/untitled.odt"
+        into "src/main/assets/templates"
+}
+
+task copyKitConfig(type: Copy) {

... etc. - the rest is truncated


More information about the Libreoffice-commits mailing list