/* -*- 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/. */ #include #include "LOOLWSD.hpp" #include "RequestDetails.hpp" #include "common/Log.hpp" #include #include "Exceptions.hpp" namespace { std::map getParams(const std::string& uri) { std::map result; for (const auto& param : Poco::URI(uri).getQueryParameters()) { std::string key; Poco::URI::decode(param.first, key); std::string value; Poco::URI::decode(param.second, value); LOG_TRC("Decoding param [" << param.first << "] = [" << param.second << "] -> [" << key << "] = [" << value << "]."); result.emplace(key, value); } return result; } /// Returns true iff the two containers are equal. template bool equal(const T& lhs, const T& rhs) { if (lhs.size() != rhs.size()) { LOG_ERR("!!! Size mismatch: [" << lhs.size() << "] != [" << rhs.size() << "]."); return false; } const auto endLeft = std::end(lhs); auto itRight = std::begin(rhs); for (auto itLeft = std::begin(lhs); itLeft != endLeft; ++itLeft, ++itRight) { const auto subLeft = lhs.getParam(*itLeft); const auto subRight = rhs.getParam(*itRight); if (subLeft != subRight) { LOG_ERR("!!! Data mismatch: [" << subLeft << "] != [" << subRight << ']'); return false; } } return true; } } RequestDetails::RequestDetails(Poco::Net::HTTPRequest &request, const std::string& serviceRoot) : _isMobile(false) { // Check and remove the ServiceRoot from the request.getURI() if (!Util::startsWith(request.getURI(), serviceRoot)) throw BadRequestException("The request does not start with prefix: " + serviceRoot); // re-writes ServiceRoot out of request _uriString = request.getURI().substr(serviceRoot.length()); dehexify(); request.setURI(_uriString); const std::string &method = request.getMethod(); _isGet = method == "GET"; _isHead = method == "HEAD"; auto it = request.find("ProxyPrefix"); _isProxy = it != request.end(); if (_isProxy) _proxyPrefix = it->second; it = request.find("Upgrade"); _isWebSocket = it != request.end() && Util::iequal(it->second, "websocket"); #if MOBILEAPP // request.getHost fires an exception on mobile. #else _hostUntrusted = request.getHost(); #endif processURI(); } RequestDetails::RequestDetails(const std::string &mobileURI) : _isGet(true) , _isHead(false) , _isProxy(false) , _isWebSocket(false) { _isMobile = true; _uriString = mobileURI; dehexify(); processURI(); } void RequestDetails::dehexify() { // For now, we only hexify cool/ URLs. constexpr auto Prefix = "cool/0x"; constexpr auto PrefixLen = sizeof(Prefix) - 1; const auto hexPos = _uriString.find(Prefix); if (hexPos != std::string::npos) { // The start of the hex token. const auto start = hexPos + PrefixLen; // Find the next '/' after the hex token. const auto end = _uriString.find_first_of('/', start); std::string res = _uriString.substr(0, start - 2); // The prefix, without '0x'. const std::string encoded = _uriString.substr(start, (end == std::string::npos) ? end : end - start); std::string decoded; Util::dataFromHexString(encoded, decoded); res += decoded; res += _uriString.substr(end); // Concatinate the remainder. _uriString = res; // Replace the original uri with the decoded one. } } void RequestDetails::processURI() { // Poco::SyntaxException is thrown when the syntax is invalid. _params = getParams(_uriString); // First tokenize by '/' then by '?'. std::vector tokens; const auto len = _uriString.size(); if (len > 0) { std::size_t i, start; for (i = start = 0; i < len; ++i) { if (_uriString[i] == '/' || _uriString[i] == '?') { if (i - start > 0) // ignore empty tokens.emplace_back(start, i - start); start = i + 1; } } if (i - start > 0) // ignore empty tokens.emplace_back(start, i - start); _pathSegs = StringVector(_uriString, std::move(tokens)); } std::size_t off = 0; std::size_t posDocUri = _uriString.find_first_of('/'); if (posDocUri == 0) { off = 1; posDocUri = _uriString.find_first_of('/', 1); } _fields[Field::Type] = _uriString.substr(off, posDocUri - off); // The first is always the type. std::string uriRes = _uriString.substr(posDocUri + 1); const auto posLastWS = uriRes.rfind("/ws"); // DocumentURI is the second segment in cool URIs. if (_pathSegs.equals(0, "lool")) { //FIXME: For historic reasons the DocumentURI includes the WOPISrc. // This is problematic because decoding a URI that embedds not one, but // *two* encoded URIs within it is bound to produce an invalid URI. // Potentially three '?' might exist in the result (after decoding). std::size_t end = uriRes.rfind("/ws?"); if (end != std::string::npos) { // Until the end of the WOPISrc. // e.g. /ws?WOPISrc=&compat= end = uriRes.find_first_of("/?", end + 4, 2); // Start searching after '/ws?'. } else { end = (posLastWS != std::string::npos ? posLastWS : uriRes.find('/')); if (end == std::string::npos) end = uriRes.find('?'); // e.g. /cool/clipboard?WOPISrc=file%3A%2F%2F%2Ftmp%2Fcopypasteef324307_empty.ods... } const std::string docUri = uriRes.substr(0, end); std::string decoded; Poco::URI::decode(docUri, decoded); _fields[Field::LegacyDocumentURI] = decoded; // Find the DocumentURI proper. end = uriRes.find_first_of("/?", 0, 2); decoded.clear(); Poco::URI::decode(uriRes.substr(0, end), decoded); _fields[Field::DocumentURI] = decoded; } else // Otherwise, it's the full URI. { _fields[Field::LegacyDocumentURI] = _uriString; _fields[Field::DocumentURI] = _uriString; } _docUriParams = getParams(_fields[Field::DocumentURI]); _fields[Field::WOPISrc] = getParam("WOPISrc"); // &compat= const std::string compat = getParam("compat"); if (!compat.empty()) _fields[Field::Compat] = compat; // /ws[///] if (posLastWS != std::string::npos) { std::string lastWS = uriRes.substr(posLastWS); const auto proxyTokens = Util::tokenize(lastWS, '/'); if (proxyTokens.size() > 1) { _fields[Field::SessionId] = proxyTokens[1]; if (proxyTokens.size() > 2) { _fields[Field::Command] = proxyTokens[2]; if (proxyTokens.size() > 3) { _fields[Field::Serial] = proxyTokens[3]; } } } } } Poco::URI RequestDetails::sanitizeURI(const std::string& uri) { // The URI of the document should be url-encoded. #if !MOBILEAPP std::string decodedUri; Poco::URI::decode(uri, decodedUri); Poco::URI uriPublic(decodedUri); #else Poco::URI uriPublic(uri); #endif if (uriPublic.isRelative() || uriPublic.getScheme() == "file") { // TODO: Validate and limit access to local paths! uriPublic.normalize(); } if (uriPublic.getPath().empty()) { throw std::runtime_error("Invalid URI."); } // We decoded access token before embedding it in cool.html // So, we need to decode it now to get its actual value Poco::URI::QueryParameters queryParams = uriPublic.getQueryParameters(); for (auto& param : queryParams) { // look for encoded query params (access token as of now) if (param.first == "access_token") { std::string decodedToken; Poco::URI::decode(param.second, decodedToken); param.second = decodedToken; } } uriPublic.setQueryParameters(queryParams); LOG_DBG("Sanitized URI [" << uri << "] to [" << uriPublic.toString() << ']'); return uriPublic; } std::string RequestDetails::getDocKey(const Poco::URI& uri) { // If multiple host-names are used to access us, then // they must be aliases. Permission to access aliased hosts // is checked at the point of accepting incoming connections. // At this point storing the hostname artificially discriminates // between aliases and forces same document (when opened from // alias hosts) to load as separate documents and sharing doesn't // work. Worse, saving overwrites one another. std::string docKey; Poco::URI::encode(uri.getPath(), "", docKey); LOG_INF("DocKey from URI [" << uri.toString() << "] => [" << docKey << ']'); return docKey; } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */