Add location configuration for static resources (#331)

This commit is contained in:
antao 2020-01-12 11:05:38 +08:00
parent 62fc82cba1
commit fb2343ac74
10 changed files with 418 additions and 143 deletions

View File

@ -95,6 +95,25 @@
"cur",
"xml"
],
//locations: An array of locations of static files for GET requests.
"locations": [{
//uri_prefix: The URI prefix of the location prefixed with "/", the default value is "" that disables the location.
//"uri_prefix": "/.well-known/acme-challenge/",
//default_content_type: The default content type of the static files without
//an extension. empty string by default.
"default_content_type": "text/plain",
//alias: The location in file system, if it is prefixed with "/", it
//presents an absolute path, otherwise it presents a relative path to
//the document_root path.
//The default value is "" which means use the document root path as the location base path.
"alias": "",
//is_case_sensitive: indicates whether the URI prefix is case sensitive.
"is_case_sensitive": false,
//allow_all: true by default. If it is set to false, only static files with a valid extension can be accessed.
"allow_all": true,
//is_recursive: true by default. If it is set to false, files in sub directories can't be accessed.
"is_recursive": true
}],
//max_connections: maximum connections number,100000 by default
"max_connections": 100000,
//max_connections_per_ip: maximum connections number per clinet,0 by default which means no limit
@ -126,7 +145,7 @@
"run_as_daemon": false,
//relaunch_on_error: False by default, if true, the program will be restart by the parent after exiting;
"relaunch_on_error": false,
//use_sendfile: True by default, if ture, the program
//use_sendfile: True by default, if true, the program
//uses sendfile() system-call to send static files to clients;
"use_sendfile": true,
//use_gzip: True by default, use gzip to compress the response body's content;

View File

@ -95,6 +95,25 @@
"cur",
"xml"
],
//locations: An array of locations of static files for GET requests.
"locations": [{
//uri_prefix: The URI prefix of the location prefixed with "/", the default value is "" that disables the location.
//"uri_prefix": "/.well-known/acme-challenge/",
//default_content_type: The default content type of the static files without
//an extension. empty string by default.
"default_content_type": "text/plain",
//alias: The location in file system, if it is prefixed with "/", it
//presents an absolute path, otherwise it presents a relative path to
//the document_root path.
//The default value is "" which means use the document root path as the location base path.
"alias": "",
//is_case_sensitive: indicates whether the URI prefix is case sensitive.
"is_case_sensitive": false,
//allow_all: true by default. If it is set to false, only static files with a valid extension can be accessed.
"allow_all": true,
//is_recursive: true by default. If it is set to false, files in sub directories can't be accessed.
"is_recursive": true
}],
//max_connections: maximum connections number,100000 by default
"max_connections": 100000,
//max_connections_per_ip: maximum connections number per clinet,0 by default which means no limit
@ -126,7 +145,7 @@
"run_as_daemon": false,
//relaunch_on_error: False by default, if true, the program will be restart by the parent after exiting;
"relaunch_on_error": false,
//use_sendfile: True by default, if ture, the program
//use_sendfile: True by default, if true, the program
//uses sendfile() system-call to send static files to clients;
"use_sendfile": true,
//use_gzip: True by default, use gzip to compress the response body's content;

View File

@ -89,7 +89,7 @@
"run_as_daemon": false,
//relaunch_on_error: False by default, if true, the program will be restart by the parent after exiting;
"relaunch_on_error": false,
//use_sendfile: True by default, if ture, the program
//use_sendfile: True by default, if true, the program
//uses sendfile() system-call to send static files to clients;
"use_sendfile": true,
//use_gzip: True by default, use gzip to compress the response body's content;

View File

