f494c27024
User-Agent is designed for client-side use only, in http requests. For servers, the Server header is designed to announce the server name and version. This tries to normalize the use and documents the proper intent and usage. Change-Id: I42d68d65611cab64c45adf03fe74f9466798b093 Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
999 lines
38 KiB
C++
999 lines
38 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
|
|
/*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#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>
|
|
|
|
#include <Poco/DateTime.h>
|
|
#include <Poco/DateTimeFormat.h>
|
|
#include <Poco/DateTimeFormatter.h>
|
|
#include <Poco/Exception.h>
|
|
#include <Poco/FileStream.h>
|
|
#include <Poco/Net/HTMLForm.h>
|
|
#include <Poco/Net/HTTPBasicCredentials.h>
|
|
#include <Poco/Net/HTTPCookie.h>
|
|
#include <Poco/Net/HTTPRequest.h>
|
|
#include <Poco/Net/HTTPResponse.h>
|
|
#include <Poco/Net/NameValueCollection.h>
|
|
#include <Poco/Net/NetException.h>
|
|
#include <Poco/RegularExpression.h>
|
|
#include <Poco/Runnable.h>
|
|
#include <Poco/StreamCopier.h>
|
|
#include <Poco/URI.h>
|
|
|
|
#include "Auth.hpp"
|
|
#include <Common.hpp>
|
|
#include <Crypto.hpp>
|
|
#include "FileServer.hpp"
|
|
#include "LOOLWSD.hpp"
|
|
#include "ServerURL.hpp"
|
|
#include <Log.hpp>
|
|
#include <Protocol.hpp>
|
|
#include <Util.hpp>
|
|
#if !MOBILEAPP
|
|
#include <net/HttpHelper.hpp>
|
|
#endif
|
|
using Poco::Net::HTMLForm;
|
|
using Poco::Net::HTTPBasicCredentials;
|
|
using Poco::Net::HTTPRequest;
|
|
using Poco::Net::HTTPResponse;
|
|
using Poco::Net::NameValueCollection;
|
|
using Poco::Util::Application;
|
|
|
|
std::map<std::string, std::pair<std::string, std::string>> FileServerRequestHandler::FileHash;
|
|
|
|
/// Place from where we serve the welcome-<lang>.html; defaults to
|
|
/// welcome.html if no lang matches.
|
|
#define WELCOME_ENDPOINT "/loleaflet/dist/welcome"
|
|
|
|
namespace {
|
|
|
|
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;
|
|
}
|
|
|
|
/// Use PAM to check for user / password.
|
|
bool isPamAuthOk(const std::string& userProvidedUsr, const std::string& userProvidedPwd)
|
|
{
|
|
struct pam_conv localConversation { functionConversation, nullptr };
|
|
pam_handle_t *localAuthHandle = NULL;
|
|
int retval;
|
|
|
|
localConversation.appdata_ptr = const_cast<char *>(userProvidedPwd.c_str());
|
|
|
|
retval = pam_start("loolwsd", userProvidedUsr.c_str(), &localConversation, &localAuthHandle);
|
|
|
|
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)
|
|
{
|
|
LOG_ERR("PAM authentication failure for user \"" << userProvidedUsr << "\".");
|
|
}
|
|
else
|
|
{
|
|
LOG_ERR("pam_authenticate returned " << retval);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
LOG_INF("PAM authentication success for user \"" << userProvidedUsr << "\".");
|
|
|
|
retval = pam_end(localAuthHandle, retval);
|
|
|
|
if (retval != PAM_SUCCESS)
|
|
{
|
|
LOG_ERR("pam_end returned " << retval);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Check for user / password set in loolwsd.xml.
|
|
bool isConfigAuthOk(const std::string& userProvidedUsr, const std::string& userProvidedPwd)
|
|
{
|
|
const auto& config = Application::instance().config();
|
|
const std::string& user = config.getString("admin_console.username", "");
|
|
|
|
// Check for the username
|
|
if (user.empty())
|
|
{
|
|
LOG_ERR("Admin Console username missing, admin console disabled.");
|
|
return false;
|
|
}
|
|
else if (user != userProvidedUsr)
|
|
{
|
|
LOG_ERR("Admin Console wrong username.");
|
|
return false;
|
|
}
|
|
|
|
const char useLoolconfig[] = " Use loolconfig to configure the admin password.";
|
|
|
|
// do we have secure_password?
|
|
if (config.has("admin_console.secure_password"))
|
|
{
|
|
const std::string securePass = config.getString("admin_console.secure_password", "");
|
|
if (securePass.empty())
|
|
{
|
|
LOG_ERR("Admin Console secure password is empty, denying access." << useLoolconfig);
|
|
return false;
|
|
}
|
|
|
|
#if HAVE_PKCS5_PBKDF2_HMAC
|
|
// Extract the salt from the config
|
|
std::vector<unsigned char> saltData;
|
|
StringVector tokens = Util::tokenize(securePass, '.');
|
|
if (tokens.size() != 5 ||
|
|
!tokens.equals(0, "pbkdf2") ||
|
|
!tokens.equals(1, "sha512") ||
|
|
!Util::dataFromHexString(tokens[3], saltData))
|
|
{
|
|
LOG_ERR("Incorrect format detected for secure_password in config file." << useLoolconfig);
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
for (unsigned long j = 0; j < sizeof userProvidedPwdHash; ++j)
|
|
stream << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(userProvidedPwdHash[j]);
|
|
|
|
// now compare the hashed user-provided pwd against the stored hash
|
|
std::string string = stream.str();
|
|
return tokens.equals(4, string.c_str());
|
|
#else
|
|
const std::string pass = config.getString("admin_console.password", "");
|
|
LOG_ERR("The config file has admin_console.secure_password setting, "
|
|
<< "but this application was compiled with old OpenSSL version, "
|
|
<< "and this setting cannot be used." << (!pass.empty()? " Falling back to plain text password.": ""));
|
|
|
|
// careful, a fall-through!
|
|
#endif
|
|
}
|
|
|
|
const std::string pass = config.getString("admin_console.password", "");
|
|
if (pass.empty())
|
|
{
|
|
LOG_ERR("Admin Console password is empty, denying access." << useLoolconfig);
|
|
return false;
|
|
}
|
|
|
|
return pass == userProvidedPwd;
|
|
}
|
|
|
|
}
|
|
|
|
bool FileServerRequestHandler::isAdminLoggedIn(const HTTPRequest& request,
|
|
HTTPResponse &response)
|
|
{
|
|
assert(LOOLWSD::AdminEnabled);
|
|
|
|
const auto& config = Application::instance().config();
|
|
|
|
NameValueCollection cookies;
|
|
request.getCookies(cookies);
|
|
try
|
|
{
|
|
const std::string 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");
|
|
}
|
|
|
|
// If no cookie found, or is invalid, let the admin re-login
|
|
HTTPBasicCredentials credentials(request);
|
|
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 (config.getBool("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");
|
|
const std::string jwtToken = authAgent.getAccessToken();
|
|
|
|
Poco::Net::HTTPCookie cookie("jwt", jwtToken);
|
|
// bundlify appears to add an extra /dist -> dist/dist/admin
|
|
cookie.setPath(LOOLWSD::ServiceRoot + "/loleaflet/dist/");
|
|
cookie.setSecure(LOOLWSD::isSSLEnabled() ||
|
|
LOOLWSD::isSSLTermination());
|
|
response.addCookie(cookie);
|
|
|
|
return true;
|
|
}
|
|
|
|
void FileServerRequestHandler::handleRequest(const HTTPRequest& request,
|
|
const RequestDetails &requestDetails,
|
|
Poco::MemoryInputStream& message,
|
|
const std::shared_ptr<StreamSocket>& socket)
|
|
{
|
|
try
|
|
{
|
|
bool noCache = false;
|
|
#if ENABLE_DEBUG
|
|
noCache = true;
|
|
#endif
|
|
Poco::Net::HTTPResponse response;
|
|
Poco::URI requestUri(request.getURI());
|
|
LOG_TRC("Fileserver request: " << requestUri.toString());
|
|
requestUri.normalize(); // avoid .'s and ..'s
|
|
|
|
std::string path(requestUri.getPath());
|
|
if (path.find("loleaflet/" LOOLWSD_VERSION_HASH "/") == std::string::npos)
|
|
{
|
|
LOG_WRN("client - server version mismatch, disabling browser cache. Expected: " LOOLWSD_VERSION_HASH);
|
|
noCache = true;
|
|
}
|
|
|
|
std::vector<std::string> requestSegments;
|
|
requestUri.getPathSegments(requestSegments);
|
|
if (requestSegments.size() < 1)
|
|
throw Poco::FileNotFoundException("Invalid URI request: [" + requestUri.toString() + "].");
|
|
|
|
std::string relPath = getRequestPathname(request);
|
|
std::string endPoint = requestSegments[requestSegments.size() - 1];
|
|
const auto& config = Application::instance().config();
|
|
|
|
if (request.getMethod() == HTTPRequest::HTTP_POST && endPoint == "logging.html")
|
|
{
|
|
const std::string loleafletLogging = config.getString("loleaflet_logging", "false");
|
|
if (loleafletLogging != "false")
|
|
{
|
|
LOG_ERR(message.rdbuf());
|
|
|
|
std::ostringstream oss;
|
|
response.write(oss);
|
|
socket->send(oss.str());
|
|
return;
|
|
}
|
|
}
|
|
|
|
// handling of the language in welcome-*.html - shorten the langtag as
|
|
// necessary, if we don't have the particular language version
|
|
if (Util::startsWith(relPath, WELCOME_ENDPOINT "/"))
|
|
{
|
|
bool found = true;
|
|
while (FileHash.find(relPath) == FileHash.end())
|
|
{
|
|
size_t dot = relPath.find_last_of('.');
|
|
if (dot == std::string::npos)
|
|
{
|
|
found = false;
|
|
break;
|
|
}
|
|
|
|
size_t dash = relPath.find_last_of("-_", dot);
|
|
if (dash == std::string::npos)
|
|
{
|
|
found = false;
|
|
break;
|
|
}
|
|
|
|
relPath = relPath.substr(0, dash) + relPath.substr(dot);
|
|
LOG_TRC("Shortening welcome file request to: " << relPath);
|
|
}
|
|
|
|
if (!found)
|
|
throw Poco::FileNotFoundException("Invalid URI welcome file request: [" + requestUri.toString() + "].");
|
|
|
|
endPoint = relPath.substr(sizeof(WELCOME_ENDPOINT));
|
|
}
|
|
|
|
// Is this a file we read at startup - if not; it's not for serving.
|
|
if (FileHash.find(relPath) == FileHash.end())
|
|
throw Poco::FileNotFoundException("Invalid URI request: [" + requestUri.toString() + "].");
|
|
|
|
const std::string loleafletHtml = config.getString("loleaflet_html", "loleaflet.html");
|
|
if (endPoint == loleafletHtml ||
|
|
endPoint == "help-localizations.json" ||
|
|
endPoint == "localizations.json" ||
|
|
endPoint == "locore-localizations.json" ||
|
|
endPoint == "uno-localizations.json" ||
|
|
endPoint == "uno-localizations-override.json")
|
|
{
|
|
preprocessFile(request, requestDetails, message, socket);
|
|
return;
|
|
}
|
|
|
|
if (request.getMethod() == HTTPRequest::HTTP_GET)
|
|
{
|
|
if (endPoint == "admin.html" ||
|
|
endPoint == "adminSettings.html" ||
|
|
endPoint == "adminHistory.html" ||
|
|
endPoint == "adminAnalytics.html" ||
|
|
endPoint == "adminLog.html")
|
|
{
|
|
preprocessAdminFile(request, requestDetails, socket);
|
|
return;
|
|
}
|
|
|
|
if (endPoint == "admin-bundle.js" ||
|
|
endPoint == "admin-localizations.js")
|
|
{
|
|
noCache = true;
|
|
|
|
if (!LOOLWSD::AdminEnabled)
|
|
throw Poco::FileAccessDeniedException("Admin console disabled");
|
|
|
|
if (!FileServerRequestHandler::isAdminLoggedIn(request, response))
|
|
throw Poco::Net::NotAuthenticatedException("Invalid admin login");
|
|
|
|
// 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");
|
|
}
|
|
|
|
// Do we have an extension.
|
|
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);
|
|
std::string mimeType;
|
|
if (fileType == "js")
|
|
mimeType = "application/javascript";
|
|
else if (fileType == "css")
|
|
mimeType = "text/css";
|
|
else if (fileType == "html")
|
|
mimeType = "text/html";
|
|
else if (fileType == "png")
|
|
mimeType = "image/png";
|
|
else if (fileType == "svg")
|
|
mimeType = "image/svg+xml";
|
|
else
|
|
mimeType = "text/plain";
|
|
|
|
auto it = request.find("If-None-Match");
|
|
if (it != request.end())
|
|
{
|
|
// if ETags match avoid re-sending the file.
|
|
if (!noCache && it->second == "\"" LOOLWSD_VERSION_HASH "\"")
|
|
{
|
|
// TESTME: harder ... - do we even want ETag support ?
|
|
std::ostringstream oss;
|
|
Poco::DateTime now;
|
|
Poco::DateTime later(now.utcTime(), int64_t(1000)*1000 * 60 * 60 * 24 * 128);
|
|
std::string extraHeaders =
|
|
"Expires: " + Poco::DateTimeFormatter::format(
|
|
later, Poco::DateTimeFormat::HTTP_FORMAT) + "\r\n" +
|
|
"Cache-Control: max-age=11059200\r\n";
|
|
HttpHelper::sendErrorAndShutdown(304, socket, std::string(), extraHeaders);
|
|
return;
|
|
}
|
|
}
|
|
|
|
response.set("Server", HTTP_SERVER_STRING);
|
|
response.set("Date", Util::getHttpTimeNow());
|
|
|
|
bool gzip = request.hasToken("Accept-Encoding", "gzip");
|
|
const std::string *content;
|
|
#if ENABLE_DEBUG
|
|
if (std::getenv("LOOL_SERVE_FROM_FS"))
|
|
{
|
|
// Useful to not serve from memory sometimes especially during loleaflet development
|
|
// Avoids having to restart loolwsd everytime you make a change in loleaflet
|
|
const std::string filePath = Poco::Path(LOOLWSD::FileServerRoot, relPath).absolute().toString();
|
|
HttpHelper::sendFileAndShutdown(socket, filePath, mimeType, &response, noCache);
|
|
return;
|
|
}
|
|
#endif
|
|
if (gzip)
|
|
{
|
|
response.set("Content-Encoding", "gzip");
|
|
content = getCompressedFile(relPath);
|
|
}
|
|
else
|
|
content = getUncompressedFile(relPath);
|
|
|
|
if (!noCache)
|
|
{
|
|
// 60 * 60 * 24 * 128 (days) = 11059200
|
|
response.set("Cache-Control", "max-age=11059200");
|
|
response.set("ETag", "\"" LOOLWSD_VERSION_HASH "\"");
|
|
}
|
|
response.setContentType(mimeType);
|
|
response.add("X-Content-Type-Options", "nosniff");
|
|
|
|
std::ostringstream oss;
|
|
response.write(oss);
|
|
const std::string header = oss.str();
|
|
LOG_TRC('#' << socket->getFD() << ": Sending " <<
|
|
(!gzip ? "un":"") << "compressed : file [" << relPath << "]: " << header);
|
|
socket->send(header);
|
|
socket->send(*content);
|
|
// shutdown by caller
|
|
}
|
|
}
|
|
catch (const Poco::Net::NotAuthenticatedException& exc)
|
|
{
|
|
LOG_ERR("FileServerRequestHandler::NotAuthenticated: " << exc.displayText());
|
|
sendError(401, request, socket, "", "", "WWW-authenticate: Basic realm=\"online\"\r\n");
|
|
}
|
|
catch (const Poco::FileAccessDeniedException& exc)
|
|
{
|
|
LOG_ERR("FileServerRequestHandler: " << exc.displayText());
|
|
sendError(403, request, socket, "403 - Access denied!",
|
|
"You are unable to access");
|
|
}
|
|
catch (const Poco::FileNotFoundException& exc)
|
|
{
|
|
LOG_ERR("FileServerRequestHandler: " << exc.displayText());
|
|
sendError(404, request, socket, "404 - file not found!",
|
|
"There seems to be a problem locating");
|
|
}
|
|
}
|
|
|
|
void FileServerRequestHandler::sendError(int errorCode, const Poco::Net::HTTPRequest& request,
|
|
const std::shared_ptr<StreamSocket>& socket,
|
|
const std::string& shortMessage, const std::string& longMessage,
|
|
const std::string& extraHeader)
|
|
{
|
|
std::string body;
|
|
std::string headers = extraHeader;
|
|
if (!shortMessage.empty())
|
|
{
|
|
Poco::URI requestUri(request.getURI());
|
|
std::string pathSanitized;
|
|
Poco::URI::encode(requestUri.getPath(), "", pathSanitized);
|
|
headers += "Content-Type: text/html charset=UTF-8\r\n";
|
|
body = "<h1>Error: " + shortMessage + "</h1>" +
|
|
"<p>" + longMessage + ' ' + pathSanitized + "</p>" +
|
|
"<p>Please contact your system administrator.</p>";
|
|
}
|
|
HttpHelper::sendError(errorCode, socket, body, headers);
|
|
}
|
|
|
|
void FileServerRequestHandler::readDirToHash(const std::string &basePath, const std::string &path, const std::string &prefix)
|
|
{
|
|
LOG_DBG("Caching files in [" << basePath + path << ']');
|
|
|
|
DIR* workingdir = opendir((basePath + path).c_str());
|
|
if (!workingdir)
|
|
{
|
|
LOG_SYS("Failed to open directory [" << basePath + path << ']');
|
|
return;
|
|
}
|
|
|
|
size_t fileCount = 0;
|
|
std::string filesRead;
|
|
filesRead.reserve(1024);
|
|
|
|
struct dirent *currentFile;
|
|
while ((currentFile = readdir(workingdir)) != nullptr)
|
|
{
|
|
if (currentFile->d_name[0] == '.')
|
|
continue;
|
|
|
|
const std::string relPath = path + '/' + currentFile->d_name;
|
|
struct stat fileStat;
|
|
stat ((basePath + relPath).c_str(), &fileStat);
|
|
|
|
if (S_ISDIR(fileStat.st_mode))
|
|
readDirToHash(basePath, relPath);
|
|
|
|
else if (S_ISREG(fileStat.st_mode))
|
|
{
|
|
fileCount++;
|
|
filesRead.append(currentFile->d_name);
|
|
filesRead += ' ';
|
|
|
|
std::ifstream file(basePath + relPath, std::ios::binary);
|
|
|
|
z_stream strm;
|
|
strm.zalloc = Z_NULL;
|
|
strm.zfree = Z_NULL;
|
|
strm.opaque = Z_NULL;
|
|
deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY);
|
|
|
|
std::unique_ptr<char[]> buf(new char[fileStat.st_size]);
|
|
std::string compressedFile;
|
|
compressedFile.reserve(fileStat.st_size);
|
|
std::string uncompressedFile;
|
|
uncompressedFile.reserve(fileStat.st_size);
|
|
do
|
|
{
|
|
file.read(&buf[0], fileStat.st_size);
|
|
const long unsigned int size = file.gcount();
|
|
if (size == 0)
|
|
break;
|
|
|
|
const long unsigned int compSize = compressBound(size);
|
|
char *cbuf = (char *)calloc(compSize, sizeof(char));
|
|
|
|
strm.next_in = (unsigned char *)&buf[0];
|
|
strm.avail_in = size;
|
|
strm.avail_out = compSize;
|
|
strm.next_out = (unsigned char *)&cbuf[0];
|
|
|
|
deflate(&strm, Z_FINISH);
|
|
|
|
const long unsigned int haveComp = compSize - strm.avail_out;
|
|
std::string partialcompFile(cbuf, haveComp);
|
|
std::string partialuncompFile(buf.get(), size);
|
|
compressedFile += partialcompFile;
|
|
uncompressedFile += partialuncompFile;
|
|
free(cbuf);
|
|
|
|
} while(true);
|
|
|
|
FileHash.emplace(prefix + relPath, std::make_pair(uncompressedFile, compressedFile));
|
|
deflateEnd(&strm);
|
|
}
|
|
}
|
|
closedir(workingdir);
|
|
|
|
if (fileCount > 0)
|
|
LOG_TRC("Pre-read " << fileCount << " file(s) from directory: " << basePath << path << ": " << filesRead);
|
|
}
|
|
|
|
void FileServerRequestHandler::initialize()
|
|
{
|
|
// loleaflet files
|
|
try {
|
|
readDirToHash(LOOLWSD::FileServerRoot, "/loleaflet/dist");
|
|
} catch (...) {
|
|
LOG_ERR("Failed to read from directory " << LOOLWSD::FileServerRoot);
|
|
}
|
|
|
|
// welcome / release notes files
|
|
if (!LOOLWSD::WelcomeFilesRoot.empty())
|
|
{
|
|
try {
|
|
readDirToHash(LOOLWSD::WelcomeFilesRoot, "", WELCOME_ENDPOINT);
|
|
} catch (...) {
|
|
LOG_ERR("Failed to read from directory " << LOOLWSD::WelcomeFilesRoot);
|
|
}
|
|
}
|
|
}
|
|
|
|
const std::string *FileServerRequestHandler::getCompressedFile(const std::string &path)
|
|
{
|
|
return &FileHash[path].second;
|
|
}
|
|
|
|
const std::string *FileServerRequestHandler::getUncompressedFile(const std::string &path)
|
|
{
|
|
return &FileHash[path].first;
|
|
}
|
|
|
|
std::string FileServerRequestHandler::getRequestPathname(const HTTPRequest& request)
|
|
{
|
|
Poco::URI requestUri(request.getURI());
|
|
// avoid .'s and ..'s
|
|
requestUri.normalize();
|
|
|
|
std::string path(requestUri.getPath());
|
|
Poco::RegularExpression gitHashRe("/([0-9a-f]+)/");
|
|
std::string gitHash;
|
|
if (gitHashRe.extract(path, gitHash))
|
|
{
|
|
// Convert version back to a real file name.
|
|
Poco::replaceInPlace(path, std::string("/loleaflet" + gitHash), std::string("/loleaflet/dist/"));
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
constexpr char BRANDING[] = "branding";
|
|
#if ENABLE_SUPPORT_KEY
|
|
constexpr char BRANDING_UNSUPPORTED[] = "branding-unsupported";
|
|
#endif
|
|
|
|
namespace {
|
|
}
|
|
|
|
void FileServerRequestHandler::preprocessFile(const HTTPRequest& request,
|
|
const RequestDetails &requestDetails,
|
|
Poco::MemoryInputStream& message,
|
|
const std::shared_ptr<StreamSocket>& socket)
|
|
{
|
|
ServerURL cnxDetails(requestDetails);
|
|
|
|
const Poco::URI::QueryParameters params = Poco::URI(request.getURI()).getQueryParameters();
|
|
|
|
// Is this a file we read at startup - if not; it's not for serving.
|
|
const std::string relPath = getRequestPathname(request);
|
|
LOG_DBG("Preprocessing file: " << relPath);
|
|
std::string preprocess = *getUncompressedFile(relPath);
|
|
|
|
// We need to pass certain parameters from the loleaflet 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 loleaflet.html.m4).
|
|
HTMLForm form(request, message);
|
|
const std::string accessToken = form.get("access_token", "");
|
|
const std::string accessTokenTtl = form.get("access_token_ttl", "");
|
|
LOG_TRC("access_token=" << accessToken << ", access_token_ttl=" << accessTokenTtl);
|
|
const std::string accessHeader = form.get("access_header", "");
|
|
LOG_TRC("access_header=" << accessHeader);
|
|
const std::string uiDefaults = form.get("ui_defaults", "");
|
|
LOG_TRC("ui_defaults=" << uiDefaults);
|
|
const std::string cssVars = form.get("css_variables", "");
|
|
LOG_TRC("css_variables=" << cssVars);
|
|
|
|
// Escape bad characters in access token.
|
|
// This is placed directly in javascript in loleaflet.html, we need to make sure
|
|
// that no one can do anything nasty with their clever inputs.
|
|
std::string escapedAccessToken, escapedAccessHeader;
|
|
Poco::URI::encode(accessToken, "'", escapedAccessToken);
|
|
Poco::URI::encode(accessHeader, "'", escapedAccessHeader);
|
|
|
|
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 must be represented as the number of milliseconds since January 1, 1970 UTC, when the token will expire");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LOG_INF("WOPI host did not pass optional access_token_ttl");
|
|
}
|
|
}
|
|
|
|
std::string socketProxy = "false";
|
|
if (requestDetails.isProxy())
|
|
socketProxy = "true";
|
|
Poco::replaceInPlace(preprocess, std::string("%SOCKET_PROXY%"), socketProxy);
|
|
|
|
std::string responseRoot = cnxDetails.getResponseRoot();
|
|
std::string userInterfaceMode;
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("%ACCESS_TOKEN%"), escapedAccessToken);
|
|
Poco::replaceInPlace(preprocess, std::string("%ACCESS_TOKEN_TTL%"), std::to_string(tokenTtl));
|
|
Poco::replaceInPlace(preprocess, std::string("%ACCESS_HEADER%"), escapedAccessHeader);
|
|
Poco::replaceInPlace(preprocess, std::string("%HOST%"), cnxDetails.getWebSocketUrl());
|
|
Poco::replaceInPlace(preprocess, std::string("%VERSION%"), std::string(LOOLWSD_VERSION_HASH));
|
|
Poco::replaceInPlace(preprocess, std::string("%SERVICE_ROOT%"), responseRoot);
|
|
Poco::replaceInPlace(preprocess, std::string("%UI_DEFAULTS%"), uiDefaultsToJSON(uiDefaults, userInterfaceMode));
|
|
|
|
const auto& config = Application::instance().config();
|
|
std::string protocolDebug = "false";
|
|
if (config.getBool("logging.protocol"))
|
|
protocolDebug = "true";
|
|
Poco::replaceInPlace(preprocess, std::string("%PROTOCOL_DEBUG%"), protocolDebug);
|
|
|
|
static const std::string linkCSS("<link rel=\"stylesheet\" href=\"%s/loleaflet/" LOOLWSD_VERSION_HASH "/%s.css\">");
|
|
static const std::string scriptJS("<script src=\"%s/loleaflet/" LOOLWSD_VERSION_HASH "/%s.js\"></script>");
|
|
|
|
std::string brandCSS(Poco::format(linkCSS, responseRoot, std::string(BRANDING)));
|
|
std::string brandJS(Poco::format(scriptJS, responseRoot, std::string(BRANDING)));
|
|
|
|
#if ENABLE_SUPPORT_KEY
|
|
const std::string keyString = config.getString("support_key", "");
|
|
SupportKey key(keyString);
|
|
if (!key.verify() || key.validDaysRemaining() <= 0)
|
|
{
|
|
brandCSS = Poco::format(linkCSS, responseRoot, std::string(BRANDING_UNSUPPORTED));
|
|
brandJS = Poco::format(scriptJS, responseRoot, std::string(BRANDING_UNSUPPORTED));
|
|
}
|
|
#endif
|
|
|
|
Poco::replaceInPlace(preprocess, std::string("<!--%BRANDING_CSS%-->"), brandCSS);
|
|
Poco::replaceInPlace(preprocess, std::string("<!--%BRANDING_JS%-->"), brandJS);
|
|
Poco::replaceInPlace(preprocess, std::string("<!--%CSS_VARIABLES%-->"), cssVarsToStyle(cssVars));
|
|
|
|
// Customization related to document signing.
|
|
std::string documentSigningDiv;
|
|
const std::string documentSigningURL = config.getString("per_document.document_signing_url", "");
|
|
if (!documentSigningURL.empty())
|
|
{
|
|
documentSigningDiv = "<div id=\"document-signing-bar\"></div>";
|
|
}
|
|
Poco::replaceInPlace(preprocess, std::string("<!--%DOCUMENT_SIGNING_DIV%-->"), documentSigningDiv);
|
|
Poco::replaceInPlace(preprocess, std::string("%DOCUMENT_SIGNING_URL%"), documentSigningURL);
|
|
|
|
const auto loleafletLogging = config.getString("loleaflet_logging", "false");
|
|
Poco::replaceInPlace(preprocess, std::string("%LOLEAFLET_LOGGING%"), loleafletLogging);
|
|
const std::string outOfFocusTimeoutSecs= config.getString("per_view.out_of_focus_timeout_secs", "60");
|
|
Poco::replaceInPlace(preprocess, std::string("%OUT_OF_FOCUS_TIMEOUT_SECS%"), outOfFocusTimeoutSecs);
|
|
const std::string idleTimeoutSecs= config.getString("per_view.idle_timeout_secs", "900");
|
|
Poco::replaceInPlace(preprocess, std::string("%IDLE_TIMEOUT_SECS%"), idleTimeoutSecs);
|
|
|
|
std::string enableWelcomeMessage = "false";
|
|
if (config.getBool("welcome.enable", false))
|
|
enableWelcomeMessage = "true";
|
|
Poco::replaceInPlace(preprocess, std::string("%ENABLE_WELCOME_MSG%"), enableWelcomeMessage);
|
|
|
|
std::string enableWelcomeMessageButton = "false";
|
|
if (config.getBool("welcome.enable_button", false))
|
|
enableWelcomeMessageButton = "true";
|
|
Poco::replaceInPlace(preprocess, std::string("%ENABLE_WELCOME_MSG_BTN%"), enableWelcomeMessageButton);
|
|
|
|
if (userInterfaceMode.empty())
|
|
userInterfaceMode = config.getString("user_interface.mode", "classic");
|
|
Poco::replaceInPlace(preprocess, std::string("%USER_INTERFACE_MODE%"), userInterfaceMode);
|
|
|
|
std::string enableMacrosExecution = "false";
|
|
if (config.getBool("security.enable_macros_execution", false))
|
|
enableMacrosExecution = "true";
|
|
Poco::replaceInPlace(preprocess, std::string("%ENABLE_MACROS_EXECUTION%"), enableMacrosExecution);
|
|
|
|
// Capture cookies so we can optionally reuse them for the storage requests.
|
|
{
|
|
NameValueCollection cookies;
|
|
request.getCookies(cookies);
|
|
std::ostringstream cookieTokens;
|
|
for (auto it = cookies.begin(); it != cookies.end(); it++)
|
|
cookieTokens << (*it).first << '=' << (*it).second << (std::next(it) != cookies.end() ? ":" : "");
|
|
|
|
const std::string cookiesString = cookieTokens.str();
|
|
if (!cookiesString.empty())
|
|
LOG_DBG("Captured cookies: " << cookiesString);
|
|
Poco::replaceInPlace(preprocess, std::string("%REUSE_COOKIES%"), cookiesString);
|
|
}
|
|
|
|
const std::string mimeType = "text/html";
|
|
|
|
// Document signing: if endpoint URL is configured, whitelist that for
|
|
// iframe purposes.
|
|
std::ostringstream cspOss;
|
|
cspOss << "Content-Security-Policy: default-src 'none'; "
|
|
"frame-src 'self' blob: " << documentSigningURL << "; "
|
|
"connect-src 'self' " << cnxDetails.getWebSocketUrl() << "; "
|
|
"script-src 'unsafe-inline' 'self'; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"font-src 'self' data:; "
|
|
"object-src 'self' blob:; ";
|
|
|
|
// Frame ancestors: Allow loolwsd host, wopi host and anything configured.
|
|
std::string configFrameAncestor = config.getString("net.frame_ancestors", "");
|
|
std::string frameAncestors = configFrameAncestor;
|
|
Poco::URI uriHost(cnxDetails.getWebSocketUrl());
|
|
if (uriHost.getHost() != configFrameAncestor)
|
|
frameAncestors += ' ' + uriHost.getHost() + ":*";
|
|
|
|
for (const auto& param : params)
|
|
{
|
|
if (param.first == "WOPISrc")
|
|
{
|
|
std::string wopiFrameAncestor;
|
|
Poco::URI::decode(param.second, wopiFrameAncestor);
|
|
Poco::URI uriWopiFrameAncestor(wopiFrameAncestor);
|
|
// Remove parameters from URL
|
|
wopiFrameAncestor = uriWopiFrameAncestor.getHost();
|
|
if (wopiFrameAncestor != uriHost.getHost() && wopiFrameAncestor != configFrameAncestor)
|
|
{
|
|
frameAncestors += ' ' + wopiFrameAncestor + ":*";
|
|
LOG_TRC("Picking frame ancestor from WOPISrc: " << wopiFrameAncestor);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!frameAncestors.empty())
|
|
{
|
|
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)
|
|
cspOss << "img-src 'self' data: " << frameAncestors << "; "
|
|
<< "frame-ancestors " << frameAncestors;
|
|
Poco::replaceInPlace(preprocess, std::string("%FRAME_ANCESTORS%"), frameAncestors);
|
|
}
|
|
else
|
|
{
|
|
LOG_TRC("Denied all frame ancestors");
|
|
cspOss << "img-src 'self' data: none;";
|
|
}
|
|
|
|
cspOss << "\r\n";
|
|
|
|
std::ostringstream oss;
|
|
oss << "HTTP/1.1 200 OK\r\n"
|
|
"Date: " << Util::getHttpTimeNow() << "\r\n"
|
|
"Last-Modified: " << Util::getHttpTimeNow() << "\r\n"
|
|
"User-Agent: " << WOPI_AGENT_STRING << "\r\n"
|
|
"Cache-Control:max-age=11059200\r\n"
|
|
"ETag: \"" LOOLWSD_VERSION_HASH "\"\r\n"
|
|
"Content-Length: " << preprocess.size() << "\r\n"
|
|
"Content-Type: " << mimeType << "\r\n"
|
|
"X-Content-Type-Options: nosniff\r\n"
|
|
"X-XSS-Protection: 1; mode=block\r\n"
|
|
"Referrer-Policy: no-referrer\r\n";
|
|
|
|
// Append CSP to response headers too
|
|
oss << cspOss.str();
|
|
|
|
// Setup HTTP Public key pinning
|
|
if ((LOOLWSD::isSSLEnabled() || LOOLWSD::isSSLTermination()) && config.getBool("ssl.hpkp[@enable]", false))
|
|
{
|
|
size_t i = 0;
|
|
std::string pinPath = "ssl.hpkp.pins.pin[" + std::to_string(i) + ']';
|
|
std::ostringstream hpkpOss;
|
|
bool keysPinned = false;
|
|
while (config.has(pinPath))
|
|
{
|
|
const std::string pin = config.getString(pinPath, "");
|
|
if (!pin.empty())
|
|
{
|
|
hpkpOss << "pin-sha256=\"" << pin << "\"; ";
|
|
keysPinned = true;
|
|
}
|
|
pinPath = "ssl.hpkp.pins.pin[" + std::to_string(++i) + ']';
|
|
}
|
|
|
|
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)
|
|
{
|
|
LOG_ERR("Invalid value of HPKP's max-age directive found in config file. Defaulting to "
|
|
<< 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
|
|
oss << "Public-Key-Pins-Report-Only: " << hpkpOss.str() << "\r\n";
|
|
}
|
|
else
|
|
{
|
|
oss << "Public-Key-Pins: " << hpkpOss.str() << "\r\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
oss << "\r\n"
|
|
<< preprocess;
|
|
|
|
socket->send(oss.str());
|
|
LOG_DBG("Sent file: " << relPath << ": " << preprocess);
|
|
}
|
|
|
|
void FileServerRequestHandler::preprocessAdminFile(const HTTPRequest& request,
|
|
const RequestDetails &requestDetails,
|
|
const std::shared_ptr<StreamSocket>& socket)
|
|
{
|
|
Poco::Net::HTTPResponse response;
|
|
|
|
if (!LOOLWSD::AdminEnabled)
|
|
throw Poco::FileAccessDeniedException("Admin console disabled");
|
|
|
|
if (!FileServerRequestHandler::isAdminLoggedIn(request, response))
|
|
throw Poco::Net::NotAuthenticatedException("Invalid admin login");
|
|
|
|
ServerURL cnxDetails(requestDetails);
|
|
std::string responseRoot = cnxDetails.getResponseRoot();
|
|
|
|
static const std::string scriptJS("<script src=\"%s/loleaflet/" LOOLWSD_VERSION_HASH "/%s.js\"></script>");
|
|
static const std::string footerPage("<footer class=\"footer has-text-centered\"><strong>Key:</strong> %s <strong>Expiry Date:</strong> %s</footer>");
|
|
|
|
const std::string relPath = getRequestPathname(request);
|
|
LOG_DBG("Preprocessing file: " << relPath);
|
|
std::string adminFile = *getUncompressedFile(relPath);
|
|
std::vector<std::string> templatePath_vec = Util::splitStringToVector(relPath, '/');
|
|
std::string templatePath = "";
|
|
for (unsigned int i = 0; i < templatePath_vec.size() - 1; i++)
|
|
{
|
|
templatePath += templatePath_vec[i] + "/";
|
|
}
|
|
templatePath = "/" + templatePath + "admintemplate.html";
|
|
std::string templateFile = *getUncompressedFile(templatePath);
|
|
Poco::replaceInPlace(templateFile, std::string("<!--%MAIN_CONTENT%-->"), adminFile); // Now template has the main content..
|
|
|
|
std::string brandJS(Poco::format(scriptJS, responseRoot, std::string(BRANDING)));
|
|
std::string brandFooter;
|
|
|
|
#if ENABLE_SUPPORT_KEY
|
|
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, std::string(BRANDING_UNSUPPORTED));
|
|
brandFooter = Poco::format(footerPage, key.data(), Poco::DateTimeFormatter::format(key.expiry(), Poco::DateTimeFormat::RFC822_FORMAT));
|
|
}
|
|
#endif
|
|
|
|
Poco::replaceInPlace(templateFile, std::string("<!--%BRANDING_JS%-->"), brandJS);
|
|
Poco::replaceInPlace(templateFile, std::string("<!--%FOOTER%-->"), brandFooter);
|
|
Poco::replaceInPlace(templateFile, std::string("%VERSION%"), std::string(LOOLWSD_VERSION_HASH));
|
|
Poco::replaceInPlace(templateFile, std::string("%SERVICE_ROOT%"), responseRoot);
|
|
|
|
// 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");
|
|
response.set("Server", HTTP_SERVER_STRING);
|
|
response.set("Date", Util::getHttpTimeNow());
|
|
|
|
response.setContentType("text/html");
|
|
response.setChunkedTransferEncoding(false);
|
|
|
|
std::ostringstream oss;
|
|
response.write(oss);
|
|
oss << templateFile;
|
|
socket->send(oss.str());
|
|
}
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|