#include "HttpControlServer.h" #include "../json/JsonWriter.h" #include #include #include #include namespace RenderCadenceCompositor { namespace { bool IsKnownPostEndpoint(const std::string& path) { return path == "/api/layers/add" || path == "/api/layers/remove" || path == "/api/layers/move" || path == "/api/layers/reorder" || path == "/api/layers/set-bypass" || path == "/api/layers/set-shader" || path == "/api/layers/update-parameter" || path == "/api/layers/reset-parameters" || path == "/api/stack-presets/save" || path == "/api/stack-presets/load" || path == "/api/reload" || path == "/api/screenshot"; } } HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& request) const { if (request.path == "/api/state") return JsonResponse("200 OK", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}"); if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml") return ServeOpenApiSpec(); if (request.path == "/docs" || request.path == "/docs/") return ServeSwaggerDocs(); if (request.path == "/" || request.path == "/index.html") return ServeUiAsset("index.html"); if (request.path.rfind("/assets/", 0) == 0) return ServeUiAsset(request.path.substr(1)); if (request.path.size() > 1) { const HttpResponse asset = ServeUiAsset(request.path.substr(1)); if (asset.status != "404 Not Found") return asset; } return ServeUiAsset("index.html"); } HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const { if (!IsKnownPostEndpoint(request.path)) return TextResponse("404 Not Found", "Not Found"); if (mCallbacks.executePost) { const ControlActionResult result = mCallbacks.executePost(request.path, request.body); return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error)); } if (request.path == "/api/layers/add" && mCallbacks.addLayer) { const ControlActionResult result = mCallbacks.addLayer(request.body); return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error)); } if (request.path == "/api/layers/remove" && mCallbacks.removeLayer) { const ControlActionResult result = mCallbacks.removeLayer(request.body); return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error)); } return { "400 Bad Request", "application/json", ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.") }; } HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const { const std::filesystem::path path = mDocsRoot / "openapi.yaml"; const std::string body = LoadTextFile(path); return body.empty() ? TextResponse("404 Not Found", "OpenAPI spec not found") : HttpResponse{ "200 OK", GuessContentType(path), body }; } HttpControlServer::HttpResponse HttpControlServer::ServeSwaggerDocs() const { std::ostringstream html; html << "\n" << "Video Shader Toys API Docs\n" << "\n" << "
\n" << "\n" << "\n" << "\n"; return { "200 OK", "text/html", html.str() }; } HttpControlServer::HttpResponse HttpControlServer::ServeUiAsset(const std::string& relativePath) const { if (mUiRoot.empty()) return TextResponse("404 Not Found", "UI root is not configured"); const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal(); if (!IsSafeRelativePath(sanitizedPath)) return TextResponse("404 Not Found", "Not Found"); const std::filesystem::path path = mUiRoot / sanitizedPath; const std::string body = LoadTextFile(path); if (body.empty()) return TextResponse("404 Not Found", "Not Found"); return { "200 OK", GuessContentType(path), body }; } std::string HttpControlServer::LoadTextFile(const std::filesystem::path& path) const { std::ifstream input(path, std::ios::binary); if (!input) return std::string(); std::ostringstream buffer; buffer << input.rdbuf(); return buffer.str(); } HttpControlServer::HttpResponse HttpControlServer::JsonResponse(const std::string& status, const std::string& body) { return { status, "application/json", body }; } HttpControlServer::HttpResponse HttpControlServer::TextResponse(const std::string& status, const std::string& body) { return { status, "text/plain", body }; } HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::string& status, const std::string& body) { return { status, "text/html", body }; } std::string HttpControlServer::ActionResponse(bool ok, const std::string& error) { JsonWriter writer; writer.BeginObject(); writer.KeyBool("ok", ok); if (!error.empty()) writer.KeyString("error", error); writer.EndObject(); return writer.StringValue(); } std::string HttpControlServer::GuessContentType(const std::filesystem::path& path) { const std::string extension = ToLower(path.extension().string()); if (extension == ".yaml" || extension == ".yml") return "application/yaml"; if (extension == ".json") return "application/json"; if (extension == ".js" || extension == ".mjs") return "text/javascript"; if (extension == ".css") return "text/css"; if (extension == ".html" || extension == ".htm") return "text/html"; if (extension == ".svg") return "image/svg+xml"; if (extension == ".png") return "image/png"; if (extension == ".jpg" || extension == ".jpeg") return "image/jpeg"; if (extension == ".ico") return "image/x-icon"; if (extension == ".map") return "application/json"; return "text/plain"; } bool HttpControlServer::IsSafeRelativePath(const std::filesystem::path& path) { if (path.empty() || path.is_absolute()) return false; for (const std::filesystem::path& part : path) { if (part == "..") return false; } return true; } std::string HttpControlServer::ToLower(std::string text) { std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) { return static_cast(std::tolower(character)); }); return text; } }