0a73175764
Change-Id: Ib38d7ebf1e66680779720cc1ce4d9bd07f9cab0a Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
998 lines
38 KiB
C++
998 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
|
|
|
|
if (requestUri.getPath().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: */
|