cpp_server/static_file_handler.hpp

167 lines
4.3 KiB
C++

#pragma once
#include "parser.hpp"
#include "response.hpp"
#include "common.hpp"
#include <string>
#include <string_view>
#include <unordered_map>
#include <fstream>
#include <filesystem>
#include <chrono>
#include <zlib.h>
#include <cstring>
using std::string_view;
using std::string;
namespace fs = std::filesystem;
class StaticFileHandler {
private:
string url_prefix_;
string root_path_;
std::unordered_map<string, std::pair<string, std::chrono::time_point<std::chrono::file_clock>>> cache_;
size_t max_cache_size_ = 50 * 1024 * 1024; // 50MB cache limit
string compress_gzip(const string& data) const {
z_stream zs;
memset(&zs, 0, sizeof(zs));
if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
return "";
}
zs.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(data.data()));
zs.avail_in = data.size();
string compressed;
compressed.reserve(data.size() / 2);
char buffer[32768];
do {
zs.next_out = reinterpret_cast<Bytef*>(buffer);
zs.avail_out = sizeof(buffer);
int ret = deflate(&zs, Z_FINISH);
if (ret == Z_STREAM_ERROR) break;
size_t have = sizeof(buffer) - zs.avail_out;
compressed.append(buffer, have);
} while (zs.avail_out == 0);
deflateEnd(&zs);
return compressed;
}
string read_file(const string& path) const {
std::ifstream file(path, std::ios::binary);
if (!file.is_open()) return "";
file.seekg(0, std::ios::end);
size_t size = file.tellg();
file.seekg(0, std::ios::beg);
string content(size, '\0');
file.read(content.data(), size);
return content;
}
bool is_path_safe(string_view path) const {
return path.find("..") == string::npos;
}
public:
explicit StaticFileHandler(string_view root_path, string_view url_prefix = "")
: url_prefix_(url_prefix), root_path_(root_path) {
if (!root_path_.empty() && root_path_.back() != '/') {
root_path_ += '/';
}
if (!url_prefix_.empty() && url_prefix_.back() != '/') {
url_prefix_ += '/';
}
}
bool handle(const Request& req, Response& res) {
if (req.method != HttpMethod::GET && req.method != HttpMethod::HEAD) {
return false;
}
// Check URL prefix match
if (!url_prefix_.empty()) {
if (req.path.size() < url_prefix_.size() ||
req.path.substr(0, url_prefix_.size()) != url_prefix_) {
return false;
}
}
// Get path after prefix
string_view file_path_part = url_prefix_.empty() ? req.path : req.path.substr(url_prefix_.size());
if (!is_path_safe(file_path_part)) {
return false;
}
string file_path = root_path_;
if (file_path_part.empty() || file_path_part == "/") {
file_path += "index.html";
} else {
// Remove leading slash if present
if (file_path_part[0] == '/') file_path_part = file_path_part.substr(1);
file_path += string(file_path_part);
}
if (!fs::exists(file_path) || !fs::is_regular_file(file_path)) {
return false;
}
// Get file info
auto last_write = fs::last_write_time(file_path);
auto file_size = fs::file_size(file_path);
// Check cache
auto cache_it = cache_.find(file_path);
if (cache_it != cache_.end() && cache_it->second.second == last_write) {
res.body = cache_it->second.first;
} else {
// Read file
res.body = read_file(file_path);
if (res.body.empty()) {
return false;
}
// Cache if small enough
if (file_size < max_cache_size_ / 10) {
cache_[file_path] = {res.body, last_write};
}
}
// Set MIME type
string ext = fs::path(file_path).extension();
res.content_type = MimeTypes::get_mime_type(ext);
// Check if client accepts gzip
auto accept_encoding = req.headers.find("Accept-Encoding");
bool client_accepts_gzip = accept_encoding != req.headers.end() &&
accept_encoding->second.find("gzip") != string::npos;
// Compress if appropriate
if (client_accepts_gzip && MimeTypes::should_compress(res.content_type) && res.body.size() > 1024) {
string compressed = compress_gzip(res.body);
if (!compressed.empty() && compressed.size() < res.body.size()) {
res.body = std::move(compressed);
res.headers["Content-Encoding"] = "gzip";
}
}
// Add caching headers
res.headers["Cache-Control"] = "public, max-age=3600";
res.headers["ETag"] = "\"" + std::to_string(std::hash<string>{}(res.body)) + "\"";
if (req.method == HttpMethod::HEAD) {
res.body.clear();
}
return true;
}
};