From aeb7b9acd588c489e6de671a33ad879ec8a877c0 Mon Sep 17 00:00:00 2001 From: Elias Bachaalany Date: Tue, 23 Jun 2026 13:48:18 -0700 Subject: [PATCH] feat(http): honor continue_on_error/include_sql/format on all HTTP paths Pin libxsql v1.0.7 -> v1.0.8 (statement_executor + json_to_script_result). - REPL .http (IDAHTTPServer): add an executor start() overload that sets the thinclient's statement_executor (with auth_token); the .http callback now runs one statement and the thinclient owns run_script + option parsing + formatting. Plugin keeps the legacy string query_fn (whole-script execute_sync), so its proven IDA-main-thread marshaling is untouched. - --http (run_http_mode, hand-rolled queue): parse continue_on_error/include_sql/ format from the query string, thread them through the main-thread command, and render via xsql::script_result_to_{json,text,csv,tsv}. Fixes the previously no-op documented ?continue_on_error=1 / ?include_sql=1 and adds ?format=text|csv|tsv on --http. Behavior is unified on libxsql's run_script + formatters; the --http httplib skeleton remains (full IDAHTTPServer swap TBD). --- CMakeLists.txt | 2 +- src/cli/main.cpp | 53 ++++++++++++++++++++++++++++++++------ src/common/http_server.cpp | 40 +++++++++++++++++++++------- src/common/http_server.hpp | 17 +++++++++++- 4 files changed, 92 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a6b8fa2..49dff5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,7 +67,7 @@ else() message(STATUS "idasql: Fetching libxsql from GitHub...") FetchContent_Declare(libxsql GIT_REPOSITORY https://github.com/0xeb/libxsql.git - GIT_TAG v1.0.7 + GIT_TAG v1.0.8 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(libxsql) diff --git a/src/cli/main.cpp b/src/cli/main.cpp index 6297209..e7271fd 100644 --- a/src/cli/main.cpp +++ b/src/cli/main.cpp @@ -388,9 +388,16 @@ static void run_repl(idasql::Database& db) { } // SQL executor - called on main thread via run_until_stopped() - idasql::HTTPQueryCallback sql_cb = [&db](const std::string& sql) -> std::string { - return query_result_to_json(db, sql); - }; + idasql::HTTPStatementExecutor sql_cb = + [&db](const std::string& stmt, xsql::ScriptStatementResult& out) { + idasql::QueryResult r = db.query(stmt); + out.columns = r.columns; + out.rows.reserve(r.rows.size()); + for (const auto& row : r.rows) out.rows.push_back(row.values); + out.elapsed_ms = static_cast(r.elapsed_ms); + out.success = r.success; + out.error = r.error; + }; // Start with use_queue=true (CLI mode) int port = g_repl_http_server->start(req_port, sql_cb, addr, true); @@ -578,6 +585,8 @@ static void http_signal_handler(int) { // Command queue for main-thread execution (needed for Hex-Rays decompiler) struct HttpPendingCommand { std::string sql; + xsql::ScriptOptions opts; // continue_on_error / include_sql from query string + std::string format = "json"; // json | text | csv | tsv std::string result; bool started = false; bool canceled = false; @@ -592,13 +601,17 @@ static std::deque> g_http_pending_commands; static std::atomic g_http_running{false}; // Queue a command and wait for main thread to execute it -static std::string http_queue_and_wait(const std::string& sql) { +static std::string http_queue_and_wait(const std::string& sql, + const xsql::ScriptOptions& opts = {}, + const std::string& format = "json") { if (!g_http_running.load()) { return xsql::json{{"success", false}, {"error", "Server not running"}}.dump(); } auto cmd = std::make_shared(); cmd->sql = sql; + cmd->opts = opts; + cmd->format = format; cmd->completed = false; { @@ -746,8 +759,26 @@ static int run_http_mode(idasql::Database& db, int port, const std::string& bind res.set_content(xsql::json{{"success", false}, {"error", "Empty query"}}.dump(), "application/json"); return; } - // Queue command for main thread execution - res.set_content(http_queue_and_wait(req.body), "application/json"); + // Parse query-string options (same surface as the libxsql thinclient). + xsql::ScriptOptions opts; + { + auto it = req.params.find("continue_on_error"); + if (it != req.params.end() && it->second == "1") opts.continue_on_error = true; + auto incl = req.params.find("include_sql"); + if (incl != req.params.end() && incl->second == "1") opts.include_sql = true; + } + std::string format = "json"; + { + auto it = req.params.find("format"); + if (it != req.params.end() && !it->second.empty()) format = it->second; + } + // Queue command for main thread execution (Hex-Rays thread affinity). + std::string body = http_queue_and_wait(req.body, opts, format); + const char* ctype = format == "text" ? "text/plain" + : format == "csv" ? "text/csv" + : format == "tsv" ? "text/tab-separated-values" + : "application/json"; + res.set_content(body, ctype); }); // GET /status - Also needs main thread for db.query() @@ -853,8 +884,14 @@ static int run_http_mode(idasql::Database& db, int port, const std::string& bind } if (should_execute) { - // Execute query on main thread - safe for Hex-Rays decompiler - std::string result = query_result_to_json(db, cmd->sql); + // Execute on main thread (safe for Hex-Rays) with the request's + // options, then render per the requested format. + xsql::ScriptResult sr = idasql::run_sql_script(db, cmd->sql, cmd->opts); + std::string result = + cmd->format == "text" ? xsql::script_result_to_text(sr) + : cmd->format == "csv" ? xsql::script_result_to_csv(sr) + : cmd->format == "tsv" ? xsql::script_result_to_tsv(sr) + : xsql::script_result_to_json(sr, cmd->opts.include_sql); { std::lock_guard lock(cmd->done_mutex); cmd->result = std::move(result); diff --git a/src/common/http_server.cpp b/src/common/http_server.cpp index 216839f..cbb17ec 100644 --- a/src/common/http_server.cpp +++ b/src/common/http_server.cpp @@ -43,20 +43,13 @@ static std::string build_http_help_text() { return out.str(); } -int IDAHTTPServer::start(int port, HTTPQueryCallback query_cb, - const std::string& bind_addr, bool use_queue) { - if (impl_ && impl_->is_running()) { - return impl_->port(); - } - - bind_addr_ = bind_addr.empty() ? "127.0.0.1" : bind_addr; - +static xsql::thinclient::http_query_server_config make_idasql_config( + int port, const std::string& bind_addr, bool use_queue) { xsql::thinclient::http_query_server_config config; config.tool_name = "idasql"; config.help_text = build_http_help_text(); config.port = port; - config.bind_address = bind_addr_; - config.query_fn = std::move(query_cb); + config.bind_address = bind_addr; config.use_queue = use_queue; config.queue_admission_timeout_ms_fn = []() { return idasql::runtime_settings().queue_admission_timeout_ms(); @@ -74,7 +67,34 @@ int IDAHTTPServer::start(int port, HTTPQueryCallback query_cb, {"hints_enabled", settings.hints_enabled ? 1 : 0} }; }; + return config; +} +// Legacy: whole-script JSON callback (used by the in-process plugin). +int IDAHTTPServer::start(int port, HTTPQueryCallback query_cb, + const std::string& bind_addr, bool use_queue) { + if (impl_ && impl_->is_running()) { + return impl_->port(); + } + bind_addr_ = bind_addr.empty() ? "127.0.0.1" : bind_addr; + auto config = make_idasql_config(port, bind_addr_, use_queue); + config.query_fn = std::move(query_cb); + impl_ = std::make_unique(config); + return impl_->start(); +} + +// Preferred: single-statement executor (CLI). Enables continue_on_error/ +// include_sql and round-trip-free format=. +int IDAHTTPServer::start(int port, HTTPStatementExecutor executor, + const std::string& bind_addr, bool use_queue, + const std::string& auth_token) { + if (impl_ && impl_->is_running()) { + return impl_->port(); + } + bind_addr_ = bind_addr.empty() ? "127.0.0.1" : bind_addr; + auto config = make_idasql_config(port, bind_addr_, use_queue); + config.statement_executor = std::move(executor); + if (!auth_token.empty()) config.auth_token = auth_token; impl_ = std::make_unique(config); return impl_->start(); } diff --git a/src/common/http_server.hpp b/src/common/http_server.hpp index e26d2ca..3d297c4 100644 --- a/src/common/http_server.hpp +++ b/src/common/http_server.hpp @@ -28,9 +28,17 @@ namespace idasql { -// Callback for handling SQL queries +// Legacy callback: SQL script in, JSON string out. Used by the in-process +// plugin, which marshals whole-script execution to IDA's main thread via +// execute_sync and returns the serialized envelope itself. using HTTPQueryCallback = std::function; +// Preferred path (CLI): a single-statement executor. The thinclient owns +// multi-statement orchestration, query-string options (continue_on_error/ +// include_sql), and output formatting (json/text/csv/tsv) from the ScriptResult. +using HTTPStatementExecutor = + std::function; + class IDAHTTPServer { public: IDAHTTPServer() = default; @@ -54,6 +62,13 @@ class IDAHTTPServer { const std::string& bind_addr = "127.0.0.1", bool use_queue = false); + // Preferred overload: drive the server with a single-statement executor + // (enables continue_on_error/include_sql and round-trip-free format=). + int start(int port, HTTPStatementExecutor executor, + const std::string& bind_addr = "127.0.0.1", + bool use_queue = false, + const std::string& auth_token = ""); + /** * Block until server stops, processing commands on the calling thread. * Only needed when use_queue=true (CLI mode).