Skip to content

Commit e2baa0e

Browse files
DuanYHhevake
authored andcommitted
opt(http): FileDownloaderMiddleware 支持 Range/ETag/CORS,适配 iOS AVPlayer 视频播放
* tidy * feat:优化代码 * tidy * tidy: 修正代码格式 * opt(http): FileDownloaderMiddleware 支持 Range/ETag/CORS,适配 iOS AVPlayer 视频播放
1 parent 6c5f2d5 commit e2baa0e

1 file changed

Lines changed: 215 additions & 44 deletions

File tree

modules/http/server/middlewares/file_downloader_middleware.cpp

Lines changed: 215 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,26 @@
2222
#include <fstream>
2323
#include <sstream>
2424
#include <algorithm>
25+
#include <sys/stat.h>
26+
#include <ctime>
2527

2628
#include <tbox/base/log.h>
2729
#include <tbox/base/assert.h>
2830
#include <tbox/util/string.h>
2931
#include <tbox/util/fs.h>
32+
#include <tbox/util/string_to.h>
3033
#include <tbox/eventx/work_thread.h>
34+
#include <tbox/base/defines.h>
3135
#include <tbox/base/recorder.h>
3236

3337
namespace tbox {
3438
namespace http {
3539
namespace server {
3640

3741
namespace {
38-
bool IsPathSafe(const std::string& path) {
42+
43+
bool IsPathSafe(const std::string& path)
44+
{
3945
//! 检查是否有".."路径组件,这可能导致目录遍历
4046
std::istringstream path_stream(path);
4147
std::string component;
@@ -47,6 +53,79 @@ bool IsPathSafe(const std::string& path) {
4753

4854
return true;
4955
}
56+
57+
std::string GenerateETag(time_t mtime, off_t size)
58+
{
59+
return "\"" + std::to_string(static_cast<long long>(mtime))
60+
+ "-" + std::to_string(static_cast<long long>(size)) + "\"";
61+
}
62+
63+
std::string HttpDateToString(time_t t)
64+
{
65+
struct tm tm_buf;
66+
gmtime_r(&t, &tm_buf);
67+
char buf[64];
68+
strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm_buf);
69+
return buf;
70+
}
71+
72+
time_t StringToHttpDate(const std::string& s)
73+
{
74+
struct tm t = {};
75+
if (strptime(s.c_str(), "%a, %d %b %Y %H:%M:%S GMT", &t) == nullptr)
76+
return -1;
77+
return timegm(&t);
78+
}
79+
80+
std::string GetHeader(const tbox::http::Headers& headers, const std::string& lower_name)
81+
{
82+
for (const auto& h : headers) {
83+
if (tbox::util::string::ToLower(h.first) == lower_name)
84+
return h.second;
85+
}
86+
return "";
87+
}
88+
89+
//! 解析
90+
bool ParseRangeString(const std::string &range_str, size_t file_size, size_t &range_begin, size_t &range_end)
91+
{
92+
std::string range_val = range_str.substr(6);
93+
auto dash_pos = range_val.find('-');
94+
if (dash_pos == std::string::npos)
95+
return false;
96+
97+
//!FIXME: 暂不支持 1000-1999, 3000-3555, 多段返回的情况
98+
auto comma_pos = range_val.find(',', dash_pos);
99+
if (comma_pos != std::string::npos) {
100+
return false;
101+
}
102+
103+
std::string begin_str = range_val.substr(0, dash_pos);
104+
std::string end_str = range_val.substr(dash_pos + 1);
105+
106+
if (begin_str.empty()) { //! 出现:-500
107+
size_t suffix_size = 0;
108+
if (!util::StringTo(end_str, suffix_size))
109+
return false;
110+
111+
range_begin = (suffix_size >= file_size) ? 0 : file_size - suffix_size;
112+
range_end = file_size - 1;
113+
114+
} else { //! 出现:1000-1999 或 1000-
115+
if (!util::StringTo(begin_str, range_begin))
116+
return false;
117+
118+
if (!end_str.empty()) {
119+
if (!util::StringTo(end_str, range_end))
120+
return false;
121+
} else {
122+
range_end = file_size - 1;
123+
}
124+
}
125+
126+
return true;
127+
}
128+
50129
}
51130

52131
//! 目录配置项
@@ -58,8 +137,8 @@ struct DirectoryConfig {
58137

59138
//! 中间件私有数据结构
60139
struct FileDownloaderMiddleware::Data {
61-
eventx::ThreadExecutor *worker = nullptr;
62-
eventx::WorkThread *inner_worker = nullptr;
140+
eventx::ThreadExecutor *worker = nullptr;
141+
eventx::WorkThread *inner_worker = nullptr;
63142
std::vector<DirectoryConfig> directories; //! 目录配置列表
64143
std::map<std::string, std::string> path_mappings;//! 特定路径映射
65144
std::map<std::string, std::string> mime_types; //! MIME类型映射
@@ -74,10 +153,9 @@ struct FileDownloaderMiddleware::Data {
74153
, switch_to_worker_filesize_threshold(100 << 10)
75154
{
76155
if (worker == nullptr) {
77-
inner_worker = new tbox::eventx::WorkThread(wp_loop);
156+
inner_worker = new eventx::WorkThread(wp_loop);
78157
worker = inner_worker;
79158
}
80-
81159
//! 初始化常见MIME类型
82160
mime_types["html"] = "text/html";
83161
mime_types["htm"] = "text/html";
@@ -104,21 +182,24 @@ struct FileDownloaderMiddleware::Data {
104182
mime_types["ttf"] = "font/ttf";
105183
mime_types["otf"] = "font/otf";
106184
}
107-
~Data() {
185+
186+
~Data()
187+
{
108188
CHECK_DELETE_RESET_OBJ(inner_worker);
109189
}
110190
};
111191

112192
FileDownloaderMiddleware::FileDownloaderMiddleware(event::Loop *wp_loop, eventx::ThreadExecutor *wp_thread_executor)
113-
: d_(new Data(wp_loop, wp_thread_executor))
193+
: d_(new Data(wp_loop, wp_thread_executor))
114194
{ }
115195

116196
FileDownloaderMiddleware::~FileDownloaderMiddleware()
117197
{ delete d_; }
118198

119199
bool FileDownloaderMiddleware::addDirectory(const std::string& url_prefix,
120200
const std::string& local_path,
121-
const std::string& default_file) {
201+
const std::string& default_file)
202+
{
122203
//! 验证URL前缀是否以'/'开头
123204
if (url_prefix.empty() || url_prefix[0] != '/') {
124205
LogErr("Invalid URL prefix: %s. Must start with '/'", url_prefix.c_str());
@@ -146,25 +227,42 @@ bool FileDownloaderMiddleware::addDirectory(const std::string& url_prefix,
146227
return true;
147228
}
148229

149-
void FileDownloaderMiddleware::setDirectoryListingEnabled(bool enable) {
230+
void FileDownloaderMiddleware::setDirectoryListingEnabled(bool enable)
231+
{
150232
d_->directory_listing_enabled = enable;
151233
}
152234

153-
void FileDownloaderMiddleware::setPathMapping(const std::string& url, const std::string& file) {
235+
void FileDownloaderMiddleware::setPathMapping(const std::string& url, const std::string& file)
236+
{
154237
d_->path_mappings[url] = file;
155238
}
156239

157-
void FileDownloaderMiddleware::setDefaultMimeType(const std::string& mime_type) {
240+
void FileDownloaderMiddleware::setDefaultMimeType(const std::string& mime_type)
241+
{
158242
d_->default_mime_type = mime_type;
159243
}
160244

161-
void FileDownloaderMiddleware::setMimeType(const std::string& ext, const std::string& mime_type) {
245+
void FileDownloaderMiddleware::setMimeType(const std::string& ext, const std::string& mime_type)
246+
{
162247
d_->mime_types[ext] = mime_type;
163248
}
164249

165-
void FileDownloaderMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) {
250+
void FileDownloaderMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next)
251+
{
166252
const auto& request = sp_ctx->req();
167253

254+
//! 处理 OPTIONS 预检请求(浏览器跨域访问)
255+
if (request.method == Method::kOptions) {
256+
auto& res = sp_ctx->res();
257+
res.status_code = StatusCode::k200_OK;
258+
res.headers["Access-Control-Allow-Origin"] = "*";
259+
res.headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS";
260+
res.headers["Access-Control-Allow-Headers"] = "Auth, Range, If-Range, If-None-Match, If-Modified-Since";
261+
res.headers["Access-Control-Allow-Private-Network"] = "true";
262+
res.headers["Access-Control-Max-Age"] = "86400";
263+
return;
264+
}
265+
168266
//! 只处理GET和HEAD请求
169267
if (request.method != Method::kGet && request.method != Method::kHead) {
170268
next();
@@ -241,7 +339,8 @@ void FileDownloaderMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next)
241339
next();
242340
}
243341

244-
std::string FileDownloaderMiddleware::getMimeType(const std::string& filename) const {
342+
std::string FileDownloaderMiddleware::getMimeType(const std::string& filename) const
343+
{
245344
//! 查找最后一个点的位置
246345
size_t dot_pos = filename.find_last_of('.');
247346
if (dot_pos != std::string::npos) {
@@ -256,61 +355,133 @@ std::string FileDownloaderMiddleware::getMimeType(const std::string& filename) c
256355
return d_->default_mime_type;
257356
}
258357

259-
bool FileDownloaderMiddleware::respondFile(ContextSptr sp_ctx, const std::string& file_path) {
358+
bool FileDownloaderMiddleware::respondFile(ContextSptr sp_ctx, const std::string& file_path)
359+
{
260360
auto& res = sp_ctx->res();
261361

262-
//! 打开文件
263-
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
264-
if (!file.is_open()) {
362+
//! 用 stat() 获取文件元信息,同时验证文件是否存在
363+
struct stat file_stat;
364+
if (::stat(file_path.c_str(), &file_stat) != 0) {
265365
res.status_code = StatusCode::k404_NotFound;
266366
return true;
267367
}
268368

269-
res.headers["Content-Type"] = getMimeType(file_path);
369+
size_t file_size = static_cast<size_t>(file_stat.st_size);
370+
time_t file_mtime = file_stat.st_mtime;
371+
std::string etag = GenerateETag(file_mtime, file_stat.st_size);
372+
std::string last_modified = HttpDateToString(file_mtime);
373+
374+
res.headers["Content-Type"] = getMimeType(file_path);
375+
res.headers["Accept-Ranges"] = "bytes";
376+
res.headers["ETag"] = etag;
377+
res.headers["Last-Modified"] = last_modified;
378+
res.headers["Cache-Control"] = "public, max-age=0, must-revalidate";
379+
res.headers["Access-Control-Allow-Origin"] = "*";
380+
res.headers["Access-Control-Expose-Headers"] = "Content-Range, Content-Length, ETag, Last-Modified";
381+
382+
//! 条件请求:If-None-Match 优先于 If-Modified-Since(RFC 7232 §6)
383+
std::string if_none_match = GetHeader(sp_ctx->req().headers, "if-none-match");
384+
if (!if_none_match.empty()) {
385+
if (if_none_match == etag) {
386+
res.status_code = StatusCode::k304_NotModified;
387+
return true;
388+
}
389+
} else {
390+
std::string if_modified_since = GetHeader(sp_ctx->req().headers, "if-modified-since");
391+
if (!if_modified_since.empty()) {
392+
time_t since = StringToHttpDate(if_modified_since);
393+
if (since != -1 && file_mtime <= since) {
394+
res.status_code = StatusCode::k304_NotModified;
395+
return true;
396+
}
397+
}
398+
}
270399

271-
//! 获取文件大小
272-
size_t file_size = static_cast<size_t>(file.tellg());
273-
file.seekg(0, std::ios::beg);
400+
//! 解析 Range 请求头(需在 HEAD 判断之前,确保非法 Range 返回 416)
401+
size_t range_begin = 0;
402+
size_t range_end = file_size > 0 ? file_size - 1 : 0;
403+
bool has_range = false;
274404

275-
//! 如果是HEAD请求,不返回内容
276-
if (sp_ctx->req().method == Method::kHead) {
277-
res.status_code = StatusCode::k200_OK;
278-
res.headers["Content-Length"] = std::to_string(file_size);
279-
return true;
405+
if (file_size > 0) {
406+
auto range_str = GetHeader(sp_ctx->req().headers, "range");
407+
if (util::string::IsStartWith(range_str, "bytes=")) {
408+
has_range = ParseRangeString(range_str, file_size, range_begin, range_end);
409+
}
410+
411+
//! If-Range:ETag 不匹配时降级为全量响应,保证数据一致性
412+
if (has_range) {
413+
std::string if_range = GetHeader(sp_ctx->req().headers, "if-range");
414+
if (!if_range.empty() && if_range != etag) {
415+
has_range = false;
416+
range_begin = 0;
417+
range_end = file_size - 1;
418+
}
419+
}
420+
421+
//! 校验范围合法性
422+
if (has_range && (range_begin >= file_size || range_end >= file_size || range_begin > range_end)) {
423+
res.status_code = StatusCode::k416_RequestedRangeNotSatisfiable;
424+
res.headers["Content-Range"] = "bytes */" + std::to_string(file_size);
425+
return true;
426+
}
280427
}
281428

282-
//! 文件是否大于100KB
283-
if (file_size < d_->switch_to_worker_filesize_threshold) {
284-
//! 小文件就直接读了
429+
size_t content_length = file_size > 0 ? range_end - range_begin + 1 : 0;
430+
res.headers["Content-Length"] = std::to_string(content_length);
431+
432+
if (has_range) {
433+
res.status_code = StatusCode::k206_PartialContent;
434+
res.headers["Content-Range"] = "bytes " + std::to_string(range_begin) + "-" +
435+
std::to_string(range_end) + "/" + std::to_string(file_size);
436+
} else {
285437
res.status_code = StatusCode::k200_OK;
286-
res.headers["Content-Length"] = std::to_string(file_size);
287-
//! 将文件内容读到body中去
288-
res.body = std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
289-
LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), file_size);
438+
}
439+
440+
//! HEAD 请求不返回 body
441+
if (sp_ctx->req().method == Method::kHead)
442+
return true;
290443

444+
//! 空文件直接返回
445+
if (file_size == 0)
446+
return true;
447+
448+
if (content_length < d_->switch_to_worker_filesize_threshold) {
449+
std::ifstream file(file_path, std::ios::binary);
450+
if (!file.is_open()) {
451+
res.status_code = StatusCode::k500_InternalServerError;
452+
return true;
453+
}
454+
file.seekg(static_cast<std::streamoff>(range_begin));
455+
res.body.resize(content_length);
456+
file.read(&res.body[0], static_cast<std::streamsize>(content_length));
457+
LogInfo("Served file: %s (bytes %zu-%zu/%zu)",
458+
file_path.c_str(), range_begin, range_end, file_size);
291459
} else {
292-
//! 文件太大就采用子线程去读
293460
d_->worker->execute(
294-
[sp_ctx, file_path] {
461+
[sp_ctx, file_path, range_begin, content_length] {
295462
auto& res = sp_ctx->res();
296-
if (util::fs::ReadBinaryFromFile(file_path, res.body)) {
297-
res.status_code = StatusCode::k200_OK;
298-
res.headers["Content-Length"] = std::to_string(res.body.size());
299-
LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), res.body.size());
463+
std::ifstream f(file_path, std::ios::binary);
464+
if (f.is_open()) {
465+
f.seekg(static_cast<std::streamoff>(range_begin));
466+
res.body.resize(content_length);
467+
f.read(&res.body[0], static_cast<std::streamsize>(content_length));
468+
LogInfo("Served file(worker): %s (%zu bytes from %zu)",
469+
file_path.c_str(), content_length, range_begin);
300470
} else {
301-
res.status_code = StatusCode::k404_NotFound;
471+
res.status_code = StatusCode::k500_InternalServerError;
302472
}
303473
},
304-
[sp_ctx] { } //! 这是为了确保sp_ctx在主线程上析构
305-
);
474+
[sp_ctx] { } //! 确保 sp_ctx 在主线程上析构
475+
);
306476
}
307477

308478
return true;
309479
}
310480

311481
bool FileDownloaderMiddleware::respondDirectory(ContextSptr sp_ctx,
312482
const std::string& dir_path,
313-
const std::string& url_path) {
483+
const std::string& url_path)
484+
{
314485
try {
315486
//! 生成HTML目录列表
316487
std::ostringstream html_oss;

0 commit comments

Comments
 (0)