////////////////////////////////////////////////////////////////////// // This file is part of Remere's Map Editor ////////////////////////////////////////////////////////////////////// // Remere's Map Editor is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Remere's Map Editor is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . ////////////////////////////////////////////////////////////////////// #include "main.h" #include "lua_api_http.h" #include #include #include #include #include #include #include #include #include #include namespace LuaAPI { // StreamSession class for managing streaming HTTP requests class StreamSession { public: static constexpr size_t kMaxBufferedBytes = 4 * 1024 * 1024; // 4 MB StreamSession() = default; ~StreamSession() { if (!worker_.joinable()) { return; } if (worker_.get_id() == std::this_thread::get_id()) { worker_.detach(); } else { worker_.join(); } } bool appendChunk(const std::string &chunk) { std::scoped_lock lock(mutex_); if (bufferedBytes_ + chunk.size() > kMaxBufferedBytes) { hasError_ = true; finished_ = true; errorMessage_ = "HTTP stream buffer limit exceeded"; cv_.notify_all(); return false; } chunks_.push(chunk); bufferedBytes_ += chunk.size(); cv_.notify_one(); return true; } std::string getNextChunk() { std::scoped_lock lock(mutex_); if (chunks_.empty()) { return ""; } std::string chunk = chunks_.front(); chunks_.pop(); bufferedBytes_ -= chunk.size(); return chunk; } bool hasChunks() { std::scoped_lock lock(mutex_); return !chunks_.empty(); } void setFinished() { std::scoped_lock lock(mutex_); finished_ = true; cv_.notify_all(); } bool isFinished() { std::scoped_lock lock(mutex_); return finished_; } void setError(std::string_view error) { std::scoped_lock lock(mutex_); hasError_ = true; errorMessage_ = error; finished_ = true; cv_.notify_all(); } bool hasError() { std::scoped_lock lock(mutex_); return hasError_; } std::string getError() { std::scoped_lock lock(mutex_); return errorMessage_; } void setStatusCode(int code) { statusCode_ = code; } int getStatusCode() { return statusCode_; } void setHeaders(const cpr::Header &headers) { std::scoped_lock lock(mutex_); responseHeaders_ = headers; } cpr::Header getHeaders() { std::scoped_lock lock(mutex_); return responseHeaders_; } void cancel() { finished_ = true; cv_.notify_all(); } void setWorker(std::thread &&t) { worker_ = std::move(t); } private: std::queue chunks_; size_t bufferedBytes_ = 0; std::mutex mutex_; std::condition_variable cv_; std::atomic finished_ = false; std::atomic hasError_ = false; std::string errorMessage_; std::atomic statusCode_ = 0; cpr::Header responseHeaders_; std::thread worker_; }; static std::map> &streamSessions() { static std::map> instance; return instance; } static std::mutex &sessionsMutex() { static std::mutex instance; return instance; } static int generateSessionId() { static int id = 0; return ++id; } // Security helper: Block localhost and loopback static bool isUrlSafe(const std::string &url_str) { std::string low = url_str; std::transform(low.begin(), low.end(), low.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); // Block common localhost patterns if (low.contains("localhost") || low.contains("127.") || low.contains("0.0.0.0") || low.contains("[::1]") || low.contains("[::ffff:127.") || low.contains("//[::")) { return false; } // Block file:// and other dangerous schemes if (low.find("file://") == 0 || low.find("ftp://") == 0) { return false; } return true; } // HTTP GET request static sol::table httpGet(sol::this_state ts, const std::string &url, sol::optional optHeaders) { sol::state_view lua(ts); sol::table result = lua.create_table(); if (!isUrlSafe(url)) { result["error"] = "Security: URL blocked (Localhost access denied)"; result["ok"] = false; return result; } cpr::Header headers; if (optHeaders) { sol::table headersTable = *optHeaders; for (auto &pair : headersTable) { if (pair.first.is() && pair.second.is()) { headers[pair.first.as()] = pair.second.as(); } } } cpr::Response response = cpr::Get(cpr::Url { url }, headers, cpr::Timeout { 10000 }); result["status"] = static_cast(response.status_code); result["body"] = response.text; result["error"] = response.error.message; result["ok"] = response.status_code >= 200 && response.status_code < 300; // Parse response headers sol::table respHeaders = lua.create_table(); for (const auto &h : response.header) { respHeaders[h.first] = h.second; } result["headers"] = respHeaders; return result; } // HTTP POST request static sol::table httpPost(sol::this_state ts, const std::string &url, const std::string &body, sol::optional optHeaders) { sol::state_view lua(ts); sol::table result = lua.create_table(); if (!isUrlSafe(url)) { result["error"] = "Security: URL blocked (Localhost access denied)"; result["ok"] = false; return result; } cpr::Header headers; if (optHeaders) { sol::table headersTable = *optHeaders; for (auto &pair : headersTable) { if (pair.first.is() && pair.second.is()) { headers[pair.first.as()] = pair.second.as(); } } } cpr::Response response = cpr::Post( cpr::Url { url }, cpr::Body { body }, headers, cpr::Timeout { 10000 } ); result["status"] = static_cast(response.status_code); result["body"] = response.text; result["error"] = response.error.message; result["ok"] = response.status_code >= 200 && response.status_code < 300; // Parse response headers sol::table respHeaders = lua.create_table(); for (const auto &h : response.header) { respHeaders[h.first] = h.second; } result["headers"] = respHeaders; return result; } // Helper function to convert Lua table to JSON static nlohmann::json luaToJson(sol::object obj, std::unordered_set &visited) { if (obj.is()) { return obj.as(); } else if (obj.is()) { return obj.as(); } else if (obj.is()) { return obj.as(); } else if (obj.is()) { return obj.as(); } else if (obj.is()) { sol::table tbl = obj.as(); auto ptr = tbl.pointer(); if (!visited.insert(ptr).second) { throw sol::error("Cyclic table detected in JSON conversion"); } // Check if it's an array (sequential integer keys starting at 1) bool isArray = true; size_t expectedKey = 1; for (auto &pair : tbl) { if (!pair.first.is() || pair.first.as() != expectedKey) { isArray = false; break; } expectedKey++; } nlohmann::json result; if (isArray && expectedKey > 1) { result = nlohmann::json::array(); for (auto &pair : tbl) { result.push_back(luaToJson(pair.second, visited)); } } else { result = nlohmann::json::object(); for (auto &pair : tbl) { std::string key; if (pair.first.is()) { key = pair.first.as(); } else if (pair.first.is()) { key = std::to_string(pair.first.as()); } else { continue; } result[key] = luaToJson(pair.second, visited); } } visited.erase(ptr); return result; } else if (obj.is()) { return nullptr; } return nullptr; } // HTTP POST with JSON body static sol::table httpPostJson(sol::this_state ts, const std::string &url, sol::table jsonBody, sol::optional optHeaders) { sol::state_view lua(ts); std::unordered_set visited; std::string jsonStr = luaToJson(jsonBody, visited).dump(); // Add Content-Type header if not present sol::table headers = lua.create_table(); headers["Content-Type"] = "application/json"; if (optHeaders) { for (auto &pair : *optHeaders) { if (pair.first.is() && pair.second.is()) { headers[pair.first.as()] = pair.second.as(); } } } return httpPost(ts, url, jsonStr, headers); } // Start a streaming POST request - returns session ID static sol::table httpPostStream(sol::this_state ts, const std::string &url, const std::string &body, sol::optional optHeaders) { sol::state_view lua(ts); sol::table result = lua.create_table(); if (!isUrlSafe(url)) { result["error"] = "Security: URL blocked (Localhost access denied)"; result["ok"] = false; return result; } auto session = std::make_shared(); int sessionId; { std::scoped_lock lock(sessionsMutex()); sessionId = generateSessionId(); streamSessions()[sessionId] = session; } cpr::Header headers; if (optHeaders) { sol::table headersTable = *optHeaders; for (auto &pair : headersTable) { if (pair.first.is() && pair.second.is()) { headers[pair.first.as()] = pair.second.as(); } } } // Start the streaming request in a separate thread session->setWorker(std::thread([session, url, body, headers]() { std::function writeCallback = [session](std::string_view data, intptr_t /*userdata*/) { return session->appendChunk(std::string(data)); }; cpr::Response response = cpr::Post( cpr::Url { url }, cpr::Body { body }, headers, cpr::WriteCallback { writeCallback, 0 }, cpr::Timeout { 30000 } ); session->setStatusCode(static_cast(response.status_code)); session->setHeaders(response.header); if (response.error) { session->setError(response.error.message); } else { session->setFinished(); } })); result["sessionId"] = sessionId; result["ok"] = true; return result; } // Start a streaming POST request with JSON body - returns session ID static sol::table httpPostJsonStream(sol::this_state ts, const std::string &url, sol::table jsonBody, sol::optional optHeaders) { sol::state_view lua(ts); std::unordered_set visited; std::string jsonStr = luaToJson(jsonBody, visited).dump(); // Add Content-Type header if not present sol::table headers = lua.create_table(); headers["Content-Type"] = "application/json"; if (optHeaders) { for (auto &pair : *optHeaders) { if (pair.first.is() && pair.second.is()) { headers[pair.first.as()] = pair.second.as(); } } } return httpPostStream(ts, url, jsonStr, headers); } // Read available chunks from a stream session static sol::table httpStreamRead(sol::this_state ts, int sessionId) { sol::state_view lua(ts); sol::table result = lua.create_table(); std::shared_ptr session; { std::scoped_lock lock(sessionsMutex()); auto it = streamSessions().find(sessionId); if (it == streamSessions().end()) { result["ok"] = false; result["error"] = "Invalid session ID"; result["finished"] = true; return result; } session = it->second; } // Collect all available chunks std::string data; while (session->hasChunks()) { data += session->getNextChunk(); } result["data"] = data; result["finished"] = session->isFinished(); result["hasError"] = session->hasError(); if (session->hasError()) { result["error"] = session->getError(); result["ok"] = false; } else if (session->isFinished()) { int status = session->getStatusCode(); result["ok"] = status >= 200 && status < 300; } else { result["ok"] = true; } if (session->isFinished()) { result["status"] = session->getStatusCode(); // Return headers when finished sol::table respHeaders = lua.create_table(); for (const auto &h : session->getHeaders()) { respHeaders[h.first] = h.second; } result["headers"] = respHeaders; } return result; } // Close and cleanup a stream session static bool httpStreamClose(int sessionId) { std::scoped_lock lock(sessionsMutex()); if (auto it = streamSessions().find(sessionId); it != streamSessions().end()) { it->second->cancel(); streamSessions().erase(it); return true; } return false; } // Check if a stream session is finished static sol::table httpStreamStatus(sol::this_state ts, int sessionId) { sol::state_view lua(ts); sol::table result = lua.create_table(); std::shared_ptr session; { std::scoped_lock lock(sessionsMutex()); auto it = streamSessions().find(sessionId); if (it == streamSessions().end()) { result["valid"] = false; result["finished"] = true; return result; } session = it->second; } result["valid"] = true; result["finished"] = session->isFinished(); result["hasError"] = session->hasError(); result["hasData"] = session->hasChunks(); if (session->hasError()) { result["error"] = session->getError(); } if (session->isFinished()) { result["status"] = session->getStatusCode(); } return result; } void registerHttp(sol::state &lua) { sol::table http = lua.create_named_table("http"); // Basic HTTP methods http["get"] = httpGet; http["post"] = httpPost; http["postJson"] = httpPostJson; // Streaming HTTP methods http["postStream"] = httpPostStream; http["postJsonStream"] = httpPostJsonStream; http["streamRead"] = httpStreamRead; http["streamClose"] = httpStreamClose; http["streamStatus"] = httpStreamStatus; } } // namespace LuaAPI