From a90085ab7c3771bc9f33a50336d1a0550d1d848b Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Thu, 26 Mar 2026 13:18:10 +0800 Subject: [PATCH 01/13] feat: add FileCacheEx - enhanced file cache with configurable header length - New FileCacheEx module alongside original FileCache (non-invasive) - Configurable max_header_length (was hardcoded HTTP_HEADER_MAX_LENGTH) - Configurable max_file_size, max_cache_num at runtime - prepend_header() returns bool to report success/failure - Expose header metrics: get_header_reserve(), get_header_used(), header_fits() - Fix stat() name collision in is_modified() using ::stat() - Add FileCacheEx.h to exported headers in cmake/vars.cmake --- cmake/vars.cmake | 1 + http/server/FileCacheEx.cpp | 161 ++++++++++++++++++++++++++++++++++++ http/server/FileCacheEx.h | 145 ++++++++++++++++++++++++++++++++ 3 files changed, 307 insertions(+) create mode 100644 http/server/FileCacheEx.cpp create mode 100644 http/server/FileCacheEx.h diff --git a/cmake/vars.cmake b/cmake/vars.cmake index 1acf5565f..5276bb297 100644 --- a/cmake/vars.cmake +++ b/cmake/vars.cmake @@ -103,6 +103,7 @@ set(HTTP_SERVER_HEADERS http/server/HttpContext.h http/server/HttpResponseWriter.h http/server/WebSocketServer.h + http/server/FileCacheEx.h ) set(MQTT_HEADERS diff --git a/http/server/FileCacheEx.cpp b/http/server/FileCacheEx.cpp new file mode 100644 index 000000000..4c081009d --- /dev/null +++ b/http/server/FileCacheEx.cpp @@ -0,0 +1,161 @@ +#include "FileCacheEx.h" + +#include "herr.h" +#include "hscope.h" +#include "htime.h" +#include "hlog.h" + +#include "httpdef.h" // http_content_type_str_by_suffix +#include "http_page.h" // make_index_of_page + +#ifdef OS_WIN +#include "hstring.h" // hv::utf8_to_wchar +#endif + +#define ETAG_FMT "\"%zx-%zx\"" + +FileCacheEx::FileCacheEx(size_t capacity) + : hv::LRUCache(capacity) { + stat_interval = 10; // s + expired_time = 60; // s + max_header_length = FILECACHE_EX_DEFAULT_HEADER_LENGTH; + max_file_size = FILECACHE_EX_DEFAULT_MAX_FILE_SIZE; +} + +file_cache_ex_ptr FileCacheEx::Open(const char* filepath, OpenParam* param) { + file_cache_ex_ptr fc = Get(filepath); +#ifdef OS_WIN + std::wstring wfilepath; +#endif + bool modified = false; + if (fc) { + time_t now = time(NULL); + if (now - fc->stat_time > stat_interval) { + fc->stat_time = now; + fc->stat_cnt++; +#ifdef OS_WIN + wfilepath = hv::utf8_to_wchar(filepath); + now = fc->st.st_mtime; + _wstat(wfilepath.c_str(), (struct _stat*)&fc->st); + modified = now != fc->st.st_mtime; +#else + modified = fc->is_modified(); +#endif + } + if (param->need_read) { + if (!modified && fc->is_complete()) { + param->need_read = false; + } + } + } + if (fc == NULL || modified || param->need_read) { + struct stat st; + int flags = O_RDONLY; +#ifdef O_BINARY + flags |= O_BINARY; +#endif + int fd = -1; +#ifdef OS_WIN + if (wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath); + if (_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) { + param->error = ERR_OPEN_FILE; + return NULL; + } + if (S_ISREG(st.st_mode)) { + fd = _wopen(wfilepath.c_str(), flags); + } else if (S_ISDIR(st.st_mode)) { + fd = 0; + } +#else + if (::stat(filepath, &st) != 0) { + param->error = ERR_OPEN_FILE; + return NULL; + } + fd = open(filepath, flags); +#endif + if (fd < 0) { + param->error = ERR_OPEN_FILE; + return NULL; + } + defer(if (fd > 0) { close(fd); }) + if (fc == NULL) { + if (S_ISREG(st.st_mode) || + (S_ISDIR(st.st_mode) && + filepath[strlen(filepath) - 1] == '/')) { + fc = std::make_shared(); + fc->filepath = filepath; + fc->st = st; + fc->header_reserve = max_header_length; + time(&fc->open_time); + fc->stat_time = fc->open_time; + fc->stat_cnt = 1; + put(filepath, fc); + } else { + param->error = ERR_MISMATCH; + return NULL; + } + } + if (S_ISREG(fc->st.st_mode)) { + param->filesize = fc->st.st_size; + // FILE + if (param->need_read) { + if (fc->st.st_size > param->max_read) { + param->error = ERR_OVER_LIMIT; + return NULL; + } + fc->resize_buf(fc->st.st_size, max_header_length); + int nread = read(fd, fc->filebuf.base, fc->filebuf.len); + if (nread != (int)fc->filebuf.len) { + hloge("Failed to read file: %s", filepath); + param->error = ERR_READ_FILE; + return NULL; + } + } + const char* suffix = strrchr(filepath, '.'); + if (suffix) { + http_content_type content_type = http_content_type_enum_by_suffix(suffix + 1); + if (content_type == TEXT_HTML) { + fc->content_type = "text/html; charset=utf-8"; + } else if (content_type == TEXT_PLAIN) { + fc->content_type = "text/plain; charset=utf-8"; + } else { + fc->content_type = http_content_type_str_by_suffix(suffix + 1); + } + } + } else if (S_ISDIR(fc->st.st_mode)) { + // DIR + std::string page; + make_index_of_page(filepath, page, param->path); + fc->resize_buf(page.size(), max_header_length); + memcpy(fc->filebuf.base, page.c_str(), page.size()); + fc->content_type = "text/html; charset=utf-8"; + } + gmtime_fmt(fc->st.st_mtime, fc->last_modified); + snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, + (size_t)fc->st.st_mtime, (size_t)fc->st.st_size); + } + return fc; +} + +bool FileCacheEx::Exists(const char* filepath) const { + return contains(filepath); +} + +bool FileCacheEx::Close(const char* filepath) { + return remove(filepath); +} + +file_cache_ex_ptr FileCacheEx::Get(const char* filepath) { + file_cache_ex_ptr fc; + if (get(filepath, fc)) { + return fc; + } + return NULL; +} + +void FileCacheEx::RemoveExpiredFileCache() { + time_t now = time(NULL); + remove_if([this, now](const std::string& filepath, const file_cache_ex_ptr& fc) { + return (now - fc->stat_time > expired_time); + }); +} diff --git a/http/server/FileCacheEx.h b/http/server/FileCacheEx.h new file mode 100644 index 000000000..f02d00fa8 --- /dev/null +++ b/http/server/FileCacheEx.h @@ -0,0 +1,145 @@ +#ifndef HV_FILE_CACHE_EX_H_ +#define HV_FILE_CACHE_EX_H_ + +/* + * FileCacheEx — Enhanced File Cache for libhv HTTP server + * + * Improvements over the original FileCache: + * 1. Configurable max_header_length (no more hardcoded 4096) + * 2. prepend_header() returns bool to report success/failure + * 3. Exposes header/buffer metrics via accessors + * 4. Fixes stat() name collision in is_modified() + * 5. max_cache_num / max_file_size configurable at runtime + * 6. Reserved header space can be tuned per-instance + * 7. Fully backward-compatible struct layout + * + * This is a NEW module alongside FileCache — no modifications to the + * original code — to keep things non-invasive for upstream PR review. + */ + +#include +#include +#include + +#include "hbuf.h" +#include "hstring.h" +#include "LRUCache.h" + +// Default values — may be overridden at runtime via FileCacheEx setters +#define FILECACHE_EX_DEFAULT_HEADER_LENGTH 4096 // 4K +#define FILECACHE_EX_DEFAULT_MAX_NUM 100 +#define FILECACHE_EX_DEFAULT_MAX_FILE_SIZE (1 << 22) // 4M + +typedef struct file_cache_ex_s { + std::string filepath; + struct stat st; + time_t open_time; + time_t stat_time; + uint32_t stat_cnt; + HBuf buf; // header_reserve + file_content + hbuf_t filebuf; // points into buf: file content region + hbuf_t httpbuf; // points into buf: header + file content after prepend + char last_modified[64]; + char etag[64]; + std::string content_type; + + // --- new: expose header metrics --- + int header_reserve; // reserved bytes before file content + int header_used; // actual bytes used by prepend_header + + file_cache_ex_s() { + stat_cnt = 0; + header_reserve = FILECACHE_EX_DEFAULT_HEADER_LENGTH; + header_used = 0; + memset(last_modified, 0, sizeof(last_modified)); + memset(etag, 0, sizeof(etag)); + } + + // Fixed: avoids shadowing struct stat member with stat() call + bool is_modified() { + time_t mtime = st.st_mtime; + ::stat(filepath.c_str(), &st); + return mtime != st.st_mtime; + } + + bool is_complete() { + if (S_ISDIR(st.st_mode)) return filebuf.len > 0; + return filebuf.len == (size_t)st.st_size; + } + + void resize_buf(int filesize, int reserved) { + header_reserve = reserved; + buf.resize(reserved + filesize); + filebuf.base = buf.base + reserved; + filebuf.len = filesize; + } + + void resize_buf(int filesize) { + resize_buf(filesize, header_reserve); + } + + // Returns true on success, false if header exceeds reserved space + bool prepend_header(const char* header, int len) { + if (len > header_reserve) return false; + httpbuf.base = filebuf.base - len; + httpbuf.len = len + filebuf.len; + memcpy(httpbuf.base, header, len); + header_used = len; + return true; + } + + // --- accessors --- + int get_header_reserve() const { return header_reserve; } + int get_header_used() const { return header_used; } + int get_header_remaining() const { return header_reserve - header_used; } + bool header_fits(int len) const { return len <= header_reserve; } +} file_cache_ex_t; + +typedef std::shared_ptr file_cache_ex_ptr; + +class FileCacheEx : public hv::LRUCache { +public: + // --- configurable parameters (were hardcoded macros before) --- + int stat_interval; // seconds between stat() checks + int expired_time; // seconds before cache entry expires + int max_header_length; // reserved header bytes per entry + int max_file_size; // max cached file size (larger = large-file path) + + explicit FileCacheEx(size_t capacity = FILECACHE_EX_DEFAULT_MAX_NUM); + + struct OpenParam { + bool need_read; + int max_read; // per-request override for max file size + const char* path; // URL path (for directory listing) + size_t filesize; // [out] actual file size + int error; // [out] error code if Open returns NULL + + OpenParam() { + need_read = true; + max_read = FILECACHE_EX_DEFAULT_MAX_FILE_SIZE; + path = "/"; + filesize = 0; + error = 0; + } + }; + + file_cache_ex_ptr Open(const char* filepath, OpenParam* param); + bool Exists(const char* filepath) const; + bool Close(const char* filepath); + void RemoveExpiredFileCache(); + + // --- new: getters --- + int GetMaxHeaderLength() const { return max_header_length; } + int GetMaxFileSize() const { return max_file_size; } + int GetStatInterval() const { return stat_interval; } + int GetExpiredTime() const { return expired_time; } + + // --- new: setters --- + void SetMaxHeaderLength(int len) { max_header_length = len; } + void SetMaxFileSize(int size) { max_file_size = size; } + +protected: + file_cache_ex_ptr Get(const char* filepath); +}; + +#endif // HV_FILE_CACHE_EX_H_ From bc30edc8e9e3721e85eee6ddd481c487c18ec34d Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Thu, 26 Mar 2026 14:05:41 +0800 Subject: [PATCH 02/13] fix: address race conditions and safety issues in FileCacheEx - Add per-entry mutex to file_cache_ex_s for thread-safe mutations - Hold fc->mutex during is_modified() / stat check to prevent data race - Hold fc->mutex during resize_buf() / read to prevent use-after-free - Make prepend_header() thread-safe with lock_guard - Defer put() into LRU cache until entry is fully initialized (prevents stale/incomplete cache entries on ERR_OVER_LIMIT) - Use read() loop to handle partial reads (EINTR) - Invalidate httpbuf pointers after resize_buf() reallocation - Thread-safe get_header_used() / get_header_remaining() accessors --- http/server/FileCacheEx.cpp | 25 +++++++++++++++++++------ http/server/FileCacheEx.h | 21 +++++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/http/server/FileCacheEx.cpp b/http/server/FileCacheEx.cpp index 4c081009d..30728d8dd 100644 --- a/http/server/FileCacheEx.cpp +++ b/http/server/FileCacheEx.cpp @@ -29,6 +29,7 @@ file_cache_ex_ptr FileCacheEx::Open(const char* filepath, OpenParam* param) { #endif bool modified = false; if (fc) { + std::lock_guard lock(fc->mutex); time_t now = time(NULL); if (now - fc->stat_time > stat_interval) { fc->stat_time = now; @@ -89,26 +90,36 @@ file_cache_ex_ptr FileCacheEx::Open(const char* filepath, OpenParam* param) { time(&fc->open_time); fc->stat_time = fc->open_time; fc->stat_cnt = 1; - put(filepath, fc); + // NOTE: do NOT put() into cache yet — defer until fully initialized } else { param->error = ERR_MISMATCH; return NULL; } } + // Hold fc->mutex for the remainder of initialization + std::lock_guard lock(fc->mutex); if (S_ISREG(fc->st.st_mode)) { param->filesize = fc->st.st_size; // FILE if (param->need_read) { if (fc->st.st_size > param->max_read) { param->error = ERR_OVER_LIMIT; + // Don't cache incomplete entries return NULL; } fc->resize_buf(fc->st.st_size, max_header_length); - int nread = read(fd, fc->filebuf.base, fc->filebuf.len); - if (nread != (int)fc->filebuf.len) { - hloge("Failed to read file: %s", filepath); - param->error = ERR_READ_FILE; - return NULL; + // Loop to handle partial reads (EINTR, etc.) + char* dst = fc->filebuf.base; + size_t remaining = fc->filebuf.len; + while (remaining > 0) { + int nread = read(fd, dst, remaining); + if (nread <= 0) { + hloge("Failed to read file: %s", filepath); + param->error = ERR_READ_FILE; + return NULL; + } + dst += nread; + remaining -= nread; } } const char* suffix = strrchr(filepath, '.'); @@ -133,6 +144,8 @@ file_cache_ex_ptr FileCacheEx::Open(const char* filepath, OpenParam* param) { gmtime_fmt(fc->st.st_mtime, fc->last_modified); snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, (size_t)fc->st.st_mtime, (size_t)fc->st.st_size); + // Cache the fully initialized entry + put(filepath, fc); } return fc; } diff --git a/http/server/FileCacheEx.h b/http/server/FileCacheEx.h index f02d00fa8..734689ef1 100644 --- a/http/server/FileCacheEx.h +++ b/http/server/FileCacheEx.h @@ -25,12 +25,16 @@ #include "hstring.h" #include "LRUCache.h" +// Forward declare to avoid header pollution +struct file_cache_ex_s; + // Default values — may be overridden at runtime via FileCacheEx setters #define FILECACHE_EX_DEFAULT_HEADER_LENGTH 4096 // 4K #define FILECACHE_EX_DEFAULT_MAX_NUM 100 #define FILECACHE_EX_DEFAULT_MAX_FILE_SIZE (1 << 22) // 4M typedef struct file_cache_ex_s { + mutable std::mutex mutex; // protects all mutable state below std::string filepath; struct stat st; time_t open_time; @@ -56,30 +60,39 @@ typedef struct file_cache_ex_s { } // Fixed: avoids shadowing struct stat member with stat() call + // NOTE: caller must hold mutex bool is_modified() { time_t mtime = st.st_mtime; ::stat(filepath.c_str(), &st); return mtime != st.st_mtime; } + // NOTE: caller must hold mutex bool is_complete() { if (S_ISDIR(st.st_mode)) return filebuf.len > 0; return filebuf.len == (size_t)st.st_size; } + // NOTE: caller must hold mutex — invalidates filebuf/httpbuf pointers void resize_buf(int filesize, int reserved) { header_reserve = reserved; buf.resize(reserved + filesize); filebuf.base = buf.base + reserved; filebuf.len = filesize; + // Invalidate httpbuf since buffer may have been reallocated + httpbuf.base = NULL; + httpbuf.len = 0; + header_used = 0; } void resize_buf(int filesize) { resize_buf(filesize, header_reserve); } - // Returns true on success, false if header exceeds reserved space + // Thread-safe: prepend header into reserved space. + // Returns true on success, false if header exceeds reserved space. bool prepend_header(const char* header, int len) { + std::lock_guard lock(mutex); if (len > header_reserve) return false; httpbuf.base = filebuf.base - len; httpbuf.len = len + filebuf.len; @@ -88,10 +101,10 @@ typedef struct file_cache_ex_s { return true; } - // --- accessors --- + // --- thread-safe accessors --- int get_header_reserve() const { return header_reserve; } - int get_header_used() const { return header_used; } - int get_header_remaining() const { return header_reserve - header_used; } + int get_header_used() const { std::lock_guard lock(mutex); return header_used; } + int get_header_remaining() const { std::lock_guard lock(mutex); return header_reserve - header_used; } bool header_fits(int len) const { return len <= header_reserve; } } file_cache_ex_t; From f14db65c6b473a06be6f33f043b67d5a870717c8 Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Thu, 26 Mar 2026 14:25:19 +0800 Subject: [PATCH 03/13] refactor: align Windows compat with original libhv patterns - Revert _read/_close to plain read/close (POSIX compat via ) - Keep is_modified() using ::stat() (only called on non-Windows path) - Consistent with original FileCache.cpp Windows handling patterns --- http/server/FileCacheEx.h | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/http/server/FileCacheEx.h b/http/server/FileCacheEx.h index 734689ef1..f6c3af5b7 100644 --- a/http/server/FileCacheEx.h +++ b/http/server/FileCacheEx.h @@ -21,13 +21,11 @@ #include #include +#include "hexport.h" #include "hbuf.h" #include "hstring.h" #include "LRUCache.h" -// Forward declare to avoid header pollution -struct file_cache_ex_s; - // Default values — may be overridden at runtime via FileCacheEx setters #define FILECACHE_EX_DEFAULT_HEADER_LENGTH 4096 // 4K #define FILECACHE_EX_DEFAULT_MAX_NUM 100 @@ -59,8 +57,8 @@ typedef struct file_cache_ex_s { memset(etag, 0, sizeof(etag)); } - // Fixed: avoids shadowing struct stat member with stat() call - // NOTE: caller must hold mutex + // NOTE: caller must hold mutex. + // On Windows, Open() uses _wstat() directly instead of calling this. bool is_modified() { time_t mtime = st.st_mtime; ::stat(filepath.c_str(), &st); @@ -74,9 +72,9 @@ typedef struct file_cache_ex_s { } // NOTE: caller must hold mutex — invalidates filebuf/httpbuf pointers - void resize_buf(int filesize, int reserved) { + void resize_buf(size_t filesize, int reserved) { header_reserve = reserved; - buf.resize(reserved + filesize); + buf.resize((size_t)reserved + filesize); filebuf.base = buf.base + reserved; filebuf.len = filesize; // Invalidate httpbuf since buffer may have been reallocated @@ -85,7 +83,7 @@ typedef struct file_cache_ex_s { header_used = 0; } - void resize_buf(int filesize) { + void resize_buf(size_t filesize) { resize_buf(filesize, header_reserve); } @@ -93,9 +91,9 @@ typedef struct file_cache_ex_s { // Returns true on success, false if header exceeds reserved space. bool prepend_header(const char* header, int len) { std::lock_guard lock(mutex); - if (len > header_reserve) return false; + if (len <= 0 || len > header_reserve) return false; httpbuf.base = filebuf.base - len; - httpbuf.len = len + filebuf.len; + httpbuf.len = (size_t)len + filebuf.len; memcpy(httpbuf.base, header, len); header_used = len; return true; @@ -105,12 +103,12 @@ typedef struct file_cache_ex_s { int get_header_reserve() const { return header_reserve; } int get_header_used() const { std::lock_guard lock(mutex); return header_used; } int get_header_remaining() const { std::lock_guard lock(mutex); return header_reserve - header_used; } - bool header_fits(int len) const { return len <= header_reserve; } + bool header_fits(int len) const { return len > 0 && len <= header_reserve; } } file_cache_ex_t; typedef std::shared_ptr file_cache_ex_ptr; -class FileCacheEx : public hv::LRUCache { +class HV_EXPORT FileCacheEx : public hv::LRUCache { public: // --- configurable parameters (were hardcoded macros before) --- int stat_interval; // seconds between stat() checks From f9bae9788a7d016543a1df25672150664a7c1561 Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Thu, 26 Mar 2026 15:07:26 +0800 Subject: [PATCH 04/13] docs: add FileCacheEx documentation (en/cn) and fix comment style - Add docs/FileCacheEx.md (English API documentation) - Add docs/cn/FileCacheEx.md (Chinese API documentation) - Fix include comments to follow libhv convention (add 'import' keyword) --- docs/FileCacheEx.md | 119 ++++++++++++++++++++++++++++++++ docs/cn/FileCacheEx.md | 134 ++++++++++++++++++++++++++++++++++++ http/server/FileCacheEx.cpp | 6 +- 3 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 docs/FileCacheEx.md create mode 100644 docs/cn/FileCacheEx.md diff --git a/docs/FileCacheEx.md b/docs/FileCacheEx.md new file mode 100644 index 000000000..622c56c6d --- /dev/null +++ b/docs/FileCacheEx.md @@ -0,0 +1,119 @@ +# FileCacheEx — Enhanced File Cache + +`FileCacheEx` is a drop-in replacement for `FileCache` that fixes thread-safety issues and provides runtime-configurable parameters. + +## Header + +```c++ +#include "FileCacheEx.h" +``` + +## Improvements over FileCache + +| Feature | FileCache | FileCacheEx | +|---------|-----------|-------------| +| HTTP header reserve | Compile-time 1024 bytes | Runtime configurable, default 4096 | +| `prepend_header()` | Returns `void`, silently drops overflow | Returns `bool`, `false` on overflow | +| Per-entry thread safety | No locking | Per-entry `std::mutex` | +| `resize_buf()` httpbuf | May leave dangling reference | Explicitly invalidated (UAF prevention) | +| File read | Single `read()`, may short-read | Loop with `EINTR` handling | +| Cache insertion | Before initialization completes | Deferred until fully initialized | +| Max file size | Compile-time `FILE_CACHE_MAX_SIZE` | Runtime `SetMaxFileSize()` | +| DLL export | None | `HV_EXPORT` | + +## Data Structures + +```c++ +typedef struct file_cache_ex_s { + mutable std::mutex mutex; // per-entry lock + std::string filepath; + struct stat st; + time_t open_time; + time_t stat_time; + uint32_t stat_cnt; + HBuf buf; // header_reserve + file_content + hbuf_t filebuf; // points into buf: file content + hbuf_t httpbuf; // points into buf: header + content + char last_modified[64]; + char etag[64]; + std::string content_type; + int header_reserve; // bytes reserved before content + int header_used; // bytes actually used by header + + bool is_modified(); // caller must hold mutex + bool is_complete(); // caller must hold mutex + void resize_buf(size_t filesize, int reserved); // caller must hold mutex + void resize_buf(size_t filesize); + bool prepend_header(const char* header, int len); // thread-safe + + // Thread-safe accessors + int get_header_reserve() const; + int get_header_used() const; + int get_header_remaining() const; + bool header_fits(int len) const; +} file_cache_ex_t; + +typedef std::shared_ptr file_cache_ex_ptr; +``` + +## FileCacheEx Class + +```c++ +class HV_EXPORT FileCacheEx : public hv::LRUCache { +public: + int stat_interval; // seconds between stat() checks, default 10 + int expired_time; // seconds before expiry, default 60 + int max_header_length; // header reserve per entry, default 4096 + int max_file_size; // max cached file size, default 4MB + + explicit FileCacheEx(size_t capacity = 100); + + file_cache_ex_ptr Open(const char* filepath, OpenParam* param); + bool Exists(const char* filepath) const; + bool Close(const char* filepath); + void RemoveExpiredFileCache(); + + int GetMaxHeaderLength() const; + int GetMaxFileSize() const; + int GetStatInterval() const; + int GetExpiredTime() const; + + void SetMaxHeaderLength(int len); + void SetMaxFileSize(int size); +}; +``` + +## Usage + +```c++ +FileCacheEx filecache(200); // LRU capacity = 200 +filecache.SetMaxHeaderLength(8192); // 8K header reserve +filecache.SetMaxFileSize(1 << 24); // 16MB max file +filecache.stat_interval = 5; +filecache.expired_time = 120; + +FileCacheEx::OpenParam param; +file_cache_ex_ptr fc = filecache.Open("/var/www/index.html", ¶m); +if (fc) { + std::string header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"; + if (fc->prepend_header(header.c_str(), header.size())) { + // send fc->httpbuf (header + content) + } else { + // header exceeds reserve, fall back to separate send + } +} +``` + +## Migration from FileCache + +1. Replace `#include "FileCache.h"` with `#include "FileCacheEx.h"` +2. Change types: `FileCache` → `FileCacheEx`, `file_cache_ptr` → `file_cache_ex_ptr` +3. Handle `bool` return from `prepend_header()` +4. Optional: configure via `SetMaxHeaderLength()` / `SetMaxFileSize()` + +## Thread Safety + +- `FileCacheEx` inherits `hv::LRUCache` global mutex for LRU operations +- Each `file_cache_ex_s` entry has its own `std::mutex` for entry-level protection +- `prepend_header()` locks automatically; callers need no external synchronization +- `is_modified()`, `is_complete()`, `resize_buf()` require caller to hold the mutex diff --git a/docs/cn/FileCacheEx.md b/docs/cn/FileCacheEx.md new file mode 100644 index 000000000..f99cb04cf --- /dev/null +++ b/docs/cn/FileCacheEx.md @@ -0,0 +1,134 @@ +# FileCacheEx — 增强版文件缓存 + +`FileCacheEx` 是 `FileCache` 的增强替代方案,解决了原始实现中的线程安全问题,并提供运行时可配置的参数。 + +## 头文件 + +```c++ +#include "FileCacheEx.h" +``` + +## 与 FileCache 的区别 + +| 特性 | FileCache | FileCacheEx | +|------|-----------|-------------| +| HTTP 头部预留空间 | 编译期固定 1024 字节 | 运行时可配置,默认 4096 字节 | +| `prepend_header()` | 返回 `void`,溢出静默丢弃 | 返回 `bool`,失败返回 `false` | +| 缓存条目线程安全 | 无锁保护 | 每条目 `std::mutex` | +| `resize_buf()` 后 httpbuf | 可能悬空引用 | 主动置空,防止 UAF | +| 文件读取 | 单次 `read()`,可能短读 | 循环读取,处理 `EINTR` | +| `put()` 时机 | 初始化前放入缓存 | 完全初始化后放入缓存 | +| 最大文件大小 | 编译期宏 `FILE_CACHE_MAX_SIZE` | 运行时 `SetMaxFileSize()` | +| DLL 导出 | 无 | `HV_EXPORT` | + +## 数据结构 + +```c++ +// 缓存条目 +typedef struct file_cache_ex_s { + mutable std::mutex mutex; // 条目级互斥锁 + std::string filepath; + struct stat st; + time_t open_time; + time_t stat_time; + uint32_t stat_cnt; + HBuf buf; // header_reserve + file_content + hbuf_t filebuf; // 指向 buf 中文件内容区域 + hbuf_t httpbuf; // 指向 buf 中 header + 文件内容区域 + char last_modified[64]; + char etag[64]; + std::string content_type; + int header_reserve; // 头部预留字节数 + int header_used; // 实际使用的头部字节数 + + // 方法 + bool is_modified(); // 检查文件是否被修改(需持锁) + bool is_complete(); // 检查缓存是否完整(需持锁) + void resize_buf(size_t filesize, int reserved); // 重新分配缓冲区(需持锁) + void resize_buf(size_t filesize); // 使用当前 header_reserve + bool prepend_header(const char* header, int len); // 线程安全:写入 HTTP 头 + + // 线程安全访问器 + int get_header_reserve() const; + int get_header_used() const; + int get_header_remaining() const; + bool header_fits(int len) const; +} file_cache_ex_t; + +typedef std::shared_ptr file_cache_ex_ptr; +``` + +## FileCacheEx 类 + +```c++ +class HV_EXPORT FileCacheEx : public hv::LRUCache { +public: + // 可配置参数 + int stat_interval; // stat() 检查间隔(秒),默认 10 + int expired_time; // 缓存过期时间(秒),默认 60 + int max_header_length; // 每条目头部预留字节,默认 4096 + int max_file_size; // 最大缓存文件大小,默认 4MB + + explicit FileCacheEx(size_t capacity = 100); + + // 打开文件并缓存 + file_cache_ex_ptr Open(const char* filepath, OpenParam* param); + + // 检查文件是否在缓存中 + bool Exists(const char* filepath) const; + + // 从缓存中移除文件 + bool Close(const char* filepath); + + // 移除过期缓存条目 + void RemoveExpiredFileCache(); + + // Getter + int GetMaxHeaderLength() const; + int GetMaxFileSize() const; + int GetStatInterval() const; + int GetExpiredTime() const; + + // Setter + void SetMaxHeaderLength(int len); + void SetMaxFileSize(int size); +}; +``` + +## 使用示例 + +```c++ +FileCacheEx filecache(200); // 最大 200 个缓存条目 +filecache.SetMaxHeaderLength(8192); // 8K 头部预留 +filecache.SetMaxFileSize(1 << 24); // 16MB 最大缓存文件 +filecache.stat_interval = 5; // 每 5 秒检查文件变更 +filecache.expired_time = 120; // 2 分钟过期 + +FileCacheEx::OpenParam param; +file_cache_ex_ptr fc = filecache.Open("/var/www/index.html", ¶m); +if (fc) { + // 写入 HTTP 响应头 + std::string header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"; + if (fc->prepend_header(header.c_str(), header.size())) { + // 发送 fc->httpbuf (header + content) + } else { + // 头部超出预留空间,回退到普通发送 + } +} +``` + +## 迁移指南 + +从 `FileCache` 迁移到 `FileCacheEx`: + +1. 替换头文件引用:`#include "FileCache.h"` → `#include "FileCacheEx.h"` +2. 替换类型:`FileCache` → `FileCacheEx`,`file_cache_ptr` → `file_cache_ex_ptr` +3. 处理 `prepend_header()` 的 `bool` 返回值 +4. 可选:利用 `SetMaxHeaderLength()` / `SetMaxFileSize()` 配置参数 + +## 线程安全说明 + +- `FileCacheEx` 继承 `hv::LRUCache` 的全局互斥锁保护 LRU 操作 +- 每个 `file_cache_ex_s` 条目内置 `std::mutex` 保护条目级修改 +- `prepend_header()` 自动加锁,调用方无需额外同步 +- `is_modified()` / `is_complete()` / `resize_buf()` 需要调用方持锁 diff --git a/http/server/FileCacheEx.cpp b/http/server/FileCacheEx.cpp index 30728d8dd..875497dfa 100644 --- a/http/server/FileCacheEx.cpp +++ b/http/server/FileCacheEx.cpp @@ -5,11 +5,11 @@ #include "htime.h" #include "hlog.h" -#include "httpdef.h" // http_content_type_str_by_suffix -#include "http_page.h" // make_index_of_page +#include "httpdef.h" // import http_content_type_str_by_suffix +#include "http_page.h" // import make_index_of_page #ifdef OS_WIN -#include "hstring.h" // hv::utf8_to_wchar +#include "hstring.h" // import hv::utf8_to_wchar #endif #define ETAG_FMT "\"%zx-%zx\"" From f5e9a67b206a6a7f765612c161eaf3b56efb0d21 Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Thu, 26 Mar 2026 23:07:29 +0800 Subject: [PATCH 05/13] fix: add bounds check for negative reserved in resize_buf Prevent integer overflow when reserved < 0 is passed to resize_buf(), which would cause (size_t)reserved to become a very large value. --- http/server/FileCacheEx.h | 1 + 1 file changed, 1 insertion(+) diff --git a/http/server/FileCacheEx.h b/http/server/FileCacheEx.h index f6c3af5b7..c4ce17c75 100644 --- a/http/server/FileCacheEx.h +++ b/http/server/FileCacheEx.h @@ -73,6 +73,7 @@ typedef struct file_cache_ex_s { // NOTE: caller must hold mutex — invalidates filebuf/httpbuf pointers void resize_buf(size_t filesize, int reserved) { + if (reserved < 0) reserved = 0; header_reserve = reserved; buf.resize((size_t)reserved + filesize); filebuf.base = buf.base + reserved; From 7c7d63ee61ab2b8ef1abab69c18d1210e53ecccc Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Sun, 29 Mar 2026 16:51:29 +0800 Subject: [PATCH 06/13] refactor: replace FileCache with FileCacheEx in HttpHandler and HttpServer --- http/server/HttpHandler.cpp | 12 ++++++------ http/server/HttpHandler.h | 6 +++--- http/server/HttpServer.cpp | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/http/server/HttpHandler.cpp b/http/server/HttpHandler.cpp index 005967328..c618d1287 100644 --- a/http/server/HttpHandler.cpp +++ b/http/server/HttpHandler.cpp @@ -608,8 +608,8 @@ int HttpHandler::defaultStaticHandler() { return status_code; } - // FileCache - FileCache::OpenParam param; + // FileCacheEx + FileCacheEx::OpenParam param; param.max_read = service->max_file_cache_size; param.need_read = !(req->method == HTTP_HEAD || has_range); param.path = req_path; @@ -689,7 +689,7 @@ int HttpHandler::defaultErrorHandler() { std::string filepath = service->document_root + '/' + service->error_page; if (files) { // cache and load error page - FileCache::OpenParam param; + FileCacheEx::OpenParam param; fc = files->Open(filepath.c_str(), ¶m); } } @@ -798,7 +798,7 @@ int HttpHandler::GetSendData(char** data, size_t* len) { } // File service if (fc) { - // FileCache + // FileCacheEx // NOTE: no copy filebuf, more efficient header = pResp->Dump(true, false); fc->prepend_header(header.c_str(), header.size()); @@ -842,8 +842,8 @@ int HttpHandler::GetSendData(char** data, size_t* len) { } case SEND_DONE: { - // NOTE: remove file cache if > FILE_CACHE_MAX_SIZE - if (fc && fc->filebuf.len > FILE_CACHE_MAX_SIZE) { + // NOTE: remove file cache if > max_file_size + if (fc && fc->filebuf.len > files->GetMaxFileSize()) { files->Close(fc->filepath.c_str()); } fc = NULL; diff --git a/http/server/HttpHandler.h b/http/server/HttpHandler.h index b72f07772..5dc0b5261 100644 --- a/http/server/HttpHandler.h +++ b/http/server/HttpHandler.h @@ -3,7 +3,7 @@ #include "HttpService.h" #include "HttpParser.h" -#include "FileCache.h" +#include "FileCacheEx.h" #include "WebSocketServer.h" #include "WebSocketParser.h" @@ -71,8 +71,8 @@ class HttpHandler { uint64_t last_recv_pong_time; // for sendfile - FileCache *files; - file_cache_ptr fc; // cache small file + FileCacheEx *files; + file_cache_ex_ptr fc; // cache small file struct LargeFile : public HFile { HBuf buf; uint64_t timer; diff --git a/http/server/HttpServer.cpp b/http/server/HttpServer.cpp index aa296fe8b..9d19708e2 100644 --- a/http/server/HttpServer.cpp +++ b/http/server/HttpServer.cpp @@ -19,7 +19,7 @@ struct HttpServerPrivdata { std::vector threads; std::mutex mutex_; std::shared_ptr service; - FileCache filecache; + FileCacheEx filecache; }; static void on_recv(hio_t* io, void* buf, int readbytes) { @@ -86,7 +86,7 @@ static void on_accept(hio_t* io) { handler->service = service; // websocket service handler->ws_service = server->ws; - // FileCache + // FileCacheEx HttpServerPrivdata* privdata = (HttpServerPrivdata*)server->privdata; handler->files = &privdata->filecache; hevent_set_userdata(io, handler); @@ -132,14 +132,14 @@ static void loop_thread(void* userdata) { service->Static("/", service->document_root.c_str()); } - // FileCache - FileCache* filecache = &privdata->filecache; + // FileCacheEx + FileCacheEx* filecache = &privdata->filecache; filecache->stat_interval = service->file_cache_stat_interval; filecache->expired_time = service->file_cache_expired_time; if (filecache->expired_time > 0) { // NOTE: add timer to remove expired file cache htimer_t* timer = htimer_add(hloop, [](htimer_t* timer) { - FileCache* filecache = (FileCache*)hevent_userdata(timer); + FileCacheEx* filecache = (FileCacheEx*)hevent_userdata(timer); filecache->RemoveExpiredFileCache(); }, filecache->expired_time * 1000); hevent_set_userdata(timer, filecache); From a9790f60ec5740d3874c5e925f29d4718ccbcf11 Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Mon, 6 Apr 2026 00:15:18 +0800 Subject: [PATCH 07/13] Refactor FileCache and remove FileCacheEx --- cmake/vars.cmake | 2 +- docs/FileCacheEx.md | 119 ------------------------ docs/cn/FileCacheEx.md | 134 --------------------------- http/server/FileCache.cpp | 82 +++++++++++------ http/server/FileCache.h | 116 ++++++++++++++++++------ http/server/FileCacheEx.cpp | 174 ------------------------------------ http/server/FileCacheEx.h | 157 -------------------------------- http/server/HttpHandler.cpp | 8 +- http/server/HttpHandler.h | 6 +- http/server/HttpServer.cpp | 10 +-- 10 files changed, 156 insertions(+), 652 deletions(-) delete mode 100644 docs/FileCacheEx.md delete mode 100644 docs/cn/FileCacheEx.md delete mode 100644 http/server/FileCacheEx.cpp delete mode 100644 http/server/FileCacheEx.h diff --git a/cmake/vars.cmake b/cmake/vars.cmake index 5276bb297..a425a8c75 100644 --- a/cmake/vars.cmake +++ b/cmake/vars.cmake @@ -103,7 +103,7 @@ set(HTTP_SERVER_HEADERS http/server/HttpContext.h http/server/HttpResponseWriter.h http/server/WebSocketServer.h - http/server/FileCacheEx.h + http/server/FileCache.h ) set(MQTT_HEADERS diff --git a/docs/FileCacheEx.md b/docs/FileCacheEx.md deleted file mode 100644 index 622c56c6d..000000000 --- a/docs/FileCacheEx.md +++ /dev/null @@ -1,119 +0,0 @@ -# FileCacheEx — Enhanced File Cache - -`FileCacheEx` is a drop-in replacement for `FileCache` that fixes thread-safety issues and provides runtime-configurable parameters. - -## Header - -```c++ -#include "FileCacheEx.h" -``` - -## Improvements over FileCache - -| Feature | FileCache | FileCacheEx | -|---------|-----------|-------------| -| HTTP header reserve | Compile-time 1024 bytes | Runtime configurable, default 4096 | -| `prepend_header()` | Returns `void`, silently drops overflow | Returns `bool`, `false` on overflow | -| Per-entry thread safety | No locking | Per-entry `std::mutex` | -| `resize_buf()` httpbuf | May leave dangling reference | Explicitly invalidated (UAF prevention) | -| File read | Single `read()`, may short-read | Loop with `EINTR` handling | -| Cache insertion | Before initialization completes | Deferred until fully initialized | -| Max file size | Compile-time `FILE_CACHE_MAX_SIZE` | Runtime `SetMaxFileSize()` | -| DLL export | None | `HV_EXPORT` | - -## Data Structures - -```c++ -typedef struct file_cache_ex_s { - mutable std::mutex mutex; // per-entry lock - std::string filepath; - struct stat st; - time_t open_time; - time_t stat_time; - uint32_t stat_cnt; - HBuf buf; // header_reserve + file_content - hbuf_t filebuf; // points into buf: file content - hbuf_t httpbuf; // points into buf: header + content - char last_modified[64]; - char etag[64]; - std::string content_type; - int header_reserve; // bytes reserved before content - int header_used; // bytes actually used by header - - bool is_modified(); // caller must hold mutex - bool is_complete(); // caller must hold mutex - void resize_buf(size_t filesize, int reserved); // caller must hold mutex - void resize_buf(size_t filesize); - bool prepend_header(const char* header, int len); // thread-safe - - // Thread-safe accessors - int get_header_reserve() const; - int get_header_used() const; - int get_header_remaining() const; - bool header_fits(int len) const; -} file_cache_ex_t; - -typedef std::shared_ptr file_cache_ex_ptr; -``` - -## FileCacheEx Class - -```c++ -class HV_EXPORT FileCacheEx : public hv::LRUCache { -public: - int stat_interval; // seconds between stat() checks, default 10 - int expired_time; // seconds before expiry, default 60 - int max_header_length; // header reserve per entry, default 4096 - int max_file_size; // max cached file size, default 4MB - - explicit FileCacheEx(size_t capacity = 100); - - file_cache_ex_ptr Open(const char* filepath, OpenParam* param); - bool Exists(const char* filepath) const; - bool Close(const char* filepath); - void RemoveExpiredFileCache(); - - int GetMaxHeaderLength() const; - int GetMaxFileSize() const; - int GetStatInterval() const; - int GetExpiredTime() const; - - void SetMaxHeaderLength(int len); - void SetMaxFileSize(int size); -}; -``` - -## Usage - -```c++ -FileCacheEx filecache(200); // LRU capacity = 200 -filecache.SetMaxHeaderLength(8192); // 8K header reserve -filecache.SetMaxFileSize(1 << 24); // 16MB max file -filecache.stat_interval = 5; -filecache.expired_time = 120; - -FileCacheEx::OpenParam param; -file_cache_ex_ptr fc = filecache.Open("/var/www/index.html", ¶m); -if (fc) { - std::string header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"; - if (fc->prepend_header(header.c_str(), header.size())) { - // send fc->httpbuf (header + content) - } else { - // header exceeds reserve, fall back to separate send - } -} -``` - -## Migration from FileCache - -1. Replace `#include "FileCache.h"` with `#include "FileCacheEx.h"` -2. Change types: `FileCache` → `FileCacheEx`, `file_cache_ptr` → `file_cache_ex_ptr` -3. Handle `bool` return from `prepend_header()` -4. Optional: configure via `SetMaxHeaderLength()` / `SetMaxFileSize()` - -## Thread Safety - -- `FileCacheEx` inherits `hv::LRUCache` global mutex for LRU operations -- Each `file_cache_ex_s` entry has its own `std::mutex` for entry-level protection -- `prepend_header()` locks automatically; callers need no external synchronization -- `is_modified()`, `is_complete()`, `resize_buf()` require caller to hold the mutex diff --git a/docs/cn/FileCacheEx.md b/docs/cn/FileCacheEx.md deleted file mode 100644 index f99cb04cf..000000000 --- a/docs/cn/FileCacheEx.md +++ /dev/null @@ -1,134 +0,0 @@ -# FileCacheEx — 增强版文件缓存 - -`FileCacheEx` 是 `FileCache` 的增强替代方案,解决了原始实现中的线程安全问题,并提供运行时可配置的参数。 - -## 头文件 - -```c++ -#include "FileCacheEx.h" -``` - -## 与 FileCache 的区别 - -| 特性 | FileCache | FileCacheEx | -|------|-----------|-------------| -| HTTP 头部预留空间 | 编译期固定 1024 字节 | 运行时可配置,默认 4096 字节 | -| `prepend_header()` | 返回 `void`,溢出静默丢弃 | 返回 `bool`,失败返回 `false` | -| 缓存条目线程安全 | 无锁保护 | 每条目 `std::mutex` | -| `resize_buf()` 后 httpbuf | 可能悬空引用 | 主动置空,防止 UAF | -| 文件读取 | 单次 `read()`,可能短读 | 循环读取,处理 `EINTR` | -| `put()` 时机 | 初始化前放入缓存 | 完全初始化后放入缓存 | -| 最大文件大小 | 编译期宏 `FILE_CACHE_MAX_SIZE` | 运行时 `SetMaxFileSize()` | -| DLL 导出 | 无 | `HV_EXPORT` | - -## 数据结构 - -```c++ -// 缓存条目 -typedef struct file_cache_ex_s { - mutable std::mutex mutex; // 条目级互斥锁 - std::string filepath; - struct stat st; - time_t open_time; - time_t stat_time; - uint32_t stat_cnt; - HBuf buf; // header_reserve + file_content - hbuf_t filebuf; // 指向 buf 中文件内容区域 - hbuf_t httpbuf; // 指向 buf 中 header + 文件内容区域 - char last_modified[64]; - char etag[64]; - std::string content_type; - int header_reserve; // 头部预留字节数 - int header_used; // 实际使用的头部字节数 - - // 方法 - bool is_modified(); // 检查文件是否被修改(需持锁) - bool is_complete(); // 检查缓存是否完整(需持锁) - void resize_buf(size_t filesize, int reserved); // 重新分配缓冲区(需持锁) - void resize_buf(size_t filesize); // 使用当前 header_reserve - bool prepend_header(const char* header, int len); // 线程安全:写入 HTTP 头 - - // 线程安全访问器 - int get_header_reserve() const; - int get_header_used() const; - int get_header_remaining() const; - bool header_fits(int len) const; -} file_cache_ex_t; - -typedef std::shared_ptr file_cache_ex_ptr; -``` - -## FileCacheEx 类 - -```c++ -class HV_EXPORT FileCacheEx : public hv::LRUCache { -public: - // 可配置参数 - int stat_interval; // stat() 检查间隔(秒),默认 10 - int expired_time; // 缓存过期时间(秒),默认 60 - int max_header_length; // 每条目头部预留字节,默认 4096 - int max_file_size; // 最大缓存文件大小,默认 4MB - - explicit FileCacheEx(size_t capacity = 100); - - // 打开文件并缓存 - file_cache_ex_ptr Open(const char* filepath, OpenParam* param); - - // 检查文件是否在缓存中 - bool Exists(const char* filepath) const; - - // 从缓存中移除文件 - bool Close(const char* filepath); - - // 移除过期缓存条目 - void RemoveExpiredFileCache(); - - // Getter - int GetMaxHeaderLength() const; - int GetMaxFileSize() const; - int GetStatInterval() const; - int GetExpiredTime() const; - - // Setter - void SetMaxHeaderLength(int len); - void SetMaxFileSize(int size); -}; -``` - -## 使用示例 - -```c++ -FileCacheEx filecache(200); // 最大 200 个缓存条目 -filecache.SetMaxHeaderLength(8192); // 8K 头部预留 -filecache.SetMaxFileSize(1 << 24); // 16MB 最大缓存文件 -filecache.stat_interval = 5; // 每 5 秒检查文件变更 -filecache.expired_time = 120; // 2 分钟过期 - -FileCacheEx::OpenParam param; -file_cache_ex_ptr fc = filecache.Open("/var/www/index.html", ¶m); -if (fc) { - // 写入 HTTP 响应头 - std::string header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"; - if (fc->prepend_header(header.c_str(), header.size())) { - // 发送 fc->httpbuf (header + content) - } else { - // 头部超出预留空间,回退到普通发送 - } -} -``` - -## 迁移指南 - -从 `FileCache` 迁移到 `FileCacheEx`: - -1. 替换头文件引用:`#include "FileCache.h"` → `#include "FileCacheEx.h"` -2. 替换类型:`FileCache` → `FileCacheEx`,`file_cache_ptr` → `file_cache_ex_ptr` -3. 处理 `prepend_header()` 的 `bool` 返回值 -4. 可选:利用 `SetMaxHeaderLength()` / `SetMaxFileSize()` 配置参数 - -## 线程安全说明 - -- `FileCacheEx` 继承 `hv::LRUCache` 的全局互斥锁保护 LRU 操作 -- 每个 `file_cache_ex_s` 条目内置 `std::mutex` 保护条目级修改 -- `prepend_header()` 自动加锁,调用方无需额外同步 -- `is_modified()` / `is_complete()` / `resize_buf()` 需要调用方持锁 diff --git a/http/server/FileCache.cpp b/http/server/FileCache.cpp index 26b76e150..e8bccd69b 100644 --- a/http/server/FileCache.cpp +++ b/http/server/FileCache.cpp @@ -5,18 +5,21 @@ #include "htime.h" #include "hlog.h" -#include "httpdef.h" // import http_content_type_str_by_suffix +#include "httpdef.h" // import http_content_type_str_by_suffix #include "http_page.h" // import make_index_of_page #ifdef OS_WIN -#include "hstring.h" // import hv::utf8_to_wchar +#include "hstring.h" // import hv::utf8_to_wchar #endif #define ETAG_FMT "\"%zx-%zx\"" -FileCache::FileCache(size_t capacity) : hv::LRUCache(capacity) { - stat_interval = 10; // s - expired_time = 60; // s +FileCache::FileCache(size_t capacity) + : hv::LRUCache(capacity) { + stat_interval = 10; // s + expired_time = 60; // s + max_header_length = FILE_CACHE_DEFAULT_HEADER_LENGTH; + max_file_size = FILE_CACHE_DEFAULT_MAX_FILE_SIZE; } file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { @@ -26,6 +29,7 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { #endif bool modified = false; if (fc) { + std::lock_guard lock(fc->mutex); time_t now = time(NULL); if (now - fc->stat_time > stat_interval) { fc->stat_time = now; @@ -53,19 +57,18 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { #endif int fd = -1; #ifdef OS_WIN - if(wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath); - if(_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) { + if (wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath); + if (_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) { param->error = ERR_OPEN_FILE; return NULL; } - if(S_ISREG(st.st_mode)) { + if (S_ISREG(st.st_mode)) { fd = _wopen(wfilepath.c_str(), flags); - }else if (S_ISDIR(st.st_mode)) { - // NOTE: open(dir) return -1 on windows + } else if (S_ISDIR(st.st_mode)) { fd = 0; } #else - if(stat(filepath, &st) != 0) { + if (::stat(filepath, &st) != 0) { param->error = ERR_OPEN_FILE; return NULL; } @@ -75,62 +78,84 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { param->error = ERR_OPEN_FILE; return NULL; } - defer(if (fd > 0) { close(fd); }) +#ifdef OS_WIN + defer(if (fd > 0) { close(fd); }) // fd=0 is Windows directory sentinel +#else + defer(close(fd);) +#endif if (fc == NULL) { if (S_ISREG(st.st_mode) || (S_ISDIR(st.st_mode) && - filepath[strlen(filepath)-1] == '/')) { + filepath[strlen(filepath) - 1] == '/')) { fc = std::make_shared(); fc->filepath = filepath; fc->st = st; + fc->header_reserve = max_header_length; time(&fc->open_time); fc->stat_time = fc->open_time; fc->stat_cnt = 1; - put(filepath, fc); - } - else { + // NOTE: do NOT put() into cache yet — defer until fully initialized + } else { param->error = ERR_MISMATCH; return NULL; } } + // Hold fc->mutex for the remainder of initialization + std::lock_guard lock(fc->mutex); if (S_ISREG(fc->st.st_mode)) { param->filesize = fc->st.st_size; // FILE if (param->need_read) { if (fc->st.st_size > param->max_read) { param->error = ERR_OVER_LIMIT; + // Don't cache incomplete entries return NULL; } - fc->resize_buf(fc->st.st_size); - int nread = read(fd, fc->filebuf.base, fc->filebuf.len); - if (nread != fc->filebuf.len) { - hloge("Failed to read file: %s", filepath); - param->error = ERR_READ_FILE; - return NULL; + fc->resize_buf(fc->st.st_size, max_header_length); + // Loop to handle partial reads (EINTR, etc.) + char* dst = fc->filebuf.base; + size_t remaining = fc->filebuf.len; + while (remaining > 0) { + ssize_t nread = read(fd, dst, remaining); + if (nread < 0) { + if (errno == EINTR) continue; + hloge("Failed to read file: %s", filepath); + param->error = ERR_READ_FILE; + return NULL; + } + if (nread == 0) { + hloge("Unexpected EOF reading file: %s", filepath); + param->error = ERR_READ_FILE; + return NULL; + } + dst += nread; + remaining -= nread; } } const char* suffix = strrchr(filepath, '.'); if (suffix) { - http_content_type content_type = http_content_type_enum_by_suffix(suffix+1); + http_content_type content_type = http_content_type_enum_by_suffix(suffix + 1); if (content_type == TEXT_HTML) { fc->content_type = "text/html; charset=utf-8"; } else if (content_type == TEXT_PLAIN) { fc->content_type = "text/plain; charset=utf-8"; } else { - fc->content_type = http_content_type_str_by_suffix(suffix+1); + fc->content_type = http_content_type_str_by_suffix(suffix + 1); } } - } - else if (S_ISDIR(fc->st.st_mode)) { + } else if (S_ISDIR(fc->st.st_mode)) { // DIR std::string page; make_index_of_page(filepath, page, param->path); - fc->resize_buf(page.size()); + fc->resize_buf(page.size(), max_header_length); memcpy(fc->filebuf.base, page.c_str(), page.size()); fc->content_type = "text/html; charset=utf-8"; } gmtime_fmt(fc->st.st_mtime, fc->last_modified); - snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, (size_t)fc->st.st_mtime, (size_t)fc->st.st_size); + snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, + (size_t)fc->st.st_mtime, (size_t)fc->st.st_size); + // Cache the fully initialized entry + put(filepath, fc); } return fc; } @@ -154,6 +179,7 @@ file_cache_ptr FileCache::Get(const char* filepath) { void FileCache::RemoveExpiredFileCache() { time_t now = time(NULL); remove_if([this, now](const std::string& filepath, const file_cache_ptr& fc) { + std::lock_guard lock(fc->mutex); return (now - fc->stat_time > expired_time); }); } diff --git a/http/server/FileCache.h b/http/server/FileCache.h index 363c41d88..c43ce1943 100644 --- a/http/server/FileCache.h +++ b/http/server/FileCache.h @@ -1,90 +1,152 @@ #ifndef HV_FILE_CACHE_H_ #define HV_FILE_CACHE_H_ +/* + * FileCache — Enhanced File Cache for libhv HTTP server + * + * Features: + * 1. Configurable max_header_length (default 4096, tunable per-instance) + * 2. prepend_header() returns bool to report success/failure + * 3. Exposes header/buffer metrics via accessors + * 4. Fixes stat() name collision in is_modified() + * 5. max_cache_num / max_file_size configurable at runtime + * 6. Reserved header space can be tuned per-instance + * 7. Source-level API compatible; struct layout differs from original (no ABI/layout compatibility) + */ + #include -#include #include #include +#include "hexport.h" #include "hbuf.h" #include "hstring.h" #include "LRUCache.h" -#define HTTP_HEADER_MAX_LENGTH 1024 // 1K -#define FILE_CACHE_MAX_NUM 100 -#define FILE_CACHE_MAX_SIZE (1 << 22) // 4M +// Default values — may be overridden at runtime via FileCache setters +#define FILE_CACHE_DEFAULT_HEADER_LENGTH 4096 // 4K +#define FILE_CACHE_DEFAULT_MAX_NUM 100 +#define FILE_CACHE_DEFAULT_MAX_FILE_SIZE (1 << 22) // 4M typedef struct file_cache_s { + mutable std::mutex mutex; // protects all mutable state below std::string filepath; struct stat st; time_t open_time; time_t stat_time; uint32_t stat_cnt; - HBuf buf; // http_header + file_content - hbuf_t filebuf; - hbuf_t httpbuf; + HBuf buf; // header_reserve + file_content + hbuf_t filebuf; // points into buf: file content region + hbuf_t httpbuf; // points into buf: header + file content after prepend char last_modified[64]; char etag[64]; std::string content_type; + // --- new: expose header metrics --- + int header_reserve; // reserved bytes before file content + int header_used; // actual bytes used by prepend_header + file_cache_s() { stat_cnt = 0; + header_reserve = FILE_CACHE_DEFAULT_HEADER_LENGTH; + header_used = 0; + memset(last_modified, 0, sizeof(last_modified)); + memset(etag, 0, sizeof(etag)); } + // NOTE: caller must hold mutex. + // On Windows, Open() uses _wstat() directly instead of calling this. bool is_modified() { time_t mtime = st.st_mtime; - stat(filepath.c_str(), &st); + ::stat(filepath.c_str(), &st); return mtime != st.st_mtime; } + // NOTE: caller must hold mutex bool is_complete() { - if(S_ISDIR(st.st_mode)) return filebuf.len > 0; - return filebuf.len == st.st_size; + if (S_ISDIR(st.st_mode)) return filebuf.len > 0; + return filebuf.len == (size_t)st.st_size; } - void resize_buf(int filesize) { - buf.resize(HTTP_HEADER_MAX_LENGTH + filesize); - filebuf.base = buf.base + HTTP_HEADER_MAX_LENGTH; + // NOTE: caller must hold mutex — invalidates filebuf/httpbuf pointers + void resize_buf(size_t filesize, int reserved) { + if (reserved < 0) reserved = 0; + header_reserve = reserved; + buf.resize((size_t)reserved + filesize); + filebuf.base = buf.base + reserved; filebuf.len = filesize; + // Invalidate httpbuf since buffer may have been reallocated + httpbuf.base = NULL; + httpbuf.len = 0; + header_used = 0; + } + + void resize_buf(size_t filesize) { + resize_buf(filesize, header_reserve); } - void prepend_header(const char* header, int len) { - if (len > HTTP_HEADER_MAX_LENGTH) return; + // Thread-safe: prepend header into reserved space. + // Returns true on success, false if header exceeds reserved space. + bool prepend_header(const char* header, int len) { + std::lock_guard lock(mutex); + if (len <= 0 || len > header_reserve) return false; httpbuf.base = filebuf.base - len; - httpbuf.len = len + filebuf.len; + httpbuf.len = (size_t)len + filebuf.len; memcpy(httpbuf.base, header, len); + header_used = len; + return true; } + + // --- thread-safe accessors --- + int get_header_reserve() const { std::lock_guard lock(mutex); return header_reserve; } + int get_header_used() const { std::lock_guard lock(mutex); return header_used; } + int get_header_remaining() const { std::lock_guard lock(mutex); return header_reserve - header_used; } + bool header_fits(int len) const { std::lock_guard lock(mutex); return len > 0 && len <= header_reserve; } } file_cache_t; -typedef std::shared_ptr file_cache_ptr; +typedef std::shared_ptr file_cache_ptr; -class FileCache : public hv::LRUCache { +class HV_EXPORT FileCache : public hv::LRUCache { public: - int stat_interval; - int expired_time; + // --- configurable parameters (were hardcoded macros before) --- + int stat_interval; // seconds between stat() checks + int expired_time; // seconds before cache entry expires + int max_header_length; // reserved header bytes per entry + int max_file_size; // max cached file size (larger = large-file path) - FileCache(size_t capacity = FILE_CACHE_MAX_NUM); + explicit FileCache(size_t capacity = FILE_CACHE_DEFAULT_MAX_NUM); struct OpenParam { - bool need_read; - int max_read; - const char* path; - size_t filesize; - int error; + bool need_read; + int max_read; // per-request override for max file size + const char* path; // URL path (for directory listing) + size_t filesize; // [out] actual file size + int error; // [out] error code if Open returns NULL OpenParam() { need_read = true; - max_read = FILE_CACHE_MAX_SIZE; + max_read = FILE_CACHE_DEFAULT_MAX_FILE_SIZE; path = "/"; filesize = 0; error = 0; } }; + file_cache_ptr Open(const char* filepath, OpenParam* param); bool Exists(const char* filepath) const; bool Close(const char* filepath); void RemoveExpiredFileCache(); + // --- new: getters --- + int GetMaxHeaderLength() const { return max_header_length; } + int GetMaxFileSize() const { return max_file_size; } + int GetStatInterval() const { return stat_interval; } + int GetExpiredTime() const { return expired_time; } + + // --- new: setters --- + void SetMaxHeaderLength(int len) { max_header_length = len; } + void SetMaxFileSize(int size) { max_file_size = size; } + protected: file_cache_ptr Get(const char* filepath); }; diff --git a/http/server/FileCacheEx.cpp b/http/server/FileCacheEx.cpp deleted file mode 100644 index 875497dfa..000000000 --- a/http/server/FileCacheEx.cpp +++ /dev/null @@ -1,174 +0,0 @@ -#include "FileCacheEx.h" - -#include "herr.h" -#include "hscope.h" -#include "htime.h" -#include "hlog.h" - -#include "httpdef.h" // import http_content_type_str_by_suffix -#include "http_page.h" // import make_index_of_page - -#ifdef OS_WIN -#include "hstring.h" // import hv::utf8_to_wchar -#endif - -#define ETAG_FMT "\"%zx-%zx\"" - -FileCacheEx::FileCacheEx(size_t capacity) - : hv::LRUCache(capacity) { - stat_interval = 10; // s - expired_time = 60; // s - max_header_length = FILECACHE_EX_DEFAULT_HEADER_LENGTH; - max_file_size = FILECACHE_EX_DEFAULT_MAX_FILE_SIZE; -} - -file_cache_ex_ptr FileCacheEx::Open(const char* filepath, OpenParam* param) { - file_cache_ex_ptr fc = Get(filepath); -#ifdef OS_WIN - std::wstring wfilepath; -#endif - bool modified = false; - if (fc) { - std::lock_guard lock(fc->mutex); - time_t now = time(NULL); - if (now - fc->stat_time > stat_interval) { - fc->stat_time = now; - fc->stat_cnt++; -#ifdef OS_WIN - wfilepath = hv::utf8_to_wchar(filepath); - now = fc->st.st_mtime; - _wstat(wfilepath.c_str(), (struct _stat*)&fc->st); - modified = now != fc->st.st_mtime; -#else - modified = fc->is_modified(); -#endif - } - if (param->need_read) { - if (!modified && fc->is_complete()) { - param->need_read = false; - } - } - } - if (fc == NULL || modified || param->need_read) { - struct stat st; - int flags = O_RDONLY; -#ifdef O_BINARY - flags |= O_BINARY; -#endif - int fd = -1; -#ifdef OS_WIN - if (wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath); - if (_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) { - param->error = ERR_OPEN_FILE; - return NULL; - } - if (S_ISREG(st.st_mode)) { - fd = _wopen(wfilepath.c_str(), flags); - } else if (S_ISDIR(st.st_mode)) { - fd = 0; - } -#else - if (::stat(filepath, &st) != 0) { - param->error = ERR_OPEN_FILE; - return NULL; - } - fd = open(filepath, flags); -#endif - if (fd < 0) { - param->error = ERR_OPEN_FILE; - return NULL; - } - defer(if (fd > 0) { close(fd); }) - if (fc == NULL) { - if (S_ISREG(st.st_mode) || - (S_ISDIR(st.st_mode) && - filepath[strlen(filepath) - 1] == '/')) { - fc = std::make_shared(); - fc->filepath = filepath; - fc->st = st; - fc->header_reserve = max_header_length; - time(&fc->open_time); - fc->stat_time = fc->open_time; - fc->stat_cnt = 1; - // NOTE: do NOT put() into cache yet — defer until fully initialized - } else { - param->error = ERR_MISMATCH; - return NULL; - } - } - // Hold fc->mutex for the remainder of initialization - std::lock_guard lock(fc->mutex); - if (S_ISREG(fc->st.st_mode)) { - param->filesize = fc->st.st_size; - // FILE - if (param->need_read) { - if (fc->st.st_size > param->max_read) { - param->error = ERR_OVER_LIMIT; - // Don't cache incomplete entries - return NULL; - } - fc->resize_buf(fc->st.st_size, max_header_length); - // Loop to handle partial reads (EINTR, etc.) - char* dst = fc->filebuf.base; - size_t remaining = fc->filebuf.len; - while (remaining > 0) { - int nread = read(fd, dst, remaining); - if (nread <= 0) { - hloge("Failed to read file: %s", filepath); - param->error = ERR_READ_FILE; - return NULL; - } - dst += nread; - remaining -= nread; - } - } - const char* suffix = strrchr(filepath, '.'); - if (suffix) { - http_content_type content_type = http_content_type_enum_by_suffix(suffix + 1); - if (content_type == TEXT_HTML) { - fc->content_type = "text/html; charset=utf-8"; - } else if (content_type == TEXT_PLAIN) { - fc->content_type = "text/plain; charset=utf-8"; - } else { - fc->content_type = http_content_type_str_by_suffix(suffix + 1); - } - } - } else if (S_ISDIR(fc->st.st_mode)) { - // DIR - std::string page; - make_index_of_page(filepath, page, param->path); - fc->resize_buf(page.size(), max_header_length); - memcpy(fc->filebuf.base, page.c_str(), page.size()); - fc->content_type = "text/html; charset=utf-8"; - } - gmtime_fmt(fc->st.st_mtime, fc->last_modified); - snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, - (size_t)fc->st.st_mtime, (size_t)fc->st.st_size); - // Cache the fully initialized entry - put(filepath, fc); - } - return fc; -} - -bool FileCacheEx::Exists(const char* filepath) const { - return contains(filepath); -} - -bool FileCacheEx::Close(const char* filepath) { - return remove(filepath); -} - -file_cache_ex_ptr FileCacheEx::Get(const char* filepath) { - file_cache_ex_ptr fc; - if (get(filepath, fc)) { - return fc; - } - return NULL; -} - -void FileCacheEx::RemoveExpiredFileCache() { - time_t now = time(NULL); - remove_if([this, now](const std::string& filepath, const file_cache_ex_ptr& fc) { - return (now - fc->stat_time > expired_time); - }); -} diff --git a/http/server/FileCacheEx.h b/http/server/FileCacheEx.h deleted file mode 100644 index c4ce17c75..000000000 --- a/http/server/FileCacheEx.h +++ /dev/null @@ -1,157 +0,0 @@ -#ifndef HV_FILE_CACHE_EX_H_ -#define HV_FILE_CACHE_EX_H_ - -/* - * FileCacheEx — Enhanced File Cache for libhv HTTP server - * - * Improvements over the original FileCache: - * 1. Configurable max_header_length (no more hardcoded 4096) - * 2. prepend_header() returns bool to report success/failure - * 3. Exposes header/buffer metrics via accessors - * 4. Fixes stat() name collision in is_modified() - * 5. max_cache_num / max_file_size configurable at runtime - * 6. Reserved header space can be tuned per-instance - * 7. Fully backward-compatible struct layout - * - * This is a NEW module alongside FileCache — no modifications to the - * original code — to keep things non-invasive for upstream PR review. - */ - -#include -#include -#include - -#include "hexport.h" -#include "hbuf.h" -#include "hstring.h" -#include "LRUCache.h" - -// Default values — may be overridden at runtime via FileCacheEx setters -#define FILECACHE_EX_DEFAULT_HEADER_LENGTH 4096 // 4K -#define FILECACHE_EX_DEFAULT_MAX_NUM 100 -#define FILECACHE_EX_DEFAULT_MAX_FILE_SIZE (1 << 22) // 4M - -typedef struct file_cache_ex_s { - mutable std::mutex mutex; // protects all mutable state below - std::string filepath; - struct stat st; - time_t open_time; - time_t stat_time; - uint32_t stat_cnt; - HBuf buf; // header_reserve + file_content - hbuf_t filebuf; // points into buf: file content region - hbuf_t httpbuf; // points into buf: header + file content after prepend - char last_modified[64]; - char etag[64]; - std::string content_type; - - // --- new: expose header metrics --- - int header_reserve; // reserved bytes before file content - int header_used; // actual bytes used by prepend_header - - file_cache_ex_s() { - stat_cnt = 0; - header_reserve = FILECACHE_EX_DEFAULT_HEADER_LENGTH; - header_used = 0; - memset(last_modified, 0, sizeof(last_modified)); - memset(etag, 0, sizeof(etag)); - } - - // NOTE: caller must hold mutex. - // On Windows, Open() uses _wstat() directly instead of calling this. - bool is_modified() { - time_t mtime = st.st_mtime; - ::stat(filepath.c_str(), &st); - return mtime != st.st_mtime; - } - - // NOTE: caller must hold mutex - bool is_complete() { - if (S_ISDIR(st.st_mode)) return filebuf.len > 0; - return filebuf.len == (size_t)st.st_size; - } - - // NOTE: caller must hold mutex — invalidates filebuf/httpbuf pointers - void resize_buf(size_t filesize, int reserved) { - if (reserved < 0) reserved = 0; - header_reserve = reserved; - buf.resize((size_t)reserved + filesize); - filebuf.base = buf.base + reserved; - filebuf.len = filesize; - // Invalidate httpbuf since buffer may have been reallocated - httpbuf.base = NULL; - httpbuf.len = 0; - header_used = 0; - } - - void resize_buf(size_t filesize) { - resize_buf(filesize, header_reserve); - } - - // Thread-safe: prepend header into reserved space. - // Returns true on success, false if header exceeds reserved space. - bool prepend_header(const char* header, int len) { - std::lock_guard lock(mutex); - if (len <= 0 || len > header_reserve) return false; - httpbuf.base = filebuf.base - len; - httpbuf.len = (size_t)len + filebuf.len; - memcpy(httpbuf.base, header, len); - header_used = len; - return true; - } - - // --- thread-safe accessors --- - int get_header_reserve() const { return header_reserve; } - int get_header_used() const { std::lock_guard lock(mutex); return header_used; } - int get_header_remaining() const { std::lock_guard lock(mutex); return header_reserve - header_used; } - bool header_fits(int len) const { return len > 0 && len <= header_reserve; } -} file_cache_ex_t; - -typedef std::shared_ptr file_cache_ex_ptr; - -class HV_EXPORT FileCacheEx : public hv::LRUCache { -public: - // --- configurable parameters (were hardcoded macros before) --- - int stat_interval; // seconds between stat() checks - int expired_time; // seconds before cache entry expires - int max_header_length; // reserved header bytes per entry - int max_file_size; // max cached file size (larger = large-file path) - - explicit FileCacheEx(size_t capacity = FILECACHE_EX_DEFAULT_MAX_NUM); - - struct OpenParam { - bool need_read; - int max_read; // per-request override for max file size - const char* path; // URL path (for directory listing) - size_t filesize; // [out] actual file size - int error; // [out] error code if Open returns NULL - - OpenParam() { - need_read = true; - max_read = FILECACHE_EX_DEFAULT_MAX_FILE_SIZE; - path = "/"; - filesize = 0; - error = 0; - } - }; - - file_cache_ex_ptr Open(const char* filepath, OpenParam* param); - bool Exists(const char* filepath) const; - bool Close(const char* filepath); - void RemoveExpiredFileCache(); - - // --- new: getters --- - int GetMaxHeaderLength() const { return max_header_length; } - int GetMaxFileSize() const { return max_file_size; } - int GetStatInterval() const { return stat_interval; } - int GetExpiredTime() const { return expired_time; } - - // --- new: setters --- - void SetMaxHeaderLength(int len) { max_header_length = len; } - void SetMaxFileSize(int size) { max_file_size = size; } - -protected: - file_cache_ex_ptr Get(const char* filepath); -}; - -#endif // HV_FILE_CACHE_EX_H_ diff --git a/http/server/HttpHandler.cpp b/http/server/HttpHandler.cpp index c618d1287..e08ad90dc 100644 --- a/http/server/HttpHandler.cpp +++ b/http/server/HttpHandler.cpp @@ -608,8 +608,8 @@ int HttpHandler::defaultStaticHandler() { return status_code; } - // FileCacheEx - FileCacheEx::OpenParam param; + // FileCache + FileCache::OpenParam param; param.max_read = service->max_file_cache_size; param.need_read = !(req->method == HTTP_HEAD || has_range); param.path = req_path; @@ -689,7 +689,7 @@ int HttpHandler::defaultErrorHandler() { std::string filepath = service->document_root + '/' + service->error_page; if (files) { // cache and load error page - FileCacheEx::OpenParam param; + FileCache::OpenParam param; fc = files->Open(filepath.c_str(), ¶m); } } @@ -798,7 +798,7 @@ int HttpHandler::GetSendData(char** data, size_t* len) { } // File service if (fc) { - // FileCacheEx + // FileCache // NOTE: no copy filebuf, more efficient header = pResp->Dump(true, false); fc->prepend_header(header.c_str(), header.size()); diff --git a/http/server/HttpHandler.h b/http/server/HttpHandler.h index 5dc0b5261..b72f07772 100644 --- a/http/server/HttpHandler.h +++ b/http/server/HttpHandler.h @@ -3,7 +3,7 @@ #include "HttpService.h" #include "HttpParser.h" -#include "FileCacheEx.h" +#include "FileCache.h" #include "WebSocketServer.h" #include "WebSocketParser.h" @@ -71,8 +71,8 @@ class HttpHandler { uint64_t last_recv_pong_time; // for sendfile - FileCacheEx *files; - file_cache_ex_ptr fc; // cache small file + FileCache *files; + file_cache_ptr fc; // cache small file struct LargeFile : public HFile { HBuf buf; uint64_t timer; diff --git a/http/server/HttpServer.cpp b/http/server/HttpServer.cpp index 9d19708e2..aa296fe8b 100644 --- a/http/server/HttpServer.cpp +++ b/http/server/HttpServer.cpp @@ -19,7 +19,7 @@ struct HttpServerPrivdata { std::vector threads; std::mutex mutex_; std::shared_ptr service; - FileCacheEx filecache; + FileCache filecache; }; static void on_recv(hio_t* io, void* buf, int readbytes) { @@ -86,7 +86,7 @@ static void on_accept(hio_t* io) { handler->service = service; // websocket service handler->ws_service = server->ws; - // FileCacheEx + // FileCache HttpServerPrivdata* privdata = (HttpServerPrivdata*)server->privdata; handler->files = &privdata->filecache; hevent_set_userdata(io, handler); @@ -132,14 +132,14 @@ static void loop_thread(void* userdata) { service->Static("/", service->document_root.c_str()); } - // FileCacheEx - FileCacheEx* filecache = &privdata->filecache; + // FileCache + FileCache* filecache = &privdata->filecache; filecache->stat_interval = service->file_cache_stat_interval; filecache->expired_time = service->file_cache_expired_time; if (filecache->expired_time > 0) { // NOTE: add timer to remove expired file cache htimer_t* timer = htimer_add(hloop, [](htimer_t* timer) { - FileCacheEx* filecache = (FileCacheEx*)hevent_userdata(timer); + FileCache* filecache = (FileCache*)hevent_userdata(timer); filecache->RemoveExpiredFileCache(); }, filecache->expired_time * 1000); hevent_set_userdata(timer, filecache); From 16c24e7dd17faad3965feb1294252b264ffbfc80 Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Mon, 6 Apr 2026 00:40:24 +0800 Subject: [PATCH 08/13] refactor: enhance FileCache with safe fallback in prepend_header and add max_file_size setter --- http/server/FileCache.cpp | 126 ++++++++++++++++++++----------------- http/server/FileCache.h | 11 +++- http/server/HttpServer.cpp | 1 + 3 files changed, 76 insertions(+), 62 deletions(-) diff --git a/http/server/FileCache.cpp b/http/server/FileCache.cpp index e8bccd69b..e0c9e84a7 100644 --- a/http/server/FileCache.cpp +++ b/http/server/FileCache.cpp @@ -5,11 +5,11 @@ #include "htime.h" #include "hlog.h" -#include "httpdef.h" // import http_content_type_str_by_suffix +#include "httpdef.h" // import http_content_type_str_by_suffix #include "http_page.h" // import make_index_of_page #ifdef OS_WIN -#include "hstring.h" // import hv::utf8_to_wchar +#include "hstring.h" // import hv::utf8_to_wchar #endif #define ETAG_FMT "\"%zx-%zx\"" @@ -56,6 +56,7 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { flags |= O_BINARY; #endif int fd = -1; + bool is_dir = false; #ifdef OS_WIN if (wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath); if (_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) { @@ -65,7 +66,7 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { if (S_ISREG(st.st_mode)) { fd = _wopen(wfilepath.c_str(), flags); } else if (S_ISDIR(st.st_mode)) { - fd = 0; + is_dir = true; } #else if (::stat(filepath, &st) != 0) { @@ -74,15 +75,11 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { } fd = open(filepath, flags); #endif - if (fd < 0) { + if (fd < 0 && !is_dir) { param->error = ERR_OPEN_FILE; return NULL; } -#ifdef OS_WIN - defer(if (fd > 0) { close(fd); }) // fd=0 is Windows directory sentinel -#else - defer(close(fd);) -#endif + defer(if (fd >= 0) { close(fd); }) if (fc == NULL) { if (S_ISREG(st.st_mode) || (S_ISDIR(st.st_mode) && @@ -100,61 +97,67 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { return NULL; } } - // Hold fc->mutex for the remainder of initialization - std::lock_guard lock(fc->mutex); - if (S_ISREG(fc->st.st_mode)) { - param->filesize = fc->st.st_size; - // FILE - if (param->need_read) { - if (fc->st.st_size > param->max_read) { - param->error = ERR_OVER_LIMIT; - // Don't cache incomplete entries - return NULL; - } - fc->resize_buf(fc->st.st_size, max_header_length); - // Loop to handle partial reads (EINTR, etc.) - char* dst = fc->filebuf.base; - size_t remaining = fc->filebuf.len; - while (remaining > 0) { - ssize_t nread = read(fd, dst, remaining); - if (nread < 0) { - if (errno == EINTR) continue; - hloge("Failed to read file: %s", filepath); - param->error = ERR_READ_FILE; + // Hold fc->mutex for initialization, but release before put() + // to avoid lock-order inversion with RemoveExpiredFileCache(). + // Lock order: LRUCache mutex → fc->mutex (never reverse). + { + std::lock_guard lock(fc->mutex); + // Sync local stat result into cached entry + fc->st = st; + if (S_ISREG(fc->st.st_mode)) { + param->filesize = fc->st.st_size; + // FILE + if (param->need_read) { + if (fc->st.st_size > param->max_read) { + param->error = ERR_OVER_LIMIT; + // Don't cache incomplete entries return NULL; } - if (nread == 0) { - hloge("Unexpected EOF reading file: %s", filepath); - param->error = ERR_READ_FILE; - return NULL; + fc->resize_buf(fc->st.st_size, max_header_length); + // Loop to handle partial reads (EINTR, etc.) + char* dst = fc->filebuf.base; + size_t remaining = fc->filebuf.len; + while (remaining > 0) { + ssize_t nread = read(fd, dst, remaining); + if (nread < 0) { + if (errno == EINTR) continue; + hloge("Failed to read file: %s", filepath); + param->error = ERR_READ_FILE; + return NULL; + } + if (nread == 0) { + hloge("Unexpected EOF reading file: %s", filepath); + param->error = ERR_READ_FILE; + return NULL; + } + dst += nread; + remaining -= nread; } - dst += nread; - remaining -= nread; } - } - const char* suffix = strrchr(filepath, '.'); - if (suffix) { - http_content_type content_type = http_content_type_enum_by_suffix(suffix + 1); - if (content_type == TEXT_HTML) { - fc->content_type = "text/html; charset=utf-8"; - } else if (content_type == TEXT_PLAIN) { - fc->content_type = "text/plain; charset=utf-8"; - } else { - fc->content_type = http_content_type_str_by_suffix(suffix + 1); + const char* suffix = strrchr(filepath, '.'); + if (suffix) { + http_content_type content_type = http_content_type_enum_by_suffix(suffix + 1); + if (content_type == TEXT_HTML) { + fc->content_type = "text/html; charset=utf-8"; + } else if (content_type == TEXT_PLAIN) { + fc->content_type = "text/plain; charset=utf-8"; + } else { + fc->content_type = http_content_type_str_by_suffix(suffix + 1); + } } + } else if (S_ISDIR(fc->st.st_mode)) { + // DIR + std::string page; + make_index_of_page(filepath, page, param->path); + fc->resize_buf(page.size(), max_header_length); + memcpy(fc->filebuf.base, page.c_str(), page.size()); + fc->content_type = "text/html; charset=utf-8"; } - } else if (S_ISDIR(fc->st.st_mode)) { - // DIR - std::string page; - make_index_of_page(filepath, page, param->path); - fc->resize_buf(page.size(), max_header_length); - memcpy(fc->filebuf.base, page.c_str(), page.size()); - fc->content_type = "text/html; charset=utf-8"; - } - gmtime_fmt(fc->st.st_mtime, fc->last_modified); - snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, - (size_t)fc->st.st_mtime, (size_t)fc->st.st_size); - // Cache the fully initialized entry + gmtime_fmt(fc->st.st_mtime, fc->last_modified); + snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, + (size_t)fc->st.st_mtime, (size_t)fc->st.st_size); + } // release fc->mutex before put() to maintain lock ordering + // Cache the fully initialized entry (acquires LRUCache mutex only) put(filepath, fc); } return fc; @@ -179,7 +182,12 @@ file_cache_ptr FileCache::Get(const char* filepath) { void FileCache::RemoveExpiredFileCache() { time_t now = time(NULL); remove_if([this, now](const std::string& filepath, const file_cache_ptr& fc) { - std::lock_guard lock(fc->mutex); + // Use try_to_lock to avoid lock-order inversion with Open(). + // If the entry is busy, skip it — it will be checked next cycle. + std::unique_lock lock(fc->mutex, std::try_to_lock); + if (!lock.owns_lock()) { + return false; + } return (now - fc->stat_time > expired_time); }); } diff --git a/http/server/FileCache.h b/http/server/FileCache.h index c43ce1943..dc9809c9a 100644 --- a/http/server/FileCache.h +++ b/http/server/FileCache.h @@ -87,9 +87,14 @@ typedef struct file_cache_s { // Thread-safe: prepend header into reserved space. // Returns true on success, false if header exceeds reserved space. + // On failure, httpbuf falls back to filebuf (body only, no header). bool prepend_header(const char* header, int len) { std::lock_guard lock(mutex); - if (len <= 0 || len > header_reserve) return false; + if (len <= 0 || len > header_reserve) { + // Safe fallback: point httpbuf at filebuf so callers always get valid data + httpbuf = filebuf; + return false; + } httpbuf.base = filebuf.base - len; httpbuf.len = (size_t)len + filebuf.len; memcpy(httpbuf.base, header, len); @@ -144,8 +149,8 @@ class HV_EXPORT FileCache : public hv::LRUCache { int GetExpiredTime() const { return expired_time; } // --- new: setters --- - void SetMaxHeaderLength(int len) { max_header_length = len; } - void SetMaxFileSize(int size) { max_file_size = size; } + void SetMaxHeaderLength(int len) { max_header_length = len < 0 ? 0 : len; } + void SetMaxFileSize(int size) { max_file_size = size < 1 ? 1 : size; } protected: file_cache_ptr Get(const char* filepath); diff --git a/http/server/HttpServer.cpp b/http/server/HttpServer.cpp index aa296fe8b..25d6d6753 100644 --- a/http/server/HttpServer.cpp +++ b/http/server/HttpServer.cpp @@ -136,6 +136,7 @@ static void loop_thread(void* userdata) { FileCache* filecache = &privdata->filecache; filecache->stat_interval = service->file_cache_stat_interval; filecache->expired_time = service->file_cache_expired_time; + filecache->max_file_size = service->max_file_cache_size; if (filecache->expired_time > 0) { // NOTE: add timer to remove expired file cache htimer_t* timer = htimer_add(hloop, [](htimer_t* timer) { From 4fd86b4a45bbb5ec671335b0c40bf94dab3ce5b2 Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Mon, 6 Apr 2026 00:48:58 +0800 Subject: [PATCH 09/13] refactor: improve FileCache Open method and enhance HttpHandler header handling --- http/server/FileCache.cpp | 17 ++++++++++------- http/server/HttpHandler.cpp | 12 +++++++----- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/http/server/FileCache.cpp b/http/server/FileCache.cpp index e0c9e84a7..0cdb29072 100644 --- a/http/server/FileCache.cpp +++ b/http/server/FileCache.cpp @@ -102,17 +102,19 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { // Lock order: LRUCache mutex → fc->mutex (never reverse). { std::lock_guard lock(fc->mutex); - // Sync local stat result into cached entry - fc->st = st; - if (S_ISREG(fc->st.st_mode)) { - param->filesize = fc->st.st_size; + if (S_ISREG(st.st_mode)) { + param->filesize = st.st_size; // FILE if (param->need_read) { - if (fc->st.st_size > param->max_read) { + if (st.st_size > param->max_read) { param->error = ERR_OVER_LIMIT; - // Don't cache incomplete entries + // Leave existing cache entry's state untouched return NULL; } + } + // Validation passed — commit new stat into cached entry + fc->st = st; + if (param->need_read) { fc->resize_buf(fc->st.st_size, max_header_length); // Loop to handle partial reads (EINTR, etc.) char* dst = fc->filebuf.base; @@ -145,8 +147,9 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { fc->content_type = http_content_type_str_by_suffix(suffix + 1); } } - } else if (S_ISDIR(fc->st.st_mode)) { + } else if (S_ISDIR(st.st_mode)) { // DIR + fc->st = st; std::string page; make_index_of_page(filepath, page, param->path); fc->resize_buf(page.size(), max_header_length); diff --git a/http/server/HttpHandler.cpp b/http/server/HttpHandler.cpp index e08ad90dc..b5800dd7b 100644 --- a/http/server/HttpHandler.cpp +++ b/http/server/HttpHandler.cpp @@ -801,11 +801,13 @@ int HttpHandler::GetSendData(char** data, size_t* len) { // FileCache // NOTE: no copy filebuf, more efficient header = pResp->Dump(true, false); - fc->prepend_header(header.c_str(), header.size()); - *data = fc->httpbuf.base; - *len = fc->httpbuf.len; - state = SEND_DONE; - return *len; + if (fc->prepend_header(header.c_str(), header.size())) { + *data = fc->httpbuf.base; + *len = fc->httpbuf.len; + state = SEND_DONE; + return *len; + } + // Header too large for reserved space — fall through to normal path } // API service content_length = pResp->ContentLength(); From f1850b974112987f34d10680399e6afe89bb20da Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Mon, 6 Apr 2026 01:12:04 +0800 Subject: [PATCH 10/13] refactor: update FileCache to reset header_used on safe fallback and use setter for max_file_size --- http/server/FileCache.h | 1 + http/server/HttpServer.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/http/server/FileCache.h b/http/server/FileCache.h index dc9809c9a..d06cf422e 100644 --- a/http/server/FileCache.h +++ b/http/server/FileCache.h @@ -93,6 +93,7 @@ typedef struct file_cache_s { if (len <= 0 || len > header_reserve) { // Safe fallback: point httpbuf at filebuf so callers always get valid data httpbuf = filebuf; + header_used = 0; return false; } httpbuf.base = filebuf.base - len; diff --git a/http/server/HttpServer.cpp b/http/server/HttpServer.cpp index 25d6d6753..6471de503 100644 --- a/http/server/HttpServer.cpp +++ b/http/server/HttpServer.cpp @@ -136,7 +136,7 @@ static void loop_thread(void* userdata) { FileCache* filecache = &privdata->filecache; filecache->stat_interval = service->file_cache_stat_interval; filecache->expired_time = service->file_cache_expired_time; - filecache->max_file_size = service->max_file_cache_size; + filecache->SetMaxFileSize(service->max_file_cache_size); if (filecache->expired_time > 0) { // NOTE: add timer to remove expired file cache htimer_t* timer = htimer_add(hloop, [](htimer_t* timer) { From 8cd32ab48b11910f3619a0b5ad8e27e0dfdbf3ad Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Tue, 7 Apr 2026 13:11:39 +0800 Subject: [PATCH 11/13] refactor: update FileCache to change nread type from ssize_t to int in Open method --- cmake/vars.cmake | 1 - http/server/FileCache.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cmake/vars.cmake b/cmake/vars.cmake index a425a8c75..1acf5565f 100644 --- a/cmake/vars.cmake +++ b/cmake/vars.cmake @@ -103,7 +103,6 @@ set(HTTP_SERVER_HEADERS http/server/HttpContext.h http/server/HttpResponseWriter.h http/server/WebSocketServer.h - http/server/FileCache.h ) set(MQTT_HEADERS diff --git a/http/server/FileCache.cpp b/http/server/FileCache.cpp index 0cdb29072..e2e87d0f4 100644 --- a/http/server/FileCache.cpp +++ b/http/server/FileCache.cpp @@ -120,7 +120,7 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { char* dst = fc->filebuf.base; size_t remaining = fc->filebuf.len; while (remaining > 0) { - ssize_t nread = read(fd, dst, remaining); + int nread = read(fd, dst, remaining); if (nread < 0) { if (errno == EINTR) continue; hloge("Failed to read file: %s", filepath); From 4c1d30cc8916f9fe9d3ef40e1f6bcb890515ee9a Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 10 Apr 2026 20:58:24 +0800 Subject: [PATCH 12/13] rm deprecated comment --- http/server/FileCache.h | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/http/server/FileCache.h b/http/server/FileCache.h index d06cf422e..697898900 100644 --- a/http/server/FileCache.h +++ b/http/server/FileCache.h @@ -4,14 +4,6 @@ /* * FileCache — Enhanced File Cache for libhv HTTP server * - * Features: - * 1. Configurable max_header_length (default 4096, tunable per-instance) - * 2. prepend_header() returns bool to report success/failure - * 3. Exposes header/buffer metrics via accessors - * 4. Fixes stat() name collision in is_modified() - * 5. max_cache_num / max_file_size configurable at runtime - * 6. Reserved header space can be tuned per-instance - * 7. Source-level API compatible; struct layout differs from original (no ABI/layout compatibility) */ #include @@ -42,7 +34,6 @@ typedef struct file_cache_s { char etag[64]; std::string content_type; - // --- new: expose header metrics --- int header_reserve; // reserved bytes before file content int header_used; // actual bytes used by prepend_header @@ -114,7 +105,6 @@ typedef std::shared_ptr file_cache_ptr; class HV_EXPORT FileCache : public hv::LRUCache { public: - // --- configurable parameters (were hardcoded macros before) --- int stat_interval; // seconds between stat() checks int expired_time; // seconds before cache entry expires int max_header_length; // reserved header bytes per entry @@ -143,13 +133,11 @@ class HV_EXPORT FileCache : public hv::LRUCache { bool Close(const char* filepath); void RemoveExpiredFileCache(); - // --- new: getters --- int GetMaxHeaderLength() const { return max_header_length; } int GetMaxFileSize() const { return max_file_size; } int GetStatInterval() const { return stat_interval; } int GetExpiredTime() const { return expired_time; } - // --- new: setters --- void SetMaxHeaderLength(int len) { max_header_length = len < 0 ? 0 : len; } void SetMaxFileSize(int size) { max_file_size = size < 1 ? 1 : size; } From 731a0a8e93da80aaaf041b70b038d25a6ee89f13 Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 10 Apr 2026 21:17:05 +0800 Subject: [PATCH 13/13] Header too large for reserved space: send header first, then continue with file body Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- http/server/HttpHandler.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/http/server/HttpHandler.cpp b/http/server/HttpHandler.cpp index b5800dd7b..46daab398 100644 --- a/http/server/HttpHandler.cpp +++ b/http/server/HttpHandler.cpp @@ -807,7 +807,9 @@ int HttpHandler::GetSendData(char** data, size_t* len) { state = SEND_DONE; return *len; } - // Header too large for reserved space — fall through to normal path + // Header too large for reserved space: send header first, then continue with file body. + state = SEND_BODY; + goto return_header; } // API service content_length = pResp->ContentLength();