2016-07-19 19:14:32 +02:00
|
|
|
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
|
|
|
|
/*
|
2023-10-30 22:58:54 +01:00
|
|
|
* Copyright the Collabora Online contributors.
|
|
|
|
*
|
|
|
|
* SPDX-License-Identifier: MPL-2.0
|
2023-11-09 19:23:00 +01:00
|
|
|
*
|
|
|
|
* 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/.
|
2016-07-19 19:14:32 +02:00
|
|
|
*/
|
|
|
|
|
2017-12-20 14:06:26 +01:00
|
|
|
#include <config.h>
|
2022-08-07 10:29:45 +02:00
|
|
|
#include <config_version.h>
|
2016-07-19 19:14:32 +02:00
|
|
|
|
2024-03-15 12:23:24 +01:00
|
|
|
#include "FileServer.hpp"
|
2016-07-19 19:14:32 +02:00
|
|
|
|
2024-03-15 12:23:24 +01:00
|
|
|
#include "Auth.hpp"
|
|
|
|
#include "COOLWSD.hpp"
|
|
|
|
#include "Exceptions.hpp"
|
|
|
|
#include "FileUtil.hpp"
|
2024-03-17 12:45:30 +01:00
|
|
|
#include "HttpRequest.hpp"
|
2024-03-15 12:23:24 +01:00
|
|
|
#include "RequestDetails.hpp"
|
|
|
|
#include "ServerURL.hpp"
|
|
|
|
#include <Common.hpp>
|
|
|
|
#include <Crypto.hpp>
|
|
|
|
#include <Log.hpp>
|
|
|
|
#include <Protocol.hpp>
|
|
|
|
#include <Util.hpp>
|
2024-04-18 20:47:57 +02:00
|
|
|
#include <JsonUtil.hpp>
|
2024-03-15 12:23:24 +01:00
|
|
|
#include <common/ConfigUtil.hpp>
|
|
|
|
#include <common/LangUtil.hpp>
|
|
|
|
#if !MOBILEAPP
|
|
|
|
#include <net/HttpHelper.hpp>
|
|
|
|
#endif
|
|
|
|
#include <ContentSecurityPolicy.hpp>
|
2017-05-24 21:37:20 +02:00
|
|
|
|
2017-03-11 23:28:59 +01:00
|
|
|
#include <Poco/DateTime.h>
|
2017-02-27 14:28:04 +01:00
|
|
|
#include <Poco/DateTimeFormat.h>
|
|
|
|
#include <Poco/DateTimeFormatter.h>
|
2016-07-19 19:55:16 +02:00
|
|
|
#include <Poco/Exception.h>
|
2016-07-19 19:14:32 +02:00
|
|
|
#include <Poco/FileStream.h>
|
2024-03-15 12:23:24 +01:00
|
|
|
#include <Poco/MemoryStream.h>
|
2016-07-19 19:14:32 +02:00
|
|
|
#include <Poco/Net/HTMLForm.h>
|
2017-11-08 09:34:37 +01:00
|
|
|
#include <Poco/Net/HTTPBasicCredentials.h>
|
|
|
|
#include <Poco/Net/HTTPCookie.h>
|
2016-07-19 19:14:32 +02:00
|
|
|
#include <Poco/Net/HTTPRequest.h>
|
2017-11-08 09:34:37 +01:00
|
|
|
#include <Poco/Net/HTTPResponse.h>
|
2016-07-19 19:55:16 +02:00
|
|
|
#include <Poco/Net/NameValueCollection.h>
|
2016-07-19 19:14:32 +02:00
|
|
|
#include <Poco/Net/NetException.h>
|
2018-05-18 09:48:07 +02:00
|
|
|
#include <Poco/RegularExpression.h>
|
2016-07-19 19:14:32 +02:00
|
|
|
#include <Poco/Runnable.h>
|
2024-03-15 12:23:24 +01:00
|
|
|
#include <Poco/SHA1Engine.h>
|
2016-07-19 19:14:32 +02:00
|
|
|
#include <Poco/StreamCopier.h>
|
|
|
|
#include <Poco/URI.h>
|
2024-03-15 12:23:24 +01:00
|
|
|
#include <Poco/Util/LayeredConfiguration.h>
|
2016-07-19 19:14:32 +02:00
|
|
|
|
2024-03-15 12:23:24 +01:00
|
|
|
#include <chrono>
|
|
|
|
#include <iomanip>
|
|
|
|
#include <string>
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
#include <dirent.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <zlib.h>
|
|
|
|
#include <security/pam_appl.h>
|
|
|
|
|
|
|
|
#include <openssl/evp.h>
|
2023-06-08 11:38:03 +02:00
|
|
|
|
2016-07-19 19:14:32 +02:00
|
|
|
using Poco::Net::HTMLForm;
|
2017-11-08 09:34:37 +01:00
|
|
|
using Poco::Net::HTTPBasicCredentials;
|
2016-07-19 19:14:32 +02:00
|
|
|
using Poco::Net::HTTPRequest;
|
|
|
|
using Poco::Net::HTTPResponse;
|
2016-07-19 19:55:16 +02:00
|
|
|
using Poco::Net::NameValueCollection;
|
2016-07-19 19:14:32 +02:00
|
|
|
using Poco::Util::Application;
|
|
|
|
|
2017-04-26 15:50:00 +02:00
|
|
|
std::map<std::string, std::pair<std::string, std::string>> FileServerRequestHandler::FileHash;
|
|
|
|
|
2024-03-15 12:24:24 +01:00
|
|
|
namespace
|
|
|
|
{
|
2018-01-22 11:57:39 +01:00
|
|
|
|
|
|
|
int functionConversation(int /*num_msg*/, const struct pam_message** /*msg*/,
|
|
|
|
struct pam_response **reply, void *appdata_ptr)
|
|
|
|
{
|
|
|
|
*reply = (struct pam_response *)malloc(sizeof(struct pam_response));
|
|
|
|
(*reply)[0].resp = strdup(static_cast<char *>(appdata_ptr));
|
|
|
|
(*reply)[0].resp_retcode = 0;
|
|
|
|
|
|
|
|
return PAM_SUCCESS;
|
|
|
|
}
|
|
|
|
|
2018-05-02 12:48:41 +02:00
|
|
|
/// Use PAM to check for user / password.
|
|
|
|
bool isPamAuthOk(const std::string& userProvidedUsr, const std::string& userProvidedPwd)
|
2018-01-22 11:57:39 +01:00
|
|
|
{
|
|
|
|
struct pam_conv localConversation { functionConversation, nullptr };
|
|
|
|
pam_handle_t *localAuthHandle = NULL;
|
|
|
|
int retval;
|
|
|
|
|
2018-05-02 12:48:41 +02:00
|
|
|
localConversation.appdata_ptr = const_cast<char *>(userProvidedPwd.c_str());
|
2018-01-22 11:57:39 +01:00
|
|
|
|
2021-11-18 13:08:14 +01:00
|
|
|
retval = pam_start("coolwsd", userProvidedUsr.c_str(), &localConversation, &localAuthHandle);
|
2018-01-22 11:57:39 +01:00
|
|
|
|
|
|
|
if (retval != PAM_SUCCESS)
|
|
|
|
{
|
|
|
|
LOG_ERR("pam_start returned " << retval);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
retval = pam_authenticate(localAuthHandle, 0);
|
|
|
|
|
|
|
|
if (retval != PAM_SUCCESS)
|
|
|
|
{
|
|
|
|
if (retval == PAM_AUTH_ERR)
|
|
|
|
{
|
2018-05-02 12:48:41 +02:00
|
|
|
LOG_ERR("PAM authentication failure for user \"" << userProvidedUsr << "\".");
|
2018-01-22 11:57:39 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
LOG_ERR("pam_authenticate returned " << retval);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-05-02 12:48:41 +02:00
|
|
|
LOG_INF("PAM authentication success for user \"" << userProvidedUsr << "\".");
|
2018-01-22 11:57:39 +01:00
|
|
|
|
|
|
|
retval = pam_end(localAuthHandle, retval);
|
|
|
|
|
|
|
|
if (retval != PAM_SUCCESS)
|
|
|
|
{
|
2021-02-17 13:00:11 +01:00
|
|
|
LOG_ERR("pam_end returned " << retval);
|
2018-01-22 11:57:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-11-18 13:08:14 +01:00
|
|
|
/// Check for user / password set in coolwsd.xml.
|
2018-05-02 12:48:41 +02:00
|
|
|
bool isConfigAuthOk(const std::string& userProvidedUsr, const std::string& userProvidedPwd)
|
2016-07-19 19:14:32 +02:00
|
|
|
{
|
|
|
|
const auto& config = Application::instance().config();
|
2018-05-02 12:48:41 +02:00
|
|
|
const std::string& user = config.getString("admin_console.username", "");
|
2016-07-19 19:14:32 +02:00
|
|
|
|
2018-05-02 12:48:41 +02:00
|
|
|
// Check for the username
|
|
|
|
if (user.empty())
|
2016-07-19 19:14:32 +02:00
|
|
|
{
|
2018-05-02 12:48:41 +02:00
|
|
|
LOG_ERR("Admin Console username missing, admin console disabled.");
|
|
|
|
return false;
|
2016-07-19 19:14:32 +02:00
|
|
|
}
|
2018-05-02 12:48:41 +02:00
|
|
|
else if (user != userProvidedUsr)
|
2016-07-19 19:55:16 +02:00
|
|
|
{
|
2018-05-02 12:48:41 +02:00
|
|
|
LOG_ERR("Admin Console wrong username.");
|
|
|
|
return false;
|
2016-07-19 19:55:16 +02:00
|
|
|
}
|
2016-07-19 19:14:32 +02:00
|
|
|
|
2021-11-15 14:47:53 +01:00
|
|
|
const char useCoolconfig[] = " Use coolconfig to configure the admin password.";
|
2017-07-10 18:22:05 +02:00
|
|
|
|
2018-05-02 12:48:41 +02:00
|
|
|
// do we have secure_password?
|
2017-05-24 21:37:20 +02:00
|
|
|
if (config.has("admin_console.secure_password"))
|
|
|
|
{
|
2018-05-02 12:48:41 +02:00
|
|
|
const std::string securePass = config.getString("admin_console.secure_password", "");
|
|
|
|
if (securePass.empty())
|
|
|
|
{
|
2021-11-15 14:47:53 +01:00
|
|
|
LOG_ERR("Admin Console secure password is empty, denying access." << useCoolconfig);
|
2018-05-02 12:48:41 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-07-10 18:22:05 +02:00
|
|
|
#if HAVE_PKCS5_PBKDF2_HMAC
|
2017-05-24 21:37:20 +02:00
|
|
|
// Extract the salt from the config
|
|
|
|
std::vector<unsigned char> saltData;
|
2022-03-30 03:37:57 +02:00
|
|
|
StringVector tokens = StringVector::tokenize(securePass, '.');
|
2017-05-24 21:37:20 +02:00
|
|
|
if (tokens.size() != 5 ||
|
2020-03-09 09:05:30 +01:00
|
|
|
!tokens.equals(0, "pbkdf2") ||
|
|
|
|
!tokens.equals(1, "sha512") ||
|
2017-05-24 21:37:20 +02:00
|
|
|
!Util::dataFromHexString(tokens[3], saltData))
|
|
|
|
{
|
2021-11-15 14:47:53 +01:00
|
|
|
LOG_ERR("Incorrect format detected for secure_password in config file." << useCoolconfig);
|
2018-05-02 12:48:41 +02:00
|
|
|
return false;
|
2017-05-24 21:37:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
unsigned char userProvidedPwdHash[tokens[4].size() / 2];
|
|
|
|
PKCS5_PBKDF2_HMAC(userProvidedPwd.c_str(), -1,
|
|
|
|
saltData.data(), saltData.size(),
|
|
|
|
std::stoi(tokens[2]),
|
|
|
|
EVP_sha512(),
|
|
|
|
sizeof userProvidedPwdHash, userProvidedPwdHash);
|
|
|
|
|
|
|
|
std::stringstream stream;
|
2017-11-06 09:14:30 +01:00
|
|
|
for (unsigned long j = 0; j < sizeof userProvidedPwdHash; ++j)
|
2017-05-24 21:37:20 +02:00
|
|
|
stream << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(userProvidedPwdHash[j]);
|
|
|
|
|
2018-05-02 12:48:41 +02:00
|
|
|
// now compare the hashed user-provided pwd against the stored hash
|
2022-03-21 00:43:15 +01:00
|
|
|
return tokens.equals(4, stream.str());
|
2017-07-10 18:22:05 +02:00
|
|
|
#else
|
2018-05-02 12:48:41 +02:00
|
|
|
const std::string pass = config.getString("admin_console.password", "");
|
2017-07-10 18:22:05 +02:00
|
|
|
LOG_ERR("The config file has admin_console.secure_password setting, "
|
|
|
|
<< "but this application was compiled with old OpenSSL version, "
|
2018-05-02 12:48:41 +02:00
|
|
|
<< "and this setting cannot be used." << (!pass.empty()? " Falling back to plain text password.": ""));
|
|
|
|
|
|
|
|
// careful, a fall-through!
|
2017-07-10 18:22:05 +02:00
|
|
|
#endif
|
2017-05-24 21:37:20 +02:00
|
|
|
}
|
|
|
|
|
2018-05-02 12:48:41 +02:00
|
|
|
const std::string pass = config.getString("admin_console.password", "");
|
|
|
|
if (pass.empty())
|
2016-07-19 19:14:32 +02:00
|
|
|
{
|
2021-11-15 14:47:53 +01:00
|
|
|
LOG_ERR("Admin Console password is empty, denying access." << useCoolconfig);
|
2016-07-19 19:14:32 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-05-02 12:48:41 +02:00
|
|
|
return pass == userProvidedPwd;
|
|
|
|
}
|
|
|
|
|
2024-03-15 12:24:24 +01:00
|
|
|
std::string stringifyBoolFromConfig(const Poco::Util::LayeredConfiguration& config,
|
|
|
|
const std::string& propertyName, bool defaultValue)
|
|
|
|
{
|
|
|
|
return config.getBool(propertyName, defaultValue) ? "true" : "false";
|
2018-05-02 12:48:41 +02:00
|
|
|
}
|
|
|
|
|
2024-03-15 12:24:24 +01:00
|
|
|
} // namespace
|
|
|
|
|
2023-08-06 11:06:22 +02:00
|
|
|
FileServerRequestHandler::FileServerRequestHandler(const std::string& root)
|
|
|
|
{
|
|
|
|
// Read all files that we can serve into memory and compress them.
|
|
|
|
// cool files
|
|
|
|
try
|
|
|
|
{
|
|
|
|
readDirToHash(root, "/browser/dist");
|
|
|
|
}
|
|
|
|
catch (...)
|
|
|
|
{
|
|
|
|
LOG_ERR("Failed to read from directory " << root);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
FileServerRequestHandler::~FileServerRequestHandler()
|
|
|
|
{
|
|
|
|
// Clean cached files.
|
|
|
|
FileHash.clear();
|
|
|
|
}
|
|
|
|
|
2024-03-15 11:56:05 +01:00
|
|
|
bool FileServerRequestHandler::isAdminLoggedIn(const Poco::Net::HTTPRequest& request,
|
|
|
|
std::string& jwtToken)
|
|
|
|
{
|
|
|
|
assert(COOLWSD::AdminEnabled);
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
NameValueCollection cookies;
|
|
|
|
request.getCookies(cookies);
|
|
|
|
jwtToken = cookies.get("jwt");
|
|
|
|
LOG_INF("Verifying JWT token: " << jwtToken);
|
|
|
|
JWTAuth authAgent("admin", "admin", "admin");
|
|
|
|
if (authAgent.verify(jwtToken))
|
|
|
|
{
|
|
|
|
LOG_TRC("JWT token is valid");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
LOG_INF("Invalid JWT token, let the administrator re-login");
|
|
|
|
}
|
|
|
|
catch (const Poco::Exception& exc)
|
|
|
|
{
|
|
|
|
LOG_INF("No existing JWT cookie found");
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-03-15 12:46:06 +01:00
|
|
|
bool FileServerRequestHandler::authenticateAdmin(const Poco::Net::HTTPBasicCredentials& credentials,
|
2024-03-17 12:45:30 +01:00
|
|
|
http::Response& response, std::string& jwtToken)
|
2024-03-15 12:46:06 +01:00
|
|
|
{
|
|
|
|
assert(COOLWSD::AdminEnabled);
|
|
|
|
|
|
|
|
const std::string& userProvidedUsr = credentials.getUsername();
|
|
|
|
const std::string& userProvidedPwd = credentials.getPassword();
|
|
|
|
|
|
|
|
// Deny attempts to login without providing a username / pwd and fail right away
|
|
|
|
// We don't even want to allow a password-less PAM module to be used here,
|
|
|
|
// or anything.
|
|
|
|
if (userProvidedUsr.empty() || userProvidedPwd.empty())
|
|
|
|
{
|
|
|
|
LOG_ERR("An attempt to log into Admin Console without username or password.");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if the user is allowed to use the admin console
|
|
|
|
if (COOLWSD::getConfigValue<bool>("admin_console.enable_pam", false))
|
|
|
|
{
|
|
|
|
// use PAM - it needs the username too
|
|
|
|
if (!isPamAuthOk(userProvidedUsr, userProvidedPwd))
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// use the hash or password in the config file
|
|
|
|
if (!isConfigAuthOk(userProvidedUsr, userProvidedPwd))
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// authentication passed, generate and set the cookie
|
|
|
|
JWTAuth authAgent("admin", "admin", "admin");
|
|
|
|
jwtToken = authAgent.getAccessToken();
|
|
|
|
|
|
|
|
Poco::Net::HTTPCookie cookie("jwt", jwtToken);
|
|
|
|
// bundlify appears to add an extra /dist -> dist/dist/admin
|
|
|
|
cookie.setPath(COOLWSD::ServiceRoot + "/browser/dist/");
|
|
|
|
cookie.setSecure(COOLWSD::isSSLEnabled());
|
2024-03-17 12:45:30 +01:00
|
|
|
response.header().addCookie(cookie.toString());
|
2024-03-15 12:46:06 +01:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-03-17 12:45:30 +01:00
|
|
|
bool FileServerRequestHandler::isAdminLoggedIn(const HTTPRequest& request, http::Response& response)
|
2018-05-02 12:48:41 +02:00
|
|
|
{
|
2024-03-15 12:52:57 +01:00
|
|
|
std::string jwtToken;
|
|
|
|
return isAdminLoggedIn(request, jwtToken) ||
|
|
|
|
authenticateAdmin(Poco::Net::HTTPBasicCredentials(request), response, jwtToken);
|
2016-07-19 19:14:32 +02:00
|
|
|
}
|
|
|
|
|
2021-10-25 10:22:49 +02:00
|
|
|
#if ENABLE_DEBUG
|
|
|
|
// Represents basic file's attributes.
|
|
|
|
// Used for localFile
|
|
|
|
class LocalFileInfo
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
// Attributes of file
|
|
|
|
std::string localPath;
|
2023-07-08 09:28:51 +02:00
|
|
|
std::string fileName;
|
2021-10-25 10:22:49 +02:00
|
|
|
std::string size;
|
|
|
|
|
|
|
|
// Last modified time of the file
|
|
|
|
std::chrono::system_clock::time_point fileLastModifiedTime;
|
|
|
|
|
2021-11-18 13:08:14 +01:00
|
|
|
enum class COOLStatusCode
|
2021-10-25 10:22:49 +02:00
|
|
|
{
|
|
|
|
DocChanged = 1010 // Document changed externally in storage
|
|
|
|
};
|
|
|
|
|
2023-07-08 09:28:51 +02:00
|
|
|
std::string getLastModifiedTime()
|
|
|
|
{
|
|
|
|
return Util::getIso8601FracformatTime(fileLastModifiedTime);
|
|
|
|
}
|
2021-10-25 10:22:49 +02:00
|
|
|
|
2023-07-08 09:28:51 +02:00
|
|
|
LocalFileInfo() = delete;
|
|
|
|
LocalFileInfo(const std::string &lPath, const std::string &fName)
|
2021-10-25 10:22:49 +02:00
|
|
|
{
|
|
|
|
fileName = fName;
|
2023-07-08 09:28:51 +02:00
|
|
|
localPath = lPath;
|
2021-10-25 10:22:49 +02:00
|
|
|
const FileUtil::Stat stat(localPath);
|
|
|
|
size = std::to_string(stat.size());
|
|
|
|
fileLastModifiedTime = stat.modifiedTimepoint();
|
|
|
|
}
|
2023-07-08 09:28:51 +02:00
|
|
|
private:
|
|
|
|
// Internal tracking of known files: to store various data
|
|
|
|
// on files - rather than writing it back to the file-system.
|
|
|
|
static std::vector<std::shared_ptr<LocalFileInfo>> fileInfoVec;
|
|
|
|
|
|
|
|
public:
|
|
|
|
// Lookup a file in our file-list
|
|
|
|
static std::shared_ptr<LocalFileInfo> getOrCreateFile(const std::string &lpath, const std::string &fname)
|
2021-10-25 10:22:49 +02:00
|
|
|
{
|
2023-07-08 09:28:51 +02:00
|
|
|
auto it = std::find_if(fileInfoVec.begin(), fileInfoVec.end(), [&lpath](const std::shared_ptr<LocalFileInfo> obj)
|
2021-10-25 10:22:49 +02:00
|
|
|
{
|
2023-07-08 09:28:51 +02:00
|
|
|
return obj->localPath == lpath;
|
2021-10-25 10:22:49 +02:00
|
|
|
});
|
2023-06-20 11:53:48 +02:00
|
|
|
|
2021-10-25 10:22:49 +02:00
|
|
|
if (it != fileInfoVec.end())
|
2023-07-08 09:28:51 +02:00
|
|
|
return *it;
|
|
|
|
|
|
|
|
auto fileInfo = std::make_shared<LocalFileInfo>(lpath, fname);
|
|
|
|
fileInfoVec.emplace_back(fileInfo);
|
|
|
|
return fileInfo;
|
2021-10-25 10:22:49 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
std::atomic<unsigned> lastLocalId;
|
2023-07-08 09:28:51 +02:00
|
|
|
std::vector<std::shared_ptr<LocalFileInfo>> LocalFileInfo::fileInfoVec;
|
2021-10-25 10:22:49 +02:00
|
|
|
|
|
|
|
//handles request starts with /wopi/files
|
|
|
|
void handleWopiRequest(const HTTPRequest& request,
|
2023-07-08 09:28:51 +02:00
|
|
|
const RequestDetails &requestDetails,
|
|
|
|
Poco::MemoryInputStream& message,
|
|
|
|
const std::shared_ptr<StreamSocket>& socket)
|
2021-10-25 10:22:49 +02:00
|
|
|
{
|
2021-11-19 10:08:55 +01:00
|
|
|
Poco::URI requestUri(request.getURI());
|
|
|
|
const Poco::Path path = requestUri.getPath();
|
2021-10-25 10:22:49 +02:00
|
|
|
const std::string prefix = "/wopi/files";
|
|
|
|
const std::string suffix = "/contents";
|
|
|
|
std::string localPath;
|
2024-02-22 05:12:57 +01:00
|
|
|
if (path.toString().ends_with(suffix))
|
2021-10-25 10:22:49 +02:00
|
|
|
{
|
|
|
|
localPath = path.toString().substr(prefix.length(), path.toString().length() - prefix.length() - suffix.length());
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
localPath = path.toString().substr(prefix.length());
|
|
|
|
}
|
2022-10-13 11:38:20 +02:00
|
|
|
|
|
|
|
if (!FileUtil::Stat(localPath).exists())
|
2021-10-25 10:22:49 +02:00
|
|
|
{
|
|
|
|
LOG_ERR("Local file URI [" << localPath << "] invalid or doesn't exist.");
|
|
|
|
throw BadRequestException("Invalid URI: " + localPath);
|
|
|
|
}
|
2022-10-13 11:38:20 +02:00
|
|
|
|
2024-02-22 05:12:57 +01:00
|
|
|
if (request.getMethod() == "GET" && !path.toString().ends_with(suffix))
|
2021-10-25 10:22:49 +02:00
|
|
|
{
|
2023-07-08 09:28:51 +02:00
|
|
|
std::shared_ptr<LocalFileInfo> localFile =
|
|
|
|
LocalFileInfo::getOrCreateFile(localPath, path.getFileName());
|
|
|
|
|
2021-10-25 10:22:49 +02:00
|
|
|
std::string userId = std::to_string(lastLocalId++);
|
|
|
|
std::string userNameString = "LocalUser#" + userId;
|
|
|
|
Poco::JSON::Object::Ptr fileInfo = new Poco::JSON::Object();
|
2021-11-19 10:08:55 +01:00
|
|
|
|
|
|
|
std::string postMessageOrigin;
|
|
|
|
config::isSslEnabled() ? postMessageOrigin = "https://" : postMessageOrigin = "http://";
|
|
|
|
postMessageOrigin += requestDetails.getHostUntrusted();
|
|
|
|
|
2023-07-08 09:28:51 +02:00
|
|
|
fileInfo->set("BaseFileName", localFile->fileName);
|
|
|
|
fileInfo->set("Size", localFile->size);
|
2021-10-25 10:22:49 +02:00
|
|
|
fileInfo->set("Version", "1.0");
|
|
|
|
fileInfo->set("OwnerId", "test");
|
|
|
|
fileInfo->set("UserId", userId);
|
|
|
|
fileInfo->set("UserFriendlyName", userNameString);
|
2024-03-14 15:58:36 +01:00
|
|
|
fileInfo->set("UserCanWrite", (requestDetails.getParam("permission") != "readonly") ? "true": "false");
|
2021-11-19 10:08:55 +01:00
|
|
|
fileInfo->set("PostMessageOrigin", postMessageOrigin);
|
2023-07-08 09:28:51 +02:00
|
|
|
fileInfo->set("LastModifiedTime", localFile->getLastModifiedTime());
|
2021-10-25 10:22:49 +02:00
|
|
|
fileInfo->set("EnableOwnerTermination", "true");
|
|
|
|
|
|
|
|
std::ostringstream jsonStream;
|
|
|
|
fileInfo->stringify(jsonStream);
|
|
|
|
|
2023-06-20 11:53:48 +02:00
|
|
|
http::Response httpResponse(http::StatusCode::OK);
|
|
|
|
httpResponse.set("Last-Modified", Util::getHttpTime(localFile->fileLastModifiedTime));
|
|
|
|
httpResponse.setBody(jsonStream.str(), "application/json; charset=utf-8");
|
|
|
|
socket->send(httpResponse);
|
2021-10-25 10:22:49 +02:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
2024-02-22 05:12:57 +01:00
|
|
|
else if(request.getMethod() == "GET" && path.toString().ends_with(suffix))
|
2021-10-25 10:22:49 +02:00
|
|
|
{
|
2023-07-08 09:28:51 +02:00
|
|
|
std::shared_ptr<LocalFileInfo> localFile =
|
|
|
|
LocalFileInfo::getOrCreateFile(localPath,path.getFileName());
|
2021-10-25 10:22:49 +02:00
|
|
|
auto ss = std::ostringstream{};
|
2023-07-08 09:28:51 +02:00
|
|
|
std::ifstream inputFile(localFile->localPath);
|
2021-10-25 10:22:49 +02:00
|
|
|
ss << inputFile.rdbuf();
|
2023-06-20 11:53:48 +02:00
|
|
|
|
|
|
|
http::Response httpResponse(http::StatusCode::OK);
|
|
|
|
httpResponse.set("Last-Modified", Util::getHttpTime(localFile->fileLastModifiedTime));
|
|
|
|
httpResponse.setBody(ss.str(), "text/plain; charset=utf-8");
|
|
|
|
socket->send(httpResponse);
|
2021-10-25 10:22:49 +02:00
|
|
|
return;
|
|
|
|
}
|
2024-02-22 05:12:57 +01:00
|
|
|
else if (request.getMethod() == "POST" && path.toString().ends_with(suffix))
|
2021-10-25 10:22:49 +02:00
|
|
|
{
|
2023-07-08 09:28:51 +02:00
|
|
|
std::shared_ptr<LocalFileInfo> localFile =
|
|
|
|
LocalFileInfo::getOrCreateFile(localPath,path.getFileName());
|
2021-11-18 13:08:14 +01:00
|
|
|
std::string wopiTimestamp = request.get("X-COOL-WOPI-Timestamp", std::string());
|
2021-11-19 07:39:37 +01:00
|
|
|
if (wopiTimestamp.empty())
|
|
|
|
wopiTimestamp = request.get("X-LOOL-WOPI-Timestamp", std::string());
|
2023-07-08 09:28:51 +02:00
|
|
|
|
2021-10-25 10:22:49 +02:00
|
|
|
if (!wopiTimestamp.empty())
|
|
|
|
{
|
2023-07-08 09:28:51 +02:00
|
|
|
if (wopiTimestamp != localFile->getLastModifiedTime())
|
2021-10-25 10:22:49 +02:00
|
|
|
{
|
2023-01-17 00:38:59 +01:00
|
|
|
http::Response httpResponse(http::StatusCode::Conflict);
|
2023-06-20 11:53:48 +02:00
|
|
|
httpResponse.setBody("{\"COOLStatusCode\":" +
|
|
|
|
std::to_string(static_cast<int>(
|
|
|
|
LocalFileInfo::COOLStatusCode::DocChanged)) +
|
|
|
|
',' + "{\"LOOLStatusCode\":" +
|
|
|
|
std::to_string(static_cast<int>(
|
|
|
|
LocalFileInfo::COOLStatusCode::DocChanged)) +
|
|
|
|
'}',
|
|
|
|
"application/json; charset=utf-8");
|
2021-10-25 10:22:49 +02:00
|
|
|
socket->send(httpResponse);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::streamsize size = request.getContentLength();
|
2021-11-23 17:20:01 +01:00
|
|
|
std::vector<char> buffer(size);
|
|
|
|
message.read(buffer.data(), size);
|
2023-07-08 09:28:51 +02:00
|
|
|
localFile->fileLastModifiedTime = std::chrono::system_clock::now();
|
2021-10-25 10:22:49 +02:00
|
|
|
|
|
|
|
std::ofstream outfile;
|
2023-07-08 09:28:51 +02:00
|
|
|
outfile.open(localFile->localPath, std::ofstream::binary);
|
2021-11-23 17:20:01 +01:00
|
|
|
outfile.write(buffer.data(), size);
|
2021-10-25 10:22:49 +02:00
|
|
|
outfile.close();
|
|
|
|
|
|
|
|
const std::string body = "{\"LastModifiedTime\": \"" +
|
2023-07-08 09:28:51 +02:00
|
|
|
localFile->getLastModifiedTime() + "\" }";
|
2023-01-17 00:38:59 +01:00
|
|
|
http::Response httpResponse(http::StatusCode::OK);
|
2023-06-20 11:53:48 +02:00
|
|
|
httpResponse.setBody(body, "application/json; charset=utf-8");
|
2021-10-25 10:22:49 +02:00
|
|
|
socket->send(httpResponse);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2020-05-12 17:19:41 +02:00
|
|
|
void FileServerRequestHandler::handleRequest(const HTTPRequest& request,
|
2024-01-13 23:34:04 +01:00
|
|
|
const RequestDetails& requestDetails,
|
2020-05-12 17:19:41 +02:00
|
|
|
Poco::MemoryInputStream& message,
|
2024-01-13 23:34:04 +01:00
|
|
|
const std::shared_ptr<StreamSocket>& socket,
|
|
|
|
ResourceAccessDetails& accessDetails)
|
2016-07-19 19:14:32 +02:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2017-03-13 22:48:19 +01:00
|
|
|
bool noCache = false;
|
2018-05-18 09:48:07 +02:00
|
|
|
#if ENABLE_DEBUG
|
2023-04-15 13:43:49 +02:00
|
|
|
noCache = !COOLWSD::ForceCaching; // for cypress
|
2018-05-18 09:48:07 +02:00
|
|
|
#endif
|
2023-10-22 23:23:41 +02:00
|
|
|
http::Response response(http::StatusCode::OK);
|
2021-12-21 08:15:29 +01:00
|
|
|
|
|
|
|
const auto& config = Application::instance().config();
|
|
|
|
|
|
|
|
// HSTS hardening. Disabled in debug builds.
|
|
|
|
#if !ENABLE_DEBUG
|
|
|
|
if (COOLWSD::isSSLEnabled() || COOLWSD::isSSLTermination())
|
|
|
|
{
|
|
|
|
if (config.getBool("ssl.sts.enabled", false))
|
|
|
|
{
|
2024-01-13 23:34:04 +01:00
|
|
|
const auto maxAge =
|
|
|
|
config.getInt("ssl.sts.max_age", 31536000); // Default 1 year.
|
2021-12-21 08:15:29 +01:00
|
|
|
response.add("Strict-Transport-Security",
|
2024-01-13 23:34:04 +01:00
|
|
|
"max-age=" + std::to_string(maxAge) + "; includeSubDomains");
|
2021-12-21 08:15:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2017-04-10 11:34:51 +02:00
|
|
|
Poco::URI requestUri(request.getURI());
|
|
|
|
LOG_TRC("Fileserver request: " << requestUri.toString());
|
|
|
|
requestUri.normalize(); // avoid .'s and ..'s
|
|
|
|
|
2024-04-26 03:54:32 +02:00
|
|
|
if (requestUri.getPath().find("browser/" COOLWSD_VERSION_HASH "/") == std::string::npos &&
|
|
|
|
requestUri.getPath().find("admin/") == std::string::npos)
|
2018-05-18 09:48:07 +02:00
|
|
|
{
|
2022-07-16 03:50:36 +02:00
|
|
|
LOG_WRN("Client - server version mismatch, disabling browser cache. "
|
|
|
|
"Expected: " COOLWSD_VERSION_HASH "; Actual URI path with version hash: "
|
|
|
|
<< requestUri.getPath());
|
2018-05-18 09:48:07 +02:00
|
|
|
noCache = true;
|
|
|
|
}
|
|
|
|
|
2017-04-10 11:34:51 +02:00
|
|
|
std::vector<std::string> requestSegments;
|
|
|
|
requestUri.getPathSegments(requestSegments);
|
2020-04-15 15:37:49 +02:00
|
|
|
if (requestSegments.size() < 1)
|
|
|
|
throw Poco::FileNotFoundException("Invalid URI request: [" + requestUri.toString() + "].");
|
|
|
|
|
2023-11-27 13:57:06 +01:00
|
|
|
const std::string relPath = getRequestPathname(request, requestDetails);
|
2022-04-22 00:59:33 +02:00
|
|
|
const std::string endPoint = requestSegments[requestSegments.size() - 1];
|
2019-07-02 02:25:10 +02:00
|
|
|
|
2021-11-18 13:08:14 +01:00
|
|
|
static std::string etagString = "\"" COOLWSD_VERSION_HASH +
|
2021-11-03 18:44:51 +01:00
|
|
|
config.getString("ver_suffix", "") + "\"";
|
|
|
|
|
2021-10-25 10:22:49 +02:00
|
|
|
#if ENABLE_DEBUG
|
2024-02-22 05:12:57 +01:00
|
|
|
if (relPath.starts_with("/wopi/files")) {
|
2021-11-19 10:08:55 +01:00
|
|
|
handleWopiRequest(request, requestDetails, message, socket);
|
2021-10-25 10:22:49 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
#endif
|
2019-07-02 02:25:10 +02:00
|
|
|
if (request.getMethod() == HTTPRequest::HTTP_POST && endPoint == "logging.html")
|
|
|
|
{
|
2021-11-11 10:16:59 +01:00
|
|
|
const std::string coolLogging = config.getString("browser_logging", "false");
|
2021-11-03 15:58:27 +01:00
|
|
|
if (coolLogging != "false")
|
2019-07-02 02:25:10 +02:00
|
|
|
{
|
2023-08-23 16:33:52 +02:00
|
|
|
std::string token;
|
|
|
|
Poco::SHA1Engine engine;
|
|
|
|
|
|
|
|
engine.update(COOLWSD::LogToken);
|
|
|
|
std::getline(message, token, ' ');
|
|
|
|
|
|
|
|
if (Poco::DigestEngine::digestToHex(engine.digest()) == token)
|
|
|
|
{
|
|
|
|
LOG_ERR(message.rdbuf());
|
|
|
|
|
|
|
|
http::Response httpResponse(http::StatusCode::OK);
|
|
|
|
socket->send(httpResponse);
|
|
|
|
return;
|
|
|
|
}
|
2019-07-02 02:25:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-09 12:44:14 +02:00
|
|
|
// Is this a file we read at startup - if not; it's not for serving.
|
2023-11-07 02:42:10 +01:00
|
|
|
if (FileHash.find(relPath) == FileHash.end() &&
|
|
|
|
FileHash.find(relPath + ".br") == FileHash.end())
|
2017-04-10 11:34:51 +02:00
|
|
|
throw Poco::FileNotFoundException("Invalid URI request: [" + requestUri.toString() + "].");
|
2016-07-19 19:14:32 +02:00
|
|
|
|
2022-03-30 17:43:55 +02:00
|
|
|
if (endPoint == "welcome.html")
|
|
|
|
{
|
2024-03-18 11:03:17 +01:00
|
|
|
preprocessWelcomeFile(request, response, requestDetails, message, socket);
|
2022-03-30 17:43:55 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-11-11 10:05:49 +01:00
|
|
|
if (endPoint == "cool.html" ||
|
2022-02-07 16:45:40 +01:00
|
|
|
endPoint == "help-localizations.json" ||
|
|
|
|
endPoint == "localizations.json" ||
|
|
|
|
endPoint == "locore-localizations.json" ||
|
|
|
|
endPoint == "uno-localizations.json" ||
|
|
|
|
endPoint == "uno-localizations-override.json")
|
2016-07-19 19:14:32 +02:00
|
|
|
{
|
2024-03-18 11:03:17 +01:00
|
|
|
accessDetails = preprocessFile(request, response, requestDetails, message, socket);
|
2016-07-19 19:14:32 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (request.getMethod() == HTTPRequest::HTTP_GET)
|
|
|
|
{
|
2024-01-18 07:16:38 +01:00
|
|
|
if (endPoint == "admin.html" || endPoint == "adminSettings.html" ||
|
|
|
|
endPoint == "adminHistory.html" || endPoint == "adminAnalytics.html" ||
|
|
|
|
endPoint == "adminLog.html" || endPoint == "adminClusterOverview.html" ||
|
|
|
|
endPoint == "adminClusterOverviewAbout.html")
|
2018-04-25 21:53:38 +02:00
|
|
|
{
|
2024-03-18 11:03:17 +01:00
|
|
|
preprocessAdminFile(request, response, requestDetails, socket);
|
2018-04-25 21:53:38 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (endPoint == "admin-bundle.js" ||
|
|
|
|
endPoint == "admin-localizations.js")
|
2016-07-19 19:14:32 +02:00
|
|
|
{
|
2017-03-13 22:48:19 +01:00
|
|
|
noCache = true;
|
|
|
|
|
2021-11-18 13:08:14 +01:00
|
|
|
if (!COOLWSD::AdminEnabled)
|
2018-04-17 21:47:17 +02:00
|
|
|
throw Poco::FileAccessDeniedException("Admin console disabled");
|
|
|
|
|
2017-04-10 11:34:51 +02:00
|
|
|
if (!FileServerRequestHandler::isAdminLoggedIn(request, response))
|
2016-07-19 19:14:32 +02:00
|
|
|
throw Poco::Net::NotAuthenticatedException("Invalid admin login");
|
2017-04-09 19:26:37 +02:00
|
|
|
|
|
|
|
// Ask UAs to block if they detect any XSS attempt
|
|
|
|
response.add("X-XSS-Protection", "1; mode=block");
|
2017-04-10 14:57:07 +02:00
|
|
|
// No referrer-policy
|
|
|
|
response.add("Referrer-Policy", "no-referrer");
|
2016-07-19 19:14:32 +02:00
|
|
|
}
|
|
|
|
|
2017-04-27 15:14:31 +02:00
|
|
|
// Do we have an extension.
|
2017-04-10 11:34:51 +02:00
|
|
|
const std::size_t extPoint = endPoint.find_last_of('.');
|
|
|
|
if (extPoint == std::string::npos)
|
|
|
|
throw Poco::FileNotFoundException("Invalid file.");
|
|
|
|
|
|
|
|
const std::string fileType = endPoint.substr(extPoint + 1);
|
2016-07-19 19:14:32 +02:00
|
|
|
std::string mimeType;
|
|
|
|
if (fileType == "js")
|
2019-03-15 17:26:22 +01:00
|
|
|
mimeType = "application/javascript";
|
2016-07-19 19:14:32 +02:00
|
|
|
else if (fileType == "css")
|
|
|
|
mimeType = "text/css";
|
|
|
|
else if (fileType == "html")
|
|
|
|
mimeType = "text/html";
|
2017-04-13 04:11:23 +02:00
|
|
|
else if (fileType == "png")
|
|
|
|
mimeType = "image/png";
|
2016-07-19 19:14:32 +02:00
|
|
|
else if (fileType == "svg")
|
|
|
|
mimeType = "image/svg+xml";
|
2023-12-06 04:05:59 +01:00
|
|
|
#if !MOBILEAPP
|
2023-10-26 03:17:42 +02:00
|
|
|
else if (fileType == "wasm" &&
|
|
|
|
COOLWSD::WASMState != COOLWSD::WASMActivationState::Disabled)
|
2023-10-20 00:57:58 +02:00
|
|
|
mimeType = "application/wasm";
|
2023-12-06 04:05:59 +01:00
|
|
|
#endif // !MOBILEAPP
|
2016-07-19 19:14:32 +02:00
|
|
|
else
|
|
|
|
mimeType = "text/plain";
|
|
|
|
|
2023-10-22 13:27:04 +02:00
|
|
|
response.setContentType(mimeType);
|
|
|
|
|
2017-03-11 23:28:59 +01:00
|
|
|
auto it = request.find("If-None-Match");
|
|
|
|
if (it != request.end())
|
|
|
|
{
|
|
|
|
// if ETags match avoid re-sending the file.
|
2021-11-03 18:44:51 +01:00
|
|
|
if (!noCache && it->second == etagString)
|
2017-03-11 23:28:59 +01:00
|
|
|
{
|
|
|
|
// TESTME: harder ... - do we even want ETag support ?
|
|
|
|
std::ostringstream oss;
|
2017-04-25 08:00:12 +02:00
|
|
|
Poco::DateTime now;
|
2017-03-11 23:28:59 +01:00
|
|
|
Poco::DateTime later(now.utcTime(), int64_t(1000)*1000 * 60 * 60 * 24 * 128);
|
2020-10-26 13:55:54 +01:00
|
|
|
std::string extraHeaders =
|
|
|
|
"Expires: " + Poco::DateTimeFormatter::format(
|
|
|
|
later, Poco::DateTimeFormat::HTTP_FORMAT) + "\r\n" +
|
|
|
|
"Cache-Control: max-age=11059200\r\n";
|
2023-10-22 11:57:32 +02:00
|
|
|
HttpHelper::sendErrorAndShutdown(http::StatusCode::NotModified, socket,
|
|
|
|
std::string(), extraHeaders);
|
2017-03-11 23:28:59 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-06 04:05:59 +01:00
|
|
|
#if !MOBILEAPP
|
2023-10-26 03:17:42 +02:00
|
|
|
if (COOLWSD::WASMState != COOLWSD::WASMActivationState::Disabled &&
|
|
|
|
relPath.find("wasm") != std::string::npos)
|
2023-10-20 00:57:58 +02:00
|
|
|
{
|
|
|
|
response.add("Cross-Origin-Opener-Policy", "same-origin");
|
|
|
|
response.add("Cross-Origin-Embedder-Policy", "require-corp");
|
2023-11-21 16:26:17 +01:00
|
|
|
response.add("Cross-Origin-Resource-Policy", "cross-origin");
|
2023-10-20 00:57:58 +02:00
|
|
|
}
|
2023-12-06 04:05:59 +01:00
|
|
|
#endif // !MOBILEAPP
|
2023-10-20 00:57:58 +02:00
|
|
|
|
2023-10-26 03:17:42 +02:00
|
|
|
const bool brotli = request.hasToken("Accept-Encoding", "br");
|
2017-05-20 08:46:36 +02:00
|
|
|
#if ENABLE_DEBUG
|
2021-11-18 13:08:14 +01:00
|
|
|
if (std::getenv("COOL_SERVE_FROM_FS"))
|
2017-05-05 10:59:55 +02:00
|
|
|
{
|
2021-11-03 15:31:18 +01:00
|
|
|
// Useful to not serve from memory sometimes especially during cool development
|
2021-11-03 15:36:59 +01:00
|
|
|
// Avoids having to restart cool everytime you make a change in cool
|
2023-10-26 03:17:42 +02:00
|
|
|
std::string filePath =
|
|
|
|
Poco::Path(COOLWSD::FileServerRoot, relPath).absolute().toString();
|
|
|
|
if (brotli && FileUtil::Stat(filePath + ".br").exists())
|
|
|
|
{
|
|
|
|
filePath += ".br";
|
|
|
|
response.set("Content-Encoding", "br");
|
|
|
|
}
|
|
|
|
|
2023-10-22 23:23:41 +02:00
|
|
|
HttpHelper::sendFileAndShutdown(socket, filePath, response, noCache);
|
2017-05-05 10:59:55 +02:00
|
|
|
return;
|
|
|
|
}
|
2017-05-08 10:51:06 +02:00
|
|
|
#endif
|
2023-10-22 23:23:41 +02:00
|
|
|
|
2023-10-26 03:17:42 +02:00
|
|
|
bool compressed = false;
|
2023-10-22 23:23:41 +02:00
|
|
|
const std::string* content;
|
2023-10-26 03:17:42 +02:00
|
|
|
if (brotli && FileHash.find(relPath + ".br") != FileHash.end())
|
|
|
|
{
|
|
|
|
compressed = true;
|
|
|
|
response.set("Content-Encoding", "br");
|
|
|
|
content = getUncompressedFile(relPath + ".br");
|
|
|
|
}
|
|
|
|
else if (request.hasToken("Accept-Encoding", "gzip"))
|
2017-04-26 15:50:00 +02:00
|
|
|
{
|
2023-10-26 03:17:42 +02:00
|
|
|
compressed = true;
|
2017-04-26 15:50:00 +02:00
|
|
|
response.set("Content-Encoding", "gzip");
|
2017-04-27 15:14:31 +02:00
|
|
|
content = getCompressedFile(relPath);
|
2017-04-26 15:50:00 +02:00
|
|
|
}
|
|
|
|
else
|
2017-04-27 15:14:31 +02:00
|
|
|
content = getUncompressedFile(relPath);
|
|
|
|
|
2017-05-05 10:59:55 +02:00
|
|
|
if (!noCache)
|
|
|
|
{
|
|
|
|
// 60 * 60 * 24 * 128 (days) = 11059200
|
|
|
|
response.set("Cache-Control", "max-age=11059200");
|
2021-11-03 18:44:51 +01:00
|
|
|
response.set("ETag", etagString);
|
2017-05-05 10:59:55 +02:00
|
|
|
}
|
|
|
|
response.add("X-Content-Type-Options", "nosniff");
|
|
|
|
|
2023-10-26 03:17:42 +02:00
|
|
|
LOG_TRC('#' << socket->getFD() << ": Sending " << (!compressed ? "un" : "")
|
2023-10-22 23:23:41 +02:00
|
|
|
<< "compressed : file [" << relPath << "]: " << response.header());
|
|
|
|
|
|
|
|
socket->send(response);
|
2017-04-27 15:14:31 +02:00
|
|
|
socket->send(*content);
|
2020-06-03 17:06:10 +02:00
|
|
|
// shutdown by caller
|
2016-07-19 19:14:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (const Poco::Net::NotAuthenticatedException& exc)
|
|
|
|
{
|
2016-12-20 00:24:03 +01:00
|
|
|
LOG_ERR("FileServerRequestHandler::NotAuthenticated: " << exc.displayText());
|
2023-10-22 11:57:32 +02:00
|
|
|
sendError(http::StatusCode::Unauthorized, request, socket, "", "",
|
|
|
|
"WWW-authenticate: Basic realm=\"online\"\r\n");
|
2016-07-19 19:14:32 +02:00
|
|
|
}
|
|
|
|
catch (const Poco::FileAccessDeniedException& exc)
|
|
|
|
{
|
2016-12-20 00:24:03 +01:00
|
|
|
LOG_ERR("FileServerRequestHandler: " << exc.displayText());
|
2023-10-22 11:57:32 +02:00
|
|
|
sendError(http::StatusCode::Forbidden, request, socket, "403 - Access denied!",
|
2018-07-23 14:37:34 +02:00
|
|
|
"You are unable to access");
|
2016-07-19 19:14:32 +02:00
|
|
|
}
|
|
|
|
catch (const Poco::FileNotFoundException& exc)
|
|
|
|
{
|
2021-02-17 13:00:11 +01:00
|
|
|
LOG_ERR("FileServerRequestHandler: " << exc.displayText());
|
2023-10-22 11:57:32 +02:00
|
|
|
sendError(http::StatusCode::NotFound, request, socket, "404 - file not found!",
|
2018-07-23 14:37:34 +02:00
|
|
|
"There seems to be a problem locating");
|
|
|
|
}
|
2021-12-07 11:04:14 +01:00
|
|
|
catch (Poco::SyntaxException& exc)
|
|
|
|
{
|
|
|
|
LOG_ERR("Incorrect config value: " << exc.displayText());
|
2023-10-22 11:57:32 +02:00
|
|
|
sendError(http::StatusCode::InternalServerError, request, socket,
|
|
|
|
"500 - Internal Server Error!",
|
2022-01-20 20:44:47 +01:00
|
|
|
"Cannot process the request - " + exc.displayText());
|
2021-12-07 11:04:14 +01:00
|
|
|
}
|
2018-07-23 14:37:34 +02:00
|
|
|
}
|
2017-02-27 14:37:48 +01:00
|
|
|
|
2023-10-22 11:57:32 +02:00
|
|
|
void FileServerRequestHandler::sendError(http::StatusCode errorCode,
|
|
|
|
const Poco::Net::HTTPRequest& request,
|
2018-07-23 14:37:34 +02:00
|
|
|
const std::shared_ptr<StreamSocket>& socket,
|
2023-10-22 11:57:32 +02:00
|
|
|
const std::string& shortMessage,
|
|
|
|
const std::string& longMessage,
|
2018-07-31 09:18:52 +02:00
|
|
|
const std::string& extraHeader)
|
2018-07-23 14:37:34 +02:00
|
|
|
{
|
2020-10-26 13:48:35 +01:00
|
|
|
std::string body;
|
|
|
|
std::string headers = extraHeader;
|
2018-07-23 14:37:34 +02:00
|
|
|
if (!shortMessage.empty())
|
|
|
|
{
|
2023-06-08 12:17:41 +02:00
|
|
|
const Poco::URI requestUri(request.getURI());
|
|
|
|
const std::string pathSanitized =
|
|
|
|
Util::encodeURIComponent(requestUri.getPath(), std::string());
|
2023-04-11 10:54:56 +02:00
|
|
|
// Let's keep message as plain text to avoid complications.
|
|
|
|
headers += "Content-Type: text/plain charset=UTF-8\r\n";
|
|
|
|
body = "Error: " + shortMessage + '\n' +
|
|
|
|
longMessage + ' ' + pathSanitized + '\n' +
|
|
|
|
"Please contact your system administrator.";
|
2016-07-19 19:14:32 +02:00
|
|
|
}
|
2020-10-26 13:48:35 +01:00
|
|
|
HttpHelper::sendError(errorCode, socket, body, headers);
|
2016-07-19 19:14:32 +02:00
|
|
|
}
|
|
|
|
|
2020-04-14 19:50:04 +02:00
|
|
|
void FileServerRequestHandler::readDirToHash(const std::string &basePath, const std::string &path, const std::string &prefix)
|
2017-04-26 15:50:00 +02:00
|
|
|
{
|
2023-10-26 03:17:42 +02:00
|
|
|
const std::string fullPath = basePath + path;
|
|
|
|
LOG_DBG("Caching files in [" << fullPath << ']');
|
2017-04-26 15:50:00 +02:00
|
|
|
|
2023-12-06 04:05:59 +01:00
|
|
|
#if !MOBILEAPP
|
2023-10-26 03:17:42 +02:00
|
|
|
if (COOLWSD::WASMState == COOLWSD::WASMActivationState::Disabled &&
|
|
|
|
path.find("wasm") != std::string::npos)
|
|
|
|
{
|
|
|
|
LOG_INF("Skipping [" << fullPath << "] as WASM is disabled");
|
|
|
|
return;
|
|
|
|
}
|
2023-12-06 04:05:59 +01:00
|
|
|
#endif // !MOBILEAPP
|
2023-10-26 03:17:42 +02:00
|
|
|
|
|
|
|
DIR* workingdir = opendir((fullPath).c_str());
|
2019-05-23 16:33:41 +02:00
|
|
|
if (!workingdir)
|
2020-11-29 23:26:35 +01:00
|
|
|
{
|
2023-10-26 03:17:42 +02:00
|
|
|
LOG_SYS("Failed to open directory [" << fullPath << ']');
|
2019-05-23 16:33:41 +02:00
|
|
|
return;
|
2020-11-29 23:26:35 +01:00
|
|
|
}
|
2019-05-23 16:33:41 +02:00
|
|
|
|
2020-01-21 12:55:32 +01:00
|
|
|
size_t fileCount = 0;
|
|
|
|
std::string filesRead;
|
2020-06-02 01:41:08 +02:00
|
|
|
filesRead.reserve(1024);
|
2020-01-21 12:55:32 +01:00
|
|
|
|
2020-11-29 23:26:35 +01:00
|
|
|
struct dirent *currentFile;
|
2017-12-20 17:55:04 +01:00
|
|
|
while ((currentFile = readdir(workingdir)) != nullptr)
|
2017-04-26 15:50:00 +02:00
|
|
|
{
|
2017-04-27 15:14:31 +02:00
|
|
|
if (currentFile->d_name[0] == '.')
|
2017-04-26 15:50:00 +02:00
|
|
|
continue;
|
|
|
|
|
2017-05-20 19:28:09 +02:00
|
|
|
const std::string relPath = path + '/' + currentFile->d_name;
|
2020-11-29 23:26:35 +01:00
|
|
|
struct stat fileStat;
|
2023-08-03 22:19:43 +02:00
|
|
|
if (stat ((basePath + relPath).c_str(), &fileStat) != 0)
|
|
|
|
{
|
|
|
|
LOG_ERR("Failed to stat " << relPath);
|
|
|
|
continue;
|
|
|
|
}
|
2017-04-26 15:50:00 +02:00
|
|
|
|
2017-04-27 15:14:31 +02:00
|
|
|
if (S_ISDIR(fileStat.st_mode))
|
|
|
|
readDirToHash(basePath, relPath);
|
|
|
|
|
2024-02-22 05:12:57 +01:00
|
|
|
else if (S_ISREG(fileStat.st_mode) && relPath.ends_with(".br"))
|
2023-11-07 02:42:10 +01:00
|
|
|
{
|
|
|
|
// Only cache without compressing.
|
|
|
|
fileCount++;
|
|
|
|
filesRead.append(currentFile->d_name);
|
|
|
|
filesRead += ' ';
|
|
|
|
|
|
|
|
std::ifstream file(basePath + relPath, std::ios::binary);
|
|
|
|
|
|
|
|
std::string uncompressedFile;
|
|
|
|
uncompressedFile.resize(fileStat.st_size);
|
|
|
|
long unsigned int pos = 0;
|
|
|
|
do
|
|
|
|
{
|
|
|
|
file.read(&uncompressedFile[pos], fileStat.st_size);
|
|
|
|
const long unsigned int size = file.gcount();
|
|
|
|
if (size == 0)
|
|
|
|
break;
|
|
|
|
|
|
|
|
pos += size;
|
|
|
|
|
|
|
|
} while (true);
|
|
|
|
|
|
|
|
FileHash.emplace(prefix + relPath,
|
|
|
|
std::make_pair(std::move(uncompressedFile), std::string()));
|
|
|
|
}
|
2017-04-27 15:14:31 +02:00
|
|
|
else if (S_ISREG(fileStat.st_mode))
|
2017-04-26 15:50:00 +02:00
|
|
|
{
|
2023-10-11 12:03:45 +02:00
|
|
|
z_stream strm;
|
|
|
|
strm.zalloc = Z_NULL;
|
|
|
|
strm.zfree = Z_NULL;
|
|
|
|
strm.opaque = Z_NULL;
|
2023-10-19 10:22:55 +02:00
|
|
|
const int initResult = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY);
|
|
|
|
if (initResult != Z_OK)
|
2023-10-11 12:03:45 +02:00
|
|
|
{
|
2023-10-19 10:22:55 +02:00
|
|
|
LOG_ERR("Failed to deflateInit2, result: " << initResult);
|
2023-10-11 12:03:45 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-01-21 12:55:32 +01:00
|
|
|
fileCount++;
|
2020-06-02 01:41:08 +02:00
|
|
|
filesRead.append(currentFile->d_name);
|
|
|
|
filesRead += ' ';
|
2017-04-27 15:14:31 +02:00
|
|
|
|
|
|
|
std::ifstream file(basePath + relPath, std::ios::binary);
|
2017-04-26 15:50:00 +02:00
|
|
|
|
2023-07-30 16:40:21 +02:00
|
|
|
std::unique_ptr<char[]> buf = std::make_unique<char[]>(fileStat.st_size);
|
2017-07-03 04:11:44 +02:00
|
|
|
std::string compressedFile;
|
|
|
|
compressedFile.reserve(fileStat.st_size);
|
|
|
|
std::string uncompressedFile;
|
|
|
|
uncompressedFile.reserve(fileStat.st_size);
|
2018-02-24 01:16:57 +01:00
|
|
|
do
|
|
|
|
{
|
2017-04-26 15:50:00 +02:00
|
|
|
file.read(&buf[0], fileStat.st_size);
|
|
|
|
const long unsigned int size = file.gcount();
|
2017-04-27 15:14:31 +02:00
|
|
|
if (size == 0)
|
2017-04-26 15:50:00 +02:00
|
|
|
break;
|
2017-04-27 15:14:31 +02:00
|
|
|
|
2017-07-03 04:11:44 +02:00
|
|
|
const long unsigned int compSize = compressBound(size);
|
|
|
|
char *cbuf = (char *)calloc(compSize, sizeof(char));
|
2017-04-26 15:50:00 +02:00
|
|
|
|
|
|
|
strm.next_in = (unsigned char *)&buf[0];
|
|
|
|
strm.avail_in = size;
|
|
|
|
strm.avail_out = compSize;
|
|
|
|
strm.next_out = (unsigned char *)&cbuf[0];
|
2023-06-12 10:07:13 +02:00
|
|
|
strm.total_out = strm.total_in = 0;
|
2017-04-26 15:50:00 +02:00
|
|
|
|
2023-10-19 10:22:55 +02:00
|
|
|
const int deflateResult = deflate(&strm, Z_FINISH);
|
|
|
|
if (deflateResult != Z_OK && deflateResult != Z_STREAM_END)
|
|
|
|
{
|
|
|
|
LOG_ERR("Failed to deflate, result: " << deflateResult);
|
|
|
|
free(cbuf);
|
|
|
|
break;
|
|
|
|
}
|
2017-04-26 15:50:00 +02:00
|
|
|
|
2023-08-08 12:47:32 +02:00
|
|
|
compressedFile.append(cbuf, compSize - strm.avail_out);
|
2017-07-03 04:11:44 +02:00
|
|
|
free(cbuf);
|
2017-04-26 15:50:00 +02:00
|
|
|
|
2023-08-08 12:47:32 +02:00
|
|
|
uncompressedFile.append(buf.get(), size);
|
|
|
|
|
2017-04-27 15:14:31 +02:00
|
|
|
} while(true);
|
|
|
|
|
2023-11-07 02:42:10 +01:00
|
|
|
FileHash.emplace(prefix + relPath, std::make_pair(std::move(uncompressedFile),
|
|
|
|
std::move(compressedFile)));
|
2017-07-03 04:11:44 +02:00
|
|
|
deflateEnd(&strm);
|
2017-04-26 15:50:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
closedir(workingdir);
|
2020-01-21 12:55:32 +01:00
|
|
|
|
|
|
|
if (fileCount > 0)
|
2023-10-26 03:17:42 +02:00
|
|
|
LOG_TRC("Pre-read " << fileCount << " file(s) from directory: " << fullPath << ": "
|
|
|
|
<< filesRead);
|
2017-04-26 15:50:00 +02:00
|
|
|
}
|
|
|
|
|
2017-04-27 15:14:31 +02:00
|
|
|
const std::string *FileServerRequestHandler::getCompressedFile(const std::string &path)
|
2017-04-26 15:50:00 +02:00
|
|
|
{
|
2017-04-27 15:14:31 +02:00
|
|
|
return &FileHash[path].second;
|
2017-04-26 15:50:00 +02:00
|
|
|
}
|
|
|
|
|
2017-04-27 15:14:31 +02:00
|
|
|
const std::string *FileServerRequestHandler::getUncompressedFile(const std::string &path)
|
2017-04-26 15:50:00 +02:00
|
|
|
{
|
2017-04-27 15:14:31 +02:00
|
|
|
return &FileHash[path].first;
|
2017-04-26 15:50:00 +02:00
|
|
|
}
|
|
|
|
|
2023-11-27 13:57:06 +01:00
|
|
|
std::string FileServerRequestHandler::getRequestPathname(const HTTPRequest& request,
|
|
|
|
const RequestDetails& requestDetails)
|
2016-07-19 19:14:32 +02:00
|
|
|
{
|
|
|
|
Poco::URI requestUri(request.getURI());
|
|
|
|
// avoid .'s and ..'s
|
|
|
|
requestUri.normalize();
|
|
|
|
|
|
|
|
std::string path(requestUri.getPath());
|
2023-10-26 03:17:42 +02:00
|
|
|
|
2018-05-18 09:48:07 +02:00
|
|
|
Poco::RegularExpression gitHashRe("/([0-9a-f]+)/");
|
|
|
|
std::string gitHash;
|
|
|
|
if (gitHashRe.extract(path, gitHash))
|
|
|
|
{
|
|
|
|
// Convert version back to a real file name.
|
2021-11-03 15:36:59 +01:00
|
|
|
Poco::replaceInPlace(path, std::string("/browser" + gitHash), std::string("/browser/dist/"));
|
2018-05-18 09:48:07 +02:00
|
|
|
}
|
2016-07-19 19:14:32 +02:00
|
|
|
|
2023-12-06 04:05:59 +01:00
|
|
|
#if !MOBILEAPP
|
2023-11-27 13:57:06 +01:00
|
|
|
bool isWasm = false;
|
|
|
|
|
2023-10-26 03:17:42 +02:00
|
|
|
if (COOLWSD::WASMState == COOLWSD::WASMActivationState::Forced)
|
|
|
|
{
|
2023-11-27 13:57:06 +01:00
|
|
|
isWasm = (path.find("/browser/dist/wasm/") == std::string::npos);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
const std::string wopiSrc = requestDetails.getLineModeKey(std::string());
|
|
|
|
if (!wopiSrc.empty())
|
|
|
|
{
|
|
|
|
const auto it = COOLWSD::Uri2WasmModeMap.find(wopiSrc);
|
|
|
|
if (it != COOLWSD::Uri2WasmModeMap.end())
|
|
|
|
{
|
|
|
|
const bool isRecent =
|
|
|
|
(std::chrono::steady_clock::now() - it->second) <= std::chrono::minutes(1);
|
|
|
|
isWasm = (isRecent && path.find("/browser/dist/wasm/") == std::string::npos);
|
|
|
|
|
|
|
|
// Clean up only after it expires, because we need it more than once.
|
|
|
|
if (!isRecent)
|
|
|
|
{
|
|
|
|
COOLWSD::Uri2WasmModeMap.erase(it);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isWasm)
|
|
|
|
{
|
|
|
|
std::vector<std::string> requestSegments;
|
|
|
|
requestUri.getPathSegments(requestSegments);
|
|
|
|
const std::string endPoint = requestSegments[requestSegments.size() - 1];
|
|
|
|
if (endPoint == "online.js" || endPoint == "online.worker.js" ||
|
|
|
|
endPoint == "online.wasm" || endPoint == "online.data" || endPoint == "soffice.data")
|
2023-10-26 03:17:42 +02:00
|
|
|
{
|
2023-11-27 13:57:06 +01:00
|
|
|
isWasm = true;
|
2023-10-26 03:17:42 +02:00
|
|
|
}
|
2023-11-27 13:57:06 +01:00
|
|
|
else if (endPoint == "online.wasm.debug.wasm" || endPoint == "soffice.data.js.metadata")
|
|
|
|
{
|
|
|
|
isWasm = true;
|
|
|
|
}
|
2023-10-26 03:17:42 +02:00
|
|
|
}
|
2023-11-27 13:57:06 +01:00
|
|
|
|
|
|
|
if (isWasm)
|
2023-12-06 04:05:59 +01:00
|
|
|
{
|
2023-11-27 13:57:06 +01:00
|
|
|
Poco::replaceInPlace(path, std::string("/browser/dist/"),
|
|
|
|
std::string("/browser/dist/wasm/"));
|
2023-12-06 04:05:59 +01:00
|
|
|
}
|
|
|
|
#endif // !MOBILEAPP
|
2023-10-26 03:17:42 +02:00
|
|
|
|
2016-07-19 19:14:32 +02:00
|
|
|
return path;
|
|
|
|
}
|
|
|
|
|
2024-03-23 15:00:48 +01:00
|
|
|
constexpr std::string_view BRANDING = "branding";
|
|
|
|
constexpr std::string_view SUPPORT_KEY_BRANDING_UNSUPPORTED = "branding-unsupported";
|
2018-05-24 11:54:45 +02:00
|
|
|
|
2023-06-22 18:27:06 +02:00
|
|
|
static const std::string ACCESS_TOKEN = "%ACCESS_TOKEN%";
|
2023-06-23 13:08:09 +02:00
|
|
|
static const std::string ACCESS_TOKEN_TTL = "%ACCESS_TOKEN_TTL%";
|
2023-07-18 14:13:50 +02:00
|
|
|
static const std::string ACCESS_HEADER = "%ACCESS_HEADER%";
|
2023-07-30 15:45:29 +02:00
|
|
|
static const std::string UI_DEFAULTS = "%UI_DEFAULTS%";
|
2023-08-04 12:54:15 +02:00
|
|
|
static const std::string CSS_VARS = "<!--%CSS_VARIABLES%-->";
|
2023-08-06 09:59:00 +02:00
|
|
|
static const std::string POSTMESSAGE_ORIGIN = "%POSTMESSAGE_ORIGIN%";
|
2023-08-13 15:43:24 +02:00
|
|
|
static const std::string BRANDING_THEME = "%BRANDING_THEME%";
|
2023-08-14 13:18:41 +02:00
|
|
|
static const std::string CHECK_FILE_INFO_OVERRIDE = "%CHECK_FILE_INFO_OVERRIDE%";
|
2023-08-15 15:48:52 +02:00
|
|
|
static const std::string BUYPRODUCT_URL = "%BUYPRODUCT_URL%";
|
2024-03-14 15:58:36 +01:00
|
|
|
static const std::string PERMISSION = "%PERMISSION%";
|
2023-06-22 18:27:06 +02:00
|
|
|
|
2023-06-20 17:01:24 +02:00
|
|
|
/// Per user request variables.
|
|
|
|
/// Holds access_token, css_variables, postmessage_origin, etc.
|
|
|
|
class UserRequestVars
|
|
|
|
{
|
|
|
|
std::string extractVariable(const HTMLForm& form, const std::string& field,
|
|
|
|
const std::string& var)
|
|
|
|
{
|
|
|
|
std::string value = form.get(field, "");
|
2023-08-04 12:54:15 +02:00
|
|
|
|
|
|
|
// Escape bad characters in access token.
|
|
|
|
// These are placed directly in javascript in cool.html, we need to make sure
|
|
|
|
// that no one can do anything nasty with their clever inputs.
|
2023-06-20 17:01:24 +02:00
|
|
|
const std::string escaped = Util::encodeURIComponent(value, "'");
|
|
|
|
_vars[var] = escaped;
|
|
|
|
|
|
|
|
LOG_TRC("Field [" << field << "] for var [" << var << "] = [" << escaped << ']');
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2023-08-04 12:54:15 +02:00
|
|
|
/// Like extractVariable, but without encoding the content.
|
|
|
|
std::string extractVariablePlain(const HTMLForm& form, const std::string& field,
|
|
|
|
const std::string& var)
|
|
|
|
{
|
|
|
|
std::string value = form.get(field, "");
|
|
|
|
|
|
|
|
_vars[var] = value;
|
|
|
|
|
|
|
|
LOG_TRC("Field [" << field << "] for var [" << var << "] = [" << value << ']');
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2023-06-20 17:01:24 +02:00
|
|
|
public:
|
2023-06-22 18:27:06 +02:00
|
|
|
UserRequestVars(const HTTPRequest& /*request*/, const Poco::Net::HTMLForm& form)
|
2023-06-20 17:01:24 +02:00
|
|
|
{
|
|
|
|
// We need to pass certain parameters from the cool html GET URI
|
|
|
|
// to the embedded document URI. Here we extract those params
|
|
|
|
// from the GET URI and set them in the generated html (see cool.html.m4).
|
2023-06-22 18:27:06 +02:00
|
|
|
|
|
|
|
const std::string accessToken = extractVariable(form, "access_token", ACCESS_TOKEN);
|
2023-06-23 13:08:09 +02:00
|
|
|
const std::string accessTokenTtl =
|
|
|
|
extractVariable(form, "access_token_ttl", ACCESS_TOKEN_TTL);
|
|
|
|
|
|
|
|
unsigned long tokenTtl = 0;
|
|
|
|
if (!accessToken.empty())
|
|
|
|
{
|
|
|
|
if (!accessTokenTtl.empty())
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
tokenTtl = std::stoul(accessTokenTtl);
|
|
|
|
}
|
|
|
|
catch (const std::exception& exc)
|
|
|
|
{
|
|
|
|
LOG_ERR(
|
|
|
|
"access_token_ttl ["
|
|
|
|
<< accessTokenTtl
|
|
|
|
<< "] must be represented as the number of milliseconds "
|
|
|
|
"since January 1, 1970 UTC, when the token will expire. Defaulting to "
|
|
|
|
<< tokenTtl);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
LOG_INF("WOPI host did not pass optional access_token_ttl");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_vars[ACCESS_TOKEN_TTL] = std::to_string(tokenTtl);
|
|
|
|
LOG_TRC("Field ["
|
|
|
|
<< "access_token_ttl"
|
|
|
|
<< "] for var [" << ACCESS_TOKEN_TTL << "] = [" << tokenTtl << ']');
|
2023-07-18 14:13:50 +02:00
|
|
|
|
|
|
|
extractVariable(form, "access_header", ACCESS_HEADER);
|
2023-07-30 15:45:29 +02:00
|
|
|
|
|
|
|
extractVariable(form, "ui_defaults", UI_DEFAULTS);
|
2023-08-04 12:54:15 +02:00
|
|
|
|
|
|
|
extractVariablePlain(form, "css_variables", CSS_VARS);
|
2023-08-06 09:59:00 +02:00
|
|
|
|
|
|
|
extractVariable(form, "postmessage_origin", POSTMESSAGE_ORIGIN);
|
2023-08-13 15:43:24 +02:00
|
|
|
|
|
|
|
extractVariable(form, "theme", BRANDING_THEME);
|
2023-08-14 13:18:41 +02:00
|
|
|
|
|
|
|
extractVariable(form, "checkfileinfo_override", CHECK_FILE_INFO_OVERRIDE);
|
2023-08-15 15:48:52 +02:00
|
|
|
|
|
|
|
extractVariable(form, "buy_product", BUYPRODUCT_URL);
|
|
|
|
|
2024-03-14 15:58:36 +01:00
|
|
|
extractVariable(form, "permission", PERMISSION);
|
|
|
|
|
2023-08-15 15:48:52 +02:00
|
|
|
std::string buyProduct;
|
|
|
|
{
|
|
|
|
std::lock_guard<std::mutex> lock(COOLWSD::RemoteConfigMutex);
|
|
|
|
buyProduct = COOLWSD::BuyProductUrl;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (buyProduct.empty())
|
|
|
|
{
|
|
|
|
buyProduct = form.get("buy_product", "");
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::string escapedBuyProduct = Util::encodeURIComponent(buyProduct, "'");
|
|
|
|
_vars[BUYPRODUCT_URL] = escapedBuyProduct;
|
|
|
|
|
|
|
|
LOG_TRC("Field [buy_product] for var [" << BUYPRODUCT_URL << "] = [" << escapedBuyProduct
|
|
|
|
<< ']');
|
2023-06-20 17:01:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const std::string& operator[](const std::string& key) const
|
|
|
|
{
|
|
|
|
const auto it = _vars.find(key);
|
|
|
|
return it != _vars.end() ? it->second : _blank;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
std::unordered_map<std::string, std::string> _vars;
|
|
|
|
const std::string _blank;
|
|
|
|
};
|
|
|
|
|
2024-01-13 23:34:04 +01:00
|
|
|
FileServerRequestHandler::ResourceAccessDetails FileServerRequestHandler::preprocessFile(
|
2024-03-18 12:39:06 +01:00
|
|
|
const HTTPRequest& request, http::Response& httpResponse, const RequestDetails& requestDetails,
|
|
|
|
Poco::MemoryInputStream& message, const std::shared_ptr<StreamSocket>& socket)
|
2016-07-19 19:14:32 +02:00
|
|
|
{
|
2023-08-04 12:31:58 +02:00
|
|
|
const ServerURL cnxDetails(requestDetails);
|
2020-04-28 19:05:25 +02:00
|
|
|
|
2018-02-07 10:17:59 +01:00
|
|
|
const Poco::URI::QueryParameters params = Poco::URI(request.getURI()).getQueryParameters();
|
2017-05-08 11:56:16 +02:00
|
|
|
|
2020-10-09 12:44:14 +02:00
|
|
|
// Is this a file we read at startup - if not; it's not for serving.
|
2023-11-27 13:57:06 +01:00
|
|
|
const std::string relPath = getRequestPathname(request, requestDetails);
|
2017-04-27 15:14:31 +02:00
|
|
|
LOG_DBG("Preprocessing file: " << relPath);
|
|
|
|
std::string preprocess = *getUncompressedFile(relPath);
|
2016-07-19 19:14:32 +02:00
|
|
|
|
2021-10-26 15:23:28 +02:00
|
|
|
// We need to pass certain parameters from the cool html GET URI
|
2020-06-22 14:24:11 +02:00
|
|
|
// to the embedded document URI. Here we extract those params
|
2021-10-26 15:23:28 +02:00
|
|
|
// from the GET URI and set them in the generated html (see cool.html.m4).
|
2017-02-28 04:38:18 +01:00
|
|
|
HTMLForm form(request, message);
|
2023-06-22 18:27:06 +02:00
|
|
|
|
|
|
|
const UserRequestVars urv(request, form);
|
|
|
|
|
2022-07-13 23:45:35 +02:00
|
|
|
std::string buyProduct;
|
|
|
|
{
|
|
|
|
std::lock_guard<std::mutex> lock(COOLWSD::RemoteConfigMutex);
|
|
|
|
buyProduct = COOLWSD::BuyProductUrl;
|
|
|
|
}
|
|
|
|
if (buyProduct.empty())
|
|
|
|
buyProduct = form.get("buy_product", "");
|
2022-07-29 18:25:05 +02:00
|
|
|
LOG_TRC("buy_product=" << buyProduct);
|
2021-09-07 19:19:23 +02:00
|
|
|
|
2020-03-04 14:52:51 +01:00
|
|
|
std::string socketProxy = "false";
|
2020-05-12 17:19:41 +02:00
|
|
|
if (requestDetails.isProxy())
|
2020-03-04 14:52:51 +01:00
|
|
|
socketProxy = "true";
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%SOCKET_PROXY%"), socketProxy);
|
|
|
|
|
2023-08-04 12:31:58 +02:00
|
|
|
const std::string responseRoot = cnxDetails.getResponseRoot();
|
2020-12-07 09:02:06 +01:00
|
|
|
std::string userInterfaceMode;
|
2023-05-25 23:51:17 +02:00
|
|
|
std::string userInterfaceTheme;
|
2023-10-30 23:35:17 +01:00
|
|
|
std::string savedUIState = "true";
|
2023-08-13 15:43:24 +02:00
|
|
|
const std::string& theme = urv[BRANDING_THEME];
|
2019-11-15 19:01:02 +01:00
|
|
|
|
2023-06-22 18:27:06 +02:00
|
|
|
Poco::replaceInPlace(preprocess, ACCESS_TOKEN, urv[ACCESS_TOKEN]);
|
2023-06-23 13:08:09 +02:00
|
|
|
Poco::replaceInPlace(preprocess, ACCESS_TOKEN_TTL, urv[ACCESS_TOKEN_TTL]);
|
2023-07-18 14:13:50 +02:00
|
|
|
Poco::replaceInPlace(preprocess, ACCESS_HEADER, urv[ACCESS_HEADER]);
|
2020-04-28 19:05:25 +02:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%HOST%"), cnxDetails.getWebSocketUrl());
|
2021-11-18 13:08:14 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%VERSION%"), std::string(COOLWSD_VERSION_HASH));
|
2022-03-10 13:50:06 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%COOLWSD_VERSION%"), std::string(COOLWSD_VERSION));
|
2020-03-04 14:52:51 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%SERVICE_ROOT%"), responseRoot);
|
2023-07-30 15:45:29 +02:00
|
|
|
Poco::replaceInPlace(preprocess, UI_DEFAULTS,
|
|
|
|
uiDefaultsToJSON(urv[UI_DEFAULTS], userInterfaceMode, userInterfaceTheme, savedUIState));
|
2023-07-21 17:49:17 +02:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%UI_THEME%"), userInterfaceTheme); // UI_THEME refers to light or dark theme
|
2023-08-13 15:43:24 +02:00
|
|
|
Poco::replaceInPlace(preprocess, BRANDING_THEME, urv[BRANDING_THEME]);
|
2023-10-30 23:35:17 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%SAVED_UI_STATE%"), savedUIState);
|
2023-08-06 09:59:00 +02:00
|
|
|
Poco::replaceInPlace(preprocess, POSTMESSAGE_ORIGIN, urv[POSTMESSAGE_ORIGIN]);
|
2023-08-14 13:18:41 +02:00
|
|
|
Poco::replaceInPlace(preprocess, CHECK_FILE_INFO_OVERRIDE,
|
|
|
|
checkFileInfoToJSON(urv[CHECK_FILE_INFO_OVERRIDE]));
|
2019-03-15 17:44:58 +01:00
|
|
|
|
2020-03-04 14:52:51 +01:00
|
|
|
const auto& config = Application::instance().config();
|
2021-12-07 11:04:14 +01:00
|
|
|
|
|
|
|
std::string protocolDebug = stringifyBoolFromConfig(config, "logging.protocol", false);
|
2019-11-15 19:01:02 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%PROTOCOL_DEBUG%"), protocolDebug);
|
|
|
|
|
2021-08-31 14:36:58 +02:00
|
|
|
static const std::string hexifyEmbeddedUrls =
|
2021-11-18 13:08:14 +01:00
|
|
|
COOLWSD::getConfigValue<bool>("hexify_embedded_urls", false) ? "true" : "false";
|
2021-08-31 14:36:58 +02:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%HEXIFY_URL%"), hexifyEmbeddedUrls);
|
|
|
|
|
2023-08-13 15:43:24 +02:00
|
|
|
static const bool useIntegrationTheme =
|
|
|
|
config.getBool("user_interface.use_integration_theme", true);
|
|
|
|
const bool hasIntegrationTheme =
|
|
|
|
!theme.empty() &&
|
|
|
|
FileUtil::Stat(COOLWSD::FileServerRoot + "/browser/dist/" + theme).exists();
|
|
|
|
const std::string themePreFix = hasIntegrationTheme && useIntegrationTheme ? theme + "/" : "";
|
2021-11-18 13:08:14 +01:00
|
|
|
const std::string linkCSS("<link rel=\"stylesheet\" href=\"%s/browser/" COOLWSD_VERSION_HASH "/" + themePreFix + "%s.css\">");
|
|
|
|
const std::string scriptJS("<script src=\"%s/browser/" COOLWSD_VERSION_HASH "/" + themePreFix + "%s.js\"></script>");
|
2018-05-24 11:54:45 +02:00
|
|
|
|
2020-03-04 14:52:51 +01:00
|
|
|
std::string brandCSS(Poco::format(linkCSS, responseRoot, std::string(BRANDING)));
|
|
|
|
std::string brandJS(Poco::format(scriptJS, responseRoot, std::string(BRANDING)));
|
2018-04-25 21:53:38 +02:00
|
|
|
|
2024-03-23 15:00:48 +01:00
|
|
|
if (config::isSupportKeyEnabled())
|
2018-04-25 21:53:38 +02:00
|
|
|
{
|
2024-03-23 15:00:48 +01:00
|
|
|
const std::string keyString = config.getString("support_key", "");
|
|
|
|
SupportKey key(keyString);
|
|
|
|
if (!key.verify() || key.validDaysRemaining() <= 0)
|
|
|
|
{
|
|
|
|
brandCSS = Poco::format(linkCSS, responseRoot, SUPPORT_KEY_BRANDING_UNSUPPORTED);
|
|
|
|
brandJS = Poco::format(scriptJS, responseRoot, SUPPORT_KEY_BRANDING_UNSUPPORTED);
|
|
|
|
}
|
2018-04-25 21:53:38 +02:00
|
|
|
}
|
|
|
|
|
2019-03-15 17:44:58 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("<!--%BRANDING_CSS%-->"), brandCSS);
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("<!--%BRANDING_JS%-->"), brandJS);
|
2023-08-04 12:54:15 +02:00
|
|
|
Poco::replaceInPlace(preprocess, CSS_VARS, cssVarsToStyle(urv[CSS_VARS]));
|
2019-03-15 17:44:58 +01:00
|
|
|
|
2023-08-23 16:33:52 +02:00
|
|
|
if (config.getBool("browser_logging", false))
|
|
|
|
{
|
|
|
|
Poco::SHA1Engine engine;
|
|
|
|
engine.update(COOLWSD::LogToken);
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%BROWSER_LOGGING%"),
|
|
|
|
Poco::DigestEngine::digestToHex(engine.digest()));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%BROWSER_LOGGING%"), std::string());
|
|
|
|
|
2021-12-07 11:04:14 +01:00
|
|
|
const unsigned int outOfFocusTimeoutSecs = config.getUInt("per_view.out_of_focus_timeout_secs", 60);
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%OUT_OF_FOCUS_TIMEOUT_SECS%"), std::to_string(outOfFocusTimeoutSecs));
|
|
|
|
const unsigned int idleTimeoutSecs = config.getUInt("per_view.idle_timeout_secs", 900);
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%IDLE_TIMEOUT_SECS%"), std::to_string(idleTimeoutSecs));
|
|
|
|
|
2022-06-29 15:46:01 +02:00
|
|
|
#if ENABLE_WELCOME_MESSAGE
|
|
|
|
std::string enableWelcomeMessage = "true";
|
|
|
|
std::string autoShowWelcome = "true";
|
|
|
|
if (config.getBool("home_mode.enable", false))
|
|
|
|
{
|
|
|
|
autoShowWelcome = stringifyBoolFromConfig(config, "welcome.enable", false);
|
|
|
|
}
|
|
|
|
#else // configurable
|
|
|
|
std::string enableWelcomeMessage = stringifyBoolFromConfig(config, "welcome.enable", false);
|
|
|
|
std::string autoShowWelcome = stringifyBoolFromConfig(config, "welcome.enable", false);
|
|
|
|
#endif
|
2022-03-21 17:20:35 +01:00
|
|
|
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%ENABLE_WELCOME_MSG%"), enableWelcomeMessage);
|
2022-06-29 15:46:01 +02:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%AUTO_SHOW_WELCOME%"), autoShowWelcome);
|
2020-04-20 20:02:57 +02:00
|
|
|
|
2023-06-09 11:14:27 +02:00
|
|
|
std::string enableAccessibility = stringifyBoolFromConfig(config, "accessibility.enable", false);
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%ENABLE_ACCESSIBILITY%"), enableAccessibility);
|
|
|
|
|
2022-04-22 12:00:00 +02:00
|
|
|
// the config value of 'notebookbar/tabbed' or 'classic/compact' overrides the UIMode
|
2021-11-19 15:24:18 +01:00
|
|
|
// from the WOPI
|
|
|
|
std::string userInterfaceModeConfig = config.getString("user_interface.mode", "default");
|
2022-04-22 12:00:00 +02:00
|
|
|
if (userInterfaceModeConfig == "compact")
|
|
|
|
userInterfaceModeConfig = "classic";
|
|
|
|
|
|
|
|
if (userInterfaceModeConfig == "tabbed")
|
|
|
|
userInterfaceModeConfig = "notebookbar";
|
|
|
|
|
2021-11-19 15:24:18 +01:00
|
|
|
if (userInterfaceModeConfig == "classic" || userInterfaceModeConfig == "notebookbar" || userInterfaceMode.empty())
|
|
|
|
userInterfaceMode = userInterfaceModeConfig;
|
|
|
|
|
|
|
|
// default to the notebookbar if the value is "default" or whatever
|
|
|
|
// nonsensical
|
2023-06-09 11:14:27 +02:00
|
|
|
if (enableAccessibility == "true" || (userInterfaceMode != "classic" && userInterfaceMode != "notebookbar"))
|
2021-11-19 15:24:18 +01:00
|
|
|
userInterfaceMode = "notebookbar";
|
|
|
|
|
2020-04-21 13:20:41 +02:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%USER_INTERFACE_MODE%"), userInterfaceMode);
|
|
|
|
|
2021-12-21 17:01:34 +01:00
|
|
|
std::string uiRtlSettings;
|
2023-03-11 23:40:07 +01:00
|
|
|
if (LangUtil::isRtlLanguage(requestDetails.getParam("lang")))
|
2021-12-21 17:01:34 +01:00
|
|
|
uiRtlSettings = " dir=\"rtl\" ";
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%UI_RTL_SETTINGS%"), uiRtlSettings);
|
|
|
|
|
2022-07-08 20:33:28 +02:00
|
|
|
const std::string useIntegrationThemeString = useIntegrationTheme && hasIntegrationTheme ? "true" : "false";
|
2021-12-10 23:07:03 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%USE_INTEGRATION_THEME%"), useIntegrationThemeString);
|
|
|
|
|
2021-12-07 11:04:14 +01:00
|
|
|
std::string enableMacrosExecution = stringifyBoolFromConfig(config, "security.enable_macros_execution", false);
|
2021-03-11 15:54:13 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%ENABLE_MACROS_EXECUTION%"), enableMacrosExecution);
|
2021-05-25 21:28:05 +02:00
|
|
|
|
2023-06-02 13:57:25 +02:00
|
|
|
|
2022-06-29 15:46:01 +02:00
|
|
|
if (!config.getBool("feedback.show", true) && config.getBool("home_mode.enable", false))
|
|
|
|
{
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%AUTO_SHOW_FEEDBACK%"), (std::string)"false");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%AUTO_SHOW_FEEDBACK%"), (std::string)"true");
|
|
|
|
}
|
2023-02-15 15:19:03 +01:00
|
|
|
|
|
|
|
|
2022-03-30 17:43:55 +02:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%FEEDBACK_URL%"), std::string(FEEDBACK_URL));
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%WELCOME_URL%"), std::string(WELCOME_URL));
|
2023-02-15 15:19:03 +01:00
|
|
|
|
2023-08-15 15:48:52 +02:00
|
|
|
Poco::replaceInPlace(preprocess, BUYPRODUCT_URL, urv[BUYPRODUCT_URL]);
|
2023-02-15 15:19:03 +01:00
|
|
|
|
2022-11-01 12:04:47 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%DEEPL_ENABLED%"), (config.getBool("deepl.enabled", false) ? std::string("true"): std::string("false")));
|
2023-01-14 09:34:31 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%ZOTERO_ENABLED%"), (config.getBool("zotero.enable", true) ? std::string("true"): std::string("false")));
|
2024-01-05 16:37:27 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%WASM_ENABLED%"), (COOLWSD::getConfigValue<bool>("wasm.enable", false) ? std::string("true"): std::string("false")));
|
2022-12-03 07:43:02 +01:00
|
|
|
Poco::URI indirectionURI(config.getString("indirection_endpoint.url", ""));
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%INDIRECTION_URL%"), indirectionURI.toString());
|
2021-03-11 15:54:13 +01:00
|
|
|
|
2017-02-27 14:28:04 +01:00
|
|
|
const std::string mimeType = "text/html";
|
2016-07-19 19:14:32 +02:00
|
|
|
|
2023-06-09 13:42:46 +02:00
|
|
|
// Document signing: if endpoint URL is configured, whitelist that for
|
|
|
|
// iframe purposes.
|
2023-06-08 11:38:03 +02:00
|
|
|
ContentSecurityPolicy csp;
|
|
|
|
csp.appendDirective("default-src", "'none'");
|
|
|
|
csp.appendDirective("frame-src", "'self'");
|
|
|
|
csp.appendDirective("frame-src", WELCOME_URL);
|
|
|
|
csp.appendDirective("frame-src", FEEDBACK_URL);
|
2023-08-15 15:48:52 +02:00
|
|
|
csp.appendDirective("frame-src", Util::decodeURIComponent(urv[BUYPRODUCT_URL]));
|
2023-06-08 11:38:03 +02:00
|
|
|
csp.appendDirective("frame-src", "blob:"); // Equivalent to unsafe-eval!
|
|
|
|
csp.appendDirective("connect-src", "'self'");
|
|
|
|
csp.appendDirective("connect-src", "https://www.zotero.org");
|
|
|
|
csp.appendDirective("connect-src", "https://api.zotero.org");
|
|
|
|
csp.appendDirective("connect-src", cnxDetails.getWebSocketUrl());
|
|
|
|
csp.appendDirective("connect-src", cnxDetails.getWebServerUrl());
|
|
|
|
csp.appendDirective("connect-src", indirectionURI.getAuthority());
|
|
|
|
csp.appendDirective("script-src", "'self'");
|
|
|
|
csp.appendDirective("script-src", "'unsafe-inline'");
|
|
|
|
csp.appendDirective("style-src", "'self'");
|
|
|
|
csp.appendDirective("style-src", "'unsafe-inline'");
|
|
|
|
csp.appendDirective("font-src", "'self'");
|
|
|
|
csp.appendDirective("font-src", "data:"); // Equivalent to unsafe-inline!
|
|
|
|
csp.appendDirective("object-src", "'self'");
|
|
|
|
csp.appendDirective("object-src", "blob:"); // Equivalent to unsafe-eval!
|
|
|
|
csp.appendDirective("media-src", "'self'");
|
2023-08-04 11:30:57 +02:00
|
|
|
csp.appendDirective("media-src", cnxDetails.getWebServerUrl());
|
2023-06-08 11:44:08 +02:00
|
|
|
csp.appendDirective("img-src", "'self'");
|
|
|
|
csp.appendDirective("img-src", "data:"); // Equivalent to unsafe-inline!
|
|
|
|
csp.appendDirective("img-src", "https://www.collaboraoffice.com/");
|
2023-06-08 11:38:03 +02:00
|
|
|
|
2021-11-18 13:08:14 +01:00
|
|
|
// Frame ancestors: Allow coolwsd host, wopi host and anything configured.
|
2023-06-09 13:42:46 +02:00
|
|
|
const std::string configFrameAncestor = config.getString("net.frame_ancestors", "");
|
|
|
|
if (!configFrameAncestor.empty())
|
|
|
|
{
|
|
|
|
static bool warned = false;
|
|
|
|
if (!warned)
|
|
|
|
{
|
|
|
|
warned = true;
|
|
|
|
LOG_WRN("The config entry net.frame_ancestors is obsolete and will be removed in the "
|
|
|
|
"future. Please add 'frame-ancestors "
|
2023-08-04 11:30:57 +02:00
|
|
|
<< configFrameAncestor << ";' in the net.content_security_policy config");
|
2023-06-09 13:42:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-23 09:33:13 +01:00
|
|
|
std::string frameAncestors = configFrameAncestor;
|
2020-04-28 19:05:25 +02:00
|
|
|
Poco::URI uriHost(cnxDetails.getWebSocketUrl());
|
2018-11-23 09:33:13 +01:00
|
|
|
if (uriHost.getHost() != configFrameAncestor)
|
2020-05-24 15:10:18 +02:00
|
|
|
frameAncestors += ' ' + uriHost.getHost() + ":*";
|
2018-11-23 09:33:13 +01:00
|
|
|
|
2024-01-13 23:34:04 +01:00
|
|
|
std::string wopiSrc;
|
2018-11-23 09:33:13 +01:00
|
|
|
for (const auto& param : params)
|
2017-05-10 14:46:21 +02:00
|
|
|
{
|
2018-11-23 09:33:13 +01:00
|
|
|
if (param.first == "WOPISrc")
|
2017-05-10 14:46:21 +02:00
|
|
|
{
|
2024-02-08 03:03:30 +01:00
|
|
|
if (!HttpHelper::verifyWOPISrc(request.getURI(), param.second, socket))
|
|
|
|
{
|
2024-01-13 23:34:04 +01:00
|
|
|
return ResourceAccessDetails();
|
2024-02-08 03:03:30 +01:00
|
|
|
}
|
|
|
|
|
2023-06-08 11:56:32 +02:00
|
|
|
const Poco::URI uriWopiFrameAncestor(Util::decodeURIComponent(param.second));
|
2024-01-13 23:34:04 +01:00
|
|
|
wopiSrc = uriWopiFrameAncestor.toString();
|
|
|
|
|
2018-11-28 09:36:24 +01:00
|
|
|
// Remove parameters from URL
|
2023-06-08 11:56:32 +02:00
|
|
|
const std::string& wopiFrameAncestor = uriWopiFrameAncestor.getHost();
|
2018-11-23 09:33:13 +01:00
|
|
|
if (wopiFrameAncestor != uriHost.getHost() && wopiFrameAncestor != configFrameAncestor)
|
2017-05-10 14:46:21 +02:00
|
|
|
{
|
2020-05-24 15:10:18 +02:00
|
|
|
frameAncestors += ' ' + wopiFrameAncestor + ":*";
|
2018-11-23 09:33:13 +01:00
|
|
|
LOG_TRC("Picking frame ancestor from WOPISrc: " << wopiFrameAncestor);
|
2017-05-10 14:46:21 +02:00
|
|
|
}
|
2018-11-23 09:33:13 +01:00
|
|
|
break;
|
2017-05-10 14:46:21 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-23 09:33:13 +01:00
|
|
|
if (!frameAncestors.empty())
|
2017-04-07 11:38:38 +02:00
|
|
|
{
|
2018-11-23 09:33:13 +01:00
|
|
|
LOG_TRC("Allowed frame ancestors: " << frameAncestors);
|
|
|
|
// X-Frame-Options supports only one ancestor, ignore that
|
|
|
|
//(it's deprecated anyway and CSP works in all major browsers)
|
2023-06-08 11:44:08 +02:00
|
|
|
// frame ancestors are also allowed for img-src in order to load the views avatars
|
|
|
|
csp.appendDirective("img-src", frameAncestors);
|
|
|
|
csp.appendDirective("frame-ancestors", frameAncestors);
|
2023-06-08 12:17:41 +02:00
|
|
|
const std::string escapedFrameAncestors = Util::encodeURIComponent(frameAncestors, "'");
|
2021-12-07 11:04:14 +01:00
|
|
|
Poco::replaceInPlace(preprocess, std::string("%FRAME_ANCESTORS%"), escapedFrameAncestors);
|
2017-04-09 20:23:45 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-11-23 09:33:13 +01:00
|
|
|
LOG_TRC("Denied all frame ancestors");
|
2017-04-07 11:38:38 +02:00
|
|
|
}
|
2019-08-29 03:18:53 +02:00
|
|
|
|
2024-03-18 12:39:06 +01:00
|
|
|
httpResponse.set("Last-Modified", Util::getHttpTimeNow());
|
|
|
|
httpResponse.set("Cache-Control", "max-age=11059200");
|
|
|
|
httpResponse.set("ETag", COOLWSD_VERSION_HASH);
|
|
|
|
|
|
|
|
httpResponse.add("X-Content-Type-Options", "nosniff");
|
|
|
|
httpResponse.add("X-XSS-Protection", "1; mode=block");
|
|
|
|
httpResponse.add("Referrer-Policy", "no-referrer");
|
2020-01-14 17:56:01 +01:00
|
|
|
|
2023-12-06 04:05:59 +01:00
|
|
|
#if !MOBILEAPP
|
2023-11-21 22:02:11 +01:00
|
|
|
// if we have richdocuments with:
|
|
|
|
// addHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
|
|
// addHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
|
|
// then we seem to have to have this to avoid
|
2023-12-05 15:08:28 +01:00
|
|
|
// NS_ERROR_DOM_CORP_FAILED.
|
|
|
|
//
|
|
|
|
// We expect richdocuments to require these headers if our
|
|
|
|
// capabilities shows hasWASMSupport
|
|
|
|
if (COOLWSD::WASMState != COOLWSD::WASMActivationState::Disabled)
|
|
|
|
{
|
2024-03-18 12:39:06 +01:00
|
|
|
httpResponse.add("Cross-Origin-Opener-Policy", "same-origin");
|
|
|
|
httpResponse.add("Cross-Origin-Embedder-Policy", "require-corp");
|
|
|
|
httpResponse.add("Cross-Origin-Resource-Policy", "cross-origin");
|
2023-12-05 15:08:28 +01:00
|
|
|
}
|
2023-11-21 22:02:11 +01:00
|
|
|
|
2023-10-20 00:57:58 +02:00
|
|
|
const bool wasm = (relPath.find("wasm") != std::string::npos);
|
|
|
|
if (wasm)
|
|
|
|
{
|
2023-10-26 03:17:42 +02:00
|
|
|
LOG_ASSERT(COOLWSD::WASMState != COOLWSD::WASMActivationState::Disabled);
|
2023-10-20 00:57:58 +02:00
|
|
|
csp.appendDirective("script-src", "'unsafe-eval'");
|
|
|
|
}
|
2023-12-06 04:05:59 +01:00
|
|
|
#endif // !MOBILEAPP
|
2023-10-20 00:57:58 +02:00
|
|
|
|
|
|
|
csp.merge(config.getString("net.content_security_policy", ""));
|
|
|
|
|
2017-04-12 16:28:08 +02:00
|
|
|
// Append CSP to response headers too
|
2024-03-18 12:39:06 +01:00
|
|
|
httpResponse.add("Content-Security-Policy", csp.generate());
|
2017-04-12 16:28:08 +02:00
|
|
|
|
2017-04-10 10:12:29 +02:00
|
|
|
// Setup HTTP Public key pinning
|
2021-11-18 13:08:14 +01:00
|
|
|
if ((COOLWSD::isSSLEnabled() || COOLWSD::isSSLTermination()) && config.getBool("ssl.hpkp[@enable]", false))
|
2017-04-10 10:12:29 +02:00
|
|
|
{
|
|
|
|
size_t i = 0;
|
2020-05-24 15:10:18 +02:00
|
|
|
std::string pinPath = "ssl.hpkp.pins.pin[" + std::to_string(i) + ']';
|
2017-04-10 10:12:29 +02:00
|
|
|
std::ostringstream hpkpOss;
|
|
|
|
bool keysPinned = false;
|
|
|
|
while (config.has(pinPath))
|
|
|
|
{
|
2018-02-07 10:17:59 +01:00
|
|
|
const std::string pin = config.getString(pinPath, "");
|
2017-04-10 10:12:29 +02:00
|
|
|
if (!pin.empty())
|
|
|
|
{
|
|
|
|
hpkpOss << "pin-sha256=\"" << pin << "\"; ";
|
|
|
|
keysPinned = true;
|
|
|
|
}
|
2020-05-24 15:10:18 +02:00
|
|
|
pinPath = "ssl.hpkp.pins.pin[" + std::to_string(++i) + ']';
|
2017-04-10 10:12:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (keysPinned && config.getBool("ssl.hpkp.max_age[@enable]", false))
|
|
|
|
{
|
|
|
|
int maxAge = 1000; // seconds
|
|
|
|
try
|
|
|
|
{
|
|
|
|
maxAge = config.getInt("ssl.hpkp.max_age", maxAge);
|
|
|
|
}
|
|
|
|
catch (Poco::SyntaxException& exc)
|
|
|
|
{
|
2021-02-17 13:00:11 +01:00
|
|
|
LOG_ERR("Invalid value of HPKP's max-age directive found in config file. Defaulting to "
|
2017-04-10 10:12:29 +02:00
|
|
|
<< maxAge);
|
|
|
|
}
|
|
|
|
hpkpOss << "max-age=" << maxAge << "; ";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (keysPinned && config.getBool("ssl.hpkp.report_uri[@enable]", false))
|
|
|
|
{
|
|
|
|
const std::string reportUri = config.getString("ssl.hpkp.report_uri", "");
|
|
|
|
if (!reportUri.empty())
|
|
|
|
{
|
|
|
|
hpkpOss << "report-uri=" << reportUri << "; ";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!hpkpOss.str().empty())
|
|
|
|
{
|
|
|
|
if (config.getBool("ssl.hpkp[@report_only]", false))
|
|
|
|
{
|
|
|
|
// Only send validation failure reports to reportUri while still allowing UAs to
|
|
|
|
// connect to the server
|
2024-03-18 12:39:06 +01:00
|
|
|
httpResponse.add("Public-Key-Pins-Report-Only", hpkpOss.str());
|
2017-04-10 10:12:29 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2024-03-18 12:39:06 +01:00
|
|
|
httpResponse.add("Public-Key-Pins", hpkpOss.str());
|
2017-04-10 10:12:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-18 12:39:06 +01:00
|
|
|
httpResponse.setBody(preprocess, mimeType);
|
2016-07-19 19:14:32 +02:00
|
|
|
|
2024-03-18 12:39:06 +01:00
|
|
|
socket->send(httpResponse);
|
wsd: reduce debug and generally redundant logs
In 'debug' log-level we expect a detailed, but
still readable output. Having one area with
disproportionately large number of logs reduces
the overall utility of the log output.
This patch reduces a number of redundant log
entries, including errors that are already
logged. It also reduces the level of some
others from 'information' to 'debug' and
from 'debug' to 'trace'.
The goal is to make 'debug' level as useful as
possible to read the progress and be able to
understand what was going on, such that one is
able to decide which area to dig deeper into.
Then, trace level could be used to get more
insight into that area, if necessary. For
example, when investigating a test failure,
one first enables 'debug' logs and reads through.
Once a section between two debug entries is
identified as being of interest, enabling 'trace'
level logs becomes more productive as it's
now possible to easily reach the first DBG
entry and read through until the second one.
It's unfortunate that we don't have per-area
control for enabling/disabling logs, so it
is common to see more and more 'debug' log
entries added all around, making logs
less and less readable.
It is also a limitation of the levels we have
that we really only have 3 usable levels:
one, two, many. That is, 'information' for
the most important events, 'debug' for
technical details needed to investigate issues,
and 'trace' for everything else. ('warning'
and 'error' aren't really 'levels'; they have
semantics that makes them special-cases.)
So we have to avoid degrading one into the
other, or have differences without distinction.
If any of these entries are needed to be
displayed more frequently, changing them
back to 'debug' or even 'information' should
be done. Though for me they seem special
cases that don't benefit most log readings.
Change-Id: Id2c6a9dc027483b81a066b0b4b50a298c5eff449
Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
2022-02-19 14:30:26 +01:00
|
|
|
LOG_TRC("Sent file: " << relPath << ": " << preprocess);
|
2024-01-13 23:34:04 +01:00
|
|
|
|
2024-03-14 15:58:36 +01:00
|
|
|
return ResourceAccessDetails(wopiSrc, urv[ACCESS_TOKEN], urv[PERMISSION]);
|
2016-07-28 10:24:25 +02:00
|
|
|
}
|
|
|
|
|
2022-03-30 17:43:55 +02:00
|
|
|
void FileServerRequestHandler::preprocessWelcomeFile(const HTTPRequest& request,
|
2024-03-18 11:03:17 +01:00
|
|
|
http::Response& httpResponse,
|
2023-11-27 13:57:06 +01:00
|
|
|
const RequestDetails& requestDetails,
|
2023-06-22 11:45:02 +02:00
|
|
|
Poco::MemoryInputStream& message,
|
2022-03-30 17:43:55 +02:00
|
|
|
const std::shared_ptr<StreamSocket>& socket)
|
|
|
|
{
|
2023-11-27 13:57:06 +01:00
|
|
|
const std::string relPath = getRequestPathname(request, requestDetails);
|
2022-03-30 17:43:55 +02:00
|
|
|
LOG_DBG("Preprocessing file: " << relPath);
|
|
|
|
std::string templateWelcome = *getUncompressedFile(relPath);
|
|
|
|
|
2023-06-22 11:45:02 +02:00
|
|
|
HTMLForm form(request, message);
|
|
|
|
std::string uiTheme = form.get("ui_theme", "");
|
|
|
|
uiTheme = (uiTheme == "dark") ? "dark" : "light";
|
|
|
|
Poco::replaceInPlace(templateWelcome, std::string("%UI_THEME%"), uiTheme);
|
|
|
|
|
2022-03-30 17:43:55 +02:00
|
|
|
// Ask UAs to block if they detect any XSS attempt
|
2023-08-04 12:41:23 +02:00
|
|
|
httpResponse.add("X-XSS-Protection", "1; mode=block");
|
2022-03-30 17:43:55 +02:00
|
|
|
// No referrer-policy
|
2023-08-04 12:41:23 +02:00
|
|
|
httpResponse.add("Referrer-Policy", "no-referrer");
|
|
|
|
httpResponse.add("X-Content-Type-Options", "nosniff");
|
2022-03-30 17:43:55 +02:00
|
|
|
|
2023-08-04 12:41:23 +02:00
|
|
|
httpResponse.setBody(std::move(templateWelcome));
|
|
|
|
socket->send(httpResponse);
|
2022-03-30 17:43:55 +02:00
|
|
|
|
|
|
|
LOG_TRC("Sent file: " << relPath);
|
|
|
|
}
|
|
|
|
|
2020-05-12 17:19:41 +02:00
|
|
|
void FileServerRequestHandler::preprocessAdminFile(const HTTPRequest& request,
|
2024-03-18 11:03:17 +01:00
|
|
|
http::Response& response,
|
|
|
|
const RequestDetails& requestDetails,
|
2020-05-12 17:19:41 +02:00
|
|
|
const std::shared_ptr<StreamSocket>& socket)
|
2018-04-25 21:53:38 +02:00
|
|
|
{
|
2021-11-18 13:08:14 +01:00
|
|
|
if (!COOLWSD::AdminEnabled)
|
2018-05-24 11:54:45 +02:00
|
|
|
throw Poco::FileAccessDeniedException("Admin console disabled");
|
2018-04-25 21:53:38 +02:00
|
|
|
|
2024-03-15 12:58:49 +01:00
|
|
|
std::string jwtToken;
|
|
|
|
if (!isAdminLoggedIn(request, jwtToken))
|
|
|
|
{
|
|
|
|
// Not logged in, so let's log in now.
|
|
|
|
if (!authenticateAdmin(Poco::Net::HTTPBasicCredentials(request), response, jwtToken))
|
|
|
|
{
|
|
|
|
throw Poco::Net::NotAuthenticatedException("Invalid admin login");
|
|
|
|
}
|
|
|
|
|
|
|
|
// New login, log.
|
|
|
|
static bool showLog =
|
|
|
|
COOLWSD::getConfigValue<bool>("admin_console.logging.admin_login", true);
|
|
|
|
if (showLog)
|
|
|
|
{
|
|
|
|
LOG_ANY("Admin logged in with source IPAddress [" << socket->clientAddress() << ']');
|
|
|
|
}
|
|
|
|
}
|
2018-04-25 21:53:38 +02:00
|
|
|
|
2023-08-04 12:31:58 +02:00
|
|
|
const ServerURL cnxDetails(requestDetails);
|
|
|
|
const std::string responseRoot = cnxDetails.getResponseRoot();
|
2020-03-04 14:52:51 +01:00
|
|
|
|
2021-11-18 13:08:14 +01:00
|
|
|
static const std::string scriptJS("<script src=\"%s/browser/" COOLWSD_VERSION_HASH "/%s.js\"></script>");
|
2020-06-04 00:36:46 +02:00
|
|
|
static const std::string footerPage("<footer class=\"footer has-text-centered\"><strong>Key:</strong> %s <strong>Expiry Date:</strong> %s</footer>");
|
2018-05-24 11:54:45 +02:00
|
|
|
|
2023-11-27 13:57:06 +01:00
|
|
|
const std::string relPath = getRequestPathname(request, requestDetails);
|
2018-04-25 21:53:38 +02:00
|
|
|
LOG_DBG("Preprocessing file: " << relPath);
|
|
|
|
std::string adminFile = *getUncompressedFile(relPath);
|
2022-02-19 14:51:38 +01:00
|
|
|
const std::string templatePath =
|
|
|
|
Poco::Path(relPath).setFileName("admintemplate.html").toString();
|
2020-06-04 00:36:46 +02:00
|
|
|
std::string templateFile = *getUncompressedFile(templatePath);
|
2023-07-31 18:10:20 +02:00
|
|
|
|
2023-08-07 15:19:09 +02:00
|
|
|
const std::string escapedJwtToken = Util::encodeURIComponent(jwtToken, "'");
|
|
|
|
Poco::replaceInPlace(templateFile, std::string("%JWT_TOKEN%"), escapedJwtToken);
|
2024-01-18 07:16:38 +01:00
|
|
|
if (relPath == "/browser/dist/admin/adminClusterOverview.html" ||
|
|
|
|
relPath == "/browser/dist/admin/adminClusterOverviewAbout.html")
|
|
|
|
{
|
|
|
|
std::string bodyPath = Poco::Path(relPath).setFileName("adminClusterBody.html").toString();
|
|
|
|
std::string bodyFile = *getUncompressedFile(bodyPath);
|
|
|
|
Poco::replaceInPlace(templateFile, std::string("<!--%BODY%-->"), bodyFile);
|
|
|
|
Poco::replaceInPlace(templateFile, std::string("<!--%MAIN_CONTENT%-->"), adminFile);
|
2023-08-18 12:34:49 +02:00
|
|
|
Poco::replaceInPlace(templateFile, std::string("%ROUTE_TOKEN%"), COOLWSD::RouteToken);
|
2024-01-18 07:16:38 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-08-02 08:55:34 +02:00
|
|
|
std::string bodyPath = Poco::Path(relPath).setFileName("adminBody.html").toString();
|
|
|
|
std::string bodyFile = *getUncompressedFile(bodyPath);
|
|
|
|
Poco::replaceInPlace(templateFile, std::string("<!--%BODY%-->"), bodyFile);
|
2024-01-18 07:16:38 +01:00
|
|
|
Poco::replaceInPlace(templateFile, std::string("<!--%MAIN_CONTENT%-->"),
|
|
|
|
adminFile); // Now template has the main content..
|
2023-08-01 13:58:09 +02:00
|
|
|
}
|
2020-06-04 00:36:46 +02:00
|
|
|
|
2020-03-04 14:52:51 +01:00
|
|
|
std::string brandJS(Poco::format(scriptJS, responseRoot, std::string(BRANDING)));
|
2018-04-25 21:53:38 +02:00
|
|
|
std::string brandFooter;
|
|
|
|
|
2024-03-23 15:00:48 +01:00
|
|
|
if (config::isSupportKeyEnabled())
|
2018-04-25 21:53:38 +02:00
|
|
|
{
|
2024-03-23 15:00:48 +01:00
|
|
|
const auto& config = Application::instance().config();
|
|
|
|
const std::string keyString = config.getString("support_key", "");
|
|
|
|
SupportKey key(keyString);
|
|
|
|
|
|
|
|
if (!key.verify() || key.validDaysRemaining() <= 0)
|
|
|
|
{
|
|
|
|
brandJS = Poco::format(scriptJS, SUPPORT_KEY_BRANDING_UNSUPPORTED);
|
|
|
|
brandFooter = Poco::format(footerPage, key.data(), Poco::DateTimeFormatter::format(key.expiry(), Poco::DateTimeFormat::RFC822_FORMAT));
|
|
|
|
}
|
2018-04-25 21:53:38 +02:00
|
|
|
}
|
|
|
|
|
2020-06-04 00:36:46 +02:00
|
|
|
Poco::replaceInPlace(templateFile, std::string("<!--%BRANDING_JS%-->"), brandJS);
|
|
|
|
Poco::replaceInPlace(templateFile, std::string("<!--%FOOTER%-->"), brandFooter);
|
2021-11-18 13:08:14 +01:00
|
|
|
Poco::replaceInPlace(templateFile, std::string("%VERSION%"), std::string(COOLWSD_VERSION_HASH));
|
2020-06-04 00:36:46 +02:00
|
|
|
Poco::replaceInPlace(templateFile, std::string("%SERVICE_ROOT%"), responseRoot);
|
2018-04-25 21:53:38 +02:00
|
|
|
|
|
|
|
// 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");
|
2024-02-20 18:23:58 +01:00
|
|
|
response.set("Server", http::getServerString());
|
2019-05-20 07:40:12 +02:00
|
|
|
response.set("Date", Util::getHttpTimeNow());
|
2018-04-25 21:53:38 +02:00
|
|
|
|
2024-03-17 12:45:30 +01:00
|
|
|
response.setBody(std::move(templateFile));
|
|
|
|
socket->send(response);
|
2018-04-25 21:53:38 +02:00
|
|
|
}
|
|
|
|
|
2016-07-19 19:14:32 +02:00
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|