|
// 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;
|
|
}
|