diff --git a/src/cli/main.cpp b/src/cli/main.cpp index 85a0ff2..56e7fe7 100644 --- a/src/cli/main.cpp +++ b/src/cli/main.cpp @@ -229,9 +229,6 @@ static void print_query_warnings(std::ostream& os, const idasql::QueryResult& re // REPL - Interactive Mode (Local) // ============================================================================ -// Forward declaration (defined in HTTP section below) -static std::string query_result_to_json(idasql::Database& db, const std::string& sql); - static void run_repl(idasql::Database& db) { std::string line; std::string query; @@ -574,138 +571,12 @@ static bool execute_file(idasql::Database& db, const char* path) { // HTTP Server Mode // ============================================================================ -static xsql::thinclient::server* g_http_server = nullptr; static std::atomic g_http_stop_requested{false}; static void http_signal_handler(int) { g_http_stop_requested.store(true); - if (g_http_server) g_http_server->stop(); } -// 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; - bool completed = false; - std::mutex done_mutex; - std::condition_variable done_cv; -}; - -static std::mutex g_http_queue_mutex; -static std::condition_variable g_http_queue_cv; -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, - 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; - - { - std::lock_guard lock(g_http_queue_mutex); - const size_t max_queue = idasql::runtime_settings().max_queue(); - if (max_queue > 0 && g_http_pending_commands.size() >= max_queue) { - return xsql::json{ - {"success", false}, - {"error", "Queue full"}, - {"hint", "Raise PRAGMA idasql.max_queue or reduce request concurrency"} - }.dump(); - } - g_http_pending_commands.push_back(cmd); - } - g_http_queue_cv.notify_one(); - - // Wait for completion (or queue admission timeout). - const int timeout_ms = idasql::runtime_settings().queue_admission_timeout_ms(); - std::unique_lock lock(cmd->done_mutex); - if (timeout_ms <= 0) { - while (!cmd->completed) { - cmd->done_cv.wait_for(lock, std::chrono::milliseconds(100)); - } - } else { - const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms); - while (!cmd->completed) { - if (cmd->started) { - cmd->done_cv.wait_for(lock, std::chrono::milliseconds(100)); - continue; - } - - if (cmd->done_cv.wait_until(lock, deadline, - [&]() { return cmd->completed || cmd->started; })) { - continue; - } - - // Timed out before command admission: mark canceled and remove from pending queue. - if (!cmd->completed && !cmd->started) { - cmd->canceled = true; - } - lock.unlock(); - { - std::lock_guard qlock(g_http_queue_mutex); - auto it = std::find(g_http_pending_commands.begin(), g_http_pending_commands.end(), cmd); - if (it != g_http_pending_commands.end()) { - g_http_pending_commands.erase(it); - } - } - - return xsql::json{ - {"success", false}, - {"error", "Request timed out while waiting in queue"}, - {"hint", "Raise PRAGMA idasql.queue_admission_timeout_ms or reduce request concurrency"} - }.dump(); - } - } - - return cmd->result; -} - -static std::string query_result_to_json(idasql::Database& db, const std::string& sql) { - auto result = idasql::run_sql_script(db, sql); - return xsql::script_result_to_json(result); -} - -static std::string build_cli_http_help_text() { - std::ostringstream out; - out << "IDASQL HTTP REST API\n" - << "====================\n\n" - << "SQL interface for IDA Pro databases via HTTP.\n\n" - << "Endpoints:\n" - << " GET / - Welcome message\n" - << " GET /help - This documentation (for LLM discovery)\n" - << " POST /query - Execute SQL query or script (body = raw SQL, response = JSON)\n" - << " GET /status - Server health\n" - << " POST /shutdown - Stop server\n\n" - << "Discover Schema:\n" - << " SELECT name, type FROM sqlite_master WHERE type IN ('table','view') ORDER BY type, name;\n" - << " PRAGMA table_info(funcs);\n\n" - << "Starter Queries:\n" - << " SELECT * FROM welcome;\n" - << " SELECT name, start_ea, size FROM funcs ORDER BY size DESC LIMIT 10;\n\n" - << "Response Format:\n" - << " Success: {\"success\": true, \"columns\": [...], \"rows\": [[...]], \"row_count\": N}\n" - << " Script: {\"success\": true, \"statements\": [{\"columns\": [...], \"rows\": [[...]], \"row_count\": N}], \"statement_count\": N}\n" - << " Error: {\"success\": false, \"error\": \"message\"}\n\n" - << "Authentication (if enabled):\n" - << " Header: Authorization: Bearer \n" - << " Or: X-XSQL-Token: \n\n" - << "Example:\n" - << " curl http://localhost:8080/help\n" - << " " << idasql::format_query_curl_example("http://localhost:8080") << "\n"; - return out.str(); -} // CLI --http server, on the shared libxsql thinclient (use_queue=true: queries // run on this main thread via run_until_stopped, for Hex-Rays thread affinity). @@ -756,241 +627,6 @@ static int run_http_mode(idasql::Database& db, int port, const std::string& bind return 0; } -// Superseded by the thinclient-based run_http_mode above; retained briefly and -// no longer called (cleanup follow-up). -static int run_http_mode_legacy(idasql::Database& db, int port, const std::string& bind_addr, const std::string& auth_token) { - xsql::thinclient::server_config cfg; - cfg.port = port; - cfg.bind_address = bind_addr.empty() ? "127.0.0.1" : bind_addr; - if (!auth_token.empty()) cfg.auth_token = auth_token; - // Allow non-loopback binds if explicitly requested (with warning) - if (!bind_addr.empty() && bind_addr != "127.0.0.1" && bind_addr != "localhost") { - cfg.allow_insecure_no_auth = auth_token.empty(); - std::cerr << "WARNING: Binding to non-loopback address " << bind_addr << "\n"; - if (auth_token.empty()) { - std::cerr << "WARNING: No authentication token set. Server is accessible without authentication.\n"; - std::cerr << " Consider using --token for remote access.\n"; - } - } - - cfg.setup_routes = [&auth_token, port](httplib::Server& svr) { - svr.Get("/", [port](const httplib::Request&, httplib::Response& res) { - const std::string base_url = "http://localhost:" + std::to_string(port); - std::string welcome = "IDASQL HTTP Server\n\nEndpoints:\n" - " GET /help - API documentation\n" - " POST /query - Execute SQL query or script\n" - " GET /status - Health check\n" - " POST /shutdown - Stop server\n\n" - "Example: " + idasql::format_query_curl_example(base_url) + "\n"; - res.set_content(welcome, "text/plain"); - }); - - svr.Get("/help", [](const httplib::Request&, httplib::Response& res) { - res.set_content(build_cli_http_help_text(), "text/plain"); - }); - - // POST /query - Queue command for main thread execution - // This is necessary because IDA's Hex-Rays decompiler has thread affinity - svr.Post("/query", [&auth_token](const httplib::Request& req, httplib::Response& res) { - if (!auth_token.empty()) { - std::string token; - if (req.has_header("X-XSQL-Token")) token = req.get_header_value("X-XSQL-Token"); - else if (req.has_header("Authorization")) { - auto auth = req.get_header_value("Authorization"); - if (auth.rfind("Bearer ", 0) == 0) token = auth.substr(7); - } - if (token != auth_token) { - res.status = 401; - res.set_content(xsql::json{{"success", false}, {"error", "Unauthorized"}}.dump(), "application/json"); - return; - } - } - if (req.body.empty()) { - res.status = 400; - res.set_content(xsql::json{{"success", false}, {"error", "Empty query"}}.dump(), "application/json"); - return; - } - // 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() - svr.Get("/status", [&auth_token](const httplib::Request& req, httplib::Response& res) { - if (!auth_token.empty()) { - std::string token; - if (req.has_header("X-XSQL-Token")) token = req.get_header_value("X-XSQL-Token"); - else if (req.has_header("Authorization")) { - auto auth = req.get_header_value("Authorization"); - if (auth.rfind("Bearer ", 0) == 0) token = auth.substr(7); - } - if (token != auth_token) { - res.status = 401; - res.set_content(xsql::json{{"success", false}, {"error", "Unauthorized"}}.dump(), "application/json"); - return; - } - } - // Queue for main thread - std::string result = http_queue_and_wait("SELECT COUNT(*) FROM funcs"); - // Parse result to extract count - try { - auto j = xsql::json::parse(result); - if (j.value("success", false) && j.contains("rows") && !j["rows"].empty()) { - int count = std::stoi(j["rows"][0][0].get()); - res.set_content(xsql::json{{"success", true}, {"status", "ok"}, {"tool", "idasql"}, {"functions", count}}.dump(), "application/json"); - return; - } - } catch (...) {} - res.set_content(xsql::json{{"success", true}, {"status", "ok"}, {"tool", "idasql"}, {"functions", "?"}}.dump(), "application/json"); - }); - - svr.Post("/shutdown", [&svr, &auth_token](const httplib::Request& req, httplib::Response& res) { - if (!auth_token.empty()) { - std::string token; - if (req.has_header("X-XSQL-Token")) token = req.get_header_value("X-XSQL-Token"); - else if (req.has_header("Authorization")) { - auto auth = req.get_header_value("Authorization"); - if (auth.rfind("Bearer ", 0) == 0) token = auth.substr(7); - } - if (token != auth_token) { - res.status = 401; - res.set_content(xsql::json{{"success", false}, {"error", "Unauthorized"}}.dump(), "application/json"); - return; - } - } - res.set_content(xsql::json{{"success", true}, {"message", "Shutting down"}}.dump(), "application/json"); - g_http_stop_requested.store(true); - g_http_queue_cv.notify_all(); - std::thread([&svr] { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - svr.stop(); - }).detach(); - }); - }; - - xsql::thinclient::server http_server(cfg); - g_http_server = &http_server; - g_http_running.store(true); - g_http_stop_requested.store(false); - - auto old_handler = std::signal(SIGINT, http_signal_handler); -#ifdef _WIN32 - auto old_break_handler = std::signal(SIGBREAK, http_signal_handler); -#else - auto old_term_handler = std::signal(SIGTERM, http_signal_handler); -#endif - - // Start HTTP server on a background thread (resolves random port) - http_server.run_async(); - int actual_port = http_server.port(); - - std::cout << "IDASQL HTTP server: http://" << cfg.bind_address << ":" << actual_port << "\n"; - std::cout << "Database: " << db.info() << "\n"; - std::cout << "Press Ctrl+C to stop.\n\n"; - std::cout.flush(); - - // Main thread processes the command queue (required for Hex-Rays thread affinity) - while (g_http_running.load() && !g_http_stop_requested.load()) { - std::shared_ptr cmd; - - { - std::unique_lock lock(g_http_queue_mutex); - if (g_http_queue_cv.wait_for(lock, std::chrono::milliseconds(100), - []() { return !g_http_pending_commands.empty() || - g_http_stop_requested.load(); })) { - if (!g_http_pending_commands.empty()) { - cmd = g_http_pending_commands.front(); - g_http_pending_commands.pop_front(); - } - } - } - - if (cmd) { - bool should_execute = false; - { - std::lock_guard lock(cmd->done_mutex); - if (!cmd->completed && !cmd->canceled) { - cmd->started = true; - should_execute = true; - } else if (!cmd->completed && cmd->canceled) { - cmd->completed = true; - } - } - - if (should_execute) { - // 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); - cmd->completed = true; - } - } - - cmd->done_cv.notify_one(); - } - } - - // Cleanup - g_http_running.store(false); - g_http_queue_cv.notify_all(); - - // Complete any pending commands with error - std::deque> pending; - { - std::lock_guard lock(g_http_queue_mutex); - pending.swap(g_http_pending_commands); - } - while (!pending.empty()) { - auto cmd = pending.front(); - pending.pop_front(); - if (!cmd) continue; - { - std::lock_guard dlock(cmd->done_mutex); - if (!cmd->completed) { - cmd->result = xsql::json{{"success", false}, {"error", "Server stopped"}}.dump(); - cmd->completed = true; - } - } - cmd->done_cv.notify_one(); - } - - // Stop HTTP server (run_async thread joined internally) - http_server.stop(); - - std::signal(SIGINT, old_handler); -#ifdef _WIN32 - std::signal(SIGBREAK, old_break_handler); -#else - std::signal(SIGTERM, old_term_handler); -#endif - g_http_server = nullptr; - std::cout << "\nHTTP server stopped.\n"; - return 0; -} // ============================================================================ // Main