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
3337namespace tbox {
3438namespace http {
3539namespace server {
3640
3741namespace {
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// ! 中间件私有数据结构
60139struct 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
112192FileDownloaderMiddleware::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
116196FileDownloaderMiddleware::~FileDownloaderMiddleware ()
117197{ delete d_; }
118198
119199bool 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
311481bool 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