// sinja: ultra-fast JSON templating REST server
// Only dependency: nlohmann::json
// Build: see CMakeLists.txt
// Run : ./sinja --templates /path/to/templates --address 0.0.0.0 --port 8083 --threads $(nproc)
#include <nlohmann/json.hpp>
#include <atomic>
#include <cassert>
#include <chrono>
#include <condition_variable>
#include <cctype>
#include <cerrno>
#include <csignal>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <unordered_map>
#include <vector>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
using json = nlohmann::json;
namespace fs = std::filesystem;
// ---- Tunables ----------------------------------------------------------------
static constexpr const char* kServerName = "sinja/1.0";
static constexpr size_t kMaxBodyBytes = 8 * 1024 * 1024; // 8 MiB
static constexpr size_t kMaxHeaderBytes = 64 * 1024; // 64 KiB
static constexpr int kRecvTimeoutSec = 5; // per recv()
static constexpr int kSendTimeoutSec = 5;
static constexpr int kBacklog = 1024;
// ---- 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<s.size() && std::isspace((unsigned char)s[i])) ++i; return s.substr(i);
}
static inline std::string rtrim(std::string s){
if(s.empty()) return s;
size_t i=s.size()-1;
while (i<s.size() && std::isspace((unsigned char)s[i])) { if(i==0) return ""; --i; }
return s.substr(0, i+1);
}
static inline std::string trim(std::string s){ return rtrim(ltrim(std::move(s))); }
static inline bool starts_with(const std::string& s, const std::string& p){
return s.size()>=p.size() && std::equal(p.begin(), p.end(), s.begin());
}
static std::string to_lower(std::string s){
for (auto& c: s) c = (char)std::tolower((unsigned char)c);
return s;
}
// ---- Secure template resolution ----------------------------------------------
static fs::path secure_resolve_template(const fs::path& root, const std::string& req, std::string& reason) {
if (req.empty()) { reason = "empty template name"; return {}; }
fs::path rel(req);
if (rel.is_absolute()) { reason = "absolute path not allowed"; return {}; }
fs::path cand = fs::weakly_canonical(root / rel);
fs::path canon_root = fs::weakly_canonical(root);
std::string c1 = cand.string(), c0 = canon_root.string();
if (!starts_with(c1, c0)) { reason = "path traversal rejected"; return {}; }
if (!fs::exists(cand) || !fs::is_regular_file(cand)) { reason = "template file not found"; return {}; }
return cand;
}
// ---- Mini Inja (subset): {{ a.b[0].c }} and trims {{- ... -}} ----------------
struct MiniInja {
enum class TokType { Text, Var };
struct Tok {
TokType type;
std::string payload; // Var: expression; Text: literal
bool trim_left = false;
bool trim_right = false;
};
static std::vector<Tok> compile(const std::string& src) {
std::vector<Tok> out;
size_t i = 0, n = src.size();
while (i < n) {
size_t open = src.find("{{", i);
if (open == std::string::npos) {
out.push_back({TokType::Text, src.substr(i), false, false});
break;
}
if (open > i)
out.push_back({TokType::Text, src.substr(i, open - i), false, false});
bool trimL = (open + 2 < n && src[open+2] == '-');
size_t expr_start = open + 2 + (trimL ? 1 : 0);
size_t close = src.find("}}", expr_start);
if (close == std::string::npos) throw std::runtime_error("Unclosed {{ tag");
bool trimR = (close>0 && src[close-1]=='-');
size_t expr_end = close - (trimR ? 1 : 0);
std::string expr = trim(src.substr(expr_start, expr_end - expr_start));
out.push_back({TokType::Var, expr, trimL, trimR});
i = close + 2;
}
return out;
}
static const json* resolve_ptr(const json& ctx, const std::string& path) {
// supports a.b.c[2].d
const json* cur = &ctx;
size_t i = 0, n = path.size();
auto error = [&]{ throw std::runtime_error("Missing key in path: " + path); };
while (i < n) {
if (path[i] == '[') { // index starts
size_t r = path.find(']', i+1); if (r == std::string::npos) error();
int idx = std::stoi(path.substr(i+1, r-i-1));
if (!cur->is_array() || idx < 0 || (size_t)idx >= cur->size()) error();
cur = &(*cur)[(size_t)idx];
i = r + 1; if (i<n && path[i]=='.') ++i; continue;
}
size_t dot = path.find_first_of(".[", i);
std::string key = (dot==std::string::npos) ? path.substr(i) : path.substr(i, dot-i);
if (!cur->contains(key)) error();
cur = &(*cur)[key];
i = (dot==std::string::npos)? n : dot;
while (i<n && path[i]=='[') {
size_t r = path.find(']', i+1); if (r==std::string::npos) error();
int idx = std::stoi(path.substr(i+1, r-i-1));
if (!cur->is_array() || idx < 0 || (size_t)idx >= cur->size()) error();
cur = &(*cur)[(size_t)idx];
i = r + 1;
}
if (i<n && path[i]=='.') ++i;
}
return cur;
}
static std::string val_to_string(const json& v) {
if (v.is_string()) return v.get<std::string>();
if (v.is_boolean()) return v.get<bool>() ? "true" : "false";
if (v.is_number_integer()) return std::to_string(v.get<long long>());
if (v.is_number_unsigned()) return std::to_string(v.get<unsigned long long>());
if (v.is_number_float()) return std::to_string(v.get<double>());
return v.dump(); // arrays/objects
}
static std::string render(const std::vector<Tok>& prog, const json& ctx) {
std::string out;
out.reserve(1024);
for (size_t k = 0; k < prog.size(); ++k) {
const auto& t = prog[k];
if (t.type == TokType::Text) {
out.append(t.payload);
} else {
if (t.trim_left) {
while (!out.empty() && (out.back()==' '||out.back()=='\t'||out.back()=='\r'||out.back()=='\n'))
out.pop_back();
}
const json* pv = resolve_ptr(ctx, t.payload);
out.append(val_to_string(*pv));
if (t.trim_right && k+1 < prog.size() && prog[k+1].type == TokType::Text) {
auto &nxt = const_cast<Tok&>(prog[k+1]);
size_t j=0; while (j<nxt.payload.size() && (nxt.payload[j]==' '||nxt.payload[j]=='\t'||nxt.payload[j]=='\r'||nxt.payload[j]=='\n')) ++j;
if (j>0) nxt.payload.erase(0, j);
}
}
}
return out;
}
};
// Per-thread template cache
struct ThreadLocalTemplates {
struct Entry { fs::file_time_type mtime; std::vector<MiniInja::Tok> program; };
std::unordered_map<std::string, Entry> cache;
fs::path root;
explicit ThreadLocalTemplates(fs::path root_) : root(std::move(root_)) {}
const std::vector<MiniInja::Tok>& get(const fs::path& full) {
std::string key = full.string();
auto mtime = fs::last_write_time(full);
auto it = cache.find(key);
if (it != cache.end() && it->second.mtime == mtime) return it->second.program;
std::ifstream f(full, std::ios::binary);
if (!f) throw std::runtime_error("cannot open template");
std::string src((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
auto prog = MiniInja::compile(src);
cache[key] = Entry{mtime, std::move(prog)};
return cache[key].program;
}
};
static thread_local std::unique_ptr<ThreadLocalTemplates> tltpl;
// ---- HTTP primitives ---------------------------------------------------------
struct Request {
std::string method, target, version;
std::map<std::string,std::string> headers; // lower-cased keys
std::string body;
bool keep_alive = true;
};
struct Response {
int status = 200;
std::string reason = "OK";
std::vector<std::pair<std::string,std::string>> 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){
timeval tv{.tv_sec = kRecvTimeoutSec, .tv_usec = 0};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
tv = timeval{.tv_sec = kSendTimeoutSec, .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, 0);
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(128 + res.body.size() + 128);
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 (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<std::string,std::string>& h){
auto it = h.find("content-type");
if (it==h.end()) return false;
auto v = to_lower(it->second);
return v.find("application/json") != std::string::npos;
}
// Parse chunked body; returns <bytes_consumed, body>
static std::optional<std::pair<size_t,std::string>>
parse_chunked_body(const std::string& buf, size_t start_off) {
size_t p = start_off;
std::string out;
out.reserve(1024);
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) {
// last-chunk, then optional trailers terminated by CRLFCRLF
size_t trailer_end = buf.find("\r\n\r\n", p);
if (trailer_end == std::string::npos) return std::nullopt;
size_t consumed = (trailer_end + 4) - start_off;
return std::make_pair(consumed, 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;
}
}
// Parse a full HTTP request if available in inbuf
static std::optional<Request> 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;
{ // Request line
std::string reqline = head.substr(0, line_end);
size_t sp1 = reqline.find(' ');
size_t sp2 = reqline.find(' ', sp1+1);
if (sp1==std::string::npos||sp2==std::string::npos) throw std::runtime_error("bad request line");
r.method = reqline.substr(0, sp1);
r.target = reqline.substr(sp1+1, sp2 - sp1 - 1);
r.version = reqline.substr(sp2+1);
}
// Headers
size_t off = line_end + 2;
while (off < head.size()) {
size_t nxt = head.find("\r\n", off);
if (nxt == std::string::npos) break;
std::string line = head.substr(off, nxt - off);
off = nxt + 2;
if (line.empty()) break;
size_t colon = line.find(':'); if (colon == std::string::npos) continue;
std::string k = to_lower(trim(line.substr(0, colon)));
std::string v = trim(line.substr(colon+1));
r.headers[k] = v;
}
const size_t body_start = hdr_end + 4;
const bool has_te = r.headers.count("transfer-encoding") &&
to_lower(r.headers.at("transfer-encoding")).find("chunked") != std::string::npos;
if (has_te) {
auto parsed = parse_chunked_body(inbuf, body_start);
if (!parsed) return std::nullopt; // need more data
const auto& [consumed_body, body] = *parsed;
r.body = body;
inbuf.erase(0, body_start + consumed_body);
} else {
size_t content_len = 0;
if (r.headers.count("content-length")) {
content_len = (size_t)std::stoull(r.headers["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.assign(inbuf.data() + body_start, content_len);
inbuf.erase(0, body_start + content_len);
}
r.keep_alive = true;
if (r.version == "HTTP/1.0") {
r.keep_alive = (to_lower(r.headers["connection"]) == "keep-alive");
} else {
if (r.headers.count("connection") && to_lower(r.headers["connection"]) == "close") r.keep_alive = false;
}
return r;
}
static bool send_100_continue_if_needed(int fd, const std::string& head_block){
// Search for "Expect: 100-continue" case-insensitive in headers block
size_t off = 0;
while (true) {
size_t nxt = head_block.find("\r\n", off);
if (nxt == std::string::npos) break;
std::string line = head_block.substr(off, nxt - off);
off = nxt + 2;
if (line.empty()) break;
size_t colon = line.find(':'); if (colon == std::string::npos) continue;
std::string k = to_lower(trim(line.substr(0, colon)));
if (k == "expect") {
std::string v = to_lower(trim(line.substr(colon+1)));
if (v.find("100-continue") != std::string::npos) {
static const char kCont[] = "HTTP/1.1 100 Continue\r\n\r\n";
return send_all(fd, kCont, sizeof(kCont)-1);
}
}
}
return true;
}
// ---- Blocking queue & server -------------------------------------------------
template<typename T>
class BlockingQueue {
std::mutex m;
std::condition_variable cv;
std::vector<T> q;
public:
void push(T v){ {std::lock_guard<std::mutex> lk(m); q.push_back(std::move(v)); } cv.notify_one(); }
bool pop(T& out){ std::unique_lock<std::mutex> lk(m); cv.wait(lk,[&]{ return !q.empty(); }); out = std::move(q.back()); q.pop_back(); return true; }
};
struct Server {
Settings cfg;
int listen_fd = -1;
std::atomic<bool> running{true};
BlockingQueue<int> queue;
std::vector<std::thread> workers;
explicit Server(Settings s): cfg(std::move(s)) {}
void start() {
listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) { perror("socket"); std::exit(1); }
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 (cfg.address == "0.0.0.0") {
addr.sin_addr.s_addr = htonl(INADDR_ANY);
} else {
in_addr a{}; if (::inet_pton(AF_INET, cfg.address.c_str(), &a) != 1) { std::cerr << "Invalid address\n"; std::exit(1); }
addr.sin_addr = a;
}
if (::bind(listen_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); std::exit(1); }
if (::listen(listen_fd, kBacklog) < 0) { perror("listen"); std::exit(1); }
// workers
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";
accept_loop();
}
void accept_loop(){
while (running.load(std::memory_order_relaxed)) {
int fd = ::accept(listen_fd, nullptr, nullptr);
if (fd < 0) {
if (errno==EINTR) continue;
perror("accept"); continue;
}
set_socket_timeouts(fd);
queue.push(fd);
}
}
void send_json_error(int fd, int code, const std::string& msg, bool keep_alive) {
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.headers.push_back({"X-Error-Detail", msg});
res.body = json({{"error", msg}}).dump();
auto raw = build_response(res, keep_alive);
send_all(fd, raw.data(), raw.size());
}
void worker_loop() {
if (!tltpl) tltpl = std::make_unique<ThreadLocalTemplates>(cfg.template_root);
while (true) {
int fd; queue.pop(fd);
handle_connection(fd);
::shutdown(fd, SHUT_WR);
::close(fd);
}
}
void handle_connection(int fd) {
std::string inbuf; inbuf.reserve(16*1024);
bool keep = true;
while (keep) {
// Ensure we have headers; also guard header size growth
while (true) {
size_t pos = inbuf.find("\r\n\r\n");
if (pos != std::string::npos) {
// Send 100-continue if asked
if (!send_100_continue_if_needed(fd, inbuf.substr(0, pos))) return;
break;
}
if (inbuf.size() > kMaxHeaderBytes) { send_json_error(fd, 431, "headers too large", false); return; }
if (!recv_append(fd, inbuf)) return; // closed or timeout
}
std::optional<Request> reqOpt;
try {
reqOpt = parse_request(inbuf);
} catch (const std::exception& e) {
int code = 400;
std::string what = e.what();
if (what == "payload too large") code = 413;
else if (what == "length required") code = 411;
else if (what == "headers too large") code = 431;
send_json_error(fd, code, what, false);
return;
}
if (!reqOpt) {
// Need more body bytes
if (!recv_append(fd, inbuf)) return;
continue;
}
Request req = std::move(*reqOpt);
keep = req.keep_alive;
if (!(req.method=="POST" && req.target=="/render")) {
send_json_error(fd, 404, "Not Found", keep);
continue;
}
if (!is_json_ct(req.headers)) {
send_json_error(fd, 415, "Content-Type must be application/json", keep);
continue;
}
if (req.body.size() > kMaxBodyBytes) {
send_json_error(fd, 413, "Body too large", keep);
continue;
}
// Parse JSON request
json jreq;
try { jreq = json::parse(req.body); }
catch (const std::exception& e) { send_json_error(fd, 400, std::string("Invalid JSON: ") + e.what(), keep); continue; }
if (!jreq.contains("template") || !jreq["template"].is_string()) {
send_json_error(fd, 400, R"(Missing "template" (string))", keep); continue;
}
if (jreq.contains("context") && !jreq["context"].is_object()) {
send_json_error(fd, 400, R"("context" must be an object)", keep); continue;
}
std::string reason;
fs::path full = secure_resolve_template(cfg.template_root, jreq["template"].get<std::string>(), reason);
if (full.empty()) { send_json_error(fd, 400, "Template error: " + reason, keep); continue; }
json ctx = jreq.value("context", json::object());
// Render
Response res;
auto t0 = std::chrono::steady_clock::now();
try {
const auto& prog = tltpl->get(full);
std::string rendered = MiniInja::render(prog, ctx);
auto t1 = std::chrono::steady_clock::now();
auto us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
res.status = 200; res.reason = "OK";
res.headers.push_back({"Content-Type","text/plain; charset=utf-8"});
res.headers.push_back({"Cache-Control","no-store"});
res.headers.push_back({"X-Render-Time-Us", std::to_string(us)});
res.body = std::move(rendered);
} catch (const std::exception& e) {
send_json_error(fd, 400, std::string("Render error: ") + e.what(), keep);
continue;
}
auto raw = build_response(res, keep);
if (!send_all(fd, raw.data(), raw.size())) return;
}
}
};
// ---- Args & main -------------------------------------------------------------
static Settings parse_args(int argc, char** argv){
Settings s;
for (int i=1;i<argc;i++){
std::string a = argv[i];
auto next = [&]()->std::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 [--address ADDR] [--port PORT] [--threads N]\n"
" --templates, -t Template directory (required)\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::exists(s.template_root) || !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;
}
static std::atomic<bool> g_shutdown{false};
static void on_signal(int){ g_shutdown.store(true); }
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);
std::cout << "Starting " << kServerName << "\n"
<< " Address : " << cfg.address << "\n"
<< " Port : " << cfg.port << "\n"
<< " Threads : " << cfg.threads << "\n"
<< " Templates : " << cfg.template_root << "\n";
Server srv(std::move(cfg));
// simple foreground run; Ctrl+C to stop
srv.start();
} catch (const std::exception& e) {
std::cerr << "Fatal: " << e.what() << "\n";
return 1;
}
return 0;
}