Compare commits

..

6 Commits

Author SHA1 Message Date
yhirose c7ed1796a7 Release v0.13.3 2023-07-31 21:28:33 -04:00
yhirose 44c62d838e Use memory mapped file for static file server (#1632)
* Use memory mapped file for static file server

* Fix build error
2023-07-31 07:43:50 -04:00
yhirose 6bb580cda8 Fix #1559 2023-07-31 00:27:26 -04:00
yhirose 00a8cb8e5d Code cleanup 2023-07-31 00:22:22 -04:00
yhirose 2e34a39673 Added StaticFileRanges test 2023-07-31 00:22:22 -04:00
yhirose 01b90829bc Removed unnecessary CRLF at the end of multipart ranges data 2023-07-31 00:22:22 -04:00
3 changed files with 8411 additions and 36 deletions
+166 -32
View File
@@ -8,7 +8,7 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.13.2"
#define CPPHTTPLIB_VERSION "0.13.3"
/*
* Configuration
@@ -193,6 +193,7 @@ using socket_t = SOCKET;
#endif
#include <csignal>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/un.h>
@@ -784,6 +785,7 @@ public:
bool remove_mount_point(const std::string &mount_point);
Server &set_file_extension_and_mimetype_mapping(const std::string &ext,
const std::string &mime);
Server &set_default_file_mimetype(const std::string &mime);
Server &set_file_request_handler(Handler handler);
Server &set_error_handler(HandlerWithResponse handler);
@@ -907,6 +909,7 @@ private:
};
std::vector<MountPointEntry> base_dirs_;
std::map<std::string, std::string> file_extension_and_mimetype_map_;
std::string default_file_mimetype_ = "application/octet-stream";
Handler file_request_handler_;
Handlers get_handlers_;
@@ -2142,6 +2145,29 @@ private:
std::string glowable_buffer_;
};
class mmap {
public:
mmap(const char *path);
~mmap();
bool open(const char *path);
void close();
bool is_open() const;
size_t size() const;
const char *data() const;
private:
#if defined(_WIN32)
HANDLE hFile_;
HANDLE hMapping_;
#else
int fd_;
#endif
size_t size_;
void *addr_;
};
} // namespace detail
// ----------------------------------------------------------------------------
@@ -2522,6 +2548,95 @@ inline void stream_line_reader::append(char c) {
}
}
inline mmap::mmap(const char *path)
#if defined(_WIN32)
: hFile_(NULL), hMapping_(NULL)
#else
: fd_(-1)
#endif
,
size_(0), addr_(nullptr) {
if (!open(path)) { std::runtime_error(""); }
}
inline mmap::~mmap() { close(); }
inline bool mmap::open(const char *path) {
close();
#if defined(_WIN32)
hFile_ = ::CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile_ == INVALID_HANDLE_VALUE) { return false; }
size_ = ::GetFileSize(hFile_, NULL);
hMapping_ = ::CreateFileMapping(hFile_, NULL, PAGE_READONLY, 0, 0, NULL);
if (hMapping_ == NULL) {
close();
return false;
}
addr_ = ::MapViewOfFile(hMapping_, FILE_MAP_READ, 0, 0, 0);
#else
fd_ = ::open(path, O_RDONLY);
if (fd_ == -1) { return false; }
struct stat sb;
if (fstat(fd_, &sb) == -1) {
close();
return false;
}
size_ = static_cast<size_t>(sb.st_size);
addr_ = ::mmap(NULL, size_, PROT_READ, MAP_PRIVATE, fd_, 0);
#endif
if (addr_ == nullptr) {
close();
return false;
}
return true;
}
inline bool mmap::is_open() const { return addr_ != nullptr; }
inline size_t mmap::size() const { return size_; }
inline const char *mmap::data() const { return (const char *)addr_; }
inline void mmap::close() {
#if defined(_WIN32)
if (addr_) {
::UnmapViewOfFile(addr_);
addr_ = nullptr;
}
if (hMapping_) {
::CloseHandle(hMapping_);
hMapping_ = NULL;
}
if (hFile_ != INVALID_HANDLE_VALUE) {
::CloseHandle(hFile_);
hFile_ = INVALID_HANDLE_VALUE;
}
#else
if (addr_ != nullptr) {
munmap(addr_, size_);
addr_ = nullptr;
}
if (fd_ != -1) {
::close(fd_);
fd_ = -1;
}
#endif
size_ = 0;
}
inline int close_socket(socket_t sock) {
#ifdef _WIN32
return closesocket(sock);
@@ -3197,9 +3312,10 @@ inline constexpr unsigned int operator"" _t(const char *s, size_t l) {
} // namespace udl
inline const char *
inline std::string
find_content_type(const std::string &path,
const std::map<std::string, std::string> &user_data) {
const std::map<std::string, std::string> &user_data,
const std::string &default_content_type) {
auto ext = file_extension(path);
auto it = user_data.find(ext);
@@ -3208,7 +3324,8 @@ find_content_type(const std::string &path,
using udl::operator""_t;
switch (str2tag(ext)) {
default: return nullptr;
default: return default_content_type;
case "css"_t: return "text/css";
case "csv"_t: return "text/csv";
case "htm"_t:
@@ -4111,7 +4228,7 @@ inline bool parse_range_header(const std::string &s, Ranges &ranges) try {
if (std::regex_match(s, m, re_first_range)) {
auto pos = static_cast<size_t>(m.position(1));
auto len = static_cast<size_t>(m.length(1));
bool all_valid_ranges = true;
auto all_valid_ranges = true;
split(&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) {
if (!all_valid_ranges) return;
static auto re_another_range = std::regex(R"(\s*(\d*)-(\d*))");
@@ -4489,12 +4606,13 @@ get_range_offset_and_length(const Request &req, size_t content_length,
return std::make_pair(r.first, static_cast<size_t>(r.second - r.first) + 1);
}
inline std::string make_content_range_header_field(size_t offset, size_t length,
size_t content_length) {
inline std::string
make_content_range_header_field(const std::pair<ssize_t, ssize_t> &range,
size_t content_length) {
std::string field = "bytes ";
field += std::to_string(offset);
if (range.first != -1) { field += std::to_string(range.first); }
field += "-";
field += std::to_string(offset + length - 1);
if (range.second != -1) { field += std::to_string(range.second); }
field += "/";
field += std::to_string(content_length);
return field;
@@ -4516,21 +4634,22 @@ bool process_multipart_ranges_data(const Request &req, Response &res,
ctoken("\r\n");
}
auto offsets = get_range_offset_and_length(req, res.body.size(), i);
ctoken("Content-Range: ");
const auto &range = req.ranges[i];
stoken(make_content_range_header_field(range, res.content_length_));
ctoken("\r\n");
ctoken("\r\n");
auto offsets = get_range_offset_and_length(req, res.content_length_, i);
auto offset = offsets.first;
auto length = offsets.second;
ctoken("Content-Range: ");
stoken(make_content_range_header_field(offset, length, res.body.size()));
ctoken("\r\n");
ctoken("\r\n");
if (!content(offset, length)) { return false; }
ctoken("\r\n");
}
ctoken("--");
stoken(boundary);
ctoken("--\r\n");
ctoken("--");
return true;
}
@@ -5493,6 +5612,11 @@ Server::set_file_extension_and_mimetype_mapping(const std::string &ext,
return *this;
}
inline Server &Server::set_default_file_mimetype(const std::string &mime) {
default_file_mimetype_ = mime;
return *this;
}
inline Server &Server::set_file_request_handler(Handler handler) {
file_request_handler_ = std::move(handler);
return *this;
@@ -5944,17 +6068,26 @@ inline bool Server::handle_file_request(const Request &req, Response &res,
if (path.back() == '/') { path += "index.html"; }
if (detail::is_file(path)) {
detail::read_file(path, res.body);
auto type =
detail::find_content_type(path, file_extension_and_mimetype_map_);
if (type) { res.set_header("Content-Type", type); }
for (const auto &kv : entry.headers) {
res.set_header(kv.first.c_str(), kv.second);
}
res.status = req.has_header("Range") ? 206 : 200;
auto mm = std::make_shared<detail::mmap>(path.c_str());
if (!mm->is_open()) { return false; }
res.set_content_provider(
mm->size(),
detail::find_content_type(path, file_extension_and_mimetype_map_,
default_file_mimetype_),
[mm](size_t offset, size_t length, DataSink &sink) -> bool {
sink.write(mm->data() + offset, length);
return true;
});
if (!head && file_request_handler_) {
file_request_handler_(req, res);
}
return true;
}
}
@@ -6202,10 +6335,10 @@ inline void Server::apply_ranges(const Request &req, Response &res,
} else if (req.ranges.size() == 1) {
auto offsets =
detail::get_range_offset_and_length(req, res.content_length_, 0);
auto offset = offsets.first;
length = offsets.second;
auto content_range = detail::make_content_range_header_field(
offset, length, res.content_length_);
req.ranges[0], res.content_length_);
res.set_header("Content-Range", content_range);
} else {
length = detail::get_multipart_ranges_data_length(req, res, boundary,
@@ -6228,13 +6361,15 @@ inline void Server::apply_ranges(const Request &req, Response &res,
if (req.ranges.empty()) {
;
} else if (req.ranges.size() == 1) {
auto content_range = detail::make_content_range_header_field(
req.ranges[0], res.body.size());
res.set_header("Content-Range", content_range);
auto offsets =
detail::get_range_offset_and_length(req, res.body.size(), 0);
auto offset = offsets.first;
auto length = offsets.second;
auto content_range = detail::make_content_range_header_field(
offset, length, res.body.size());
res.set_header("Content-Range", content_range);
if (offset < res.body.size()) {
res.body = res.body.substr(offset, length);
} else {
@@ -6390,7 +6525,7 @@ Server::process_request(Stream &strm, bool close_connection,
}
// Rounting
bool routed = false;
auto routed = false;
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
routed = routing(req, res, strm);
#else
@@ -7206,7 +7341,7 @@ inline ContentProviderWithoutLength ClientImpl::get_multipart_content_provider(
}
DataSink cur_sink;
bool has_data = true;
auto has_data = true;
cur_sink.write = sink.write;
cur_sink.done = [&]() { has_data = false; };
@@ -8335,10 +8470,9 @@ inline bool SSLClient::connect_with_proxy(Socket &socket, Response &res,
}
// If status code is not 200, proxy request is failed.
// Set error to ProxyConnection and return proxy response
// Set error to ProxyConnection and return proxy response
// as the response of the request
if (proxy_res.status != 200)
{
if (proxy_res.status != 200) {
error = Error::ProxyConnection;
res = std::move(proxy_res);
// Thread-safe to close everything because we are assuming there are
@@ -8353,7 +8487,7 @@ inline bool SSLClient::connect_with_proxy(Socket &socket, Response &res,
}
inline bool SSLClient::load_certs() {
bool ret = true;
auto ret = true;
std::call_once(initialize_cert_, [&]() {
std::lock_guard<std::mutex> guard(ctx_mutex_);
+53 -4
View File
@@ -2501,6 +2501,55 @@ TEST_F(ServerTest, StaticFileRange) {
EXPECT_EQ(std::string("cd"), res->body);
}
TEST_F(ServerTest, StaticFileRanges) {
auto res =
cli_.Get("/dir/test.abcde", {{make_range_header({{1, 2}, {4, -1}})}});
ASSERT_TRUE(res);
EXPECT_EQ(206, res->status);
EXPECT_TRUE(
res->get_header_value("Content-Type")
.find(
"multipart/byteranges; boundary=--cpp-httplib-multipart-data-") ==
0);
EXPECT_EQ("265", res->get_header_value("Content-Length"));
}
TEST_F(ServerTest, StaticFileRangeHead) {
auto res = cli_.Head("/dir/test.abcde", {{make_range_header({{2, 3}})}});
ASSERT_TRUE(res);
EXPECT_EQ(206, res->status);
EXPECT_EQ("text/abcde", res->get_header_value("Content-Type"));
EXPECT_EQ("2", res->get_header_value("Content-Length"));
EXPECT_EQ(true, res->has_header("Content-Range"));
}
TEST_F(ServerTest, StaticFileRangeBigFile) {
auto res = cli_.Get("/dir/1MB.txt", {{make_range_header({{-1, 5}})}});
ASSERT_TRUE(res);
EXPECT_EQ(206, res->status);
EXPECT_EQ("text/plain", res->get_header_value("Content-Type"));
EXPECT_EQ("5", res->get_header_value("Content-Length"));
EXPECT_EQ(true, res->has_header("Content-Range"));
EXPECT_EQ("LAST\n", res->body);
}
TEST_F(ServerTest, StaticFileRangeBigFile2) {
auto res = cli_.Get("/dir/1MB.txt", {{make_range_header({{1, 4097}})}});
ASSERT_TRUE(res);
EXPECT_EQ(206, res->status);
EXPECT_EQ("text/plain", res->get_header_value("Content-Type"));
EXPECT_EQ("4097", res->get_header_value("Content-Length"));
EXPECT_EQ(true, res->has_header("Content-Range"));
}
TEST_F(ServerTest, StaticFileBigFile) {
auto res = cli_.Get("/dir/1MB.txt");
ASSERT_TRUE(res);
EXPECT_EQ(200, res->status);
EXPECT_EQ("text/plain", res->get_header_value("Content-Type"));
EXPECT_EQ("1048576", res->get_header_value("Content-Length"));
}
TEST_F(ServerTest, InvalidBaseDirMount) {
EXPECT_EQ(false, svr_.set_mount_point("invalid_mount_point", "./www3"));
}
@@ -2886,9 +2935,9 @@ TEST_F(ServerTest, GetStreamedWithRangeMultipart) {
cli_.Get("/streamed-with-range", {{make_range_header({{1, 2}, {4, 5}})}});
ASSERT_TRUE(res);
EXPECT_EQ(206, res->status);
EXPECT_EQ("269", res->get_header_value("Content-Length"));
EXPECT_EQ("267", res->get_header_value("Content-Length"));
EXPECT_EQ(false, res->has_header("Content-Range"));
EXPECT_EQ(269U, res->body.size());
EXPECT_EQ(267U, res->body.size());
}
TEST_F(ServerTest, GetStreamedEndless) {
@@ -2978,9 +3027,9 @@ TEST_F(ServerTest, GetWithRangeMultipart) {
auto res = cli_.Get("/with-range", {{make_range_header({{1, 2}, {4, 5}})}});
ASSERT_TRUE(res);
EXPECT_EQ(206, res->status);
EXPECT_EQ("269", res->get_header_value("Content-Length"));
EXPECT_EQ("267", res->get_header_value("Content-Length"));
EXPECT_EQ(false, res->has_header("Content-Range"));
EXPECT_EQ(269U, res->body.size());
EXPECT_EQ(267U, res->body.size());
}
TEST_F(ServerTest, GetWithRangeMultipartOffsetGreaterThanContent) {
+8192
View File
File diff suppressed because it is too large Load Diff