@ -648,6 +648,28 @@ class HttpAppFramework : public trantor::NonCopyable
virtual HttpAppFramework &setStaticFileHeaders(
const std::vector<std::pair<std::string, std::string>> &headers) = 0;
/**
* @brief Add a location of static files for GET requests.
*
* @param uriPrefix The URI prefix of the location prefixed with "/"
* @param defaultContentType The default content type of the static files
* without an extension.
* @param alias The location in file system, if it is prefixed with "/", it
* presents an absolute path, otherwise it presents a relative path to the
* document_root path.
* @param isCaseSensitive
* @param allowAll If it is set to false, only static files with a valid extension can be accessed.
* @param isRecursive If it is set to false, files in sub directories can't be accessed.
* @return HttpAppFramework&
*/
virtual HttpAppFramework &addALocation(
const std::string &uriPrefix,
const std::string &defaultContentType = "",
const std::string &alias = "",
bool isCaseSensitive = false,
bool allowAll = true,
bool isRecursive = true) = 0;
/// Set the path to store uploaded files.
/**
* @param uploadPath is the dictionary where the uploaded files are

View File

@ -96,7 +96,8 @@ enum ContentType
CT_IMAGE_XICON,
CT_IMAGE_ICNS,
CT_IMAGE_BMP,
CT_MULTIPART_FORM_DATA
CT_MULTIPART_FORM_DATA,
CT_CUSTOM
};
enum HttpMethod

View File

@ -263,6 +263,35 @@ static void loadApp(const Json::Value &app)
}
drogon::app().setFileTypes(types);
}
// locations
if (app.isMember("locations"))
{
auto &locations = app["locations"];
if (!locations.isArray())
{
std::cerr << "The locations option must be an array\n";
exit(1);
}
for (auto &location : locations)
{
auto uri = location.get("uri_prefix", "").asString();
if (uri.empty())
continue;
auto defaultContentType =
location.get("default_content_type", "").asString();
auto alias = location.get("alias", "").asString();
auto isCaseSensitive =
location.get("is_case_sensitive", false).asBool();
auto allAll = location.get("allow_all", true).asBool();
auto isRecursive = location.get("is_recursive", true).asBool();
drogon::app().addALocation(uri,
defaultContentType,
alias,
isCaseSensitive,
allAll,
isRecursive);
}
}
// max connections
auto maxConns = app.get("max_connections", 0).asUInt64();
if (maxConns > 0)

View File

@ -62,7 +62,7 @@ using namespace drogon;
using namespace std::placeholders;
HttpAppFrameworkImpl::HttpAppFrameworkImpl()
: staticFileRouterPtr_(new StaticFileRouter(staticFileHeaders_)),
: staticFileRouterPtr_(new StaticFileRouter{}),
httpCtrlsRouterPtr_(new HttpControllersRouter(*staticFileRouterPtr_,
postRoutingAdvices_,
postRoutingObservers_,
@ -829,4 +829,28 @@ const HttpResponsePtr &HttpAppFrameworkImpl::getCustom404Page()
{
return custom404_;
}
}
HttpAppFramework &HttpAppFrameworkImpl::setStaticFileHeaders(
const std::vector<std::pair<std::string, std::string>> &headers)
{
staticFileRouterPtr_->setStaticFileHeaders(headers);
return *this;
}
HttpAppFramework &HttpAppFrameworkImpl::addALocation(
const std::string &uriPrefix,
const std::string &defaultContentType,
const std::string &alias,
bool isCaseSensitive,
bool allowAll,
bool isRecursive)
{
staticFileRouterPtr_->addALocation(uriPrefix,
defaultContentType,
alias,
isCaseSensitive,
allowAll,
isRecursive);
return *this;
}

View File

@ -189,11 +189,16 @@ class HttpAppFrameworkImpl : public HttpAppFramework
virtual HttpAppFramework &setStaticFileHeaders(
const std::vector<std::pair<std::string, std::string>> &headers)
override
{
staticFileHeaders_ = headers;
return *this;
}
override;
virtual HttpAppFramework &addALocation(
const std::string &uriPrefix,
const std::string &defaultContentType = "",
const std::string &alias = "",
bool isCaseSensitive = false,
bool allowAll = true,
bool isRecursive = true) override;
virtual const std::string &getUploadPath() const override
{
return uploadPath_;
@ -453,7 +458,6 @@ class HttpAppFrameworkImpl : public HttpAppFramework
const std::unique_ptr<orm::DbClientManager> dbClientManagerPtr_;
std::string rootPath_{"./"};
std::vector<std::pair<std::string, std::string>> staticFileHeaders_;
std::string uploadPath_;
std::atomic_bool running_{false};

View File

@ -16,10 +16,9 @@
#include "HttpAppFrameworkImpl.h"
#include "HttpRequestImpl.h"
#include "HttpResponseImpl.h"
#include <fstream>
#include <iostream>
#include <algorithm>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
@ -41,6 +40,12 @@ void StaticFileRouter::init(const std::vector<trantor::EventLoop *> &ioloops)
staticFilesCache_ = decltype(staticFilesCache_)(
new IOThreadStorage<
std::unordered_map<std::string, HttpResponsePtr>>{});
ioLocationsPtr_ =
decltype(ioLocationsPtr_)(new IOThreadStorage<std::vector<Location>>);
for (auto *loop : ioloops)
{
loop->queueInLoop([this] { **ioLocationsPtr_ = locations_; });
}
}
void StaticFileRouter::route(
@ -48,146 +53,112 @@ void StaticFileRouter::route(
std::function<void(const HttpResponsePtr &)> &&callback)
{
const std::string &path = req->path();
auto pos = path.rfind('.');
if (pos != std::string::npos)
if (path.find("/../") != std::string::npos)
{
std::string filetype = path.substr(pos + 1);
transform(filetype.begin(), filetype.end(), filetype.begin(), tolower);
if (fileTypeSet_.find(filetype) != fileTypeSet_.end())
// Downloading files from the parent folder is forbidden.
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k403Forbidden);
callback(resp);
return;
}
auto lPath = path;
std::transform(lPath.begin(), lPath.end(), lPath.begin(), tolower);
for (auto &location : **ioLocationsPtr_)
{
auto &URI = location.uriPrefix_;
auto &defaultContentType = location.defaultContentType_;
if (location.realLocation_.empty())
{
// LOG_INFO << "file query!" << path;
std::string filePath =
HttpAppFrameworkImpl::instance().getDocumentRoot() + path;
if (filePath.find("/../") != std::string::npos)
if (!location.alias_.empty())
{
if (location.alias_[0] == '/')
{
location.realLocation_ = location.alias_;
}
else
{
location.realLocation_ =
HttpAppFrameworkImpl::instance().getDocumentRoot() +
location.alias_;
}
}
else
{
location.realLocation_ =
HttpAppFrameworkImpl::instance().getDocumentRoot() +
location.uriPrefix_;
}
if (location.realLocation_[location.realLocation_.length() - 1] !=
'/')
{
location.realLocation_.append(1, '/');
}
if (!location.isCaseSensitive_)
{
std::transform(URI.begin(), URI.end(), URI.begin(), tolower);
}
}
auto &tmpPath = location.isCaseSensitive_ ? path : lPath;
if (tmpPath.length() >= URI.length() &&
std::equal(tmpPath.begin(),
tmpPath.begin() + URI.length(),
URI.begin()))
{
string_view restOfThePath{path.data() + URI.length(),
path.length() - URI.length()};
auto pos = restOfThePath.find('/');
if (pos != 0 && pos != string_view::npos && !location.isRecursive_)
{
// Downloading files from the parent folder is forbidden.
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k403Forbidden);
callback(resp);
return;
}
// find cached response
HttpResponsePtr cachedResp;
auto &cacheMap = staticFilesCache_->getThreadData();
auto iter = cacheMap.find(filePath);
if (iter != cacheMap.end())
if (!location.allowAll_)
{
cachedResp = iter->second;
}
// check last modified time,rfc2616-14.25
// If-Modified-Since: Mon, 15 Oct 2018 06:26:33 GMT
std::string timeStr;
if (enableLastModify_)
{
if (cachedResp)
pos = restOfThePath.rfind('.');
if (pos == string_view::npos)
{
if (static_cast<HttpResponseImpl *>(cachedResp.get())
->getHeaderBy("last-modified") ==
req->getHeaderBy("if-modified-since"))
{
std::shared_ptr<HttpResponseImpl> resp =
std::make_shared<HttpResponseImpl>();
resp->setStatusCode(k304NotModified);
HttpAppFrameworkImpl::instance().callCallback(req,
resp,
callback);
return;
}
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k403Forbidden);
callback(resp);
return;
}
else
std::string extension{restOfThePath.data() + pos + 1,
restOfThePath.length() - pos - 1};
std::transform(extension.begin(),
extension.end(),
extension.begin(),
tolower);
if (fileTypeSet_.find(extension) == fileTypeSet_.end())
{
struct stat fileStat;
LOG_TRACE << "enabled LastModify";
if (stat(filePath.c_str(), &fileStat) >= 0)
{
LOG_TRACE << "last modify time:" << fileStat.st_mtime;
struct tm tm1;
gmtime_r(&fileStat.st_mtime, &tm1);
timeStr.resize(64);
auto len = strftime((char *)timeStr.data(),
timeStr.size(),
"%a, %d %b %Y %T GMT",
&tm1);
timeStr.resize(len);
const std::string &modiStr =
req->getHeaderBy("if-modified-since");
if (modiStr == timeStr && !modiStr.empty())
{
LOG_TRACE << "not Modified!";
std::shared_ptr<HttpResponseImpl> resp =
std::make_shared<HttpResponseImpl>();
resp->setStatusCode(k304NotModified);
HttpAppFrameworkImpl::instance().callCallback(
req, resp, callback);
return;
}
}
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k403Forbidden);
callback(resp);
return;
}
}
if (cachedResp)
{
LOG_TRACE << "Using file cache";
HttpAppFrameworkImpl::instance().callCallback(req,
cachedResp,
callback);
return;
}
HttpResponsePtr resp;
if (gzipStaticFlag_ &&
req->getHeaderBy("accept-encoding").find("gzip") !=
std::string::npos)
{
// Find compressed file first.
auto gzipFileName = filePath + ".gz";
std::ifstream infile(gzipFileName, std::ifstream::binary);
if (infile)
{
resp = HttpResponse::newFileResponse(
gzipFileName, "", drogon::getContentType(filePath));
resp->addHeader("Content-Encoding", "gzip");
}
}
if (!resp)
resp = HttpResponse::newFileResponse(filePath);
if (resp->statusCode() != k404NotFound)
{
if (!timeStr.empty())
{
resp->addHeader("Last-Modified", timeStr);
resp->addHeader("Expires", "Thu, 01 Jan 1970 00:00:00 GMT");
}
if (!headers_.empty())
{
for (auto &header : headers_)
{
resp->addHeader(header.first, header.second);
}
}
// cache the response for 5 seconds by default
if (staticFilesCacheTime_ >= 0)
{
LOG_TRACE << "Save in cache for " << staticFilesCacheTime_
<< " seconds";
resp->setExpiredTime(staticFilesCacheTime_);
staticFilesCache_->getThreadData()[filePath] = resp;
staticFilesCacheMap_->getThreadData()->insert(
filePath, 0, staticFilesCacheTime_, [this, filePath]() {
LOG_TRACE << "Erase cache";
assert(staticFilesCache_->getThreadData().find(
filePath) !=
staticFilesCache_->getThreadData().end());
staticFilesCache_->getThreadData().erase(filePath);
});
}
HttpAppFrameworkImpl::instance().callCallback(req,
resp,
callback);
return;
}
callback(resp);
std::string filePath =
location.realLocation_ +
std::string{restOfThePath.data(), restOfThePath.length()};
sendStaticFileResponse(filePath,
req,
callback,
string_view{location.defaultContentType_});
return;
}
}
auto pos = lPath.rfind('.');
if (pos != std::string::npos)
{
std::string filetype = lPath.substr(pos + 1);
if (fileTypeSet_.find(filetype) != fileTypeSet_.end())
{
// LOG_INFO << "file query!" << path;
std::string filePath =
HttpAppFrameworkImpl::instance().getDocumentRoot() + path;
sendStaticFileResponse(filePath, req, callback, "");
return;
}
}
@ -195,6 +166,140 @@ void StaticFileRouter::route(
callback(HttpResponse::newNotFoundResponse());
}
void StaticFileRouter::sendStaticFileResponse(
const std::string &filePath,
const HttpRequestImplPtr &req,
const std::function<void(const HttpResponsePtr &)> &callback,
const string_view &defaultContentType)
{ // find cached response
HttpResponsePtr cachedResp;
auto &cacheMap = staticFilesCache_->getThreadData();
auto iter = cacheMap.find(filePath);
if (iter != cacheMap.end())
{
cachedResp = iter->second;
}
// check last modified time,rfc2616-14.25
// If-Modified-Since: Mon, 15 Oct 2018 06:26:33 GMT
std::string timeStr;
if (enableLastModify_)
{
if (cachedResp)
{
if (static_cast<HttpResponseImpl *>(cachedResp.get())
->getHeaderBy("last-modified") ==
req->getHeaderBy("if-modified-since"))
{
std::shared_ptr<HttpResponseImpl> resp =
std::make_shared<HttpResponseImpl>();
resp->setStatusCode(k304NotModified);
HttpAppFrameworkImpl::instance().callCallback(req,
resp,
callback);
return;
}
}
else
{
struct stat fileStat;
LOG_TRACE << "enabled LastModify";
if (stat(filePath.c_str(), &fileStat) >= 0)
{
LOG_TRACE << "last modify time:" << fileStat.st_mtime;
struct tm tm1;
gmtime_r(&fileStat.st_mtime, &tm1);
timeStr.resize(64);
auto len = strftime((char *)timeStr.data(),
timeStr.size(),
"%a, %d %b %Y %T GMT",
&tm1);
timeStr.resize(len);
const std::string &modiStr =
req->getHeaderBy("if-modified-since");
if (modiStr == timeStr && !modiStr.empty())
{
LOG_TRACE << "not Modified!";
std::shared_ptr<HttpResponseImpl> resp =
std::make_shared<HttpResponseImpl>();
resp->setStatusCode(k304NotModified);
HttpAppFrameworkImpl::instance().callCallback(req,
resp,
callback);
return;
}
}
}
}
if (cachedResp)
{
LOG_TRACE << "Using file cache";
HttpAppFrameworkImpl::instance().callCallback(req,
cachedResp,
callback);
return;
}
HttpResponsePtr resp;
if (gzipStaticFlag_ &&
req->getHeaderBy("accept-encoding").find("gzip") != std::string::npos)
{
// Find compressed file first.
auto gzipFileName = filePath + ".gz";
std::ifstream infile(gzipFileName, std::ifstream::binary);
if (infile)
{
resp =
HttpResponse::newFileResponse(gzipFileName,
"",
drogon::getContentType(filePath));
resp->addHeader("Content-Encoding", "gzip");
}
}
if (!resp)
resp = HttpResponse::newFileResponse(filePath);
if (resp->statusCode() != k404NotFound)
{
if (resp->getContentType() == CT_APPLICATION_OCTET_STREAM &&
!defaultContentType.empty())
{
resp->setContentTypeCodeAndCustomString(CT_CUSTOM,
defaultContentType);
}
if (!timeStr.empty())
{
resp->addHeader("Last-Modified", timeStr);
resp->addHeader("Expires", "Thu, 01 Jan 1970 00:00:00 GMT");
}
if (!headers_.empty())
{
for (auto &header : headers_)
{
resp->addHeader(header.first, header.second);
}
}
// cache the response for 5 seconds by default
if (staticFilesCacheTime_ >= 0)
{
LOG_TRACE << "Save in cache for " << staticFilesCacheTime_
<< " seconds";
resp->setExpiredTime(staticFilesCacheTime_);
staticFilesCache_->getThreadData()[filePath] = resp;
staticFilesCacheMap_->getThreadData()->insert(
filePath, 0, staticFilesCacheTime_, [this, filePath]() {
LOG_TRACE << "Erase cache";
assert(staticFilesCache_->getThreadData().find(filePath) !=
staticFilesCache_->getThreadData().end());
staticFilesCache_->getThreadData().erase(filePath);
});
}
HttpAppFrameworkImpl::instance().callCallback(req, resp, callback);
return;
}
callback(resp);
return;
}
void StaticFileRouter::setFileTypes(const std::vector<std::string> &types)
{
fileTypeSet_.clear();

View File

@ -43,10 +43,32 @@ class StaticFileRouter
gzipStaticFlag_ = useGzipStatic;
}
void init(const std::vector<trantor::EventLoop *> &ioloops);
StaticFileRouter(
const std::vector<std::pair<std::string, std::string>> &headers)
: headers_(headers)
void sendStaticFileResponse(
const std::string &filePath,
const HttpRequestImplPtr &req,
const std::function<void(const HttpResponsePtr &)> &callback,
const string_view &defaultContentType);
void addALocation(const std::string &uriPrefix,
const std::string &defaultContentType,
const std::string &alias,
bool isCaseSensitive,
bool allowAll,
bool isRecursive)
{
locations_.emplace_back(uriPrefix,
defaultContentType,
alias,
isCaseSensitive,
allowAll,
isRecursive);
}
void setStaticFileHeaders(
const std::vector<std::pair<std::string, std::string>> &headers)
{
headers_ = headers;
}
private:
@ -79,6 +101,36 @@ class StaticFileRouter
std::unique_ptr<
IOThreadStorage<std::unordered_map<std::string, HttpResponsePtr>>>
staticFilesCache_;
const std::vector<std::pair<std::string, std::string>> &headers_;
std::vector<std::pair<std::string, std::string>> headers_;
struct Location
{
std::string uriPrefix_;
std::string defaultContentType_;
std::string alias_;
std::string realLocation_;
bool isCaseSensitive_;
bool allowAll_;
bool isRecursive_;
Location(const std::string &uriPrefix,
const std::string &defaultContentType,
const std::string &alias,
bool isCaseSensitive,
bool allowAll,
bool isRecursive)
: uriPrefix_(uriPrefix),
alias_(alias),
isCaseSensitive_(isCaseSensitive),
allowAll_(allowAll),
isRecursive_(isRecursive)
{
if (!defaultContentType.empty())
{
defaultContentType_ =
std::string{"Content-Type: "} + defaultContentType + "\r\n";
}
}
};
std::unique_ptr<IOThreadStorage<std::vector<Location>>> ioLocationsPtr_;
std::vector<Location> locations_;
};
} // namespace drogon