meson build, static file handler
This commit is contained in:
parent
9fd28d5703
commit
bb49ac9275
33
.gitignore
vendored
33
.gitignore
vendored
@ -1 +1,32 @@
|
||||
build/server
|
||||
# Build directories
|
||||
build/
|
||||
|
||||
# Executables
|
||||
*.out
|
||||
*.exe
|
||||
server
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.so
|
||||
*.a
|
||||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
*.su
|
||||
*.idb
|
||||
*.pdb
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test assets
|
||||
assets/
|
||||
|
@ -1,7 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <string>
|
||||
|
||||
using std::string_view;
|
||||
|
||||
enum class HttpMethod : uint8_t {
|
||||
GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, UNKNOWN
|
||||
};
|
||||
|
||||
class MimeTypes {
|
||||
public:
|
||||
static string_view get_mime_type(string_view extension) {
|
||||
// Remove leading dot if present
|
||||
if (!extension.empty() && extension[0] == '.') {
|
||||
extension = extension.substr(1);
|
||||
}
|
||||
|
||||
switch (extension.size()) {
|
||||
case 2:
|
||||
if (extension == "js") return "application/javascript";
|
||||
break;
|
||||
case 3:
|
||||
if (extension == "css") return "text/css";
|
||||
if (extension == "htm") return "text/html";
|
||||
if (extension == "png") return "image/png";
|
||||
if (extension == "jpg") return "image/jpeg";
|
||||
if (extension == "gif") return "image/gif";
|
||||
if (extension == "svg") return "image/svg+xml";
|
||||
if (extension == "ico") return "image/x-icon";
|
||||
if (extension == "ttf") return "font/ttf";
|
||||
if (extension == "txt") return "text/plain";
|
||||
if (extension == "pdf") return "application/pdf";
|
||||
break;
|
||||
case 4:
|
||||
if (extension == "html") return "text/html";
|
||||
if (extension == "json") return "application/json";
|
||||
if (extension == "jpeg") return "image/jpeg";
|
||||
if (extension == "woff") return "font/woff";
|
||||
break;
|
||||
case 5:
|
||||
if (extension == "woff2") return "font/woff2";
|
||||
break;
|
||||
}
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
static bool should_compress(string_view mime_type) {
|
||||
return mime_type.starts_with("text/") ||
|
||||
mime_type.starts_with("application/json") ||
|
||||
mime_type.starts_with("application/javascript") ||
|
||||
mime_type.starts_with("image/svg+xml");
|
||||
}
|
||||
};
|
||||
|
16
main.cpp
16
main.cpp
@ -67,19 +67,21 @@ int main() {
|
||||
|
||||
server = new HttpServer(8080, router);
|
||||
|
||||
std::cout << "Current working directory: " << std::filesystem::current_path() << std::endl;
|
||||
|
||||
server->serve_static("../assets");
|
||||
if (std::filesystem::exists("../assets/index.html")) {
|
||||
std::cout << "Found index.html\n";
|
||||
} else {
|
||||
std::cout << "Missing ../assets/index.html\n";
|
||||
}
|
||||
|
||||
if (!server->start()) {
|
||||
std::cerr << "Failed to start server\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Server running on http://localhost:8080\n";
|
||||
std::cout << "Test routes:\n";
|
||||
std::cout << " GET /\n";
|
||||
std::cout << " GET /api/status\n";
|
||||
std::cout << " GET /users\n";
|
||||
std::cout << " GET /users/123\n";
|
||||
std::cout << " POST /users\n";
|
||||
std::cout << " GET /request-info\n";
|
||||
std::cout << "Press Ctrl+C to stop\n";
|
||||
|
||||
server->run();
|
||||
|
14
meson.build
Normal file
14
meson.build
Normal file
@ -0,0 +1,14 @@
|
||||
project('server', 'cpp', default_options: ['cpp_std=c++20'])
|
||||
add_project_arguments('-Wno-unused-parameter', language: 'cpp')
|
||||
|
||||
zlib = dependency('zlib')
|
||||
threads = dependency('threads')
|
||||
|
||||
server = executable('server',
|
||||
'main.cpp',
|
||||
dependencies: [zlib, threads],
|
||||
cpp_args: ['-O3', '-march=native', '-flto', '-Wall', '-Wextra', '-DNDEBUG'],
|
||||
link_args: ['-flto', '-s']
|
||||
)
|
||||
|
||||
run_target('run', command: server)
|
23
server.hpp
23
server.hpp
@ -4,6 +4,7 @@
|
||||
#include "router.hpp"
|
||||
#include "http_parser.hpp"
|
||||
#include "http_response.hpp"
|
||||
#include "static_file_handler.hpp"
|
||||
#include <iostream>
|
||||
#include <string.h>
|
||||
#include <string_view>
|
||||
@ -33,7 +34,7 @@ public:
|
||||
workers_.reserve(num_cores);
|
||||
|
||||
for (unsigned int i = 0; i < num_cores; ++i) {
|
||||
auto worker = std::make_unique<Worker>(port_, router_);
|
||||
auto worker = std::make_unique<Worker>(port_, router_, static_handler_);
|
||||
if (!worker->socket.start()) return false;
|
||||
workers_.push_back(std::move(worker));
|
||||
}
|
||||
@ -82,16 +83,23 @@ public:
|
||||
return "";
|
||||
}
|
||||
|
||||
void serve_static(const std::string& static_dir) {
|
||||
static_handler_ = std::make_shared<StaticFileHandler>(static_dir);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int BUFFER_SIZE = 65536;
|
||||
std::shared_ptr<StaticFileHandler> static_handler_;
|
||||
|
||||
struct Worker {
|
||||
EpollSocket socket;
|
||||
Router& router;
|
||||
std::shared_ptr<StaticFileHandler>& static_handler;
|
||||
std::array<char, BUFFER_SIZE> buffer;
|
||||
std::thread thread;
|
||||
|
||||
Worker(uint16_t port, Router& r) : socket(port), router(r) {
|
||||
Worker(uint16_t port, Router& r, std::shared_ptr<StaticFileHandler>& sh)
|
||||
: socket(port), router(r), static_handler(sh) {
|
||||
socket.on_connection([this](int fd) { handle_connection(fd); });
|
||||
socket.on_data([this](int fd) { handle_data(fd); });
|
||||
socket.on_disconnect([this](int fd) { handle_disconnect(fd); });
|
||||
@ -133,6 +141,17 @@ private:
|
||||
|
||||
HttpResponse response;
|
||||
|
||||
// Try static files first
|
||||
if (static_handler) {
|
||||
static_handler->handle(req, response);
|
||||
if (response.status == 200) {
|
||||
send_http_response(client_fd, response, req.version);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Then try router
|
||||
response = HttpResponse{}; // Reset response
|
||||
if (router.handle(req, response)) {
|
||||
send_http_response(client_fd, response, req.version);
|
||||
} else {
|
||||
|
154
static_file_handler.hpp
Normal file
154
static_file_handler.hpp
Normal file
@ -0,0 +1,154 @@
|
||||
#pragma once
|
||||
|
||||
#include "http_parser.hpp"
|
||||
#include "http_response.hpp"
|
||||
#include "http_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 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) : root_path_(root_path) {
|
||||
if (!root_path_.empty() && root_path_.back() != '/') {
|
||||
root_path_ += '/';
|
||||
}
|
||||
}
|
||||
|
||||
void handle(const HttpRequest& req, HttpResponse& res) {
|
||||
if (req.method != HttpMethod::GET && req.method != HttpMethod::HEAD) {
|
||||
res.status = 405;
|
||||
res.set_text("Method Not Allowed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_path_safe(req.path)) {
|
||||
res.status = 403;
|
||||
res.set_text("Forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
string file_path = root_path_ + string(req.path.substr(1)); // Remove leading /
|
||||
|
||||
// Default to index.html for directories
|
||||
if (file_path.back() == '/') {
|
||||
file_path += "index.html";
|
||||
}
|
||||
|
||||
if (!fs::exists(file_path) || !fs::is_regular_file(file_path)) {
|
||||
res.status = 404;
|
||||
res.set_text("Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
res.status = 500;
|
||||
res.set_text("Internal Server Error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user