wsd: correctly remove request from socket buffer

POST requests require the full request to be
left in the socket buffer to be parsed in full.
But GET requests, especially WS upgrade, must
have the request cleared from the socket, as
there is more data expected to be read after
the upgrade, which happens by the DocBroker
thread, so clearing the buffer must be done
before the upgrade.

This patch accomodates these two conflicting
cases and refactors the code slightly to
make it more structured and readable.

Change-Id: Ia7357a745a3900f986099ba14af2a0946023018b
Reviewed-on: https://gerrit.libreoffice.org/36873
Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
Tested-by: Ashod Nakashian <ashnakash@gmail.com>
This commit is contained in:
Ashod Nakashian 2017-04-24 00:44:14 -04:00 committed by Ashod Nakashian
parent 988ddaf7be
commit c851c3e93b
2 changed files with 144 additions and 118 deletions

View file

@ -34,8 +34,8 @@ constexpr auto CHILD_URI = "/loolws/child?";
constexpr auto NEW_CHILD_URI = "/loolws/newchild?";
constexpr auto LO_JAIL_SUBPATH = "lo";
/// The HTTP response User-Agent. TODO: Include version.
constexpr auto HTTP_AGENT_STRING = "LOOLWSD Agent";
/// The HTTP response User-Agent.
constexpr auto HTTP_AGENT_STRING = "LOOLWSD Agent " LOOLWSD_VERSION;
// The client port number, both loolwsd and the kits have this.
extern int ClientPortNumber;

View file

@ -1598,46 +1598,47 @@ private:
Poco::URI requestUri(request.getURI());
std::vector<std::string> reqPathSegs;
requestUri.getPathSegments(reqPathSegs);
const StringTokenizer reqPathTokens(request.getURI(), "/?", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM);
// File server
if (reqPathSegs.size() >= 1 && reqPathSegs[0] == "loleaflet")
if (request.getMethod() == HTTPRequest::HTTP_GET ||
request.getMethod() == HTTPRequest::HTTP_HEAD)
{
handleFileServerRequest(request, message);
}
// Admin connections
else if (reqPathSegs.size() >= 2 && reqPathSegs[0] == "lool" && reqPathSegs[1] == "adminws")
{
LOG_ERR("Admin request: " << request.getURI());
if (AdminSocketHandler::handleInitialRequest(_socket, request))
{
// Hand the socket over to the Admin poll.
Admin::instance().insertNewSocket(socket);
socketOwnership = SocketHandlerInterface::SocketOwnership::MOVED;
}
}
// Client post and websocket connections
else if ((request.getMethod() == HTTPRequest::HTTP_GET ||
request.getMethod() == HTTPRequest::HTTP_HEAD) &&
request.getURI() == "/")
{
handleRootRequest(request);
}
else if (request.getMethod() == HTTPRequest::HTTP_GET && request.getURI() == "/favicon.ico")
{
handleFaviconRequest(request);
}
else if (request.getMethod() == HTTPRequest::HTTP_GET && request.getURI() == "/hosting/discovery")
{
handleWopiDiscoveryRequest(request);
}
else
{
StringTokenizer reqPathTokens(request.getURI(), "/?", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM);
// Clear the request header already consumed.
in.erase(in.begin(), itBody);
// File server
if (!(request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0) &&
reqPathTokens.count() > 0 && reqPathTokens[0] == "lool")
reqPathTokens.count() >= 5 && reqPathTokens[0] == "lool")
{
// All post requests have url prefix 'lool'.
socketOwnership = handlePostRequest(request, message);
handleFileDownloadRequest(request);
}
else if (reqPathSegs.size() >= 1 && reqPathSegs[0] == "loleaflet")
{
handleFileServerRequest(request, message);
}
else if (reqPathSegs.size() >= 2 && reqPathSegs[0] == "lool" && reqPathSegs[1] == "adminws")
{
// Admin connections
LOG_INF("Admin request: " << request.getURI());
if (AdminSocketHandler::handleInitialRequest(_socket, request))
{
// Hand the socket over to the Admin poll.
Admin::instance().insertNewSocket(socket);
socketOwnership = SocketHandlerInterface::SocketOwnership::MOVED;
}
}
else if (request.getURI() == "/")
{
handleRootRequest(request);
}
else if (request.getURI() == "/favicon.ico")
{
handleFaviconRequest(request);
}
else if (request.getURI() == "/hosting/discovery")
{
handleWopiDiscoveryRequest(request);
}
else if (reqPathTokens.count() > 2 && reqPathTokens[0] == "lool" && reqPathTokens[2] == "ws" &&
request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0)
@ -1646,19 +1647,31 @@ private:
}
else
{
LOG_ERR("Unknown resource: " << request.getURI());
throw std::runtime_error("Bad request.");
}
// Bad request.
std::ostringstream oss;
oss << "HTTP/1.1 400\r\n"
<< "Date: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n"
<< "User-Agent: LOOLWSD WOPI Agent\r\n"
<< "Content-Length: 0\r\n"
<< "\r\n";
socket->send(oss.str());
socket->shutdown();
return socketOwnership;
}
else if (request.getMethod() == HTTPRequest::HTTP_POST)
{
if (reqPathSegs.size() >= 1 && reqPathSegs[0] == "loleaflet")
{
handleFileServerRequest(request, message);
return socketOwnership;
}
if (!(request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0) &&
reqPathTokens.count() > 0 && reqPathTokens[0] == "lool")
{
// All post requests have url prefix 'lool'.
socketOwnership = handlePostRequest(request, message);
in.erase(in.begin(), itBody);
return socketOwnership;
}
}
LOG_ERR("#" << socket->getFD() << " Unknown request: [" <<
LOOLProtocol::getAbbreviatedMessage(in) << "].");
}
catch (const std::exception& exc)
{
@ -1668,10 +1681,17 @@ private:
LOOLProtocol::getAbbreviatedMessage(in) << "]: " << exc.what());
}
// if we succeeded - remove the request from our input buffer
// we expect one request per socket
in.erase(in.begin(), itBody);
return socketOwnership;
// Bad request.
std::ostringstream oss;
oss << "HTTP/1.1 400\r\n"
<< "Date: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n"
<< "User-Agent: " << HTTP_AGENT_STRING << "\r\n"
<< "Content-Length: 0\r\n"
<< "\r\n";
socket->send(oss.str());
socket->shutdown();
return SocketHandlerInterface::SocketOwnership::UNCHANGED;
}
int getPollEvents(std::chrono::steady_clock::time_point /* now */,
@ -1946,77 +1966,83 @@ private:
}
}
}
else if (tokens.count() >= 6)
{
LOG_INF("File download request.");
// TODO: Check that the user in question has access to this file!
// 1. Validate the dockey
std::string decodedUri;
URI::decode(tokens[2], decodedUri);
const auto docKey = DocumentBroker::getDocKey(DocumentBroker::sanitizeURI(decodedUri));
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto docBrokerIt = DocBrokers.find(docKey);
if (docBrokerIt == DocBrokers.end())
{
throw BadRequestException("DocKey [" + docKey + "] is invalid.");
}
// 2. Cross-check if received child id is correct
if (docBrokerIt->second->getJailId() != tokens[3])
{
throw BadRequestException("ChildId does not correspond to docKey");
}
// 3. Don't let user download the file in main doc directory containing
// the document being edited otherwise we will end up deleting main directory
// after download finishes
if (docBrokerIt->second->getJailId() == tokens[4])
{
throw BadRequestException("RandomDir cannot be equal to ChildId");
}
docBrokersLock.unlock();
std::string fileName;
bool responded = false;
URI::decode(tokens[5], fileName);
const Path filePath(LOOLWSD::ChildRoot + tokens[3]
+ JAILED_DOCUMENT_ROOT + tokens[4] + "/" + fileName);
LOG_INF("HTTP request for: " << filePath.toString());
if (filePath.isAbsolute() && File(filePath).exists())
{
std::string contentType = getContentType(fileName);
if (Poco::Path(fileName).getExtension() == "pdf")
{
contentType = "application/pdf";
response.set("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
}
try
{
HttpHelper::sendFile(socket, filePath.toString(), contentType, response);
responded = true;
}
catch (const Exception& exc)
{
LOG_ERR("Error sending file to client: " << exc.displayText() <<
(exc.nested() ? " (" + exc.nested()->displayText() + ")" : ""));
}
FileUtil::removeFile(File(filePath.parent()).path(), true);
}
else
{
LOG_ERR("Download file [" << filePath.toString() << "] not found.");
}
(void)responded;
return socketOwnership;
}
throw BadRequestException("Invalid or unknown request.");
}
void handleFileDownloadRequest(const Poco::Net::HTTPRequest& request)
{
LOG_INF("File download request.");
Poco::Net::HTTPResponse response;
auto socket = _socket.lock();
StringTokenizer tokens(request.getURI(), "/?");
// TODO: Check that the user in question has access to this file!
// 1. Validate the dockey
std::string decodedUri;
URI::decode(tokens[2], decodedUri);
const auto docKey = DocumentBroker::getDocKey(DocumentBroker::sanitizeURI(decodedUri));
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto docBrokerIt = DocBrokers.find(docKey);
if (docBrokerIt == DocBrokers.end())
{
throw BadRequestException("DocKey [" + docKey + "] is invalid.");
}
// 2. Cross-check if received child id is correct
if (docBrokerIt->second->getJailId() != tokens[3])
{
throw BadRequestException("ChildId does not correspond to docKey");
}
// 3. Don't let user download the file in main doc directory containing
// the document being edited otherwise we will end up deleting main directory
// after download finishes
if (docBrokerIt->second->getJailId() == tokens[4])
{
throw BadRequestException("RandomDir cannot be equal to ChildId");
}
docBrokersLock.unlock();
std::string fileName;
bool responded = false;
URI::decode(tokens[5], fileName);
const Path filePath(LOOLWSD::ChildRoot + tokens[3]
+ JAILED_DOCUMENT_ROOT + tokens[4] + "/" + fileName);
LOG_INF("HTTP request for: " << filePath.toString());
if (filePath.isAbsolute() && File(filePath).exists())
{
std::string contentType = getContentType(fileName);
if (Poco::Path(fileName).getExtension() == "pdf")
{
contentType = "application/pdf";
response.set("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
}
try
{
HttpHelper::sendFile(socket, filePath.toString(), contentType, response);
responded = true;
}
catch (const Exception& exc)
{
LOG_ERR("Error sending file to client: " << exc.displayText() <<
(exc.nested() ? " (" + exc.nested()->displayText() + ")" : ""));
}
FileUtil::removeFile(File(filePath.parent()).path(), true);
}
else
{
LOG_ERR("Download file [" << filePath.toString() << "] not found.");
}
// TODO: Use this to send failure response.
(void)responded;
}
SocketHandlerInterface::SocketOwnership handleClientWsUpgrade(const Poco::Net::HTTPRequest& request, const std::string& url)
{
auto socket = _socket.lock();