Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
364 changes: 0 additions & 364 deletions src/cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<bool> 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<std::shared_ptr<HttpPendingCommand>> g_http_pending_commands;
static std::atomic<bool> 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<HttpPendingCommand>();
cmd->sql = sql;
cmd->opts = opts;
cmd->format = format;
cmd->completed = false;

{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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 <token>\n"
<< " Or: X-XSQL-Token: <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).
Expand Down Expand Up @@ -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 <secret> 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<std::string>());
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<HttpPendingCommand> cmd;

{
std::unique_lock<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::shared_ptr<HttpPendingCommand>> pending;
{
std::lock_guard<std::mutex> 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<std::mutex> 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
Expand Down
Loading