diff --git a/.gitignore b/.gitignore index d34d02b..b15dd75 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,32 @@ -build/server \ No newline at end of file +# 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/ diff --git a/http_common.hpp b/http_common.hpp index 65c2924..88f74cc 100644 --- a/http_common.hpp +++ b/http_common.hpp @@ -1,7 +1,56 @@ #pragma once #include +#include +#include + +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"); + } +}; diff --git a/main.cpp b/main.cpp index 7659fe1..19c280d 100644 --- a/main.cpp +++ b/main.cpp @@ -67,23 +67,25 @@ 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(); delete server; return 0; -} \ No newline at end of file +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..621e1b4 --- /dev/null +++ b/meson.build @@ -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) diff --git a/server.hpp b/server.hpp index db2d876..fbbbb7a 100644 --- a/server.hpp +++ b/server.hpp @@ -4,6 +4,7 @@ #include "router.hpp" #include "http_parser.hpp" #include "http_response.hpp" +#include "static_file_handler.hpp" #include #include #include @@ -33,7 +34,7 @@ public: workers_.reserve(num_cores); for (unsigned int i = 0; i < num_cores; ++i) { - auto worker = std::make_unique(port_, router_); + auto worker = std::make_unique(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(static_dir); + } + private: static constexpr int BUFFER_SIZE = 65536; + std::shared_ptr static_handler_; struct Worker { EpollSocket socket; Router& router; + std::shared_ptr& static_handler; std::array buffer; std::thread thread; - Worker(uint16_t port, Router& r) : socket(port), router(r) { + Worker(uint16_t port, Router& r, std::shared_ptr& 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 { @@ -170,4 +189,4 @@ private: uint16_t port_; Router& router_; std::vector> workers_; -}; \ No newline at end of file +}; diff --git a/static_file_handler.hpp b/static_file_handler.hpp new file mode 100644 index 0000000..ef479a3 --- /dev/null +++ b/static_file_handler.hpp @@ -0,0 +1,154 @@ +#pragma once + +#include "http_parser.hpp" +#include "http_response.hpp" +#include "http_common.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using std::string_view; +using std::string; +namespace fs = std::filesystem; + +class StaticFileHandler { +private: + string root_path_; + std::unordered_map>> 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(const_cast(data.data())); + zs.avail_in = data.size(); + + string compressed; + compressed.reserve(data.size() / 2); + + char buffer[32768]; + do { + zs.next_out = reinterpret_cast(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{}(res.body)) + "\""; + + if (req.method == HttpMethod::HEAD) { + res.body.clear(); + } + } +};