// sinja: blazing-fast, stable, production-grade JSON templating REST server // Architecture: Multi-threaded SO_REUSEPORT with robust, high-performance request handling. // Dependency: inja (which includes nlohmann::json) // Build: see CMakeLists.txt (ensure you link with -lpthread) // Run : ./sinja --templates /path/to/templates #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using json = nlohmann::json; namespace fs = std::filesystem; // ---- Tunables ---------------------------------------------------------------- static constexpr const char* kServerName = "sinja/5.0-stable"; static constexpr size_t kMaxBodyBytes = 8 * 1024 * 1024; static constexpr size_t kMaxHeaderBytes = 64 * 1024; static constexpr int kRecvTimeoutSec = 10; static constexpr int kSendTimeoutSec = 10; static constexpr int kBacklog = 8192; // Increased for modern kernels // ---- Global state for graceful shutdown -------------------------------------- static std::atomic g_running{true}; static void on_signal(int){ g_running.store(false); } // ---- Config ------------------------------------------------------------------ struct Settings { std::string address = "0.0.0.0"; int port = 8080; size_t threads = std::thread::hardware_concurrency() ? std::thread::hardware_concurrency() : 4; fs::path template_root; }; // ---- Small utils ------------------------------------------------------------- static inline std::string ltrim(std::string s){ size_t i=0; while(i headers; std::string body; bool keep_alive = true; }; struct Response { int status = 200; std::string reason = "OK"; std::vector> headers; std::string body; }; static std::string status_reason(int code){ switch(code){ case 200: return "OK"; case 400: return "Bad Request"; case 404: return "Not Found"; case 411: return "Length Required"; case 413: return "Payload Too Large"; case 415: return "Unsupported Media Type"; case 431: return "Request Header Fields Too Large"; case 500: return "Internal Server Error"; default: return "Error"; } } static void set_socket_timeouts(int fd, int recv_sec, int send_sec){ timeval tv{.tv_sec = recv_sec, .tv_usec = 0}; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); tv = timeval{.tv_sec = send_sec, .tv_usec = 0}; setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); int flag = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)); } static bool recv_append(int fd, std::string& buf){ char tmp[8192]; ssize_t n = ::recv(fd, tmp, sizeof(tmp), 0); if (n <= 0) return false; buf.append(tmp, (size_t)n); return true; } static bool send_all(int fd, const char* data, size_t len){ size_t off = 0; while (off < len) { ssize_t n = ::send(fd, data + off, len - off, MSG_NOSIGNAL); if (n < 0) { if (errno == EINTR) continue; return false; } off += (size_t)n; } return true; } static std::string build_response(const Response& res, bool keep_alive){ std::string out; out.reserve(256 + res.body.size()); out += "HTTP/1.1 " + std::to_string(res.status) + " " + (res.reason.empty()?status_reason(res.status):res.reason) + "\r\n"; out += "Server: "; out += kServerName; out += "\r\n"; out += "Content-Length: " + std::to_string(res.body.size()) + "\r\n"; out += std::string("Connection: ") + (keep_alive ? "keep-alive" : "close") + "\r\n"; for (const auto& h : res.headers) { out += h.first; out += ": "; out += h.second; out += "\r\n"; } out += "\r\n"; out += res.body; return out; } static bool is_json_ct(const std::map& h){ auto it = h.find("content-type"); if (it == h.end()) return false; return to_lower(it->second).find("application/json") != std::string::npos; } static std::optional> parse_chunked_body(const std::string& buf, size_t start_off) { size_t p = start_off; std::string out; while (true) { size_t eol = buf.find("\r\n", p); if (eol == std::string::npos) return std::nullopt; std::string size_line = buf.substr(p, eol - p); size_t sc = size_line.find(';'); if (sc != std::string::npos) size_line.resize(sc); size_line = trim(size_line); if (size_line.empty()) throw std::runtime_error("bad chunk size"); size_t chunk_size = 0; try { chunk_size = std::stoul(size_line, nullptr, 16); } catch (...) { throw std::runtime_error("bad chunk size"); } p = eol + 2; if (chunk_size == 0) { size_t trailer_end = buf.find("\r\n\r\n", p); if (trailer_end == std::string::npos) return std::nullopt; return std::make_pair((trailer_end + 4) - start_off, std::move(out)); } if (buf.size() < p + chunk_size + 2) return std::nullopt; if (out.size() + chunk_size > kMaxBodyBytes) throw std::runtime_error("payload too large"); out.append(buf.data() + p, chunk_size); p += chunk_size; if (!(buf[p] == '\r' && buf[p+1] == '\n')) throw std::runtime_error("bad chunk CRLF"); p += 2; } } static std::optional parse_request(std::string& inbuf) { size_t hdr_end = inbuf.find("\r\n\r\n"); if (hdr_end == std::string::npos) return std::nullopt; if (hdr_end > kMaxHeaderBytes) throw std::runtime_error("headers too large"); std::string head = inbuf.substr(0, hdr_end); size_t line_end = head.find("\r\n"); if (line_end == std::string::npos) return std::nullopt; Request r; std::string req_line = head.substr(0, line_end); size_t sp1 = req_line.find(' '); size_t sp2 = req_line.find(' ', sp1 + 1); if (sp1 == std::string::npos || sp2 == std::string::npos) throw std::runtime_error("bad request line"); r.method = req_line.substr(0, sp1); r.target = req_line.substr(sp1 + 1, sp2 - sp1 - 1); r.version = req_line.substr(sp2 + 1); size_t off = line_end + 2; while (off < head.size()) { size_t next_line = head.find("\r\n", off); std::string line = head.substr(off, next_line - off); if (next_line == std::string::npos) { off = head.size(); } else { off = next_line + 2; } if (line.empty()) break; size_t colon_pos = line.find(':'); if (colon_pos != std::string::npos) { r.headers[to_lower(trim(line.substr(0, colon_pos)))] = trim(line.substr(colon_pos + 1)); } } const size_t body_start = hdr_end + 4; size_t consumed_len = 0; auto te_it = r.headers.find("transfer-encoding"); if (te_it != r.headers.end() && to_lower(te_it->second).find("chunked") != std::string::npos) { auto parsed = parse_chunked_body(inbuf, body_start); if (!parsed) return std::nullopt; r.body = std::move(parsed->second); consumed_len = body_start + parsed->first; } else { size_t content_len = 0; auto cl_it = r.headers.find("content-length"); if (cl_it != r.headers.end()) { try { content_len = std::stoull(cl_it->second); } catch (...) { throw std::runtime_error("invalid content-length"); } if (content_len > kMaxBodyBytes) throw std::runtime_error("payload too large"); } if (inbuf.size() < body_start + content_len) return std::nullopt; r.body = inbuf.substr(body_start, content_len); consumed_len = body_start + content_len; } inbuf.erase(0, consumed_len); auto conn_it = r.headers.find("connection"); if (r.version == "HTTP/1.0") { r.keep_alive = (conn_it != r.headers.end() && to_lower(conn_it->second) == "keep-alive"); } else { r.keep_alive = (conn_it == r.headers.end() || to_lower(conn_it->second) != "close"); } return r; } // ---- Core Server Logic ------------------------------------------------------- class Server { Settings cfg; std::vector workers; std::vector listen_fds; std::mutex fds_mutex; public: explicit Server(Settings s): cfg(std::move(s)) {} void start() { std::cout << "Starting " << kServerName << "...\n"; for (size_t i = 0; i < cfg.threads; ++i) { workers.emplace_back([this]{ worker_loop(); }); } std::cout << " Listening on http://" << cfg.address << ":" << cfg.port << " with " << cfg.threads << " workers.\n"; } void stop() { g_running.store(false); std::cout << "\nShutting down... finishing active connections.\n"; std::lock_guard lock(fds_mutex); for (int fd : listen_fds) { ::shutdown(fd, SHUT_RDWR); ::close(fd); } } void join() { for (auto& t : workers) { if (t.joinable()) t.join(); } std::cout << "All workers stopped. Shutdown complete.\n"; } private: void send_json_error(int fd, int code, const std::string& msg) { Response res; res.status = code; res.reason = status_reason(code); res.headers.push_back({"Content-Type","application/json; charset=utf-8"}); res.headers.push_back({"Cache-Control","no-store"}); res.body = json({{"error", msg}}).dump(); auto raw = build_response(res, false); send_all(fd, raw.data(), raw.size()); } void handle_connection(int fd, inja::Environment& env) { std::string inbuf; inbuf.reserve(16*1024); bool keep = true; while (keep && g_running.load(std::memory_order_relaxed)) { std::optional reqOpt; while (g_running.load(std::memory_order_relaxed)) { try { reqOpt = parse_request(inbuf); if (reqOpt) break; } catch (const std::exception& e) { int code = 400; std::string what = e.what(); if (what == "payload too large") code = 413; else if (what == "headers too large") code = 431; send_json_error(fd, code, what); return; } if (!recv_append(fd, inbuf)) return; } if (!reqOpt) return; Request req = std::move(*reqOpt); keep = req.keep_alive; if (!(req.method == "POST" && req.target == "/render")) { send_json_error(fd, 404, "Not Found"); continue; } if (!is_json_ct(req.headers)) { send_json_error(fd, 415, "Content-Type must be application/json"); continue; } json jreq; try { if (req.body.empty()) throw std::runtime_error("Request body is empty"); jreq = json::parse(req.body); } catch (const std::exception& e) { send_json_error(fd, 400, std::string("Invalid JSON: ") + e.what()); continue; } if (!jreq.contains("template") || !jreq["template"].is_string()) { send_json_error(fd, 400, R"(Missing "template" (string))"); continue; } if (jreq.contains("context") && !jreq["context"].is_object()) { send_json_error(fd, 400, R"("context" must be an object)"); continue; } std::string template_name = jreq["template"].get(); if (!validate_template_path(template_name)) { send_json_error(fd, 400, "Template error: invalid path"); continue; } json ctx = jreq.value("context", json::object()); Response res; auto t0 = std::chrono::steady_clock::now(); try { res.body = env.render_file(template_name, ctx); auto t1 = std::chrono::steady_clock::now(); auto us = std::chrono::duration_cast(t1 - t0).count(); res.headers.push_back({"Content-Type","text/plain; charset=utf-8"}); res.headers.push_back({"X-Render-Time-Us", std::to_string(us)}); } catch (const std::exception& e) { send_json_error(fd, 500, std::string("Render error: ") + e.what()); continue; } auto raw = build_response(res, keep); if (!send_all(fd, raw.data(), raw.size())) return; } } void worker_loop() { inja::Environment env(cfg.template_root.string()); env.set_trim_blocks(true); env.set_lstrip_blocks(true); int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket"); return; } int yes=1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); #ifdef SO_REUSEPORT setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)); #endif sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(cfg.port); if (::inet_pton(AF_INET, cfg.address.c_str(), &addr.sin_addr) != 1) { std::cerr << "Invalid address\n"; ::close(listen_fd); return; } if (::bind(listen_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); ::close(listen_fd); return; } if (::listen(listen_fd, kBacklog) < 0) { perror("listen"); ::close(listen_fd); return; } { std::lock_guard lock(fds_mutex); listen_fds.push_back(listen_fd); } while (g_running.load(std::memory_order_relaxed)) { int client_fd = ::accept(listen_fd, nullptr, nullptr); if (client_fd < 0) { if (g_running.load(std::memory_order_relaxed)) perror("accept"); break; } set_socket_timeouts(client_fd, kRecvTimeoutSec, kSendTimeoutSec); handle_connection(client_fd, env); ::shutdown(client_fd, SHUT_WR); ::close(client_fd); } ::close(listen_fd); } }; // ---- Args & main ------------------------------------------------------------- static Settings parse_args(int argc, char** argv){ Settings s; for (int i=1;istd::string{ if (i+1>=argc) throw std::runtime_error("Missing value for "+a); return std::string(argv[++i]); }; if (a=="--templates"||a=="-t") s.template_root = next(); else if (a=="--address"||a=="-a") s.address = next(); else if (a=="--port"||a=="-p") s.port = std::stoi(next()); else if (a=="--threads"||a=="-w") s.threads = (size_t)std::stoul(next()); else if (a=="--help"||a=="-h") { std::cout << "Usage: sinja --templates DIR [options]\n" " --address, -a Bind address (default 0.0.0.0)\n" " --port, -p Port (default 8080)\n" " --threads, -w Worker threads (default HW concurrency)\n"; std::exit(0); } else throw std::runtime_error("Unknown arg: " + a); } if (s.template_root.empty()) throw std::runtime_error("Missing required --templates DIR"); if (!fs::is_directory(s.template_root)) throw std::runtime_error("Template directory invalid: " + s.template_root.string()); (s.template_root) = fs::weakly_canonical(s.template_root); return s; } int main(int argc, char** argv){ try { auto cfg = parse_args(argc, argv); std::signal(SIGPIPE, SIG_IGN); std::signal(SIGINT, on_signal); std::signal(SIGTERM, on_signal); Server srv(std::move(cfg)); srv.start(); while (g_running.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } srv.stop(); srv.join(); } catch (const std::exception& e) { std::cerr << "Fatal: " << e.what() << "\n"; return 1; } return 0; }