package eu.starsong.ghidra; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; // Added for request IDs import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; // Added for transaction helper import javax.swing.SwingUtilities; // For JSON response handling import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; import ghidra.app.decompiler.DecompInterface; import ghidra.app.decompiler.DecompileResults; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.services.ProgramManager; import ghidra.framework.main.ApplicationLevelPlugin; import ghidra.framework.model.Project; import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.util.PluginStatus; import ghidra.program.model.address.Address; import ghidra.program.model.address.GlobalNamespace; import ghidra.program.model.data.DataType; import ghidra.program.model.data.DataTypeManager; import ghidra.program.model.listing.CodeUnit; import ghidra.program.model.listing.Data; import ghidra.program.model.listing.DataIterator; import ghidra.program.model.listing.Function; import ghidra.program.model.listing.Listing; import ghidra.program.model.listing.Parameter; import ghidra.program.model.listing.Program; import ghidra.program.model.listing.VariableStorage; import ghidra.program.model.mem.MemoryBlock; import ghidra.program.model.pcode.HighFunction; import ghidra.program.model.pcode.HighFunctionDBUtil; import ghidra.program.model.pcode.HighFunctionDBUtil.ReturnCommitOption; import ghidra.program.model.pcode.HighSymbol; import ghidra.program.model.pcode.LocalSymbolMap; import ghidra.program.model.symbol.Namespace; import ghidra.program.model.symbol.SourceType; import ghidra.program.model.symbol.Symbol; import ghidra.program.model.symbol.SymbolIterator; import ghidra.program.model.symbol.SymbolTable; import ghidra.program.model.symbol.SymbolType; import ghidra.util.Msg; import ghidra.util.task.ConsoleTaskMonitor; // Functional interface for Ghidra operations that might throw exceptions @FunctionalInterface interface GhidraSupplier { T get() throws Exception; } @PluginInfo( status = PluginStatus.RELEASED, packageName = ghidra.app.DeveloperPluginPackage.NAME, category = PluginCategoryNames.ANALYSIS, shortDescription = "GhydraMCP Plugin for AI Analysis", description = "Exposes program data via HTTP API for AI-assisted reverse engineering with MCP (Model Context Protocol).", servicesRequired = { ProgramManager.class } ) public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { // Plugin version information private static final String PLUGIN_VERSION = "v1.0.0"; // Update this with each release private static final int API_VERSION = 1; // Increment when API changes in a breaking way private static final Map activeInstances = new ConcurrentHashMap<>(); private static final Object baseInstanceLock = new Object(); private HttpServer server; private int port; private boolean isBaseInstance = false; public GhydraMCPPlugin(PluginTool tool) { super(tool); this.port = findAvailablePort(); activeInstances.put(port, this); synchronized (baseInstanceLock) { if (port == 8192 || activeInstances.get(8192) == null) { this.isBaseInstance = true; Msg.info(this, "Starting as base instance on port " + port); } } Msg.info(this, "GhydraMCPPlugin loaded on port " + port); System.out.println("[GhydraMCP] Plugin loaded on port " + port); try { startServer(); } catch (IOException e) { Msg.error(this, "Failed to start HTTP server on port " + port, e); if (e.getMessage().contains("Address already in use")) { Msg.showError(this, null, "Port Conflict", "Port " + port + " is already in use. Please specify a different port with -Dghidra.mcp.port=NEW_PORT"); } } } private void startServer() throws IOException { server = HttpServer.create(new InetSocketAddress(port), 0); // Meta endpoints server.createContext("/plugin-version", exchange -> { if ("GET".equals(exchange.getRequestMethod())) { JsonObject response = createBaseResponse(exchange); response.addProperty("success", true); JsonObject result = new JsonObject(); result.addProperty("plugin_version", PLUGIN_VERSION); result.addProperty("api_version", API_VERSION); response.add("result", result); JsonObject links = new JsonObject(); links.add("self", createLink("/plugin-version")); response.add("_links", links); sendJsonResponse(exchange, response, 200); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Program resources server.createContext("/programs", exchange -> { try { if ("GET".equals(exchange.getRequestMethod())) { List> programs = new ArrayList<>(); Program program = getCurrentProgram(); if (program != null) { Map progInfo = new HashMap<>(); progInfo.put("program_id", program.getDomainFile().getPathname()); progInfo.put("name", program.getName()); progInfo.put("language_id", program.getLanguageID().getIdAsString()); progInfo.put("compiler_spec_id", program.getCompilerSpec().getCompilerSpecID().getIdAsString()); progInfo.put("image_base", program.getImageBase().toString()); progInfo.put("memory_size", program.getMemory().getSize()); progInfo.put("is_open", true); progInfo.put("analysis_complete", program.getListing().getNumDefinedData() > 0); programs.add(progInfo); } JsonObject response = createSuccessResponse(exchange, programs); response.add("_links", createLinks() .add("self", "/programs") .add("create", "/programs", "POST") .build()); sendJsonResponse(exchange, response, 200); } else if ("POST".equals(exchange.getRequestMethod())) { sendErrorResponse(exchange, 501, "Not Implemented", "NOT_IMPLEMENTED"); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { sendErrorResponse(exchange, 500, "Internal server error", "INTERNAL_ERROR"); } }); server.createContext("/programs/", exchange -> { try { String path = exchange.getRequestURI().getPath(); String programId = path.substring("/programs/".length()); if ("GET".equals(exchange.getRequestMethod())) { Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 404, "Program not found", "PROGRAM_NOT_FOUND"); return; } Map programInfo = new HashMap<>(); programInfo.put("program_id", program.getDomainFile().getPathname()); programInfo.put("name", program.getName()); programInfo.put("language_id", program.getLanguageID().getIdAsString()); programInfo.put("compiler_spec_id", program.getCompilerSpec().getCompilerSpecID().getIdAsString()); programInfo.put("image_base", program.getImageBase().toString()); programInfo.put("memory_size", program.getMemory().getSize()); programInfo.put("is_open", true); programInfo.put("analysis_complete", program.getListing().getNumDefinedData() > 0); JsonObject links = new JsonObject(); links.add("self", createLink("/programs/" + programId)); links.add("project", createLink("/projects/" + program.getDomainFile().getProjectLocator().getName())); links.add("functions", createLink("/programs/" + programId + "/functions")); links.add("symbols", createLink("/programs/" + programId + "/symbols")); links.add("data", createLink("/programs/" + programId + "/data")); links.add("segments", createLink("/programs/" + programId + "/segments")); links.add("memory", createLink("/programs/" + programId + "/memory")); links.add("xrefs", createLink("/programs/" + programId + "/xrefs")); links.add("analysis", createLink("/programs/" + programId + "/analysis")); JsonObject response = createSuccessResponse(exchange, programInfo, links); sendJsonResponse(exchange, response, 200); } else if ("DELETE".equals(exchange.getRequestMethod())) { sendErrorResponse(exchange, 501, "Not Implemented", "NOT_IMPLEMENTED"); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { sendErrorResponse(exchange, 500, "Internal server error", "INTERNAL_ERROR"); } }); // Meta endpoints server.createContext("/plugin-version", exchange -> { if ("GET".equals(exchange.getRequestMethod())) { JsonObject response = createBaseResponse(exchange); response.addProperty("success", true); JsonObject result = new JsonObject(); result.addProperty("plugin_version", PLUGIN_VERSION); result.addProperty("api_version", API_VERSION); response.add("result", result); JsonObject links = new JsonObject(); links.add("self", createLink("/plugin-version")); response.add("_links", links); sendJsonResponse(exchange, response, 200); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Project resources server.createContext("/projects", exchange -> { try { if ("GET".equals(exchange.getRequestMethod())) { List> projects = new ArrayList<>(); Project project = tool.getProject(); if (project != null) { Map projInfo = new HashMap<>(); projInfo.put("name", project.getName()); projInfo.put("location", project.getProjectLocator().toString()); projects.add(projInfo); } JsonObject response = createSuccessResponse(exchange, projects); response.add("_links", createLinks() .add("self", "/projects") .add("create", "/projects", "POST") .build()); sendJsonResponse(exchange, response, 200); } else if ("POST".equals(exchange.getRequestMethod())) { sendErrorResponse(exchange, 501, "Not Implemented", "NOT_IMPLEMENTED"); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { sendErrorResponse(exchange, 500, "Internal server error", "INTERNAL_ERROR"); } }); // Function resources server.createContext("/functions", exchange -> { try { if ("GET".equals(exchange.getRequestMethod())) { Map qparams = parseQueryParams(exchange); int offset = parseIntOrDefault(qparams.get("offset"), 0); int limit = parseIntOrDefault(qparams.get("limit"), 100); String query = qparams.get("query"); Object resultData; if (query != null && !query.isEmpty()) { // TODO: Refactor searchFunctionsByName to return List> or similar resultData = searchFunctionsByName(query, offset, limit); } else { // TODO: Refactor getAllFunctionNames to return List> or similar resultData = getAllFunctionNames(offset, limit); } // Temporary check for old error format if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { sendJsonResponse(exchange, (JsonObject)resultData, 400); } else { sendJsonResponse(exchange, resultData); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error in /functions endpoint", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); server.createContext("/functions/", exchange -> { String path = exchange.getRequestURI().getPath(); String[] pathParts = path.split("/"); if (pathParts.length < 3) { sendErrorResponse(exchange, 400, "Invalid path format", "INVALID_PATH"); return; } String functionName = ""; try { functionName = java.net.URLDecoder.decode(pathParts[2], StandardCharsets.UTF_8.name()); } catch (Exception e) { sendErrorResponse(exchange, 400, "Failed to decode function name", "INVALID_PARAMETER"); return; } if (pathParts.length > 3 && "variables".equals(pathParts[3])) { // /functions/{name}/variables/... if ("GET".equals(exchange.getRequestMethod()) && pathParts.length == 4) { // GET /functions/{name}/variables try { // TODO: Refactor listVariablesInFunction to return data directly Object resultData = listVariablesInFunction(functionName); if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { sendJsonResponse(exchange, (JsonObject)resultData, 400); } else { sendJsonResponse(exchange, resultData); } } catch (Exception e) { Msg.error(this, "Error listing function variables", e); sendErrorResponse(exchange, 500, "Error listing variables: " + e.getMessage(), "INTERNAL_ERROR"); } } else if ("POST".equals(exchange.getRequestMethod()) && pathParts.length == 5) { // POST /functions/{name}/variables/{varName} String variableName = ""; try { variableName = java.net.URLDecoder.decode(pathParts[4], StandardCharsets.UTF_8.name()); } catch (Exception e) { sendErrorResponse(exchange, 400, "Failed to decode variable name", "INVALID_PARAMETER"); return; } final String finalVariableName = variableName; final String finalFunctionName = functionName; try { Map params = parseJsonPostParams(exchange); Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); return; } if (params.containsKey("newName")) { final String newName = params.get("newName"); try { executeInTransaction(program, "Rename Variable", () -> { if (!renameVariable(finalFunctionName, finalVariableName, newName)) { throw new Exception("Rename operation failed internally."); } }); sendJsonResponse(exchange, Map.of("message", "Variable renamed successfully")); } catch (Exception e) { Msg.error(this, "Transaction failed: Rename Variable", e); sendErrorResponse(exchange, 500, "Failed to rename variable: " + e.getMessage(), "TRANSACTION_ERROR"); } } else if (params.containsKey("dataType")) { final String newType = params.get("dataType"); try { executeInTransaction(program, "Retype Variable", () -> { if (!retypeVariable(finalFunctionName, finalVariableName, newType)) { throw new Exception("Retype operation failed internally."); } }); sendJsonResponse(exchange, Map.of("message", "Variable retyped successfully")); } catch (Exception e) { Msg.error(this, "Transaction failed: Retype Variable", e); sendErrorResponse(exchange, 500, "Failed to retype variable: " + e.getMessage(), "TRANSACTION_ERROR"); } } else { sendErrorResponse(exchange, 400, "Missing required parameter: newName or dataType", "MISSING_PARAMETER"); } } catch (IOException e) { Msg.error(this, "Error parsing POST params for variable update", e); sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); } catch (Exception e) { Msg.error(this, "Error updating variable", e); sendErrorResponse(exchange, 500, "Error updating variable: " + e.getMessage(), "INTERNAL_ERROR"); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } else if (pathParts.length == 3) { // GET or POST /functions/{name} if ("GET".equals(exchange.getRequestMethod())) { try { // TODO: Refactor getFunctionDetailsByName to return data directly Object resultData = getFunctionDetailsByName(functionName); if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { sendJsonResponse(exchange, (JsonObject)resultData, 404); } else { sendJsonResponse(exchange, resultData); } } catch (Exception e) { Msg.error(this, "Error getting function details", e); sendErrorResponse(exchange, 500, "Error getting details: " + e.getMessage(), "INTERNAL_ERROR"); } } else if ("POST".equals(exchange.getRequestMethod())) { try { Map params = parseJsonPostParams(exchange); String newName = params.get("newName"); if (newName == null || newName.isEmpty()) { sendErrorResponse(exchange, 400, "Missing required parameter: newName", "MISSING_PARAMETER"); return; } Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); return; } final String finalFunctionName = functionName; final String finalNewName = newName; try { executeInTransaction(program, "Rename Function", () -> { if (!renameFunction(finalFunctionName, finalNewName)) { throw new Exception("Rename operation failed internally."); } }); sendJsonResponse(exchange, Map.of("message", "Function renamed successfully")); } catch (Exception e) { Msg.error(this, "Transaction failed: Rename Function", e); sendErrorResponse(exchange, 500, "Failed to rename function: " + e.getMessage(), "TRANSACTION_ERROR"); } } catch (IOException e) { Msg.error(this, "Error parsing POST params for function rename", e); sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); } catch (Exception e) { Msg.error(this, "Error renaming function", e); sendErrorResponse(exchange, 500, "Error renaming function: " + e.getMessage(), "INTERNAL_ERROR"); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } }); // Class resources server.createContext("/classes", exchange -> { try { if ("GET".equals(exchange.getRequestMethod())) { Map qparams = parseQueryParams(exchange); int offset = parseIntOrDefault(qparams.get("offset"), 0); int limit = parseIntOrDefault(qparams.get("limit"), 100); Object resultData = getAllClassNames(offset, limit); if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { sendJsonResponse(exchange, (JsonObject)resultData, 400); } else { sendJsonResponse(exchange, resultData); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error in /classes endpoint", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); // Memory segments server.createContext("/segments", exchange -> { try { if ("GET".equals(exchange.getRequestMethod())) { Map qparams = parseQueryParams(exchange); int offset = parseIntOrDefault(qparams.get("offset"), 0); int limit = parseIntOrDefault(qparams.get("limit"), 100); Object resultData = listSegments(offset, limit); if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { sendJsonResponse(exchange, (JsonObject)resultData, 400); } else { sendJsonResponse(exchange, resultData); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error in /segments endpoint", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); // Symbol resources (imports/exports) server.createContext("/symbols/imports", exchange -> { try { if ("GET".equals(exchange.getRequestMethod())) { Map qparams = parseQueryParams(exchange); int offset = parseIntOrDefault(qparams.get("offset"), 0); int limit = parseIntOrDefault(qparams.get("limit"), 100); Object resultData = listImports(offset, limit); if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { sendJsonResponse(exchange, (JsonObject)resultData, 400); } else { sendJsonResponse(exchange, resultData); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error in /symbols/imports endpoint", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); server.createContext("/symbols/exports", exchange -> { try { if ("GET".equals(exchange.getRequestMethod())) { Map qparams = parseQueryParams(exchange); int offset = parseIntOrDefault(qparams.get("offset"), 0); int limit = parseIntOrDefault(qparams.get("limit"), 100); Object resultData = listExports(offset, limit); if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { sendJsonResponse(exchange, (JsonObject)resultData, 400); } else { sendJsonResponse(exchange, resultData); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error in /symbols/exports endpoint", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); // Namespace resources server.createContext("/namespaces", exchange -> { try { if ("GET".equals(exchange.getRequestMethod())) { Map qparams = parseQueryParams(exchange); int offset = parseIntOrDefault(qparams.get("offset"), 0); int limit = parseIntOrDefault(qparams.get("limit"), 100); Object resultData = listNamespaces(offset, limit); if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { sendJsonResponse(exchange, (JsonObject)resultData, 400); } else { sendJsonResponse(exchange, resultData); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error in /namespaces endpoint", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); // Data resources server.createContext("/data", exchange -> { try { if ("GET".equals(exchange.getRequestMethod())) { Map qparams = parseQueryParams(exchange); int offset = parseIntOrDefault(qparams.get("offset"), 0); int limit = parseIntOrDefault(qparams.get("limit"), 100); Object resultData = listDefinedData(offset, limit); if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) { sendJsonResponse(exchange, (JsonObject)resultData, 400); } else { sendJsonResponse(exchange, resultData); } } else if ("POST".equals(exchange.getRequestMethod())) { // POST /data try { Map params = parseJsonPostParams(exchange); final String addressStr = params.get("address"); final String newName = params.get("newName"); if (addressStr == null || addressStr.isEmpty() || newName == null || newName.isEmpty()) { sendErrorResponse(exchange, 400, "Missing required parameters: address, newName", "MISSING_PARAMETER"); return; } Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); return; } try { executeInTransaction(program, "Rename Data", () -> { if (!renameDataAtAddress(addressStr, newName)) { throw new Exception("Rename data operation failed internally."); } }); sendJsonResponse(exchange, Map.of("message", "Data renamed successfully")); } catch (Exception e) { Msg.error(this, "Transaction failed: Rename Data", e); sendErrorResponse(exchange, 500, "Failed to rename data: " + e.getMessage(), "TRANSACTION_ERROR"); } } catch (IOException e) { Msg.error(this, "Error parsing POST params for data rename", e); sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); } catch (Exception e) { Msg.error(this, "Error renaming data", e); sendErrorResponse(exchange, 500, "Error renaming data: " + e.getMessage(), "INTERNAL_ERROR"); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error in /data endpoint", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); // Global variables endpoint server.createContext("/variables", exchange -> { // GET /variables try { if ("GET".equals(exchange.getRequestMethod())) { Map qparams = parseQueryParams(exchange); int offset = parseIntOrDefault(qparams.get("offset"), 0); int limit = parseIntOrDefault(qparams.get("limit"), 100); String search = qparams.get("search"); Object resultData = listVariables(offset, limit, search); if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { // Check old error format sendJsonResponse(exchange, (JsonObject)resultData, 400); } else { sendJsonResponse(exchange, resultData); // Use new success helper } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error in /variables endpoint", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); // Instance management endpoints server.createContext("/instances", exchange -> { // TODO: This endpoint might change based on HATEOAS design for projects/programs try { List> instanceData = new ArrayList<>(); for (Map.Entry entry : activeInstances.entrySet()) { Map instance = new HashMap<>(); instance.put("port", entry.getKey()); instance.put("type", entry.getValue().isBaseInstance ? "base" : "secondary"); // TODO: Add URL and program_id if available from instance info cache instanceData.add(instance); } sendJsonResponse(exchange, instanceData); // Use new success helper } catch (Exception e) { Msg.error(this, "Error in /instances endpoint", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); // Add get_function_by_address endpoint server.createContext("/get_function_by_address", exchange -> { if ("GET".equals(exchange.getRequestMethod())) { Map qparams = parseQueryParams(exchange); String address = qparams.get("address"); if (address == null || address.isEmpty()) { sendErrorResponse(exchange, 400, "Address parameter is required", "MISSING_PARAMETER"); return; } Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); return; } try { Address funcAddr = program.getAddressFactory().getAddress(address); Function func = program.getFunctionManager().getFunctionAt(funcAddr); if (func == null) { sendErrorResponse(exchange, 404, "Function not found at address: " + address, "RESOURCE_NOT_FOUND"); return; } Object resultData = getFunctionDetails(func); if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { sendJsonResponse(exchange, (JsonObject)resultData, 500); } else { sendJsonResponse(exchange, resultData); } } catch (ghidra.program.model.address.AddressFormatException afe) { Msg.warn(this, "Invalid address format: " + address, afe); sendErrorResponse(exchange, 400, "Invalid address format: " + address, "INVALID_ADDRESS"); } catch (Exception e) { Msg.error(this, "Error getting function by address", e); sendErrorResponse(exchange, 500, "Error getting function: " + e.getMessage(), "INTERNAL_ERROR"); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Add decompile function by address endpoint server.createContext("/decompile_function", exchange -> { if ("GET".equals(exchange.getRequestMethod())) { Map qparams = parseQueryParams(exchange); String address = qparams.get("address"); boolean cCode = Boolean.parseBoolean(qparams.getOrDefault("cCode", "true")); boolean syntaxTree = Boolean.parseBoolean(qparams.getOrDefault("syntaxTree", "false")); String simplificationStyle = qparams.getOrDefault("simplificationStyle", "normalize"); if (address == null || address.isEmpty()) { sendErrorResponse(exchange, 400, "Address parameter is required", "MISSING_PARAMETER"); return; } Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); return; } try { Address funcAddr = program.getAddressFactory().getAddress(address); Function func = program.getFunctionManager().getFunctionAt(funcAddr); if (func == null) { sendErrorResponse(exchange, 404, "Function not found at address: " + address, "RESOURCE_NOT_FOUND"); return; } DecompInterface decomp = new DecompInterface(); try { decomp.toggleCCode(cCode); decomp.setSimplificationStyle(simplificationStyle); decomp.toggleSyntaxTree(syntaxTree); if (!decomp.openProgram(program)) { sendErrorResponse(exchange, 500, "Failed to initialize decompiler", "DECOMPILER_ERROR"); return; } DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); if (result == null || !result.decompileCompleted()) { sendErrorResponse(exchange, 500, "Decompilation failed or timed out", "DECOMPILATION_FAILED"); return; } String decompilation = ""; String errorMessage = null; if (result.getDecompiledFunction() != null) { decompilation = result.getDecompiledFunction().getC(); if (decompilation == null || decompilation.isEmpty()) { errorMessage = "Decompilation returned empty result"; } } else { errorMessage = "DecompiledFunction is null"; } if (errorMessage != null) { Msg.error(this, "Error decompiling function: " + errorMessage); sendErrorResponse(exchange, 500, errorMessage, "DECOMPILATION_ERROR"); } else { Map resultData = new HashMap<>(); resultData.put("address", func.getEntryPoint().toString()); resultData.put("ccode", decompilation); sendJsonResponse(exchange, resultData); } } finally { decomp.dispose(); } } catch (ghidra.program.model.address.AddressFormatException afe) { Msg.warn(this, "Invalid address format: " + address, afe); sendErrorResponse(exchange, 400, "Invalid address format: " + address, "INVALID_ADDRESS"); } catch (Exception e) { Msg.error(this, "Error decompiling function", e); sendErrorResponse(exchange, 500, "Error decompiling function: " + e.getMessage(), "INTERNAL_ERROR"); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Add decompiler comment endpoint (Using POST now as per bridge) server.createContext("/set_decompiler_comment", exchange -> { if ("POST".equals(exchange.getRequestMethod())) { Map params = parseJsonPostParams(exchange); // Use specific JSON parser String address = params.get("address"); String comment = params.get("comment"); if (address == null || address.isEmpty()) { sendErrorResponse(exchange, 400, "Address parameter is required"); return; } Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program loaded"); return; } try { final Address addr = program.getAddressFactory().getAddress(address); final String finalComment = comment; executeInTransaction(program, "Set Decompiler Comment", () -> { if (!setDecompilerComment(addr, finalComment)) { throw new Exception("Set decompiler comment operation failed internally."); } }); sendJsonResponse(exchange, Map.of("message", "Decompiler comment set successfully")); } catch (ghidra.program.model.address.AddressFormatException afe) { Msg.warn(this, "Invalid address format: " + address, afe); sendErrorResponse(exchange, 400, "Invalid address format: " + address, "INVALID_ADDRESS"); } catch (Exception e) { Msg.error(this, "Error setting decompiler comment", e); sendErrorResponse(exchange, 500, "Error setting comment: " + e.getMessage(), "INTERNAL_ERROR"); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Add disassembly comment endpoint (Using POST now as per bridge) server.createContext("/set_disassembly_comment", exchange -> { if ("POST".equals(exchange.getRequestMethod())) { Map params = parseJsonPostParams(exchange); // Use specific JSON parser String address = params.get("address"); String comment = params.get("comment"); if (address == null || address.isEmpty()) { sendErrorResponse(exchange, 400, "Address parameter is required"); return; } Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program loaded"); return; } try { final Address addr = program.getAddressFactory().getAddress(address); final String finalComment = comment; executeInTransaction(program, "Set Disassembly Comment", () -> { if (!setDisassemblyComment(addr, finalComment)) { throw new Exception("Set disassembly comment operation failed internally."); } }); sendJsonResponse(exchange, Map.of("message", "Disassembly comment set successfully")); } catch (ghidra.program.model.address.AddressFormatException afe) { Msg.warn(this, "Invalid address format: " + address, afe); sendErrorResponse(exchange, 400, "Invalid address format: " + address, "INVALID_ADDRESS"); } catch (Exception e) { Msg.error(this, "Error setting disassembly comment", e); sendErrorResponse(exchange, 500, "Error setting comment: " + e.getMessage(), "INTERNAL_ERROR"); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Add rename function by address endpoint (Using POST now as per bridge) server.createContext("/rename_function_by_address", exchange -> { if ("POST".equals(exchange.getRequestMethod())) { Map params = parseJsonPostParams(exchange); // Use specific JSON parser String address = params.get("functionAddress"); // Expect camelCase String newName = params.get("newName"); // Expect camelCase if (address == null || address.isEmpty()) { sendErrorResponse(exchange, 400, "functionAddress parameter is required"); return; } if (newName == null || newName.isEmpty()) { sendErrorResponse(exchange, 400, "newName parameter is required"); return; } Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program loaded"); return; } try { final Address funcAddr = program.getAddressFactory().getAddress(address); final String finalNewName = newName; executeInTransaction(program, "Rename Function by Address", () -> { if (!renameFunctionByAddress(funcAddr, finalNewName)) { throw new Exception("Rename function by address operation failed internally."); } }); sendJsonResponse(exchange, Map.of("message", "Function renamed successfully")); } catch (ghidra.program.model.address.AddressFormatException afe) { Msg.warn(this, "Invalid address format: " + address, afe); sendErrorResponse(exchange, 400, "Invalid address format: " + address, "INVALID_ADDRESS"); } catch (Exception e) { Msg.error(this, "Error renaming function by address", e); sendErrorResponse(exchange, 500, "Error renaming function: " + e.getMessage(), "INTERNAL_ERROR"); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Add rename local variable endpoint (Using POST now as per bridge) server.createContext("/rename_local_variable", exchange -> { if ("POST".equals(exchange.getRequestMethod())) { Map params = parseJsonPostParams(exchange); String functionAddress = params.get("functionAddress"); String oldName = params.get("oldName"); String newName = params.get("newName"); if (functionAddress == null || functionAddress.isEmpty()) { sendErrorResponse(exchange, 400, "functionAddress parameter is required"); return; } if (oldName == null || oldName.isEmpty()) { sendErrorResponse(exchange, 400, "oldName parameter is required"); return; } if (newName == null || newName.isEmpty()) { sendErrorResponse(exchange, 400, "newName parameter is required"); return; } // TODO: Implement actual logic using executeInTransaction sendJsonResponse(exchange, Map.of("message", "Rename local variable request received (implementation pending)")); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Add set function prototype endpoint (Using POST now as per bridge) server.createContext("/set_function_prototype", exchange -> { if ("POST".equals(exchange.getRequestMethod())) { Map params = parseJsonPostParams(exchange); String functionAddress = params.get("functionAddress"); String prototype = params.get("prototype"); if (functionAddress == null || functionAddress.isEmpty()) { sendErrorResponse(exchange, 400, "functionAddress parameter is required"); return; } if (prototype == null || prototype.isEmpty()) { sendErrorResponse(exchange, 400, "prototype parameter is required"); return; } // TODO: Implement actual logic using executeInTransaction sendJsonResponse(exchange, Map.of("message", "Set function prototype request received (implementation pending)")); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Add set local variable type endpoint (Using POST now as per bridge) server.createContext("/set_local_variable_type", exchange -> { if ("POST".equals(exchange.getRequestMethod())) { Map params = parseJsonPostParams(exchange); String functionAddress = params.get("functionAddress"); String variableName = params.get("variableName"); String newType = params.get("newType"); if (functionAddress == null || functionAddress.isEmpty()) { sendErrorResponse(exchange, 400, "functionAddress parameter is required"); return; } if (variableName == null || variableName.isEmpty()) { sendErrorResponse(exchange, 400, "variableName parameter is required"); return; } if (newType == null || newType.isEmpty()) { sendErrorResponse(exchange, 400, "newType parameter is required"); return; } // TODO: Implement actual logic using executeInTransaction sendJsonResponse(exchange, Map.of("message", "Set local variable type request received (implementation pending)")); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Add get current address endpoint (Changed to GET to match test expectations) server.createContext("/get_current_address", exchange -> { if ("GET".equals(exchange.getRequestMethod())) { Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); return; } try { Address currentAddr = getCurrentAddress(); if (currentAddr != null) { sendJsonResponse(exchange, Map.of("address", currentAddr.toString())); } else { sendErrorResponse(exchange, 404, "No address currently selected", "RESOURCE_NOT_FOUND"); } } catch (Exception e) { Msg.error(this, "Error getting current address", e); sendErrorResponse(exchange, 500, "Error getting current address: " + e.getMessage(), "INTERNAL_ERROR"); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Add get current function endpoint (Changed to GET to match test expectations) server.createContext("/get_current_function", exchange -> { if ("GET".equals(exchange.getRequestMethod())) { Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); return; } try { Function currentFunc = getCurrentFunction(); if (currentFunc != null) { Map funcData = new HashMap<>(); funcData.put("name", currentFunc.getName()); funcData.put("address", currentFunc.getEntryPoint().toString()); funcData.put("signature", currentFunc.getSignature().getPrototypeString()); sendJsonResponse(exchange, funcData); } else { sendErrorResponse(exchange, 404, "No function currently selected", "RESOURCE_NOT_FOUND"); } } catch (Exception e) { Msg.error(this, "Error getting current function", e); sendErrorResponse(exchange, 500, "Error getting current function: " + e.getMessage(), "INTERNAL_ERROR"); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } }); // Info endpoint using new helpers server.createContext("/info", exchange -> { try { Map infoData = new HashMap<>(); infoData.put("port", port); infoData.put("isBaseInstance", isBaseInstance); Program program = getCurrentProgram(); infoData.put("file", program != null ? program.getName() : null); Project project = tool.getProject(); infoData.put("project", project != null ? project.getName() : null); sendJsonResponse(exchange, infoData); } catch (Exception e) { Msg.error(this, "Error serving /info endpoint", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); // Root endpoint - only handle exact "/" path server.createContext("/", exchange -> { if (!exchange.getRequestURI().getPath().equals("/")) { Msg.info(this, "Received request for unknown path: " + exchange.getRequestURI().getPath()); sendErrorResponse(exchange, 404, "Endpoint not found", "ENDPOINT_NOT_FOUND"); return; } try { Map rootData = new HashMap<>(); rootData.put("port", port); rootData.put("isBaseInstance", isBaseInstance); Program program = getCurrentProgram(); rootData.put("file", program != null ? program.getName() : null); Project project = tool.getProject(); rootData.put("project", project != null ? project.getName() : null); // TODO: Add HATEOAS links here (e.g., to /info, /projects, /programs) sendJsonResponse(exchange, rootData); } catch (Exception e) { Msg.error(this, "Error serving / endpoint", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); server.createContext("/registerInstance", exchange -> { try { Map params = parseJsonPostParams(exchange); int regPort = parseIntOrDefault(params.get("port"), 0); if (regPort > 0) { sendJsonResponse(exchange, Map.of("message", "Instance registration request received for port " + regPort)); } else { sendErrorResponse(exchange, 400, "Invalid or missing port number", "INVALID_PARAMETER"); } } catch (IOException e) { Msg.error(this, "Error parsing POST params for registerInstance", e); sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); } catch (Exception e) { Msg.error(this, "Error in /registerInstance", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); server.createContext("/unregisterInstance", exchange -> { try { Map params = parseJsonPostParams(exchange); int unregPort = parseIntOrDefault(params.get("port"), 0); if (unregPort > 0 && activeInstances.containsKey(unregPort)) { activeInstances.remove(unregPort); sendJsonResponse(exchange, Map.of("message", "Instance unregistered for port " + unregPort)); } else { sendErrorResponse(exchange, 404, "No instance found on port " + unregPort, "RESOURCE_NOT_FOUND"); } } catch (IOException e) { Msg.error(this, "Error parsing POST params for unregisterInstance", e); sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); } catch (Exception e) { Msg.error(this, "Error in /unregisterInstance", e); sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); } }); server.setExecutor(null); new Thread(() -> { server.start(); Msg.info(this, "GhydraMCP HTTP server started on port " + port); System.out.println("[GhydraMCP] HTTP server started on port " + port); }, "GhydraMCP-HTTP-Server").start(); } // ---------------------------------------------------------------------------------- // Pagination-aware listing methods // ---------------------------------------------------------------------------------- private JsonObject getAllFunctionNames(int offset, int limit) { // Changed return type Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } List> functions = new ArrayList<>(); for (Function f : program.getFunctionManager().getFunctions(true)) { Map func = new HashMap<>(); func.put("name", f.getName()); func.put("address", f.getEntryPoint().toString()); functions.add(func); } // Apply pagination int start = Math.max(0, offset); int end = Math.min(functions.size(), offset + limit); List> paginated = functions.subList(start, end); // Use helper to create standard response return createSuccessResponse(paginated); // Return JsonObject } private JsonObject getAllClassNames(int offset, int limit) { Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } Set classNames = new HashSet<>(); for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) { Namespace ns = symbol.getParentNamespace(); if (ns != null && !ns.isGlobal()) { classNames.add(ns.getName()); } } // Convert to sorted list and paginate List sorted = new ArrayList<>(classNames); Collections.sort(sorted); int start = Math.max(0, offset); int end = Math.min(sorted.size(), offset + limit); List paginated = sorted.subList(start, end); // Use helper to create standard response return createSuccessResponse(paginated); } private JsonObject listSegments(int offset, int limit) { // Changed return type to JsonObject Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } List> segments = new ArrayList<>(); for (MemoryBlock block : program.getMemory().getBlocks()) { Map seg = new HashMap<>(); seg.put("name", block.getName()); seg.put("start", block.getStart().toString()); seg.put("end", block.getEnd().toString()); segments.add(seg); } // Apply pagination int start = Math.max(0, offset); int end = Math.min(segments.size(), offset + limit); List> paginated = segments.subList(start, end); // Use helper to create standard response return createSuccessResponse(paginated); } private JsonObject listImports(int offset, int limit) { // Changed return type to JsonObject Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } List> imports = new ArrayList<>(); for (Symbol symbol : program.getSymbolTable().getExternalSymbols()) { Map imp = new HashMap<>(); imp.put("name", symbol.getName()); imp.put("address", symbol.getAddress().toString()); imports.add(imp); } // Apply pagination int start = Math.max(0, offset); int end = Math.min(imports.size(), offset + limit); List> paginated = imports.subList(start, end); // Use helper to create standard response return createSuccessResponse(paginated); // Return JsonObject directly } private JsonObject listExports(int offset, int limit) { // Changed return type to JsonObject Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } List> exports = new ArrayList<>(); SymbolTable table = program.getSymbolTable(); SymbolIterator it = table.getAllSymbols(true); while (it.hasNext()) { Symbol s = it.next(); if (s.isExternalEntryPoint()) { Map exp = new HashMap<>(); exp.put("name", s.getName()); exp.put("address", s.getAddress().toString()); exports.add(exp); } } // Apply pagination int start = Math.max(0, offset); int end = Math.min(exports.size(), offset + limit); List> paginated = exports.subList(start, end); // Use helper to create standard response return createSuccessResponse(paginated); // Return JsonObject directly } private JsonObject listNamespaces(int offset, int limit) { // Changed return type to JsonObject Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } Set namespaces = new HashSet<>(); for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) { Namespace ns = symbol.getParentNamespace(); if (ns != null && !(ns instanceof GlobalNamespace)) { namespaces.add(ns.getName()); } } List sorted = new ArrayList<>(namespaces); Collections.sort(sorted); // Apply pagination int start = Math.max(0, offset); int end = Math.min(sorted.size(), offset + limit); List paginated = sorted.subList(start, end); // Use helper to create standard response return createSuccessResponse(paginated); // Return JsonObject directly } private JsonObject listDefinedData(int offset, int limit) { // Changed return type to JsonObject Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } List> dataItems = new ArrayList<>(); for (MemoryBlock block : program.getMemory().getBlocks()) { DataIterator it = program.getListing().getDefinedData(block.getStart(), true); while (it.hasNext()) { Data data = it.next(); if (block.contains(data.getAddress())) { Map item = new HashMap<>(); item.put("address", data.getAddress().toString()); item.put("label", data.getLabel() != null ? data.getLabel() : "(unnamed)"); item.put("value", data.getDefaultValueRepresentation()); dataItems.add(item); } } } // Apply pagination int start = Math.max(0, offset); int end = Math.min(dataItems.size(), offset + limit); List> paginated = dataItems.subList(start, end); // Use helper to create standard response return createSuccessResponse(paginated); // Return JsonObject directly } private JsonObject searchFunctionsByName(String searchTerm, int offset, int limit) { // Changed return type to JsonObject Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } if (searchTerm == null || searchTerm.isEmpty()) { return createErrorResponse("Search term is required", 400); } List matches = new ArrayList<>(); for (Function func : program.getFunctionManager().getFunctions(true)) { String name = func.getName(); // simple substring match if (name.toLowerCase().contains(searchTerm.toLowerCase())) { matches.add(String.format("%s @ %s", name, func.getEntryPoint())); } } Collections.sort(matches); if (matches.isEmpty()) { // Return success with empty result list return createSuccessResponse(new ArrayList<>()); } // Paginate the string list representation int start = Math.max(0, offset); int end = Math.min(matches.size(), offset + limit); List sub = matches.subList(start, end); // Return paginated list using helper return createSuccessResponse(sub); } // ---------------------------------------------------------------------------------- // Logic for getting function details, rename, decompile, etc. // ---------------------------------------------------------------------------------- private JsonObject getFunctionDetailsByName(String name) { JsonObject response = new JsonObject(); Program program = getCurrentProgram(); if (program == null) { response.addProperty("success", false); response.addProperty("error", "No program loaded"); return response; } Function func = findFunctionByName(program, name); if (func == null) { response.addProperty("success", false); response.addProperty("error", "Function not found: " + name); return response; } return getFunctionDetails(func); // Use common helper } // Helper to get function details and decompilation private JsonObject getFunctionDetails(Function func) { JsonObject response = new JsonObject(); JsonObject resultObj = new JsonObject(); Program program = func.getProgram(); resultObj.addProperty("name", func.getName()); resultObj.addProperty("address", func.getEntryPoint().toString()); resultObj.addProperty("signature", func.getSignature().getPrototypeString()); DecompInterface decomp = new DecompInterface(); try { // Default to C code output and no syntax tree for better readability decomp.toggleCCode(true); decomp.setSimplificationStyle("normalize"); decomp.toggleSyntaxTree(false); if (!decomp.openProgram(program)) { resultObj.addProperty("decompilation_error", "Failed to initialize decompiler"); } else { DecompileResults decompResult = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); if (decompResult == null) { resultObj.addProperty("decompilation_error", "Decompilation returned null result"); } else if (!decompResult.decompileCompleted()) { resultObj.addProperty("decompilation_error", "Decompilation failed or timed out"); } else { // Handle decompilation result with proper JSON structure JsonObject decompilationResult = new JsonObject(); ghidra.app.decompiler.DecompiledFunction decompiledFunc = decompResult.getDecompiledFunction(); if (decompiledFunc == null) { decompilationResult.addProperty("error", "Could not get decompiled function"); } else { String decompiledCode = decompiledFunc.getC(); if (decompiledCode != null) { decompilationResult.addProperty("code", decompiledCode); } else { decompilationResult.addProperty("error", "Decompiled code is null"); } } resultObj.add("decompilation", decompilationResult); } } } catch (Exception e) { Msg.error(this, "Decompilation error for " + func.getName(), e); resultObj.addProperty("decompilation_error", "Exception during decompilation: " + e.getMessage()); } finally { decomp.dispose(); } response.addProperty("success", true); response.add("result", resultObj); response.addProperty("timestamp", System.currentTimeMillis()); response.addProperty("port", this.port); return response; } private JsonObject decompileFunctionByName(String name) { // Changed return type Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } DecompInterface decomp = new DecompInterface(); try { if (!decomp.openProgram(program)) { return createErrorResponse("Failed to initialize decompiler", 500); } Function func = findFunctionByName(program, name); if (func == null) { return createErrorResponse("Function not found: " + name, 404); } DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); if (result != null && result.decompileCompleted()) { JsonObject resultObj = new JsonObject(); resultObj.addProperty("name", func.getName()); resultObj.addProperty("address", func.getEntryPoint().toString()); resultObj.addProperty("signature", func.getSignature().getPrototypeString()); resultObj.addProperty("decompilation", result.getDecompiledFunction().getC()); // Use helper to create standard response return createSuccessResponse(resultObj); // Return JsonObject } else { return createErrorResponse("Decompilation failed", 500); } } finally { decomp.dispose(); } } private boolean renameFunctionByAddress(Address functionAddress, String newName) { Program program = getCurrentProgram(); if (program == null) return false; AtomicBoolean successFlag = new AtomicBoolean(false); try { SwingUtilities.invokeAndWait(() -> { int tx = program.startTransaction("Rename function via HTTP"); try { Function func = program.getFunctionManager().getFunctionAt(functionAddress); if (func != null) { func.setName(newName, SourceType.USER_DEFINED); successFlag.set(true); } } catch (Exception e) { Msg.error(this, "Error renaming function", e); } finally { program.endTransaction(tx, successFlag.get()); } }); } catch (InterruptedException | InvocationTargetException e) { Msg.error(this, "Failed to execute rename on Swing thread", e); } return successFlag.get(); } private boolean setDecompilerComment(Address address, String comment) { Program program = getCurrentProgram(); if (program == null) return false; AtomicBoolean successFlag = new AtomicBoolean(false); try { SwingUtilities.invokeAndWait(() -> { int tx = program.startTransaction("Set decompiler comment"); try { DecompInterface decomp = new DecompInterface(); decomp.openProgram(program); Function func = program.getFunctionManager().getFunctionContaining(address); if (func != null) { DecompileResults results = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); if (results != null && results.decompileCompleted()) { HighFunction highFunc = results.getHighFunction(); if (highFunc != null) { program.getListing().setComment(address, CodeUnit.PRE_COMMENT, comment); successFlag.set(true); } } } } catch (Exception e) { Msg.error(this, "Error setting decompiler comment", e); } finally { program.endTransaction(tx, successFlag.get()); } }); } catch (InterruptedException | InvocationTargetException e) { Msg.error(this, "Failed to execute set comment on Swing thread", e); } return successFlag.get(); } private boolean setDisassemblyComment(Address address, String comment) { Program program = getCurrentProgram(); if (program == null) return false; AtomicBoolean successFlag = new AtomicBoolean(false); try { SwingUtilities.invokeAndWait(() -> { int tx = program.startTransaction("Set disassembly comment"); try { Listing listing = program.getListing(); listing.setComment(address, CodeUnit.EOL_COMMENT, comment); successFlag.set(true); } catch (Exception e) { Msg.error(this, "Error setting disassembly comment", e); } finally { program.endTransaction(tx, successFlag.get()); } }); } catch (InterruptedException | InvocationTargetException e) { Msg.error(this, "Failed to execute set comment on Swing thread", e); } return successFlag.get(); } private boolean renameFunction(String oldName, String newName) { Program program = getCurrentProgram(); if (program == null) return false; AtomicBoolean successFlag = new AtomicBoolean(false); try { SwingUtilities.invokeAndWait(() -> { int tx = program.startTransaction("Rename function via HTTP"); try { for (Function func : program.getFunctionManager().getFunctions(true)) { if (func.getName().equals(oldName)) { func.setName(newName, SourceType.USER_DEFINED); successFlag.set(true); break; } } } catch (Exception e) { Msg.error(this, "Error renaming function", e); } finally { program.endTransaction(tx, successFlag.get()); } }); } catch (InterruptedException | InvocationTargetException e) { Msg.error(this, "Failed to execute rename on Swing thread", e); } return successFlag.get(); } private boolean renameDataAtAddress(String addressStr, String newName) { Program program = getCurrentProgram(); if (program == null) return false; AtomicBoolean successFlag = new AtomicBoolean(false); try { SwingUtilities.invokeAndWait(() -> { int tx = program.startTransaction("Rename data"); try { Address addr = program.getAddressFactory().getAddress(addressStr); Listing listing = program.getListing(); Data data = listing.getDefinedDataAt(addr); if (data != null) { SymbolTable symTable = program.getSymbolTable(); Symbol symbol = symTable.getPrimarySymbol(addr); if (symbol != null) { symbol.setName(newName, SourceType.USER_DEFINED); successFlag.set(true); } else { symTable.createLabel(addr, newName, SourceType.USER_DEFINED); successFlag.set(true); } } } catch (Exception e) { Msg.error(this, "Rename data error", e); } finally { program.endTransaction(tx, successFlag.get()); } }); } catch (InterruptedException | InvocationTargetException e) { Msg.error(this, "Failed to execute rename data on Swing thread", e); } return successFlag.get(); } // ---------------------------------------------------------------------------------- // New variable handling methods // ---------------------------------------------------------------------------------- private JsonObject listVariablesInFunction(String functionName) { // Changed return type Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } DecompInterface decomp = new DecompInterface(); try { if (!decomp.openProgram(program)) { return createErrorResponse("Failed to initialize decompiler", 500); } Function function = findFunctionByName(program, functionName); if (function == null) { return createErrorResponse("Function not found: " + functionName, 404); } DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); if (results == null || !results.decompileCompleted()) { return createErrorResponse("Failed to decompile function: " + functionName, 500); } // Get high-level pcode representation for the function HighFunction highFunction = results.getHighFunction(); if (highFunction == null) { return createErrorResponse("Failed to get high function for: " + functionName, 500); } // Get all variables (parameters and locals) List> allVariables = new ArrayList<>(); // Process all symbols Iterator symbolIter = highFunction.getLocalSymbolMap().getSymbols(); while (symbolIter.hasNext()) { HighSymbol symbol = symbolIter.next(); Map varInfo = new HashMap<>(); varInfo.put("name", symbol.getName()); DataType dt = symbol.getDataType(); String dtName = dt != null ? dt.getName() : "unknown"; varInfo.put("dataType", dtName); if (symbol.isParameter()) { varInfo.put("type", "parameter"); } else if (symbol.getHighVariable() != null) { varInfo.put("type", "local"); varInfo.put("address", symbol.getPCAddress().toString()); } else { continue; // Skip symbols without high variables that aren't parameters } allVariables.add(varInfo); } // Sort by name Collections.sort(allVariables, (a, b) -> a.get("name").compareTo(b.get("name"))); // Create JSON response JsonObject response = new JsonObject(); response.addProperty("success", true); JsonObject resultObj = new JsonObject(); resultObj.addProperty("function", functionName); resultObj.add("variables", new Gson().toJsonTree(allVariables)); // Use helper to create standard response return createSuccessResponse(resultObj); // Return JsonObject } finally { decomp.dispose(); } } private boolean renameVariable(String functionName, String oldName, String newName) { Program program = getCurrentProgram(); if (program == null) return false; DecompInterface decomp = new DecompInterface(); decomp.openProgram(program); Function func = null; for (Function f : program.getFunctionManager().getFunctions(true)) { if (f.getName().equals(functionName)) { func = f; break; } } if (func == null) { return false; } DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); if (result == null || !result.decompileCompleted()) { return false; } HighFunction highFunction = result.getHighFunction(); if (highFunction == null) { return false; } LocalSymbolMap localSymbolMap = highFunction.getLocalSymbolMap(); if (localSymbolMap == null) { return false; } HighSymbol highSymbol = null; Iterator symbols = localSymbolMap.getSymbols(); while (symbols.hasNext()) { HighSymbol symbol = symbols.next(); String symbolName = symbol.getName(); if (symbolName.equals(oldName)) { highSymbol = symbol; } if (symbolName.equals(newName)) { return false; } } if (highSymbol == null) { return false; } boolean commitRequired = checkFullCommit(highSymbol, highFunction); final HighSymbol finalHighSymbol = highSymbol; final Function finalFunction = func; AtomicBoolean successFlag = new AtomicBoolean(false); try { SwingUtilities.invokeAndWait(() -> { int tx = program.startTransaction("Rename variable"); try { if (commitRequired) { HighFunctionDBUtil.commitParamsToDatabase(highFunction, false, ReturnCommitOption.NO_COMMIT, finalFunction.getSignatureSource()); } HighFunctionDBUtil.updateDBVariable( finalHighSymbol, newName, null, SourceType.USER_DEFINED ); successFlag.set(true); } catch (Exception e) { Msg.error(this, "Failed to rename variable", e); } finally { program.endTransaction(tx, true); } }); } catch (InterruptedException | InvocationTargetException e) { Msg.error(this, "Failed to execute rename on Swing thread", e); return false; } return successFlag.get(); } /** * Copied from AbstractDecompilerAction.checkFullCommit, it's protected. * Compare the given HighFunction's idea of the prototype with the Function's idea. * Return true if there is a difference. If a specific symbol is being changed, * it can be passed in to check whether or not the prototype is being affected. * @param highSymbol (if not null) is the symbol being modified * @param hfunction is the given HighFunction * @return true if there is a difference (and a full commit is required) */ protected static boolean checkFullCommit(HighSymbol highSymbol, HighFunction hfunction) { if (highSymbol != null && !highSymbol.isParameter()) { return false; } Function function = hfunction.getFunction(); Parameter[] parameters = function.getParameters(); LocalSymbolMap localSymbolMap = hfunction.getLocalSymbolMap(); int numParams = localSymbolMap.getNumParams(); if (numParams != parameters.length) { return true; } for (int i = 0; i < numParams; i++) { HighSymbol param = localSymbolMap.getParamSymbol(i); if (param.getCategoryIndex() != i) { return true; } VariableStorage storage = param.getStorage(); // Don't compare using the equals method so that DynamicVariableStorage can match if (0 != storage.compareTo(parameters[i].getVariableStorage())) { return true; } } return false; } private boolean retypeVariable(String functionName, String varName, String dataTypeName) { if (varName == null || varName.isEmpty() || dataTypeName == null || dataTypeName.isEmpty()) { return false; } Program program = getCurrentProgram(); if (program == null) return false; AtomicBoolean result = new AtomicBoolean(false); try { SwingUtilities.invokeAndWait(() -> { int tx = program.startTransaction("Retype variable via HTTP"); try { Function function = findFunctionByName(program, functionName); if (function == null) { return; } // Initialize decompiler DecompInterface decomp = new DecompInterface(); decomp.openProgram(program); DecompileResults decompRes = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); if (decompRes == null || !decompRes.decompileCompleted()) { return; } HighFunction highFunction = decompRes.getHighFunction(); if (highFunction == null) { return; } // Find the variable by name - must match exactly and be in current scope HighSymbol targetSymbol = null; Iterator symbolIter = highFunction.getLocalSymbolMap().getSymbols(); while (symbolIter.hasNext()) { HighSymbol symbol = symbolIter.next(); if (symbol.getName().equals(varName) && symbol.getPCAddress().equals(function.getEntryPoint())) { targetSymbol = symbol; break; } } if (targetSymbol == null) { return; } // Find the data type by name DataType dataType = findDataType(program, dataTypeName); if (dataType == null) { return; } // Retype the variable HighFunctionDBUtil.updateDBVariable(targetSymbol, targetSymbol.getName(), dataType, SourceType.USER_DEFINED); result.set(true); } catch (Exception e) { Msg.error(this, "Error retyping variable", e); result.set(false); } finally { program.endTransaction(tx, true); } }); } catch (InterruptedException | InvocationTargetException e) { Msg.error(this, "Failed to execute on Swing thread", e); result.set(false); } return result.get(); } private JsonObject listVariables(int offset, int limit, String searchTerm) { Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } List> variables = new ArrayList<>(); // Get global variables SymbolTable symbolTable = program.getSymbolTable(); for (Symbol symbol : symbolTable.getDefinedSymbols()) { if (symbol.isGlobal() && !symbol.isExternal() && symbol.getSymbolType() != SymbolType.FUNCTION && symbol.getSymbolType() != SymbolType.LABEL) { Map varInfo = new HashMap<>(); varInfo.put("name", symbol.getName()); varInfo.put("address", symbol.getAddress().toString()); varInfo.put("type", "global"); varInfo.put("dataType", getDataTypeName(program, symbol.getAddress())); variables.add(varInfo); } } // Get local variables from all functions DecompInterface decomp = null; // Initialize outside try try { decomp = new DecompInterface(); // Create inside try if (!decomp.openProgram(program)) { Msg.error(this, "listVariables: Failed to open program with decompiler."); // Continue with only global variables if decompiler fails to open } else { for (Function function : program.getFunctionManager().getFunctions(true)) { try { DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); if (results != null && results.decompileCompleted()) { HighFunction highFunc = results.getHighFunction(); if (highFunc != null) { Iterator symbolIter = highFunc.getLocalSymbolMap().getSymbols(); while (symbolIter.hasNext()) { HighSymbol symbol = symbolIter.next(); if (!symbol.isParameter()) { // Only list locals, not params Map varInfo = new HashMap<>(); varInfo.put("name", symbol.getName()); varInfo.put("type", "local"); varInfo.put("function", function.getName()); // Handle null PC address for some local variables Address pcAddr = symbol.getPCAddress(); varInfo.put("address", pcAddr != null ? pcAddr.toString() : "N/A"); varInfo.put("dataType", symbol.getDataType() != null ? symbol.getDataType().getName() : "unknown"); variables.add(varInfo); } } } else { Msg.warn(this, "listVariables: Failed to get HighFunction for " + function.getName()); } } else { Msg.warn(this, "listVariables: Decompilation failed or timed out for " + function.getName()); } } catch (Exception e) { Msg.error(this, "listVariables: Error processing function " + function.getName(), e); // Continue to the next function if one fails } } } } catch (Exception e) { Msg.error(this, "listVariables: Error during local variable processing", e); // If a major error occurs, we might still have global variables } finally { if (decomp != null) { decomp.dispose(); // Ensure disposal } } // Sort by name Collections.sort(variables, (a, b) -> a.get("name").compareTo(b.get("name"))); // Apply pagination int start = Math.max(0, offset); int end = Math.min(variables.size(), offset + limit); List> paginated = variables.subList(start, end); // Create JSON response // Use helper to create standard response return createSuccessResponse(paginated); } private JsonObject searchVariables(String searchTerm, int offset, int limit) { Program program = getCurrentProgram(); if (program == null) { return createErrorResponse("No program loaded", 400); } if (searchTerm == null || searchTerm.isEmpty()) { return createErrorResponse("Search term is required", 400); } List> matchedVars = new ArrayList<>(); // Search global variables SymbolTable symbolTable = program.getSymbolTable(); SymbolIterator it = symbolTable.getSymbolIterator(); while (it.hasNext()) { Symbol symbol = it.next(); if (symbol.isGlobal() && symbol.getSymbolType() != SymbolType.FUNCTION && symbol.getSymbolType() != SymbolType.LABEL && symbol.getName().toLowerCase().contains(searchTerm.toLowerCase())) { Map varInfo = new HashMap<>(); varInfo.put("name", symbol.getName()); varInfo.put("address", symbol.getAddress().toString()); varInfo.put("type", "global"); matchedVars.add(varInfo); } } // Search local variables in functions DecompInterface decomp = new DecompInterface(); try { if (decomp.openProgram(program)) { for (Function function : program.getFunctionManager().getFunctions(true)) { DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); if (results != null && results.decompileCompleted()) { HighFunction highFunc = results.getHighFunction(); if (highFunc != null) { // Check each local variable and parameter Iterator symbolIter = highFunc.getLocalSymbolMap().getSymbols(); while (symbolIter.hasNext()) { HighSymbol symbol = symbolIter.next(); if (symbol.getName().toLowerCase().contains(searchTerm.toLowerCase())) { Map varInfo = new HashMap<>(); varInfo.put("name", symbol.getName()); varInfo.put("function", function.getName()); if (symbol.isParameter()) { varInfo.put("type", "parameter"); } else { varInfo.put("type", "local"); varInfo.put("address", symbol.getPCAddress().toString()); } matchedVars.add(varInfo); } } } } } } } finally { decomp.dispose(); } // Sort by name Collections.sort(matchedVars, (a, b) -> a.get("name").compareTo(b.get("name"))); // Apply pagination int start = Math.max(0, offset); int end = Math.min(matchedVars.size(), offset + limit); List> paginated = matchedVars.subList(start, end); // Create JSON response // Use helper to create standard response return createSuccessResponse(paginated); } // ---------------------------------------------------------------------------------- // Standardized JSON Response Helpers (Following GHIDRA_HTTP_API.md v1) // ---------------------------------------------------------------------------------- /** * Creates the base structure for all JSON responses. * Includes the request ID and instance URL. * @param exchange The HTTP exchange to extract headers from. * @return A JsonObject with 'id' and 'instance' fields. */ /** * Builder for standardized API responses */ private static class ResponseBuilder { private final HttpExchange exchange; private final int port; private JsonObject response; private JsonObject links; public ResponseBuilder(HttpExchange exchange, int port) { this.exchange = exchange; this.port = port; this.response = new JsonObject(); this.links = new JsonObject(); String requestId = exchange.getRequestHeaders().getFirst("X-Request-ID"); response.addProperty("id", requestId != null ? requestId : UUID.randomUUID().toString()); response.addProperty("instance", "http://localhost:" + port); } public ResponseBuilder success(boolean success) { response.addProperty("success", success); return this; } public ResponseBuilder result(Object data) { Gson gson = new Gson(); response.add("result", gson.toJsonTree(data)); return this; } public ResponseBuilder error(String message, String code) { JsonObject error = new JsonObject(); error.addProperty("message", message); if (code != null) { error.addProperty("code", code); } response.add("error", error); return this; } public ResponseBuilder addLink(String rel, String href) { JsonObject link = new JsonObject(); link.addProperty("href", href); links.add(rel, link); return this; } public JsonObject build() { if (links.size() > 0) { response.add("_links", links); } return response; } } private JsonObject createBaseResponse(HttpExchange exchange) { return new ResponseBuilder(exchange, port).build(); } private JsonObject createSuccessResponse(HttpExchange exchange, Object resultData, JsonObject links) { ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(resultData); if (links != null) { builder.links = links; } return builder.build(); } private JsonObject createErrorResponse(HttpExchange exchange, String message, String errorCode) { return new ResponseBuilder(exchange, port) .success(false) .error(message, errorCode) .build(); } // Overload for simple success with no data and no links private JsonObject createSuccessResponse(HttpExchange exchange) { return createSuccessResponse(exchange, null, null); } /** * Creates a standardized error response JSON object. * @param exchange The HTTP exchange. * @param message A descriptive error message. * @param errorCode An optional machine-readable error code string. * @return A JsonObject representing the error response. */ private JsonObject createErrorResponse(HttpExchange exchange, String message, String errorCode) { JsonObject response = createBaseResponse(exchange); response.addProperty("success", false); JsonObject errorObj = new JsonObject(); errorObj.addProperty("message", message != null ? message : "An unknown error occurred."); if (errorCode != null && !errorCode.isEmpty()) { errorObj.addProperty("code", errorCode); } response.add("error", errorObj); return response; } // Overload for error with just message private JsonObject createErrorResponse(HttpExchange exchange, String message) { return createErrorResponse(exchange, message, null); } // --- Deprecated Helpers (Marked for removal) --- // These are kept temporarily only if absolutely needed during refactoring, // but the goal is to replace all their usages with the new helpers above. @Deprecated private JsonObject createSuccessResponse(Object resultData) { JsonObject response = new JsonObject(); response.addProperty("success", true); if (resultData != null) { response.add("result", new Gson().toJsonTree(resultData)); } else { response.add("result", null); } response.addProperty("timestamp", System.currentTimeMillis()); // Deprecated field response.addProperty("port", this.port); // Deprecated field return response; } @Deprecated private JsonObject createErrorResponse(String errorMessage, int statusCode) { JsonObject response = new JsonObject(); response.addProperty("success", false); response.addProperty("error", errorMessage); // Deprecated structure response.addProperty("status_code", statusCode); // Deprecated field response.addProperty("timestamp", System.currentTimeMillis()); // Deprecated field response.addProperty("port", this.port); // Deprecated field return response; } // --- End Deprecated Helpers --- // ---------------------------------------------------------------------------------- // Transaction Management Helper // ---------------------------------------------------------------------------------- /** * Executes a Ghidra operation that modifies the program state within a transaction. * Handles Swing thread invocation and ensures the transaction is properly managed. * * @param The return type of the operation (can be Void for operations without return value). * @param program The program context for the transaction. Must not be null. * @param transactionName A descriptive name for the Ghidra transaction log. * @param operation A supplier function (using GhidraSupplier functional interface) * that performs the Ghidra API calls and returns a result. * This function MUST NOT start or end its own transaction. * @return The result of the operation. * @throws TransactionException If the operation fails within the transaction or * if execution on the Swing thread fails. Wraps the original cause. * @throws IllegalArgumentException If program is null. */ private T executeInTransaction(Program program, String transactionName, GhidraSupplier operation) throws TransactionException { if (program == null) { throw new IllegalArgumentException("Program cannot be null for transaction"); } final class ResultContainer { T value = null; Exception exception = null; } final ResultContainer resultContainer = new ResultContainer(); try { SwingUtilities.invokeAndWait(() -> { int txId = -1; boolean success = false; try { txId = program.startTransaction(transactionName); if (txId < 0) { throw new TransactionException("Failed to start transaction: " + transactionName + ". Already in a transaction?"); } resultContainer.value = operation.get(); success = true; } catch (Exception e) { Msg.error(this, "Exception during transaction: " + transactionName, e); resultContainer.exception = e; success = false; } finally { if (txId >= 0) { program.endTransaction(txId, success); Msg.debug(this, "Transaction '" + transactionName + "' ended. Success: " + success); } } }); } catch (InterruptedException | InvocationTargetException e) { Msg.error(this, "Failed to execute transaction '" + transactionName + "' on Swing thread", e); throw new TransactionException("Failed to execute operation on Swing thread", e); } if (resultContainer.exception != null) { throw new TransactionException("Operation failed within transaction: " + transactionName, resultContainer.exception); } return resultContainer.value; } /** * Overload of executeInTransaction for operations that don't return a value (Runnable). * @param program The program context for the transaction. * @param transactionName The name for the Ghidra transaction log. * @param operation A Runnable that performs the Ghidra API calls. * @throws TransactionException If the operation fails. */ private void executeInTransaction(Program program, String transactionName, Runnable operation) throws TransactionException { executeInTransaction(program, transactionName, () -> { operation.run(); return null; }); } /** Custom exception for transaction-related errors. */ public static class TransactionException extends Exception { public TransactionException(String message) { super(message); } public TransactionException(String message, Throwable cause) { super(message, cause); } } // ---------------------------------------------------------------------------------- // HTTP Response Sending Methods // ---------------------------------------------------------------------------------- /** * Sends a standard success JSON response with a 200 OK status. * @param exchange The HTTP exchange. * @param resultData The data payload for the 'result' field (can be null). * @param links Optional HATEOAS links. * @throws IOException If sending the response fails. */ private void sendSuccessResponse(HttpExchange exchange, Object resultData, JsonObject links) throws IOException { sendJsonResponse(exchange, createSuccessResponse(exchange, resultData, links), 200); } // Overload for success with data, no links private void sendSuccessResponse(HttpExchange exchange, Object resultData) throws IOException { sendSuccessResponse(exchange, resultData, null); } // Overload for simple success, no data, no links (e.g., for 204 No Content) private void sendSuccessResponse(HttpExchange exchange) throws IOException { sendSuccessResponse(exchange, null, null); } /** * Sends a standard error JSON response with the specified HTTP status code. * @param exchange The HTTP exchange. * @param statusCode The HTTP status code (e.g., 400, 404, 500). * @param message A descriptive error message. * @param errorCode An optional machine-readable error code string. * @throws IOException If sending the response fails. */ private void sendErrorResponse(HttpExchange exchange, int statusCode, String message, String errorCode) throws IOException { sendJsonResponse(exchange, createErrorResponse(exchange, message, errorCode), statusCode); } // Overload for error without specific code private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) throws IOException { sendErrorResponse(exchange, statusCode, message, null); } /** * Core method to send any JsonObject response with a specific status code. * Handles JSON serialization, setting headers, and writing the response body. * @param exchange The HTTP exchange. * @param jsonObj The JsonObject to send. * @param statusCode The HTTP status code to set. * @throws IOException If sending the response fails. */ private void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj, int statusCode) throws IOException { try { Gson gson = new Gson(); String json = gson.toJson(jsonObj); if (json.length() < 1024) { Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json); } else { Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json.substring(0, 1020) + "..."); } byte[] bytes = json.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); long responseLength = (statusCode == 204) ? -1 : bytes.length; exchange.sendResponseHeaders(statusCode, responseLength); if (responseLength != -1) { OutputStream os = null; try { os = exchange.getResponseBody(); os.write(bytes); os.flush(); } catch (IOException e) { Msg.error(this, "Error writing response body: " + e.getMessage(), e); throw e; } finally { if (os != null) { try { os.close(); } catch (IOException e) { /* Log or ignore */ } } } } else { exchange.getResponseBody().close(); } } catch (Exception e) { Msg.error(this, "Error sending JSON response: " + e.getMessage(), e); throw new IOException("Failed to send JSON response", e); } } // ---------------------------------------------------------------------------------- // Utility: parse query params, parse post params, pagination, etc. // ---------------------------------------------------------------------------------- /** * Executes a Ghidra operation that modifies the program state within a transaction. * Handles Swing thread invocation and ensures the transaction is properly managed. * * @param The return type of the operation (can be Void for operations without return value). * @param program The program context for the transaction. Must not be null. * @param transactionName A descriptive name for the Ghidra transaction log. * @param operation A supplier function (using GhidraSupplier functional interface) * that performs the Ghidra API calls and returns a result. * This function MUST NOT start or end its own transaction. * @return The result of the operation. * @throws TransactionException If the operation fails within the transaction or * if execution on the Swing thread fails. Wraps the original cause. * @throws IllegalArgumentException If program is null. */ private T executeInTransaction(Program program, String transactionName, GhidraSupplier operation) throws TransactionException { if (program == null) { throw new IllegalArgumentException("Program cannot be null for transaction"); } // Use a simple container to pass results/exceptions back from the Swing thread final class ResultContainer { T value = null; Exception exception = null; } final ResultContainer resultContainer = new ResultContainer(); try { // Ensure the operation runs on the Swing Event Dispatch Thread (EDT) // as required by many Ghidra API calls that modify state. SwingUtilities.invokeAndWait(() -> { int txId = -1; // Initialize transaction ID boolean success = false; try { txId = program.startTransaction(transactionName); if (txId < 0) { // Handle case where transaction could not be started (e.g., already in transaction) // This ideally shouldn't happen if called correctly, but good to check. throw new TransactionException("Failed to start transaction: " + transactionName + ". Already in a transaction?"); } resultContainer.value = operation.get(); // Execute the actual Ghidra operation success = true; // Mark as success if no exception was thrown } catch (Exception e) { // Catch any exception from the operation Msg.error(this, "Exception during transaction: " + transactionName, e); resultContainer.exception = e; // Store the exception success = false; // Ensure transaction is rolled back } finally { // Always end the transaction, committing only if success is true if (txId >= 0) { // Only end if successfully started program.endTransaction(txId, success); Msg.debug(this, "Transaction '" + transactionName + "' ended. Success: " + success); } } }); } catch (InterruptedException | InvocationTargetException e) { // Handle exceptions related to SwingUtilities.invokeAndWait Msg.error(this, "Failed to execute transaction '" + transactionName + "' on Swing thread", e); // Wrap this error in our custom exception type throw new TransactionException("Failed to execute operation on Swing thread", e); } // Check if an exception occurred within the Ghidra operation itself if (resultContainer.exception != null) { // Wrap the original Ghidra operation exception throw new TransactionException("Operation failed within transaction: " + transactionName, resultContainer.exception); } // Return the result from the operation return resultContainer.value; } /** * Overload of executeInTransaction for operations that don't return a value (Runnable). * * @param program The program context for the transaction. * @param transactionName The name for the Ghidra transaction log. * @param operation A Runnable that performs the Ghidra API calls. * @throws TransactionException If the operation fails. */ private void executeInTransaction(Program program, String transactionName, Runnable operation) throws TransactionException { // Wrap the Runnable in a GhidraSupplier that returns Void executeInTransaction(program, transactionName, () -> { operation.run(); return null; // Return null for void operations }); } /** * Custom exception for transaction-related errors. */ public static class TransactionException extends Exception { public TransactionException(String message) { super(message); } public TransactionException(String message, Throwable cause) { super(message, cause); } } // ---------------------------------------------------------------------------------- // HTTP Response Sending Methods // ---------------------------------------------------------------------------------- /** * Sends a standard success JSON response with a 200 OK status. * @param exchange The HTTP exchange. * @param resultData The data payload for the 'result' field (can be null). * @param links Optional HATEOAS links. * @throws IOException If sending the response fails. */ private void sendSuccessResponse(HttpExchange exchange, Object resultData, JsonObject links) throws IOException { sendJsonResponse(exchange, createSuccessResponse(exchange, resultData, links), 200); } // Overload for success with data, no links private void sendSuccessResponse(HttpExchange exchange, Object resultData) throws IOException { sendSuccessResponse(exchange, resultData, null); } // Overload for simple success, no data, no links (e.g., for 204 No Content) private void sendSuccessResponse(HttpExchange exchange) throws IOException { sendSuccessResponse(exchange, null, null); } /** * Sends a standard error JSON response with the specified HTTP status code. * @param exchange The HTTP exchange. * @param statusCode The HTTP status code (e.g., 400, 404, 500). * @param message A descriptive error message. * @param errorCode An optional machine-readable error code string. * @throws IOException If sending the response fails. */ private void sendErrorResponse(HttpExchange exchange, int statusCode, String message, String errorCode) throws IOException { sendJsonResponse(exchange, createErrorResponse(exchange, message, errorCode), statusCode); } // Overload for error without specific code private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) throws IOException { sendErrorResponse(exchange, statusCode, message, null); } /** * Core method to send any JsonObject response with a specific status code. * Handles JSON serialization, setting headers, and writing the response body. * @param exchange The HTTP exchange. * @param jsonObj The JsonObject to send. * @param statusCode The HTTP status code to set. * @throws IOException If sending the response fails. */ private void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj, int statusCode) throws IOException { try { Gson gson = new Gson(); String json = gson.toJson(jsonObj); // Use Msg.debug for potentially large responses if (json.length() < 1024) { Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json); } else { Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json.substring(0, 1020) + "..."); } byte[] bytes = json.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); // Ensure CORS headers are set if needed (example, adjust as necessary) // exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); // Determine response length: 0 for 204, actual length otherwise long responseLength = (statusCode == 204) ? -1 : bytes.length; exchange.sendResponseHeaders(statusCode, responseLength); // Only write body if there is content (not for 204) if (responseLength != -1) { OutputStream os = null; try { os = exchange.getResponseBody(); os.write(bytes); os.flush(); } catch (IOException e) { // Log error, but don't try to send another response if body writing fails Msg.error(this, "Error writing response body: " + e.getMessage(), e); throw e; // Re-throw to indicate failure } finally { if (os != null) { try { os.close(); } catch (IOException e) { // Log error during close, but don't mask original exception if any Msg.error(this, "Error closing output stream: " + e.getMessage(), e); } } } } else { // For 204 No Content, just close the exchange without writing body exchange.getResponseBody().close(); } } catch (Exception e) { // Catch broader exceptions during response preparation/sending Msg.error(this, "Error sending JSON response: " + e.getMessage(), e); // Avoid sending another error response here to prevent potential loops throw new IOException("Failed to send JSON response", e); } } // ---------------------------------------------------------------------------------- // Utility: parse query params, parse post params, pagination, etc. // ---------------------------------------------------------------------------------- /** * Parse query parameters from the URL, e.g. ?offset=10&limit=100 */ private Map parseQueryParams(HttpExchange exchange) { Map result = new HashMap<>(); String query = exchange.getRequestURI().getQuery(); // e.g. offset=10&limit=100 if (query != null) { String[] pairs = query.split("&"); for (String p : pairs) { String[] kv = p.split("="); if (kv.length == 2) { result.put(kv[0], kv[1]); } } } return result; } /** * Parse post body params strictly as JSON. */ private Map parseJsonPostParams(HttpExchange exchange) throws IOException { byte[] body = exchange.getRequestBody().readAllBytes(); String bodyStr = new String(body, StandardCharsets.UTF_8); Map params = new HashMap<>(); try { // Use Gson to properly parse JSON Gson gson = new Gson(); JsonObject json = gson.fromJson(bodyStr, JsonObject.class); for (Map.Entry entry : json.entrySet()) { String key = entry.getKey(); JsonElement value = entry.getValue(); if (value.isJsonPrimitive()) { params.put(key, value.getAsString()); } else { // Optionally handle non-primitive types if needed, otherwise stringify params.put(key, value.toString()); } } } catch (Exception e) { Msg.error(this, "Failed to parse JSON request body: " + e.getMessage(), e); // Throw an exception or return an empty map to indicate failure throw new IOException("Invalid JSON request body: " + e.getMessage(), e); } return params; } /** * Convert a list of strings into one big newline-delimited string, applying offset & limit. */ private String paginateList(List items, int offset, int limit) { int start = Math.max(0, offset); int end = Math.min(items.size(), offset + limit); if (start >= items.size()) { return ""; // no items in range } List sub = items.subList(start, end); return String.join("\n", sub); } /** * Parse an integer from a string, or return defaultValue if null/invalid. */ private int parseIntOrDefault(String val, int defaultValue) { if (val == null) return defaultValue; try { return Integer.parseInt(val); } catch (NumberFormatException e) { return defaultValue; } } /** * Escape non-ASCII chars to avoid potential decode issues. */ private String escapeNonAscii(String input) { if (input == null) return ""; StringBuilder sb = new StringBuilder(); for (char c : input.toCharArray()) { if (c >= 32 && c < 127) { sb.append(c); } else { sb.append("\\x"); sb.append(Integer.toHexString(c & 0xFF)); } } return sb.toString(); } /** * Get the current program from the tool */ public Program getCurrentProgram() { if (tool == null) { Msg.debug(this, "Tool is null when trying to get current program"); return null; } try { ProgramManager pm = tool.getService(ProgramManager.class); if (pm == null) { Msg.debug(this, "ProgramManager service is not available"); return null; } Program program = pm.getCurrentProgram(); Msg.debug(this, "Got current program: " + (program != null ? program.getName() : "null")); return program; } catch (Exception e) { Msg.error(this, "Error getting current program", e); return null; } } // Get the currently selected address in Ghidra's UI private Address getCurrentAddress() { try { Program program = getCurrentProgram(); if (program == null) { return null; } // Return the minimum address as a fallback return program.getMinAddress(); } catch (Exception e) { Msg.error(this, "Error getting current address", e); return null; } } // Get the currently selected function in Ghidra's UI private Function getCurrentFunction() { try { Program program = getCurrentProgram(); if (program == null) { return null; } // Return the first function as a fallback Iterator functions = program.getFunctionManager().getFunctions(true); return functions.hasNext() ? functions.next() : null; } catch (Exception e) { Msg.error(this, "Error getting current function", e); return null; } } // Removed old sendResponse method // private void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj) throws IOException { ... } // Keep the core sender // ---------------------------------------------------------------------------------- // HTTP Response Sending Methods // ---------------------------------------------------------------------------------- /** * Sends a standard success JSON response with a 200 OK status. * @param exchange The HTTP exchange. * @param resultData The data payload for the 'result' field (can be null). * @param links Optional HATEOAS links. * @throws IOException If sending the response fails. */ private void sendSuccessResponse(HttpExchange exchange, Object resultData, JsonObject links) throws IOException { sendJsonResponse(exchange, createSuccessResponse(exchange, resultData, links), 200); } // Overload for success with data, no links private void sendSuccessResponse(HttpExchange exchange, Object resultData) throws IOException { sendSuccessResponse(exchange, resultData, null); } // Overload for simple success, no data, no links private void sendSuccessResponse(HttpExchange exchange) throws IOException { sendSuccessResponse(exchange, null, null); } /** * Sends a standard error JSON response with the specified HTTP status code. * @param exchange The HTTP exchange. * @param statusCode The HTTP status code (e.g., 400, 404, 500). * @param message A descriptive error message. * @param errorCode An optional machine-readable error code string. * @throws IOException If sending the response fails. */ private void sendErrorResponse(HttpExchange exchange, int statusCode, String message, String errorCode) throws IOException { sendJsonResponse(exchange, createErrorResponse(exchange, message, errorCode), statusCode); } // Overload for error without specific code private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) throws IOException { sendErrorResponse(exchange, statusCode, message, null); } /** * Core method to send any JsonObject response with a specific status code. * Handles JSON serialization, setting headers, and writing the response body. * @param exchange The HTTP exchange. * @param jsonObj The JsonObject to send. * @param statusCode The HTTP status code to set. * @throws IOException If sending the response fails. */ private void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj, int statusCode) throws IOException { try { Gson gson = new Gson(); String json = gson.toJson(jsonObj); // Use Msg.debug for potentially large responses if (json.length() < 1024) { Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json); } else { Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json.substring(0, 1020) + "..."); } byte[] bytes = json.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); // Ensure CORS headers are set if needed (example, adjust as necessary) // exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); exchange.sendResponseHeaders(statusCode, bytes.length); // Use provided status code OutputStream os = null; try { os = exchange.getResponseBody(); os.write(bytes); os.flush(); } catch (IOException e) { // Log error, but don't try to send another response if body writing fails Msg.error(this, "Error writing response body: " + e.getMessage(), e); throw e; // Re-throw to indicate failure } finally { if (os != null) { try { os.close(); } catch (IOException e) { // Log error during close, but don't mask original exception if any Msg.error(this, "Error closing output stream: " + e.getMessage(), e); } } } } catch (Exception e) { // Catch broader exceptions during response preparation/sending Msg.error(this, "Error sending JSON response: " + e.getMessage(), e); // Avoid sending another error response here to prevent potential loops throw new IOException("Failed to send JSON response", e); } } private int findAvailablePort() { int basePort = 8192; int maxAttempts = 10; for (int attempt = 0; attempt < maxAttempts; attempt++) { int candidate = basePort + attempt; if (!activeInstances.containsKey(candidate)) { try (ServerSocket s = new ServerSocket(candidate)) { return candidate; } catch (IOException e) { continue; } } } throw new RuntimeException("Could not find available port after " + maxAttempts + " attempts"); } @Override public void dispose() { if (server != null) { server.stop(0); Msg.info(this, "HTTP server stopped on port " + port); System.out.println("[GhydraMCP] HTTP server stopped on port " + port); } activeInstances.remove(port); super.dispose(); } }