2888 lines
134 KiB
Java
2888 lines
134 KiB
Java
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> {
|
|
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<Integer, GhydraMCPPlugin> 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<Map<String, Object>> programs = new ArrayList<>();
|
|
Program program = getCurrentProgram();
|
|
if (program != null) {
|
|
Map<String, Object> 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<String, Object> 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<Map<String, String>> projects = new ArrayList<>();
|
|
Project project = tool.getProject();
|
|
if (project != null) {
|
|
Map<String, String> 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<String, String> 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<Map<String, String>> or similar
|
|
resultData = searchFunctionsByName(query, offset, limit);
|
|
} else {
|
|
// TODO: Refactor getAllFunctionNames to return List<Map<String, String>> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<Map<String, Object>> instanceData = new ArrayList<>();
|
|
for (Map.Entry<Integer, GhydraMCPPlugin> entry : activeInstances.entrySet()) {
|
|
Map<String, Object> 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<String, String> 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<String, String> 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<String, Object> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, String> 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<String, String> 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<Map<String, String>> functions = new ArrayList<>();
|
|
for (Function f : program.getFunctionManager().getFunctions(true)) {
|
|
Map<String, String> 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<Map<String, String>> 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<String> 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<String> sorted = new ArrayList<>(classNames);
|
|
Collections.sort(sorted);
|
|
int start = Math.max(0, offset);
|
|
int end = Math.min(sorted.size(), offset + limit);
|
|
List<String> 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<Map<String, String>> segments = new ArrayList<>();
|
|
for (MemoryBlock block : program.getMemory().getBlocks()) {
|
|
Map<String, String> 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<Map<String, String>> 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<Map<String, String>> imports = new ArrayList<>();
|
|
for (Symbol symbol : program.getSymbolTable().getExternalSymbols()) {
|
|
Map<String, String> 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<Map<String, String>> 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<Map<String, String>> exports = new ArrayList<>();
|
|
SymbolTable table = program.getSymbolTable();
|
|
SymbolIterator it = table.getAllSymbols(true);
|
|
|
|
while (it.hasNext()) {
|
|
Symbol s = it.next();
|
|
if (s.isExternalEntryPoint()) {
|
|
Map<String, String> 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<Map<String, String>> 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<String> 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<String> sorted = new ArrayList<>(namespaces);
|
|
Collections.sort(sorted);
|
|
|
|
// Apply pagination
|
|
int start = Math.max(0, offset);
|
|
int end = Math.min(sorted.size(), offset + limit);
|
|
List<String> 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<Map<String, String>> 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<String, String> 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<Map<String, String>> 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<String> 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<String> 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<Map<String, String>> allVariables = new ArrayList<>();
|
|
|
|
// Process all symbols
|
|
Iterator<HighSymbol> symbolIter = highFunction.getLocalSymbolMap().getSymbols();
|
|
while (symbolIter.hasNext()) {
|
|
HighSymbol symbol = symbolIter.next();
|
|
|
|
Map<String, String> 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<HighSymbol> 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<HighSymbol> 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<Map<String, String>> 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<String, String> 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<HighSymbol> symbolIter = highFunc.getLocalSymbolMap().getSymbols();
|
|
while (symbolIter.hasNext()) {
|
|
HighSymbol symbol = symbolIter.next();
|
|
if (!symbol.isParameter()) { // Only list locals, not params
|
|
Map<String, String> 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<Map<String, String>> 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<Map<String, String>> 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<String, String> 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<HighSymbol> symbolIter = highFunc.getLocalSymbolMap().getSymbols();
|
|
while (symbolIter.hasNext()) {
|
|
HighSymbol symbol = symbolIter.next();
|
|
if (symbol.getName().toLowerCase().contains(searchTerm.toLowerCase())) {
|
|
Map<String, String> 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<Map<String, String>> 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 <T> 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> T executeInTransaction(Program program, String transactionName, GhidraSupplier<T> 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 <T> 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> T executeInTransaction(Program program, String transactionName, GhidraSupplier<T> 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<String, String> parseQueryParams(HttpExchange exchange) {
|
|
Map<String, String> 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<String, String> parseJsonPostParams(HttpExchange exchange) throws IOException {
|
|
byte[] body = exchange.getRequestBody().readAllBytes();
|
|
String bodyStr = new String(body, StandardCharsets.UTF_8);
|
|
Map<String, String> params = new HashMap<>();
|
|
|
|
try {
|
|
// Use Gson to properly parse JSON
|
|
Gson gson = new Gson();
|
|
JsonObject json = gson.fromJson(bodyStr, JsonObject.class);
|
|
|
|
for (Map.Entry<String, JsonElement> 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<String> 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<String> 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<Function> 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();
|
|
}
|
|
}
|