libreoffice-online/wsd/ProxyProtocol.cpp
Luboš Luňák 897c5df270 make Socket use Buffer for input buffer too
SteamSocket::eraseFirstInputBytes() removes from the beginning
of std::vector, which is generally slow. If the buffer becomes
too big, which it may under a load, then the function will get
slow, which in turn will likely lead to the buffer getting even
bigger because of accumulated backlog.

The Buffer class is optimized for removal at the beginning,
so use it instead of std::vector, including some API additions
for it to be an in-place replacement where it's used.

Signed-off-by: Luboš Luňák <l.lunak@collabora.com>
Change-Id: I4cf7ec56c908c7d3df391dc3f8e230ad32abb162
2021-10-26 12:11:58 +02:00

376 lines
12 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* The ProxyProtocol creates a web-socket like connection over HTTP
* requests. URLs are formed like this:
* 0 1 2 3 4 5
* /lool/<encoded-document-url>/ws/<session-id>/<command>/<serial>
* <session-id> can be 'unknown'
* <command> can be 'open', 'write', 'wait', or 'close'
*/
#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& id,
const Poco::URI& uriPublic,
const bool isReadOnly,
const RequestDetails &requestDetails,
const std::shared_ptr<StreamSocket> &socket)
{
std::shared_ptr<ClientSession> clientSession;
if (requestDetails.equals(RequestDetails::Field::Command, "open"))
{
bool isLocal = socket->isLocal();
LOG_TRC("proxy: validate that socket is from localhost: " << isLocal);
if (!isLocal)
throw BadRequestException("invalid host - only connect from localhost");
LOG_TRC("proxy: Create session for " << _docKey);
clientSession = createNewClientSession(
std::make_shared<ProxyProtocolHandler>(),
id, uriPublic, isReadOnly, requestDetails);
addSession(clientSession);
LOOLWSD::checkDiskSpaceAndWarnClients(true);
LOOLWSD::checkSessionLimitsAndWarnClients();
const std::string &sessionId = clientSession->getOrCreateProxyAccess();
LOG_TRC("proxy: Returning sessionId " << sessionId);
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: " << sessionId.size() << "\r\n"
"Content-Type: application/json\r\n"
"X-Content-Type-Options: nosniff\r\n"
"\r\n" << sessionId;
socket->send(oss.str());
socket->shutdown();
return;
}
else
{
const std::string sessionId = requestDetails.getField(RequestDetails::Field::SessionId);
LOG_TRC("proxy: find session for " << _docKey << " with id " << sessionId);
for (const auto &it : _sessions)
{
if (it.second->getOrCreateProxyAccess() == 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);
if (requestDetails.equals(RequestDetails::Field::Command, "close"))
{
LOG_TRC("Close session");
proxy->notifyDisconnected();
return;
}
const bool isWaiting = requestDetails.equals(RequestDetails::Field::Command, "wait");
proxy->handleRequest(isWaiting, socket);
}
bool ProxyProtocolHandler::parseEmitIncoming(
const std::shared_ptr<StreamSocket> &socket)
{
Buffer& in = socket->getInBuffer();
#if 0 // protocol debugging.
std::stringstream oss;
socket->dumpState(oss);
LOG_TRC("Parse message:\n" << oss.str());
#endif
while (in.size() > 0)
{
// 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 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);
in.eraseFirst(len);
if (in.size() < 1 || in[0] != '\n')
{
LOG_ERR("Missing final newline");
return false;
}
in.eraseFirst(1);
if (serial != _inSerial + 1)
LOG_ERR("Serial mismatch " << serial << " vs. " << (_inSerial + 1));
_inSerial = serial;
_msgHandler->handleMessage(data);
}
return true;
}
void ProxyProtocolHandler::handleRequest(bool isWaiting, const std::shared_ptr<Socket> &socket)
{
auto streamSocket = std::static_pointer_cast<StreamSocket>(socket);
LOG_INF("proxy: handle request type: " << (isWaiting ? "wait" : "respond") <<
" on socket #" << socket->getFD());
if (!isWaiting)
{
if (!_msgHandler)
LOG_WRN("proxy: unusual - incoming message with no-one to handle it");
else if (!parseEmitIncoming(streamSocket))
{
std::stringstream oss;
streamSocket->dumpState(oss);
LOG_ERR("proxy: bad socket structure " << oss.str());
}
}
bool sentMsg = flushQueueTo(streamSocket);
if (!sentMsg && isWaiting)
{
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("proxy: 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
{
if (!sentMsg)
{
// FIXME: we should really wait around a bit.
LOG_TRC("Nothing to send - closing immediately");
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: " << 0 << "\r\n"
"\r\n";
streamSocket->send(oss.str());
}
else
LOG_TRC("Returned a reply immediately");
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());
}
void ProxyProtocolHandler::notifyDisconnected()
{
if (_msgHandler)
_msgHandler->onDisconnect();
}
int ProxyProtocolHandler::sendMessage(const char *msg, const size_t len, bool text, bool flush)
{
_writeQueue.push_back(std::make_shared<Message>(msg, len, text, _outSerial++));
if (flush)
{
auto sock = popOutSocket();
if (sock)
{
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::dumpProxyState(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 (const auto& it : _writeQueue)
Util::dumpHex(os, *it, "\twrite queue entry:", "\t\t");
if (_msgHandler)
_msgHandler->dumpState(os);
}
int ProxyProtocolHandler::getPollEvents(std::chrono::steady_clock::time_point /* now */,
int64_t &/* timeoutMaxMs */)
{
int events = POLLIN;
if (_msgHandler && _msgHandler->hasQueuedMessages())
events |= POLLOUT;
return events;
}
/// slurp from the core to us, @returns true if there are messages to send
bool ProxyProtocolHandler::slurpHasMessages(std::size_t capacity)
{
if (_msgHandler)
_msgHandler->writeQueuedMessages(capacity);
return _writeQueue.size() > 0;
}
void ProxyProtocolHandler::performWrites(std::size_t capacity)
{
if (!slurpHasMessages(capacity))
return;
auto sock = popOutSocket();
if (sock)
{
LOG_TRC("proxy: performWrites");
flushQueueTo(sock);
sock->shutdown();
}
}
bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &socket)
{
if (!slurpHasMessages(socket->getSendBufferCapacity()))
return false;
size_t totalSize = 0;
for (const auto& it : _writeQueue)
totalSize += it->size();
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"
"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 (const auto& it : _writeQueue)
socket->send(it->data(), it->size(), false);
_writeQueue.clear();
return true;
}
// LRU-ness ...
std::shared_ptr<StreamSocket> ProxyProtocolHandler::popOutSocket()
{
std::weak_ptr<StreamSocket> sock;
while (!_outSockets.empty())
{
sock = _outSockets.front();
_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>();
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */