libreoffice-online/wsd/ClientRequestDispatcher.cpp
Ashod Nakashian f1001cddcc wsd: remove unused (Is)ViewWithCommentsFileExtension(s)
Change-Id: I25158862746ce6a3e4ee16ff9d661ec96810ce24
Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
2024-05-21 04:52:12 -04:00

2026 lines
79 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* Copyright the Collabora Online contributors.
*
* SPDX-License-Identifier: MPL-2.0
*
* 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 <config_version.h>
#include <ClientRequestDispatcher.hpp>
#if ENABLE_FEATURE_LOCK
#include "CommandControl.hpp"
#endif
#include <Admin.hpp>
#include <COOLWSD.hpp>
#include <ClientSession.hpp>
#include <ConfigUtil.hpp>
#include <DocumentBroker.hpp>
#include <Exceptions.hpp>
#include <FileServer.hpp>
#include <HttpRequest.hpp>
#include <JailUtil.hpp>
#include <ProofKey.hpp>
#include <ProxyRequestHandler.hpp>
#include <RequestDetails.hpp>
#include <Socket.hpp>
#include <UserMessages.hpp>
#include <Util.hpp>
#include <net/AsyncDNS.hpp>
#include <net/HttpHelper.hpp>
#if !MOBILEAPP
#include <HostUtil.hpp>
#endif // !MOBILEAPP
#include <Poco/DOM/AutoPtr.h>
#include <Poco/DOM/DOMParser.h>
#include <Poco/DOM/DOMWriter.h>
#include <Poco/DOM/Document.h>
#include <Poco/DOM/Element.h>
#include <Poco/DOM/NodeList.h>
#include <Poco/File.h>
#include <Poco/MemoryStream.h>
#include <Poco/Net/HTMLForm.h>
#include <Poco/Net/NetException.h>
#include <Poco/Net/PartHandler.h>
#include <Poco/SAX/InputSource.h>
#include <Poco/StreamCopier.h>
#include <map>
#include <memory>
#include <string>
#include <vector>
std::map<std::string, std::string> ClientRequestDispatcher::StaticFileContentCache;
std::unordered_map<std::string, std::shared_ptr<RequestVettingStation>>
ClientRequestDispatcher::RequestVettingStations;
extern std::map<std::string, std::shared_ptr<DocumentBroker>> DocBrokers;
extern std::mutex DocBrokersMutex;
extern void cleanupDocBrokers();
namespace
{
/// Used in support key enabled builds
inline void shutdownLimitReached(const std::shared_ptr<ProtocolHandlerInterface>& proto)
{
if (!proto)
return;
const std::string error = Poco::format(PAYLOAD_UNAVAILABLE_LIMIT_REACHED, COOLWSD::MaxDocuments,
COOLWSD::MaxConnections);
LOG_INF("Sending client 'hardlimitreached' message: " << error);
try
{
// Let the client know we are shutting down.
proto->sendTextMessage(error);
// Shutdown.
proto->shutdown(true, error);
}
catch (const std::exception& ex)
{
LOG_ERR("Error while shutting down socket on reaching limit: " << ex.what());
}
}
} // end anonymous namespace
/// Find the DocumentBroker for the given docKey, if one exists.
/// Otherwise, creates and adds a new one to DocBrokers.
/// May return null if terminating or MaxDocuments limit is reached.
/// Returns the error message, if any, when no DocBroker is created/found.
std::pair<std::shared_ptr<DocumentBroker>, std::string>
findOrCreateDocBroker(DocumentBroker::ChildType type, const std::string& uri,
const std::string& docKey, const std::string& id, const Poco::URI& uriPublic,
unsigned mobileAppDocId,
std::unique_ptr<WopiStorage::WOPIFileInfo> wopiFileInfo)
{
LOG_INF("Find or create DocBroker for docKey ["
<< docKey << "] for session [" << id << "] on url ["
<< COOLWSD::anonymizeUrl(uriPublic.toString()) << ']');
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
cleanupDocBrokers();
if (SigUtil::getShutdownRequestFlag())
{
// TerminationFlag implies ShutdownRequested.
LOG_WRN((SigUtil::getTerminationFlag() ? "TerminationFlag" : "ShudownRequestedFlag")
<< " set. Not loading new session [" << id << "] for docKey [" << docKey << ']');
return std::make_pair(nullptr, "error: cmd=load kind=recycling");
}
std::shared_ptr<DocumentBroker> docBroker;
// Lookup this document.
const auto it = DocBrokers.find(docKey);
if (it != DocBrokers.end() && it->second)
{
// Get the DocumentBroker from the Cache.
LOG_DBG("Found DocumentBroker with docKey [" << docKey << ']');
docBroker = it->second;
// Destroying the document? Let the client reconnect.
if (docBroker->isUnloadingUnrecoverably())
{
LOG_WRN("DocBroker [" << docKey
<< "] is unloading. Rejecting client request to load session ["
<< id << ']');
return std::make_pair(nullptr, "error: cmd=load kind=docunloading");
}
}
else
{
LOG_DBG("No DocumentBroker with docKey [" << docKey
<< "] found. Creating new Child and Document");
}
if (SigUtil::getShutdownRequestFlag())
{
// TerminationFlag implies ShutdownRequested.
LOG_ERR((SigUtil::getTerminationFlag() ? "TerminationFlag" : "ShudownRequestedFlag")
<< " set. Not loading new session [" << id << "] for docKey [" << docKey << ']');
return std::make_pair(nullptr, "error: cmd=load kind=recycling");
}
if (!docBroker)
{
Util::assertIsLocked(DocBrokersMutex);
if (DocBrokers.size() + 1 > COOLWSD::MaxDocuments)
{
LOG_WRN("Maximum number of open documents of "
<< COOLWSD::MaxDocuments << " reached while loading new session [" << id
<< "] for docKey [" << docKey << ']');
if (config::isSupportKeyEnabled())
{
const std::string error = Poco::format(PAYLOAD_UNAVAILABLE_LIMIT_REACHED,
COOLWSD::MaxDocuments, COOLWSD::MaxConnections);
return std::make_pair(nullptr, error);
}
}
// Set the one we just created.
LOG_DBG("New DocumentBroker for docKey [" << docKey << ']');
docBroker = std::make_shared<DocumentBroker>(type, uri, uriPublic, docKey, mobileAppDocId,
std::move(wopiFileInfo));
DocBrokers.emplace(docKey, docBroker);
LOG_TRC("Have " << DocBrokers.size() << " DocBrokers after inserting [" << docKey << ']');
}
return std::make_pair(docBroker, std::string());
}
#if !MOBILEAPP
/// For clipboard setting
class ClipboardPartHandler : public Poco::Net::PartHandler
{
std::shared_ptr<std::string> _data; // large.
public:
std::shared_ptr<std::string> getData() const { return _data; }
ClipboardPartHandler() {}
virtual void handlePart(const Poco::Net::MessageHeader& /* header */,
std::istream& stream) override
{
std::istreambuf_iterator<char> eos;
_data = std::make_shared<std::string>(std::istreambuf_iterator<char>(stream), eos);
LOG_TRC("Clipboard stream from part header stored of size " << _data->length());
}
};
/// Handles the filename part of the convert-to POST request payload,
/// Also owns the file - cleaning it up when destroyed.
class ConvertToPartHandler : public Poco::Net::PartHandler
{
std::string _filename;
public:
std::string getFilename() const { return _filename; }
/// Afterwards someone else is responsible for cleaning that up.
void takeFile() { _filename.clear(); }
ConvertToPartHandler() {}
virtual ~ConvertToPartHandler()
{
if (!_filename.empty())
{
LOG_TRC("Remove un-handled temporary file '" << _filename << '\'');
StatelessBatchBroker::removeFile(_filename);
}
}
virtual void handlePart(const Poco::Net::MessageHeader& header, std::istream& stream) override
{
// Extract filename and put it to a temporary directory.
std::string disp;
Poco::Net::NameValueCollection params;
if (header.has("Content-Disposition"))
{
std::string cd = header.get("Content-Disposition");
Poco::Net::MessageHeader::splitParameters(cd, disp, params);
}
if (!params.has("filename"))
return;
// The temporary directory is child-root/<CHILDROOT_TMP_INCOMING_PATH>.
// Always create a random sub-directory to avoid file-name collision.
Poco::Path tempPath = Poco::Path::forDirectory(
FileUtil::createRandomTmpDir(COOLWSD::ChildRoot +
JailUtil::CHILDROOT_TMP_INCOMING_PATH) +
'/');
LOG_TRC("Created temporary convert-to/insert path: " << tempPath.toString());
// Prevent user inputing anything funny here.
std::string fileParam = params.get("filename");
std::string cleanFilename = Util::cleanupFilename(fileParam);
if (fileParam != cleanFilename)
LOG_DBG("Unexpected characters in conversion filename '"
<< fileParam << "' cleaned to '" << cleanFilename << "'");
// A "filename" should always be a filename, not a path
const Poco::Path filenameParam(cleanFilename);
if (filenameParam.getFileName() == "callback:")
tempPath.setFileName("incoming_file"); // A sensible name.
else
tempPath.setFileName(filenameParam.getFileName()); //TODO: Sanitize.
_filename = tempPath.toString();
LOG_DBG("Storing incoming file to: " << _filename);
// Copy the stream to _filename.
std::ofstream fileStream;
fileStream.open(_filename);
Poco::StreamCopier::copyStream(stream, fileStream);
fileStream.close();
}
};
class RenderSearchResultPartHandler : public Poco::Net::PartHandler
{
private:
std::string _filename;
std::shared_ptr<std::vector<char>> _pSearchResultContent;
public:
std::string getFilename() const { return _filename; }
/// Afterwards someone else is responsible for cleaning that up.
void takeFile() { _filename.clear(); }
const std::shared_ptr<std::vector<char>>& getSearchResultContent() const
{
return _pSearchResultContent;
}
RenderSearchResultPartHandler() = default;
virtual ~RenderSearchResultPartHandler()
{
if (!_filename.empty())
{
LOG_TRC("Remove un-handled temporary file '" << _filename << '\'');
StatelessBatchBroker::removeFile(_filename);
}
}
virtual void handlePart(const Poco::Net::MessageHeader& header, std::istream& stream) override
{
// Extract filename and put it to a temporary directory.
std::string label;
Poco::Net::NameValueCollection content;
if (header.has("Content-Disposition"))
{
Poco::Net::MessageHeader::splitParameters(header.get("Content-Disposition"), label,
content);
}
std::string name = content.get("name", "");
if (name == "document")
{
std::string filename = content.get("filename", "");
const Poco::Path filenameParam(filename);
// The temporary directory is child-root/<JAIL_TMP_INCOMING_PATH>.
// Always create a random sub-directory to avoid file-name collision.
Poco::Path tempPath = Poco::Path::forDirectory(
FileUtil::createRandomTmpDir(COOLWSD::ChildRoot +
JailUtil::CHILDROOT_TMP_INCOMING_PATH) +
'/');
LOG_TRC("Created temporary render-search-result file path: " << tempPath.toString());
// Prevent user inputting anything funny here.
// A "filename" should always be a filename, not a path
if (filenameParam.getFileName() == "callback:")
tempPath.setFileName("incoming_file"); // A sensible name.
else
tempPath.setFileName(filenameParam.getFileName()); //TODO: Sanitize.
_filename = tempPath.toString();
// Copy the stream to _filename.
std::ofstream fileStream;
fileStream.open(_filename);
Poco::StreamCopier::copyStream(stream, fileStream);
fileStream.close();
}
else if (name == "result")
{
// Copy content from the stream into a std::vector<char>
_pSearchResultContent = std::make_shared<std::vector<char>>(
std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
}
}
};
/// Constructs ConvertToBroker implamentation based on request type
std::shared_ptr<ConvertToBroker>
getConvertToBrokerImplementation(const std::string& requestType, const std::string& fromPath,
const Poco::URI& uriPublic, const std::string& docKey,
const std::string& format, const std::string& options,
const std::string& lang, const std::string& target)
{
if (requestType == "convert-to")
return std::make_shared<ConvertToBroker>(fromPath, uriPublic, docKey, format, options,
lang);
else if (requestType == "extract-link-targets")
return std::make_shared<ExtractLinkTargetsBroker>(fromPath, uriPublic, docKey, lang);
else if (requestType == "get-thumbnail")
return std::make_shared<GetThumbnailBroker>(fromPath, uriPublic, docKey, lang, target);
return nullptr;
}
class ConvertToAddressResolver : public std::enable_shared_from_this<ConvertToAddressResolver>
{
std::shared_ptr<ConvertToAddressResolver> _selfLifecycle;
std::vector<std::string> _addressesToResolve;
ClientRequestDispatcher::AsyncFn _asyncCb;
bool _allow;
public:
ConvertToAddressResolver(std::vector<std::string> addressesToResolve, ClientRequestDispatcher::AsyncFn asyncCb)
: _addressesToResolve(std::move(addressesToResolve))
, _asyncCb(std::move(asyncCb))
, _allow(true)
{
}
void testHostName(const std::string& hostToCheck)
{
_allow &= HostUtil::allowedWopiHost(hostToCheck);
}
// synchronous case
bool syncProcess()
{
assert(!_asyncCb);
while (!_addressesToResolve.empty())
{
const std::string& addressToCheck = _addressesToResolve.front();
try
{
std::string resolvedHostName = net::canonicalHostName(addressToCheck);
testHostName(resolvedHostName);
}
catch (const Poco::Exception& exc)
{
LOG_ERR_S("net::canonicalHostName(\"" << addressToCheck
<< "\") failed: " << exc.displayText());
// We can't find out the hostname, and it already failed the IP check
_allow = false;
}
if (_allow)
{
LOG_INF_S("convert-to: Requesting address is allowed: " << addressToCheck);
}
else
{
LOG_WRN_S("convert-to: Requesting address is denied: " << addressToCheck);
break;
}
_addressesToResolve.pop_back();
}
return _allow;
}
// asynchronous case
void startAsyncProcessing()
{
assert(_asyncCb);
_selfLifecycle = shared_from_this();
dispatchNextLookup();
}
std::string toState() const
{
std::string state = "ConvertToAddressResolver: ";
for (const auto& address : _addressesToResolve)
state += address + ", ";
state += "\n";
return state;
}
void dispatchNextLookup()
{
net::AsyncDNS::DNSThreadFn pushHostnameResolvedToPoll = [this](const std::string& hostname,
const std::string& exception) {
COOLWSD::getWebServerPoll()->addCallback([this, hostname, exception]() {
hostnameResolved(hostname, exception);
});
};
net::AsyncDNS::DNSThreadDumpStateFn dumpState = [this]() -> std::string {
return toState();
};
const std::string& addressToCheck = _addressesToResolve.front();
net::AsyncDNS::canonicalHostName(addressToCheck, pushHostnameResolvedToPoll, dumpState);
}
void hostnameResolved(const std::string& hostToCheck, const std::string& exception)
{
if (!exception.empty())
{
LOG_ERR_S(exception);
// We can't find out the hostname, and it already failed the IP check
_allow = false;
}
else
testHostName(hostToCheck);
const std::string& addressToCheck = _addressesToResolve.front();
if (_allow)
LOG_INF_S("convert-to: Requesting address is allowed: " << addressToCheck);
else
LOG_WRN_S("convert-to: Requesting address is denied: " << addressToCheck);
_addressesToResolve.pop_back();
// If hostToCheck is not allowed, or there are no addresses
// left to check, then do callback and end
if (!_allow || _addressesToResolve.empty())
{
_asyncCb(_allow);
_selfLifecycle.reset();
return;
}
dispatchNextLookup();
}
};
bool ClientRequestDispatcher::allowPostFrom(const std::string& address)
{
static bool init = false;
static Util::RegexListMatcher hosts;
if (!init)
{
const auto& app = Poco::Util::Application::instance();
// Parse the host allow settings.
for (size_t i = 0;; ++i)
{
const std::string path = "net.post_allow.host[" + std::to_string(i) + ']';
const auto host = app.config().getString(path, "");
if (!host.empty())
{
LOG_INF_S("Adding trusted POST_ALLOW host: [" << host << ']');
hosts.allow(host);
}
else if (!app.config().has(path))
{
break;
}
}
init = true;
}
return hosts.match(address);
}
bool ClientRequestDispatcher::allowConvertTo(const std::string& address,
const Poco::Net::HTTPRequest& request,
AsyncFn asyncCb)
{
const bool allow = allowPostFrom(address) || HostUtil::allowedWopiHost(request.getHost());
if (!allow)
{
LOG_WRN_S("convert-to: Requesting address is denied: " << address);
if (asyncCb)
asyncCb(false);
return false;
}
LOG_TRC_S("convert-to: Requesting address is allowed: " << address);
std::vector<std::string> addressesToResolve;
// Handle forwarded header and make sure all participating IPs are allowed
if (request.has("X-Forwarded-For"))
{
const std::string fowardedData = request.get("X-Forwarded-For");
StringVector tokens = StringVector::tokenize(fowardedData, ',');
for (const auto& token : tokens)
{
std::string param = tokens.getParam(token);
std::string addressToCheck = Util::trim(param);
if (!allowPostFrom(addressToCheck))
{
// postpone resolving addresses until later
addressesToResolve.push_back(addressToCheck);
continue;
}
LOG_INF_S("convert-to: Requesting address is allowed: " << addressToCheck);
}
}
if (addressesToResolve.empty())
{
if (asyncCb)
asyncCb(true);
return true;
}
auto resolver = std::make_shared<ConvertToAddressResolver>(std::move(addressesToResolve), asyncCb);
if (asyncCb)
{
resolver->startAsyncProcessing();
return false;
}
return resolver->syncProcess();
}
#endif // !MOBILEAPP
void ClientRequestDispatcher::onConnect(const std::shared_ptr<StreamSocket>& socket)
{
_id = COOLWSD::GetConnectionId();
_socket = socket;
setLogContext(socket->getFD());
LOG_TRC("Connected to ClientRequestDispatcher");
}
void ClientRequestDispatcher::handleIncomingMessage(SocketDisposition& disposition)
{
std::shared_ptr<StreamSocket> socket = _socket.lock();
if (!socket)
{
LOG_ERR("Invalid socket while handling incoming client request");
return;
}
#if !MOBILEAPP
if (!COOLWSD::isSSLEnabled() && socket->sniffSSL())
{
LOG_ERR("Looks like SSL/TLS traffic on plain http port");
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket);
return;
}
Poco::MemoryInputStream startmessage(&socket->getInBuffer()[0], socket->getInBuffer().size());
#if 0 // debug a specific command's payload
if (Util::findInVector(socket->getInBuffer(), "insertfile") != std::string::npos)
{
std::ostringstream oss;
oss << "Debug - specific command:\n";
socket->dumpState(oss);
LOG_INF(oss.str());
}
#endif
Poco::Net::HTTPRequest request;
StreamSocket::MessageMap map;
if (!socket->parseHeader("Client", startmessage, request, map))
return;
LOG_DBG("Handling request: " << request.getURI());
try
{
// We may need to re-write the chunks moving the inBuffer.
socket->compactChunks(map);
Poco::MemoryInputStream message(&socket->getInBuffer()[0], socket->getInBuffer().size());
// update the read cursor - headers are not altered by chunks.
message.seekg(startmessage.tellg(), std::ios::beg);
// re-write ServiceRoot and cache.
RequestDetails requestDetails(request, COOLWSD::ServiceRoot);
// LOG_TRC("Request details " << requestDetails.toString());
// Config & security ...
if (requestDetails.isProxy())
{
if (!COOLWSD::IsProxyPrefixEnabled)
throw BadRequestException(
"ProxyPrefix present but net.proxy_prefix is not enabled");
else if (!socket->isLocal())
throw BadRequestException("ProxyPrefix request from non-local socket");
}
// Routing
if (UnitWSD::isUnitTesting() && UnitWSD::get().handleHttpRequest(request, message, socket))
{
// Unit testing, nothing to do here
}
else if (requestDetails.equals(RequestDetails::Field::Type, "browser") ||
requestDetails.equals(RequestDetails::Field::Type, "wopi"))
{
// File server
assert(socket && "Must have a valid socket");
constexpr auto ProxyRemote = "/remote/";
constexpr auto ProxyRemoteLen = sizeof(ProxyRemote) - 1;
constexpr auto ProxyRemoteStatic = "/remote/static/";
const auto uri = requestDetails.getURI();
const auto pos = uri.find(ProxyRemoteStatic);
if (pos != std::string::npos)
{
if (uri.ends_with("lokit-extra-img.svg"))
{
ProxyRequestHandler::handleRequest(uri.substr(pos + ProxyRemoteLen), socket,
ProxyRequestHandler::getProxyRatingServer());
}
#if ENABLE_FEATURE_LOCK
else
{
const Poco::URI unlockImageUri =
CommandControl::LockManager::getUnlockImageUri();
if (!unlockImageUri.empty())
{
const std::string& serverUri =
unlockImageUri.getScheme() + "://" + unlockImageUri.getAuthority();
ProxyRequestHandler::handleRequest(
uri.substr(pos + sizeof("/remote/static") - 1), socket, serverUri);
}
}
#endif
}
else
{
FileServerRequestHandler::ResourceAccessDetails accessDetails;
COOLWSD::FileRequestHandler->handleRequest(request, requestDetails, message, socket,
accessDetails);
if (accessDetails.isValid())
{
LOG_ASSERT_MSG(Util::decodeURIComponent(
requestDetails.getField(RequestDetails::Field::WOPISrc)) ==
Util::decodeURIComponent(accessDetails.wopiSrc()),
"Expected identical WOPISrc in the request as in cool.html");
const std::string requestKey = RequestDetails::getRequestKey(
accessDetails.wopiSrc(), accessDetails.accessToken());
std::vector<std::string> options = {
"access_token=" + accessDetails.accessToken(), "access_token_ttl=0"
};
if (!accessDetails.permission().empty())
options.push_back("permission=" + accessDetails.permission());
const RequestDetails fullRequestDetails =
RequestDetails(accessDetails.wopiSrc(), options, /*compat=*/std::string());
if (RequestVettingStations.find(requestKey) != RequestVettingStations.end())
{
LOG_TRC("Found RVS under key: " << requestKey << ", nothing to do");
}
else
{
LOG_TRC("Creating RVS with key: " << requestKey << ", for DocumentLoadURI: "
<< fullRequestDetails.getDocumentURI());
auto it = RequestVettingStations.emplace(
requestKey, std::make_shared<RequestVettingStation>(
COOLWSD::getWebServerPoll(), fullRequestDetails));
it.first->second->handleRequest(_id);
}
}
socket->shutdown();
}
}
else if (requestDetails.equals(RequestDetails::Field::Type, "cool") &&
requestDetails.equals(1, "adminws"))
{
// Admin connections
LOG_INF("Admin request: " << request.getURI());
if (AdminSocketHandler::handleInitialRequest(_socket, request))
{
disposition.setMove(
[](const std::shared_ptr<Socket>& moveSocket)
{
// Hand the socket over to the Admin poll.
Admin::instance().insertNewSocket(moveSocket);
});
}
}
else if (requestDetails.equals(RequestDetails::Field::Type, "cool") &&
requestDetails.equals(1, "getMetrics"))
{
if (!COOLWSD::AdminEnabled)
throw Poco::FileAccessDeniedException("Admin console disabled");
// See metrics.txt
std::shared_ptr<http::Response> response =
std::make_shared<http::Response>(http::StatusCode::OK);
try
{
/* WARNING: security point, we may skip authentication */
bool skipAuthentication =
COOLWSD::getConfigValue<bool>("security.enable_metrics_unauthenticated", false);
if (!skipAuthentication)
if (!COOLWSD::FileRequestHandler->isAdminLoggedIn(request, *response))
throw Poco::Net::NotAuthenticatedException("Invalid admin login");
}
catch (const Poco::Net::NotAuthenticatedException& exc)
{
//LOG_ERR("FileServerRequestHandler::NotAuthenticated: " << exc.displayText());
http::Response httpResponse(http::StatusCode::Unauthorized);
httpResponse.set("Content-Type", "text/html charset=UTF-8");
httpResponse.set("WWW-authenticate", "Basic realm=\"online\"");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
FileServerRequestHandler::hstsHeaders(*response);
response->add("Last-Modified", Util::getHttpTimeNow());
// Ask UAs to block if they detect any XSS attempt
response->add("X-XSS-Protection", "1; mode=block");
// No referrer-policy
response->add("Referrer-Policy", "no-referrer");
response->add("X-Content-Type-Options", "nosniff");
disposition.setTransfer(Admin::instance(),
[response](const std::shared_ptr<Socket>& moveSocket)
{
const std::shared_ptr<StreamSocket> streamSocket =
std::static_pointer_cast<StreamSocket>(moveSocket);
Admin::instance().sendMetrics(streamSocket, response);
});
}
else if (requestDetails.isGetOrHead("/"))
handleRootRequest(requestDetails, socket);
else if (requestDetails.isGet("/favicon.ico"))
handleFaviconRequest(requestDetails, socket);
else if (requestDetails.equals(0, "hosting"))
{
if (requestDetails.equals(1, "discovery"))
handleWopiDiscoveryRequest(requestDetails, socket);
else if (requestDetails.equals(1, "capabilities"))
handleCapabilitiesRequest(request, socket);
}
else if (requestDetails.isGet("/robots.txt"))
handleRobotsTxtRequest(request, socket);
else if (requestDetails.equals(RequestDetails::Field::Type, "cool") &&
requestDetails.equals(1, "media"))
{
handleMediaRequest(request, disposition, socket);
}
else if (requestDetails.equals(RequestDetails::Field::Type, "cool") &&
requestDetails.equals(1, "clipboard"))
{
// Util::dumpHex(std::cerr, socket->getInBuffer(), "clipboard:\n"); // lots of data ...
handleClipboardRequest(request, message, disposition, socket);
}
else if (requestDetails.isProxy() && requestDetails.equals(2, "ws"))
handleClientProxyRequest(request, requestDetails, message, disposition);
else if (requestDetails.equals(RequestDetails::Field::Type, "cool") &&
requestDetails.equals(2, "ws") && requestDetails.isWebSocket())
handleClientWsUpgrade(request, requestDetails, disposition, socket);
else if (!requestDetails.isWebSocket() &&
(requestDetails.equals(RequestDetails::Field::Type, "cool") ||
requestDetails.equals(RequestDetails::Field::Type, "lool")))
{
// All post requests have url prefix 'cool', except when the prefix
// is 'lool' e.g. when integrations use the old /lool/convert-to endpoint
handlePostRequest(requestDetails, request, message, disposition, socket);
}
else if (requestDetails.equals(RequestDetails::Field::Type, "wasm"))
{
if (COOLWSD::WASMState == COOLWSD::WASMActivationState::Disabled)
{
LOG_ERR(
"WASM document request while WASM is disabled: " << requestDetails.toString());
// Bad request.
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket);
return;
}
// Tunnel to WASM.
_wopiProxy = std::make_unique<WopiProxy>(_id, requestDetails, socket);
_wopiProxy->handleRequest(COOLWSD::getWebServerPoll(), disposition);
}
else
{
LOG_ERR("Unknown resource: " << requestDetails.toString());
// Bad request.
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket);
return;
}
}
catch (const BadRequestException& ex)
{
LOG_ERR('#' << socket->getFD() << " bad request: ["
<< COOLProtocol::getAbbreviatedMessage(socket->getInBuffer())
<< "]: " << ex.what());
// Bad request.
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket);
return;
}
catch (const std::exception& exc)
{
LOG_ERR('#' << socket->getFD() << " Exception while processing incoming request: ["
<< COOLProtocol::getAbbreviatedMessage(socket->getInBuffer())
<< "]: " << exc.what());
// Bad request.
// NOTE: Check _wsState to choose between HTTP response or WebSocket (app-level) error.
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
// if we succeeded - remove the request from our input buffer
// we expect one request per socket
socket->eraseFirstInputBytes(map);
#else // !MOBILEAPP
Poco::Net::HTTPRequest request;
#ifdef IOS
// The URL of the document is sent over the FakeSocket by the code in
// -[DocumentViewController userContentController:didReceiveScriptMessage:] when it gets the
// HULLO message from the JavaScript in global.js.
// The "app document id", the numeric id of the document, from the appDocIdCounter in CODocument.mm.
char* space = strchr(socket->getInBuffer().data(), ' ');
assert(space != nullptr);
// The socket buffer is not nul-terminated so we can't just call strtoull() on the number at
// its end, it might be followed in memory by more digits. Is there really no better way to
// parse the number at the end of the buffer than to copy the bytes into a nul-terminated
// buffer?
const size_t appDocIdLen =
(socket->getInBuffer().data() + socket->getInBuffer().size()) - (space + 1);
char* appDocIdBuffer = (char*)malloc(appDocIdLen + 1);
memcpy(appDocIdBuffer, space + 1, appDocIdLen);
appDocIdBuffer[appDocIdLen] = '\0';
unsigned appDocId = std::strtoul(appDocIdBuffer, nullptr, 10);
free(appDocIdBuffer);
handleClientWsUpgrade(
request, std::string(socket->getInBuffer().data(), space - socket->getInBuffer().data()),
disposition, socket, appDocId);
#else // IOS
handleClientWsUpgrade(
request,
RequestDetails(std::string(socket->getInBuffer().data(), socket->getInBuffer().size())),
disposition, socket);
#endif // !IOS
socket->getInBuffer().clear();
#endif // MOBILEAPP
}
#if !MOBILEAPP
void ClientRequestDispatcher::handleRootRequest(const RequestDetails& requestDetails,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG("HTTP request: " << requestDetails.getURI());
const std::string mimeType = "text/plain";
const std::string responseString = "OK";
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.set("Content-Length", std::to_string(responseString.size()));
httpResponse.set("Content-Type", mimeType);
httpResponse.set("Last-Modified", Util::getHttpTimeNow());
httpResponse.set("Connection", "close");
httpResponse.writeData(socket->getOutBuffer());
if (requestDetails.isGet())
socket->send(responseString);
socket->flush();
socket->shutdown();
LOG_INF("Sent / response successfully.");
}
void ClientRequestDispatcher::handleFaviconRequest(const RequestDetails& requestDetails,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_TRC_S("Favicon request: " << requestDetails.getURI());
http::Response response(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(response);
response.setContentType("image/vnd.microsoft.icon");
std::string faviconPath =
Poco::Path(Poco::Util::Application::instance().commandPath()).parent().toString() +
"favicon.ico";
if (!Poco::File(faviconPath).exists())
faviconPath = COOLWSD::FileServerRoot + "/favicon.ico";
HttpHelper::sendFileAndShutdown(socket, faviconPath, response);
}
void ClientRequestDispatcher::handleWopiDiscoveryRequest(
const RequestDetails& requestDetails, const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG("Wopi discovery request: " << requestDetails.getURI());
std::string xml = getFileContent("discovery.xml");
std::string srvUrl =
#if ENABLE_SSL
((COOLWSD::isSSLEnabled() || COOLWSD::isSSLTermination()) ? "https://" : "http://")
#else
"http://"
#endif
+ (COOLWSD::ServerName.empty() ? requestDetails.getHostUntrusted() : COOLWSD::ServerName) +
COOLWSD::ServiceRoot;
if (requestDetails.isProxy())
srvUrl = requestDetails.getProxyPrefix();
Poco::replaceInPlace(xml, std::string("%SRV_URI%"), srvUrl);
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.setBody(xml, "text/xml");
httpResponse.set("Last-Modified", Util::getHttpTimeNow());
httpResponse.set("X-Content-Type-Options", "nosniff");
LOG_TRC("Sending back discovery.xml: " << xml);
socket->sendAndShutdown(httpResponse);
LOG_INF("Sent discovery.xml successfully.");
}
void ClientRequestDispatcher::handleClipboardRequest(const Poco::Net::HTTPRequest& request,
Poco::MemoryInputStream& message,
SocketDisposition& disposition,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG_S(
"Clipboard " << ((request.getMethod() == Poco::Net::HTTPRequest::HTTP_GET) ? "GET" : "POST")
<< " request: " << request.getURI());
Poco::URI requestUri(request.getURI());
Poco::URI::QueryParameters params = requestUri.getQueryParameters();
std::string WOPISrc, serverId, viewId, tag, mime;
for (const auto& it : params)
{
if (it.first == "WOPISrc")
WOPISrc = it.second;
else if (it.first == "ServerId")
serverId = it.second;
else if (it.first == "ViewId")
viewId = it.second;
else if (it.first == "Tag")
tag = it.second;
else if (it.first == "MimeType")
mime = it.second;
}
if (serverId != Util::getProcessIdentifier())
{
LOG_ERR_S("Cluster configuration error: mis-matching serverid ["
<< serverId << "] vs. [" << Util::getProcessIdentifier() << "] with tag [" << tag
<< "] on request to URL: " << request.getURI());
// we got the wrong request.
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
// Verify that the WOPISrc is properly encoded.
if (!HttpHelper::verifyWOPISrc(request.getURI(), WOPISrc, socket))
{
return;
}
const auto docKey = RequestDetails::getDocKey(WOPISrc);
LOG_TRC_S("Clipboard request for us: [" << serverId << "] with tag [" << tag << "] on docKey ["
<< docKey << ']');
std::shared_ptr<DocumentBroker> docBroker;
{
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto it = DocBrokers.find(docKey);
if (it != DocBrokers.end())
docBroker = it->second;
}
// If we have a valid docBroker, use it.
// Note: there is a race here as DocBroker may
// have already exited its SocketPoll, but we
// haven't cleaned up the DocBrokers container.
// Since we don't care about creating a new one,
// we simply go to the fallback below.
if (docBroker && docBroker->isAlive())
{
std::shared_ptr<std::string> data;
DocumentBroker::ClipboardRequest type;
if (request.getMethod() == Poco::Net::HTTPRequest::HTTP_GET)
{
if (mime == "text/html")
type = DocumentBroker::CLIP_REQUEST_GET_RICH_HTML_ONLY;
else if (mime == "text/html,text/plain;charset=utf-8")
type = DocumentBroker::CLIP_REQUEST_GET_HTML_PLAIN_ONLY;
else
type = DocumentBroker::CLIP_REQUEST_GET;
}
else
{
type = DocumentBroker::CLIP_REQUEST_SET;
ClipboardPartHandler handler;
Poco::Net::HTMLForm form(request, message, handler);
data = handler.getData();
if (!data || data->length() == 0)
LOG_ERR_S("Invalid zero size set clipboard content with tag ["
<< tag << "] on docKey [" << docKey << ']');
}
// Do things in the right thread.
LOG_TRC_S("Move clipboard request tag [" << tag << "] to docbroker thread with "
<< (data ? data->length() : 0)
<< " bytes of data");
docBroker->setupTransfer(
disposition,
[docBroker, type, viewId, tag, data](const std::shared_ptr<Socket>& moveSocket)
{
auto streamSocket = std::static_pointer_cast<StreamSocket>(moveSocket);
docBroker->handleClipboardRequest(type, streamSocket, viewId, tag, data);
});
LOG_TRC_S("queued clipboard command " << type << " on docBroker fetch");
}
// fallback to persistent clipboards if we can
else if (!DocumentBroker::lookupSendClipboardTag(socket, tag, false))
{
LOG_ERR_S("Invalid clipboard request to server ["
<< serverId << "] with tag [" << tag << "] and broker [" << docKey
<< "]: " << (docBroker ? "" : "not ") << "found");
std::string errMsg = "Empty clipboard item / session tag " + tag;
// Bad request.
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket, errMsg);
}
}
void ClientRequestDispatcher::handleRobotsTxtRequest(const Poco::Net::HTTPRequest& request,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG_S("HTTP request: " << request.getURI());
const std::string responseString = "User-agent: *\nDisallow: /\n";
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.set("Last-Modified", Util::getHttpTimeNow());
httpResponse.set("Content-Length", std::to_string(responseString.size()));
httpResponse.set("Content-Type", "text/plain");
httpResponse.set("Connection", "close");
httpResponse.writeData(socket->getOutBuffer());
if (request.getMethod() == Poco::Net::HTTPRequest::HTTP_GET)
{
socket->send(responseString);
}
socket->shutdown();
LOG_INF_S("Sent robots.txt response successfully");
}
void ClientRequestDispatcher::handleMediaRequest(const Poco::Net::HTTPRequest& request,
SocketDisposition& /*disposition*/,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG_S("Media request: " << request.getURI());
std::string decoded;
Poco::URI::decode(request.getURI(), decoded);
Poco::URI requestUri(decoded);
Poco::URI::QueryParameters params = requestUri.getQueryParameters();
std::string WOPISrc, serverId, viewId, tag, mime;
for (const auto& it : params)
{
if (it.first == "WOPISrc")
WOPISrc = it.second;
else if (it.first == "ServerId")
serverId = it.second;
else if (it.first == "ViewId")
viewId = it.second;
else if (it.first == "Tag")
tag = it.second;
else if (it.first == "MimeType")
mime = it.second;
}
LOG_TRC_S("Media request for us: [" << serverId << "] with tag [" << tag << "] and viewId ["
<< viewId << ']');
if (serverId != Util::getProcessIdentifier())
{
LOG_ERR_S("Cluster configuration error: mis-matching serverid ["
<< serverId << "] vs. [" << Util::getProcessIdentifier()
<< "] on request to URL: " << request.getURI());
// we got the wrong request.
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
// Verify that the WOPISrc is properly encoded.
if (!HttpHelper::verifyWOPISrc(request.getURI(), WOPISrc, socket))
{
return;
}
const auto docKey = RequestDetails::getDocKey(WOPISrc);
LOG_TRC_S("Looking up DocBroker with docKey [" << docKey << "] referenced in WOPISrc ["
<< WOPISrc
<< "] in media URL: " + request.getURI());
std::shared_ptr<DocumentBroker> docBroker;
{
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto it = DocBrokers.find(docKey);
if (it == DocBrokers.end())
{
LOG_ERR_S("Unknown DocBroker with docKey [" << docKey << "] referenced in WOPISrc ["
<< WOPISrc
<< "] in media URL: " + request.getURI());
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
docBroker = it->second;
}
// If we have a valid docBroker, use it.
// Note: there is a race here as DocBroker may
// have already exited its SocketPoll, but we
// haven't cleaned up the DocBrokers container.
// Since we don't care about creating a new one,
// we simply go to the fallback below.
if (docBroker && docBroker->isAlive())
{
// Do things in the right thread.
LOG_TRC_S("Move media request " << tag << " to docbroker thread");
std::string range = request.get("Range", "none");
docBroker->handleMediaRequest(std::move(range), socket, tag);
}
}
std::string ClientRequestDispatcher::getContentType(const std::string& fileName)
{
static std::unordered_map<std::string, std::string> aContentTypes{
{ "svg", "image/svg+xml" },
{ "pot", "application/vnd.ms-powerpoint" },
{ "xla", "application/vnd.ms-excel" },
// Writer documents
{ "sxw", "application/vnd.sun.xml.writer" },
{ "odt", "application/vnd.oasis.opendocument.text" },
{ "fodt", "application/vnd.oasis.opendocument.text-flat-xml" },
// Calc documents
{ "sxc", "application/vnd.sun.xml.calc" },
{ "ods", "application/vnd.oasis.opendocument.spreadsheet" },
{ "fods", "application/vnd.oasis.opendocument.spreadsheet-flat-xml" },
// Impress documents
{ "sxi", "application/vnd.sun.xml.impress" },
{ "odp", "application/vnd.oasis.opendocument.presentation" },
{ "fodp", "application/vnd.oasis.opendocument.presentation-flat-xml" },
// Draw documents
{ "sxd", "application/vnd.sun.xml.draw" },
{ "odg", "application/vnd.oasis.opendocument.graphics" },
{ "fodg", "application/vnd.oasis.opendocument.graphics-flat-xml" },
// Chart documents
{ "odc", "application/vnd.oasis.opendocument.chart" },
// Text master documents
{ "sxg", "application/vnd.sun.xml.writer.global" },
{ "odm", "application/vnd.oasis.opendocument.text-master" },
// Math documents
// In fact Math documents are not supported at all.
// See: https://bugs.documentfoundation.org/show_bug.cgi?id=97006
{ "sxm", "application/vnd.sun.xml.math" },
{ "odf", "application/vnd.oasis.opendocument.formula" },
// Text template documents
{ "stw", "application/vnd.sun.xml.writer.template" },
{ "ott", "application/vnd.oasis.opendocument.text-template" },
// Writer master document templates
{ "otm", "application/vnd.oasis.opendocument.text-master-template" },
// Spreadsheet template documents
{ "stc", "application/vnd.sun.xml.calc.template" },
{ "ots", "application/vnd.oasis.opendocument.spreadsheet-template" },
// Presentation template documents
{ "sti", "application/vnd.sun.xml.impress.template" },
{ "otp", "application/vnd.oasis.opendocument.presentation-template" },
// Drawing template documents
{ "std", "application/vnd.sun.xml.draw.template" },
{ "otg", "application/vnd.oasis.opendocument.graphics-template" },
// MS Word
{ "doc", "application/msword" },
{ "dot", "application/msword" },
// MS Excel
{ "xls", "application/vnd.ms-excel" },
// MS PowerPoint
{ "ppt", "application/vnd.ms-powerpoint" },
// OOXML wordprocessing
{ "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
{ "docm", "application/vnd.ms-word.document.macroEnabled.12" },
{ "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
{ "dotm", "application/vnd.ms-word.template.macroEnabled.12" },
// OOXML spreadsheet
{ "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" },
{ "xltm", "application/vnd.ms-excel.template.macroEnabled.12" },
{ "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
{ "xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" },
{ "xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" },
// OOXML presentation
{ "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
{ "pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" },
{ "potx", "application/vnd.openxmlformats-officedocument.presentationml.template" },
{ "potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" },
// Others
{ "wpd", "application/vnd.wordperfect" },
{ "pdb", "application/x-aportisdoc" },
{ "hwp", "application/x-hwp" },
{ "wps", "application/vnd.ms-works" },
{ "wri", "application/x-mswrite" },
{ "dif", "application/x-dif-document" },
{ "slk", "text/spreadsheet" },
{ "csv", "text/csv" },
{ "dbf", "application/x-dbase" },
{ "wk1", "application/vnd.lotus-1-2-3" },
{ "cgm", "image/cgm" },
{ "dxf", "image/vnd.dxf" },
{ "emf", "image/x-emf" },
{ "wmf", "image/x-wmf" },
{ "cdr", "application/coreldraw" },
{ "vsd", "application/vnd.visio2013" },
{ "vss", "application/vnd.visio" },
{ "pub", "application/x-mspublisher" },
{ "lrf", "application/x-sony-bbeb" },
{ "gnumeric", "application/x-gnumeric" },
{ "mw", "application/macwriteii" },
{ "numbers", "application/x-iwork-numbers-sffnumbers" },
{ "oth", "application/vnd.oasis.opendocument.text-web" },
{ "p65", "application/x-pagemaker" },
{ "rtf", "text/rtf" },
{ "txt", "text/plain" },
{ "fb2", "application/x-fictionbook+xml" },
{ "cwk", "application/clarisworks" },
{ "wpg", "image/x-wpg" },
{ "pages", "application/x-iwork-pages-sffpages" },
{ "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" },
{ "key", "application/x-iwork-keynote-sffkey" },
{ "abw", "application/x-abiword" },
{ "fh", "image/x-freehand" },
{ "sxs", "application/vnd.sun.xml.chart" },
{ "602", "application/x-t602" },
{ "bmp", "image/bmp" },
{ "png", "image/png" },
{ "gif", "image/gif" },
{ "tiff", "image/tiff" },
{ "jpg", "image/jpg" },
{ "jpeg", "image/jpeg" },
{ "pdf", "application/pdf" },
};
const std::string sExt = Poco::Path(fileName).getExtension();
const auto it = aContentTypes.find(sExt);
if (it != aContentTypes.end())
return it->second;
return "application/octet-stream";
}
bool ClientRequestDispatcher::isSpreadsheet(const std::string& fileName)
{
const std::string sContentType = getContentType(fileName);
return sContentType == "application/vnd.oasis.opendocument.spreadsheet" ||
sContentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
sContentType == "application/vnd.ms-excel";
}
void ClientRequestDispatcher::handlePostRequest(const RequestDetails& requestDetails,
const Poco::Net::HTTPRequest& request,
Poco::MemoryInputStream& message,
SocketDisposition& disposition,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_INF("Post request: [" << COOLWSD::anonymizeUrl(requestDetails.getURI()) << ']');
if (requestDetails.equals(1, "convert-to") ||
requestDetails.equals(1, "extract-link-targets") ||
requestDetails.equals(1, "get-thumbnail"))
{
// Validate sender - FIXME: should do this even earlier.
if (!allowConvertTo(socket->clientAddress(), request, nullptr))
{
LOG_WRN(
"Conversion requests not allowed from this address: " << socket->clientAddress());
http::Response httpResponse(http::StatusCode::Forbidden);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
ConvertToPartHandler handler;
Poco::Net::HTMLForm form(request, message, handler);
std::string format = (form.has("format") ? form.get("format") : "");
// prefer what is in the URI
if (requestDetails.size() > 2)
format = requestDetails[2];
bool hasRequiredParameters = true;
if (requestDetails.equals(1, "convert-to") && format.empty())
hasRequiredParameters = false;
const std::string fromPath = handler.getFilename();
LOG_INF("Conversion request for URI [" << fromPath << "] format [" << format << "].");
if (!fromPath.empty() && hasRequiredParameters)
{
Poco::URI uriPublic = RequestDetails::sanitizeURI(fromPath);
const std::string docKey = RequestDetails::getDocKey(uriPublic);
std::string options;
if (form.has("options"))
{
// Allow specifying options as-is, in case only data + format are used.
options = form.get("options");
}
const bool fullSheetPreview =
(form.has("FullSheetPreview") && form.get("FullSheetPreview") == "true");
if (fullSheetPreview && format == "pdf" && isSpreadsheet(fromPath))
{
//FIXME: We shouldn't have "true" as having the option already implies that
// we want it enabled (i.e. we shouldn't set the option if we don't want it).
options = ",FullSheetPreview=trueFULLSHEETPREVEND";
}
const std::string pdfVer = (form.has("PDFVer") ? form.get("PDFVer") : "");
if (!pdfVer.empty())
{
if (strcasecmp(pdfVer.c_str(), "PDF/A-1b") &&
strcasecmp(pdfVer.c_str(), "PDF/A-2b") &&
strcasecmp(pdfVer.c_str(), "PDF/A-3b") &&
strcasecmp(pdfVer.c_str(), "PDF-1.5") && strcasecmp(pdfVer.c_str(), "PDF-1.6"))
{
LOG_ERR("Wrong PDF type: " << pdfVer << ". Conversion aborted.");
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
options += ",PDFVer=" + pdfVer + "PDFVEREND";
}
std::string lang = (form.has("lang") ? form.get("lang") : std::string());
std::string target = (form.has("target") ? form.get("target") : std::string());
// This lock could become a bottleneck.
// In that case, we can use a pool and index by publicPath.
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
LOG_DBG("New DocumentBroker for docKey [" << docKey << "].");
auto docBroker = getConvertToBrokerImplementation(
requestDetails[1], fromPath, uriPublic, docKey, format, options, lang, target);
handler.takeFile();
cleanupDocBrokers();
DocBrokers.emplace(docKey, docBroker);
LOG_TRC("Have " << DocBrokers.size() << " DocBrokers after inserting [" << docKey
<< "].");
if (!docBroker->startConversion(disposition, _id))
{
LOG_WRN("Failed to create Client Session with id [" << _id << "] on docKey ["
<< docKey << "].");
cleanupDocBrokers();
}
}
else
{
LOG_INF("Missing parameters for conversion request.");
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
}
return;
}
else if (requestDetails.equals(2, "insertfile"))
{
LOG_INF("Insert file request.");
ConvertToPartHandler handler;
Poco::Net::HTMLForm form(request, message, handler);
if (form.has("childid") && form.has("name"))
{
const std::string formChildid(form.get("childid"));
const std::string formName(form.get("name"));
// Validate the docKey
const std::string decodedUri = requestDetails.getDocumentURI();
const std::string docKey = RequestDetails::getDocKey(decodedUri);
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto docBrokerIt = DocBrokers.find(docKey);
// Maybe just free the client from sending childid in form ?
if (docBrokerIt == DocBrokers.end() || docBrokerIt->second->getJailId() != formChildid)
{
throw BadRequestException("DocKey [" + docKey + "] or childid [" + formChildid +
"] is invalid.");
}
docBrokersLock.unlock();
// protect against attempts to inject something funny here
if (formChildid.find('/') == std::string::npos &&
formName.find('/') == std::string::npos)
{
const std::string dirPath =
COOLWSD::ChildRoot + formChildid + JAILED_DOCUMENT_ROOT + "insertfile";
const std::string fileName = dirPath + '/' + form.get("name");
LOG_INF("Perform insertfile: " << formChildid << ", " << formName
<< ", filename: " << fileName);
Poco::File(dirPath).createDirectories();
Poco::File(handler.getFilename()).moveTo(fileName);
// Cleanup the directory after moving.
const std::string dir = Poco::Path(handler.getFilename()).parent().toString();
if (FileUtil::isEmptyDirectory(dir))
FileUtil::removeFile(dir);
handler.takeFile();
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
}
}
else if (requestDetails.equals(2, "download"))
{
LOG_INF("File download request.");
// TODO: Check that the user in question has access to this file!
// 1. Validate the dockey
const std::string decodedUri = requestDetails.getDocumentURI();
const std::string docKey = RequestDetails::getDocKey(decodedUri);
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto docBrokerIt = DocBrokers.find(docKey);
if (docBrokerIt == DocBrokers.end())
{
throw BadRequestException("DocKey [" + docKey + "] is invalid.");
}
std::string downloadId = requestDetails[3];
std::string url = docBrokerIt->second->getDownloadURL(downloadId);
docBrokerIt->second->unregisterDownloadId(downloadId);
std::string jailId = docBrokerIt->second->getJailId();
docBrokersLock.unlock();
bool foundDownloadId = !url.empty();
std::string decoded;
Poco::URI::decode(url, decoded);
const Poco::Path filePath(COOLWSD::ChildRoot + jailId + JAILED_DOCUMENT_ROOT + decoded);
const std::string filePathAnonym = COOLWSD::anonymizeUrl(filePath.toString());
if (foundDownloadId && filePath.isAbsolute() && Poco::File(filePath).exists())
{
LOG_INF("HTTP request for: " << filePathAnonym);
const std::string& fileName = filePath.getFileName();
const Poco::URI postRequestUri(request.getURI());
const Poco::URI::QueryParameters postRequestQueryParams =
postRequestUri.getQueryParameters();
bool serveAsAttachment = true;
const auto attachmentIt =
std::find_if(postRequestQueryParams.begin(), postRequestQueryParams.end(),
[](const std::pair<std::string, std::string>& element)
{ return element.first == "attachment"; });
if (attachmentIt != postRequestQueryParams.end())
serveAsAttachment = attachmentIt->second != "0";
http::Response response(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(response);
// Instruct browsers to download the file, not display it
// with the exception of SVG where we need the browser to
// actually show it.
const std::string contentType = getContentType(fileName);
response.setContentType(contentType);
if (serveAsAttachment && contentType != "image/svg+xml")
response.set("Content-Disposition", "attachment; filename=\"" + fileName + '"');
#if !MOBILEAPP
if (COOLWSD::WASMState != COOLWSD::WASMActivationState::Disabled)
{
response.add("Cross-Origin-Opener-Policy", "same-origin");
response.add("Cross-Origin-Embedder-Policy", "require-corp");
response.add("Cross-Origin-Resource-Policy", "cross-origin");
}
#endif // !MOBILEAPP
try
{
HttpHelper::sendFileAndShutdown(socket, filePath.toString(), response);
}
catch (const Poco::Exception& exc)
{
LOG_ERR("Error sending file to client: "
<< exc.displayText()
<< (exc.nested() ? " (" + exc.nested()->displayText() + ")" : ""));
}
FileUtil::removeFile(filePath.toString());
}
else
{
if (foundDownloadId)
LOG_ERR("Download file [" << filePathAnonym << "] not found.");
else
LOG_ERR("Download with id [" << downloadId << "] not found.");
http::Response httpResponse(http::StatusCode::NotFound);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
}
return;
}
else if (requestDetails.equals(1, "render-search-result"))
{
RenderSearchResultPartHandler handler;
Poco::Net::HTMLForm form(request, message, handler);
const std::string fromPath = handler.getFilename();
LOG_INF("Create render-search-result POST command handler");
if (fromPath.empty())
return;
Poco::URI uriPublic = RequestDetails::sanitizeURI(fromPath);
const std::string docKey = RequestDetails::getDocKey(uriPublic);
// This lock could become a bottleneck.
// In that case, we can use a pool and index by publicPath.
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
LOG_DBG("New DocumentBroker for docKey [" << docKey << "].");
auto docBroker = std::make_shared<RenderSearchResultBroker>(
fromPath, uriPublic, docKey, handler.getSearchResultContent());
handler.takeFile();
cleanupDocBrokers();
DocBrokers.emplace(docKey, docBroker);
LOG_TRC("Have " << DocBrokers.size() << " DocBrokers after inserting [" << docKey << "].");
if (!docBroker->executeCommand(disposition, _id))
{
LOG_WRN("Failed to create Client Session with id [" << _id << "] on docKey [" << docKey
<< "].");
cleanupDocBrokers();
}
return;
}
throw BadRequestException("Invalid or unknown request.");
}
void ClientRequestDispatcher::handleClientProxyRequest(const Poco::Net::HTTPRequest& request,
const RequestDetails& requestDetails,
Poco::MemoryInputStream& message,
SocketDisposition& disposition)
{
//FIXME: The DocumentURI includes the WOPISrc, which makes it potentially invalid URI.
const std::string url = requestDetails.getLegacyDocumentURI();
LOG_INF("URL [" << url << "] for Proxy request.");
const auto uriPublic = RequestDetails::sanitizeURI(url);
const auto docKey = RequestDetails::getDocKey(uriPublic);
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 ["
<< COOLWSD::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;
}
}
LOG_INF("URL [" << COOLWSD::anonymizeUrl(url) << "] is "
<< (isReadOnly ? "readonly" : "writable") << '.');
(void)request;
(void)message;
(void)disposition;
// Request a kit process for this doc.
std::pair<std::shared_ptr<DocumentBroker>, std::string> pair
= findOrCreateDocBroker(DocumentBroker::ChildType::Interactive, url, docKey, _id, uriPublic,
/*mobileAppDocId=*/0, /*wopiFileInfo=*/nullptr);
auto docBroker = pair.first;
auto errorMsg = pair.second;
if (!docBroker)
{
LOG_ERR("Failed to find document [" << docKey << "]: " << errorMsg);
// badness occurred:
auto streamSocket = std::static_pointer_cast<StreamSocket>(disposition.getSocket());
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, streamSocket);
// FIXME: send docunloading & re-try on client ?
return;
}
// need to move into the DocumentBroker context before doing session lookup / creation etc.
docBroker->setupTransfer(
disposition,
[docBroker, id = _id, uriPublic = std::move(uriPublic), isReadOnly,
requestDetails](const std::shared_ptr<Socket>& moveSocket)
{
// Now inside the document broker thread ...
LOG_TRC_S("In the docbroker thread for " << docBroker->getDocKey());
const int fd = moveSocket->getFD();
auto streamSocket = std::static_pointer_cast<StreamSocket>(moveSocket);
try
{
docBroker->handleProxyRequest(id, uriPublic, isReadOnly, requestDetails,
streamSocket);
return;
}
catch (const UnauthorizedRequestException& exc)
{
LOG_ERR_S("Unauthorized Request while starting session on "
<< docBroker->getDocKey() << " for socket #" << fd
<< ". Terminating connection. Error: " << exc.what());
}
catch (const StorageConnectionException& exc)
{
LOG_ERR_S("Storage error while starting session on "
<< docBroker->getDocKey() << " for socket #" << fd
<< ". Terminating connection. Error: " << exc.what());
}
catch (const std::exception& exc)
{
LOG_ERR_S("Error while starting session on "
<< docBroker->getDocKey() << " for socket #" << fd
<< ". Terminating connection. Error: " << exc.what());
}
// badness occurred:
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, streamSocket);
});
}
#endif
void ClientRequestDispatcher::handleClientWsUpgrade(const Poco::Net::HTTPRequest& request,
const RequestDetails& requestDetails,
SocketDisposition& disposition,
const std::shared_ptr<StreamSocket>& socket,
unsigned mobileAppDocId)
{
const std::string url = requestDetails.getDocumentURI();
assert(socket && "Must have a valid socket");
// must be trace for anonymization
LOG_TRC("Client WS request: " << requestDetails.getURI() << ", url: " << url << ", socket #"
<< socket->getFD());
// First Upgrade.
auto ws = std::make_shared<WebSocketHandler>(socket, request);
// Response to clients beyond this point is done via WebSocket.
try
{
if (COOLWSD::NumConnections >= COOLWSD::MaxConnections)
{
LOG_INF("Limit on maximum number of connections of " << COOLWSD::MaxConnections
<< " reached.");
if (config::isSupportKeyEnabled())
{
shutdownLimitReached(ws);
return;
}
}
const std::string requestKey = requestDetails.getRequestKey();
if (!requestKey.empty())
{
auto it = RequestVettingStations.find(requestKey);
if (it != RequestVettingStations.end())
{
LOG_TRC("Found RVS under key: " << requestKey);
_rvs = it->second;
RequestVettingStations.erase(it);
}
}
if (!_rvs)
{
LOG_TRC("Creating RVS for key: " << requestKey);
_rvs = std::make_shared<RequestVettingStation>(COOLWSD::getWebServerPoll(),
requestDetails);
}
// Indicate to the client that document broker is searching.
static constexpr const char* const status = "progress: { \"id\":\"find\" }";
LOG_TRC("Sending to Client [" << status << ']');
ws->sendMessage(status);
_rvs->handleRequest(_id, requestDetails, ws, socket, mobileAppDocId, disposition);
}
catch (const std::exception& exc)
{
LOG_ERR("Error while handling Client WS Request: " << exc.what());
const std::string msg = "error: cmd=internal kind=load";
ws->sendMessage(msg);
ws->shutdown(WebSocketHandler::StatusCodes::ENDPOINT_GOING_AWAY, msg);
socket->ignoreInput();
}
}
/// Lookup cached file content.
const std::string& ClientRequestDispatcher::getFileContent(const std::string& filename)
{
const auto it = StaticFileContentCache.find(filename);
if (it == StaticFileContentCache.end())
{
throw Poco::FileAccessDeniedException("Invalid or forbidden file path: [" + filename +
"].");
}
return it->second;
}
/// Process the discovery.xml file and return as string.
std::string ClientRequestDispatcher::getDiscoveryXML()
{
#if MOBILEAPP
// not needed for mobile
return std::string();
#else
std::string discoveryPath =
Poco::Path(Poco::Util::Application::instance().commandPath()).parent().toString() +
"discovery.xml";
if (!Poco::File(discoveryPath).exists())
{
// http://server/hosting/discovery.xml
discoveryPath = COOLWSD::FileServerRoot + "/discovery.xml";
}
const std::string action = "action";
const std::string favIconUrl = "favIconUrl";
const std::string urlsrc = "urlsrc";
const std::string rootUriValue = "%SRV_URI%";
const std::string uriBaseValue = rootUriValue + "/browser/" COOLWSD_VERSION_HASH "/";
const std::string uriValue = uriBaseValue + "cool.html?";
LOG_DBG_S("Processing discovery.xml from " << discoveryPath);
Poco::XML::InputSource inputSrc(discoveryPath);
Poco::XML::DOMParser parser;
Poco::AutoPtr<Poco::XML::Document> docXML = parser.parse(&inputSrc);
Poco::AutoPtr<Poco::XML::NodeList> listNodes = docXML->getElementsByTagName(action);
for (unsigned long it = 0; it < listNodes->length(); ++it)
{
Poco::XML::Element* elem = static_cast<Poco::XML::Element*>(listNodes->item(it));
Poco::XML::Element* parent =
elem->parentNode() ? static_cast<Poco::XML::Element*>(elem->parentNode()) : nullptr;
if (parent && parent->getAttribute("name") == "Capabilities")
{
elem->setAttribute(urlsrc, rootUriValue + CAPABILITIES_END_POINT);
}
else
{
elem->setAttribute(urlsrc, uriValue);
}
// Set the View extensions cache as well.
if (elem->getAttribute("name") == "edit")
{
const std::string ext = elem->getAttribute("ext");
if (COOLWSD::EditFileExtensions.insert(ext).second) // Skip duplicates.
LOG_DBG_S("Enabling editing of [" << ext << "] extension files");
}
else if (elem->getAttribute("name") == "view_comment")
{
// We don't seem to treat this list differently.
// The assumption seems to be that if a file is not editable,
// then it's view-only. And if it's view-only, it supports comments.
}
}
// turn "images/img.svg" into "http://server.tld/browser/12345abcd/images/img.svg"
listNodes = docXML->getElementsByTagName("app");
for (unsigned long it = 0; it < listNodes->length(); ++it)
{
Poco::XML::Element* elem = static_cast<Poco::XML::Element*>(listNodes->item(it));
if (elem->hasAttribute(favIconUrl))
{
elem->setAttribute(favIconUrl, uriBaseValue + elem->getAttribute(favIconUrl));
}
}
const auto& proofAttribs = GetProofKeyAttributes();
if (!proofAttribs.empty())
{
// Add proof-key element to wopi-discovery root
Poco::AutoPtr<Poco::XML::Element> keyElem = docXML->createElement("proof-key");
for (const auto& attrib : proofAttribs)
keyElem->setAttribute(attrib.first, attrib.second);
docXML->documentElement()->appendChild(keyElem);
}
std::ostringstream ostrXML;
Poco::XML::DOMWriter writer;
writer.writeNode(ostrXML, docXML);
return ostrXML.str();
#endif
}
#if !MOBILEAPP
/// Create the /hosting/capabilities JSON and return as string.
static std::string getCapabilitiesJson(bool convertToAvailable)
{
// Can the convert-to be used?
Poco::JSON::Object::Ptr convert_to = new Poco::JSON::Object;
Poco::Dynamic::Var available = convertToAvailable;
convert_to->set("available", available);
if (available)
convert_to->set("endpoint", "/cool/convert-to");
Poco::JSON::Object::Ptr capabilities = new Poco::JSON::Object;
capabilities->set("convert-to", convert_to);
// Supports the TemplateSaveAs in CheckFileInfo?
// TemplateSaveAs is broken by design, disable it everywhere (and
// remove at some stage too)
capabilities->set("hasTemplateSaveAs", false);
// Supports the TemplateSource in CheckFileInfo?
capabilities->set("hasTemplateSource", true);
// Hint to encourage use on mobile devices
capabilities->set("hasMobileSupport", true);
// Set the product name
capabilities->set("productName", config::getString("product_name", APP_NAME));
// Set the Server ID
capabilities->set("serverId", Util::getProcessIdentifier());
std::string version, hash;
Util::getVersionInfo(version, hash);
// Set the product version
capabilities->set("productVersion", version);
// Set the product version hash
capabilities->set("productVersionHash", hash);
// Set that this is a proxy.php-enabled instance
capabilities->set("hasProxyPrefix", COOLWSD::IsProxyPrefixEnabled);
// Set if this instance supports Zotero
capabilities->set("hasZoteroSupport", config::getBool("zotero.enable", true));
// Set if this instance supports WASM.
capabilities->set("hasWASMSupport",
COOLWSD::WASMState != COOLWSD::WASMActivationState::Disabled);
std::ostringstream ostrJSON;
capabilities->stringify(ostrJSON);
return ostrJSON.str();
}
/// Send the /hosting/capabilities JSON to socket
static void sendCapabilities(bool convertToAvailable,
const std::shared_ptr<StreamSocket>& socket)
{
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.set("Last-Modified", Util::getHttpTimeNow());
httpResponse.setBody(getCapabilitiesJson(convertToAvailable), "application/json");
httpResponse.set("X-Content-Type-Options", "nosniff");
socket->sendAndShutdown(httpResponse);
LOG_INF("Sent capabilities.json successfully.");
}
void ClientRequestDispatcher::handleCapabilitiesRequest(const Poco::Net::HTTPRequest& request,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG("Wopi capabilities request: " << request.getURI());
AsyncFn convertToAllowedCb = [socket](bool allowedConvert){
COOLWSD::getWebServerPoll()->addCallback([socket, allowedConvert]() { sendCapabilities(allowedConvert, socket); });
};
allowConvertTo(socket->clientAddress(), request, std::move(convertToAllowedCb));
}
#endif
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */