package eu.starsong.ghidra.endpoints; import com.google.gson.JsonObject; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; import eu.starsong.ghidra.api.ResponseBuilder; import eu.starsong.ghidra.model.FunctionInfo; import eu.starsong.ghidra.util.GhidraUtil; import eu.starsong.ghidra.util.TransactionHelper; import ghidra.app.decompiler.DecompInterface; import ghidra.app.decompiler.DecompileResults; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressFactory; import ghidra.program.model.listing.Function; import ghidra.program.model.listing.Parameter; import ghidra.program.model.listing.Program; import ghidra.program.model.pcode.HighFunction; import ghidra.program.model.pcode.HighFunctionDBUtil; import ghidra.program.model.pcode.HighSymbol; import ghidra.program.model.symbol.SourceType; import ghidra.util.Msg; import ghidra.util.task.ConsoleTaskMonitor; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.io.IOException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; /** * Endpoints for managing functions within a program. * Implements the /functions endpoints with HATEOAS pattern. */ public class FunctionEndpoints extends AbstractEndpoint { private PluginTool tool; public FunctionEndpoints(Program program, int port) { super(program, port); } public FunctionEndpoints(Program program, int port, PluginTool tool) { super(program, port); this.tool = tool; } @Override protected PluginTool getTool() { return tool; } @Override public void registerEndpoints(HttpServer server) { // Register endpoints in order from most specific to least specific to ensure proper URL path matching // Specifically handle sub-resource endpoints first (these are the most specific) server.createContext("/functions/by-name/", this::handleFunctionByName); // Then handle address-based endpoints with clear pattern matching server.createContext("/functions/", this::handleFunctionByAddress); // Base endpoint last as it's least specific server.createContext("/functions", this::handleFunctions); // Register function-specific endpoints registerAdditionalEndpoints(server); } /** * Register additional convenience endpoints */ private void registerAdditionalEndpoints(HttpServer server) { // NOTE: The /function endpoint is already registered in ProgramEndpoints // We don't register it here to avoid duplicating functionality } /** * Handle requests to the /functions/{address} endpoint */ private void handleFunctionByAddress(HttpExchange exchange) throws IOException { try { String path = exchange.getRequestURI().getPath(); // Check if this is the base endpoint if (path.equals("/functions") || path.equals("/functions/")) { handleFunctions(exchange); return; } // Get the current program Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED"); return; } // Extract function address from path String functionAddress = path.substring("/functions/".length()); // Check for nested resources if (functionAddress.contains("/")) { String resource = functionAddress.substring(functionAddress.indexOf('/') + 1); functionAddress = functionAddress.substring(0, functionAddress.indexOf('/')); handleFunctionResource(exchange, functionAddress, resource); return; } Function function = findFunctionByAddress(functionAddress); if (function == null) { sendErrorResponse(exchange, 404, "Function not found at address: " + functionAddress, "FUNCTION_NOT_FOUND"); return; } String method = exchange.getRequestMethod(); if ("GET".equals(method)) { // Get function details using RESTful response structure FunctionInfo info = buildFunctionInfo(function); ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(info); // Add HATEOAS links String baseUrl = "/functions/" + functionAddress; builder.addLink("self", baseUrl); builder.addLink("program", "/program"); builder.addLink("decompile", baseUrl + "/decompile"); builder.addLink("disassembly", baseUrl + "/disassembly"); builder.addLink("variables", baseUrl + "/variables"); builder.addLink("by_name", "/functions/by-name/" + function.getName()); // Add xrefs links builder.addLink("xrefs_to", "/xrefs?to_addr=" + function.getEntryPoint()); builder.addLink("xrefs_from", "/xrefs?from_addr=" + function.getEntryPoint()); sendJsonResponse(exchange, builder.build(), 200); } else if ("PATCH".equals(method)) { // Update function handleUpdateFunctionRESTful(exchange, function); } else if ("DELETE".equals(method)) { // Delete function handleDeleteFunctionRESTful(exchange, function); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error handling /functions/{address} endpoint", e); sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR"); } } /** * Handle requests to the /functions/by-name/{name} endpoint */ private void handleFunctionByName(HttpExchange exchange) throws IOException { try { String path = exchange.getRequestURI().getPath(); // Extract function name from path (only supporting new format) String functionName = path.substring("/functions/by-name/".length()); // Check for nested resources if (functionName.contains("/")) { String resource = functionName.substring(functionName.indexOf('/') + 1); functionName = functionName.substring(0, functionName.indexOf('/')); handleFunctionResource(exchange, functionName, resource); return; } Function function = findFunctionByName(functionName); if (function == null) { sendErrorResponse(exchange, 404, "Function not found with name: " + functionName, "FUNCTION_NOT_FOUND"); return; } String method = exchange.getRequestMethod(); if ("GET".equals(method)) { // Get function details using RESTful response structure FunctionInfo info = buildFunctionInfo(function); ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(info); // Add HATEOAS links builder.addLink("self", "/functions/by-name/" + functionName); builder.addLink("program", "/program"); builder.addLink("by_address", "/functions/" + function.getEntryPoint()); builder.addLink("decompile", "/functions/" + function.getEntryPoint() + "/decompile"); builder.addLink("disassembly", "/functions/" + function.getEntryPoint() + "/disassembly"); builder.addLink("variables", "/functions/by-name/" + functionName + "/variables"); sendJsonResponse(exchange, builder.build(), 200); } else if ("PATCH".equals(method)) { // Update function handleUpdateFunctionRESTful(exchange, function); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error handling /programs/current/functions/by-name/{name} endpoint", e); sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR"); } } /** * Handle requests to all functions within the current program */ private void handleProgramFunctions(HttpExchange exchange) throws IOException { try { if ("GET".equals(exchange.getRequestMethod())) { Map params = parseQueryParams(exchange); int offset = parseIntOrDefault(params.get("offset"), 0); int limit = parseIntOrDefault(params.get("limit"), 100); String nameFilter = params.get("name"); String nameContainsFilter = params.get("name_contains"); String nameRegexFilter = params.get("name_matches_regex"); String addrFilter = params.get("addr"); Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program is currently loaded", "NO_PROGRAM_LOADED"); return; } List> functions = new ArrayList<>(); // Get all functions for (Function f : program.getFunctionManager().getFunctions(true)) { // Apply filters if (nameFilter != null && !f.getName().equals(nameFilter)) { continue; } if (nameContainsFilter != null && !f.getName().toLowerCase().contains(nameContainsFilter.toLowerCase())) { continue; } if (nameRegexFilter != null && !f.getName().matches(nameRegexFilter)) { continue; } if (addrFilter != null && !f.getEntryPoint().toString().equals(addrFilter)) { continue; } Map func = new HashMap<>(); func.put("name", f.getName()); func.put("address", f.getEntryPoint().toString()); // Add HATEOAS links Map links = new HashMap<>(); Map selfLink = new HashMap<>(); selfLink.put("href", "/programs/current/functions/" + f.getEntryPoint()); links.put("self", selfLink); Map byNameLink = new HashMap<>(); byNameLink.put("href", "/programs/current/functions/by-name/" + f.getName()); links.put("by_name", byNameLink); Map decompileLink = new HashMap<>(); decompileLink.put("href", "/programs/current/functions/" + f.getEntryPoint() + "/decompile"); links.put("decompile", decompileLink); func.put("_links", links); functions.add(func); } // Apply pagination int endIndex = Math.min(functions.size(), offset + limit); List> paginatedFunctions = offset < functions.size() ? functions.subList(offset, endIndex) : new ArrayList<>(); // Build response with pagination links ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(paginatedFunctions); // Add pagination metadata Map metadata = new HashMap<>(); metadata.put("size", functions.size()); metadata.put("offset", offset); metadata.put("limit", limit); builder.metadata(metadata); // Add query parameters for self link StringBuilder queryParams = new StringBuilder(); if (nameFilter != null) { queryParams.append("name=").append(nameFilter).append("&"); } if (nameContainsFilter != null) { queryParams.append("name_contains=").append(nameContainsFilter).append("&"); } if (nameRegexFilter != null) { queryParams.append("name_matches_regex=").append(nameRegexFilter).append("&"); } if (addrFilter != null) { queryParams.append("addr=").append(addrFilter).append("&"); } String queryString = queryParams.toString(); // Add HATEOAS links builder.addLink("self", "/programs/current/functions?" + queryString + "offset=" + offset + "&limit=" + limit); builder.addLink("program", "/programs/current"); // Add next/prev links if applicable if (endIndex < functions.size()) { builder.addLink("next", "/programs/current/functions?" + queryString + "offset=" + endIndex + "&limit=" + limit); } if (offset > 0) { int prevOffset = Math.max(0, offset - limit); builder.addLink("prev", "/programs/current/functions?" + queryString + "offset=" + prevOffset + "&limit=" + limit); } // Add link to create a new function builder.addLink("create", "/programs/current/functions", "POST"); sendJsonResponse(exchange, builder.build(), 200); } else if ("POST".equals(exchange.getRequestMethod())) { // Create a new function handleCreateFunctionRESTful(exchange); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error handling /programs/current/functions endpoint", e); sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR"); } } /** * Handle requests to function resources like /programs/current/functions/{address}/decompile */ private void handleFunctionResourceRESTful(HttpExchange exchange, String functionAddress, String resource) throws IOException { Function function = findFunctionByAddress(functionAddress); if (function == null) { sendErrorResponse(exchange, 404, "Function not found at address: " + functionAddress, "FUNCTION_NOT_FOUND"); return; } if (resource.equals("decompile")) { handleDecompileFunction(exchange, function); } else if (resource.equals("disassembly")) { handleDisassembleFunction(exchange, function); } else if (resource.equals("variables")) { handleFunctionVariables(exchange, function); } else { sendErrorResponse(exchange, 404, "Function resource not found: " + resource, "RESOURCE_NOT_FOUND"); } } /** * Handle requests to function resources by name like /programs/current/functions/by-name/{name}/variables */ private void handleFunctionResourceByNameRESTful(HttpExchange exchange, String functionName, String resource) throws IOException { Function function = findFunctionByName(functionName); if (function == null) { sendErrorResponse(exchange, 404, "Function not found with name: " + functionName, "FUNCTION_NOT_FOUND"); return; } if (resource.equals("variables")) { handleFunctionVariables(exchange, function); } else if (resource.equals("decompile")) { handleDecompileFunction(exchange, function); } else if (resource.equals("disassembly")) { handleDisassembleFunction(exchange, function); } else { sendErrorResponse(exchange, 404, "Function resource not found: " + resource, "RESOURCE_NOT_FOUND"); } } /** * Handle PATCH requests to update a function using the RESTful endpoint */ private void handleUpdateFunctionRESTful(HttpExchange exchange, Function function) throws IOException { // Implementation similar to handleUpdateFunction but with RESTful response structure Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program is currently loaded", "NO_PROGRAM_LOADED"); return; } // Parse request body Map params = parseJsonPostParams(exchange); String newName = params.get("name"); String signature = params.get("signature"); String comment = params.get("comment"); // Apply changes boolean changed = false; if (newName != null && !newName.isEmpty() && !newName.equals(function.getName())) { // Rename function try { TransactionHelper.executeInTransaction(program, "Rename Function", () -> { function.setName(newName, ghidra.program.model.symbol.SourceType.USER_DEFINED); return null; }); changed = true; } catch (Exception e) { sendErrorResponse(exchange, 400, "Failed to rename function: " + e.getMessage(), "RENAME_FAILED"); return; } } if (signature != null && !signature.isEmpty()) { // Update function signature using our utility method try { boolean success = TransactionHelper.executeInTransaction(program, "Set Function Signature", () -> { return GhidraUtil.setFunctionSignature(function, signature); }); if (!success) { sendErrorResponse(exchange, 400, "Failed to set function signature: invalid signature format", "SIGNATURE_FAILED"); return; } changed = true; } catch (Exception e) { sendErrorResponse(exchange, 400, "Failed to set function signature: " + e.getMessage(), "SIGNATURE_FAILED"); return; } } if (comment != null) { // Update comment try { TransactionHelper.executeInTransaction(program, "Set Function Comment", () -> { function.setComment(comment); return null; }); changed = true; } catch (Exception e) { sendErrorResponse(exchange, 400, "Failed to set function comment: " + e.getMessage(), "COMMENT_FAILED"); return; } } if (!changed) { sendErrorResponse(exchange, 400, "No changes specified", "NO_CHANGES"); return; } // Return updated function with RESTful response structure FunctionInfo info = buildFunctionInfo(function); ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(info); // Add HATEOAS links builder.addLink("self", "/programs/current/functions/" + function.getEntryPoint()); builder.addLink("by_name", "/programs/current/functions/by-name/" + function.getName()); builder.addLink("program", "/programs/current"); sendJsonResponse(exchange, builder.build(), 200); } /** * Handle DELETE requests to delete a function using the RESTful endpoint */ private void handleDeleteFunctionRESTful(HttpExchange exchange, Function function) throws IOException { // Placeholder for function deletion sendErrorResponse(exchange, 501, "Function deletion not implemented", "NOT_IMPLEMENTED"); } /** * Handle POST requests to create a new function using the RESTful endpoint */ private void handleCreateFunctionRESTful(HttpExchange exchange) throws IOException { Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program is currently loaded", "NO_PROGRAM_LOADED"); return; } // Parse request body Map params = parseJsonPostParams(exchange); String addressStr = params.get("address"); if (addressStr == null || addressStr.isEmpty()) { sendErrorResponse(exchange, 400, "Missing address parameter", "MISSING_PARAMETER"); return; } // Get address AddressFactory addressFactory = program.getAddressFactory(); Address address; try { address = addressFactory.getAddress(addressStr); } catch (Exception e) { sendErrorResponse(exchange, 400, "Invalid address format: " + addressStr, "INVALID_ADDRESS"); return; } if (address == null) { sendErrorResponse(exchange, 400, "Invalid address: " + addressStr, "INVALID_ADDRESS"); return; } // Check if address is in a valid memory block if (program.getMemory().getBlock(address) == null) { sendErrorResponse(exchange, 400, "Address is not in a defined memory block: " + addressStr, "INVALID_ADDRESS"); return; } // Check if function already exists if (program.getFunctionManager().getFunctionAt(address) != null) { sendErrorResponse(exchange, 409, "Function already exists at address: " + addressStr, "FUNCTION_EXISTS"); return; } // Attempt to disassemble the code at the specified address before creating a function try { TransactionHelper.executeInTransaction(program, "Disassemble Before Function Creation", () -> { // Check if there's already a defined instruction at the address if (program.getListing().getInstructionAt(address) == null) { // Attempt to directly disassemble at the address try { ghidra.app.cmd.disassemble.DisassembleCommand cmd = new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true); cmd.applyTo(program); } catch (Exception ex) { Msg.warn(this, "Basic disassembly failed: " + ex.getMessage()); } } return null; }); } catch (Exception e) { // Log the error but proceed with function creation attempt anyway Msg.warn(this, "Disassembly before function creation failed: " + e.getMessage()); } // Create function Function function; try { function = TransactionHelper.executeInTransaction(program, "Create Function", () -> { return program.getFunctionManager().createFunction(null, address, null, null); }); } catch (Exception e) { // If function creation initially fails, try a different approach try { Msg.info(this, "Initial function creation failed, attempting with code unit clearing"); // Clear any existing data at this location and try disassembling again TransactionHelper.executeInTransaction(program, "Clear and Disassemble", () -> { // Clear existing data at the address program.getListing().clearCodeUnits(address, address, false); // Try disassembling again ghidra.app.cmd.disassemble.DisassembleCommand cmd = new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true); cmd.applyTo(program); return null; }); // Try creating the function again function = TransactionHelper.executeInTransaction(program, "Create Function Retry", () -> { return program.getFunctionManager().createFunction(null, address, null, null); }); } catch (Exception e2) { // Both attempts failed, return the error sendErrorResponse(exchange, 400, "Failed to create function after multiple attempts: " + e.getMessage(), "CREATE_FAILED"); return; } } if (function == null) { sendErrorResponse(exchange, 500, "Failed to create function", "CREATE_FAILED"); return; } // Return created function with RESTful response structure FunctionInfo info = buildFunctionInfo(function); ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(info); // Add HATEOAS links builder.addLink("self", "/programs/current/functions/" + function.getEntryPoint()); builder.addLink("by_name", "/programs/current/functions/by-name/" + function.getName()); builder.addLink("program", "/programs/current"); builder.addLink("decompile", "/programs/current/functions/" + function.getEntryPoint() + "/decompile"); builder.addLink("disassembly", "/programs/current/functions/" + function.getEntryPoint() + "/disassembly"); sendJsonResponse(exchange, builder.build(), 201); } /** * Handle requests to the /functions endpoint */ public void handleFunctions(HttpExchange exchange) throws IOException { try { // Always check for program availability first Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED"); return; } if ("GET".equals(exchange.getRequestMethod())) { Map params = parseQueryParams(exchange); int offset = parseIntOrDefault(params.get("offset"), 0); int limit = parseIntOrDefault(params.get("limit"), 100); String nameFilter = params.get("name"); String nameContainsFilter = params.get("name_contains"); String nameRegexFilter = params.get("name_matches_regex"); String addrFilter = params.get("addr"); List> functions = new ArrayList<>(); // Get all functions for (Function f : program.getFunctionManager().getFunctions(true)) { // Apply filters if (nameFilter != null && !f.getName().equals(nameFilter)) { continue; } if (nameContainsFilter != null && !f.getName().toLowerCase().contains(nameContainsFilter.toLowerCase())) { continue; } if (nameRegexFilter != null && !f.getName().matches(nameRegexFilter)) { continue; } if (addrFilter != null && !f.getEntryPoint().toString().equals(addrFilter)) { continue; } Map func = new HashMap<>(); func.put("name", f.getName()); func.put("address", f.getEntryPoint().toString()); // Add HATEOAS links (fixed to use proper URL paths) Map links = new HashMap<>(); Map selfLink = new HashMap<>(); selfLink.put("href", "/functions/" + f.getEntryPoint()); links.put("self", selfLink); Map programLink = new HashMap<>(); programLink.put("href", "/program"); links.put("program", programLink); func.put("_links", links); functions.add(func); } // Apply pagination int endIndex = Math.min(functions.size(), offset + limit); List> paginatedFunctions = offset < functions.size() ? functions.subList(offset, endIndex) : new ArrayList<>(); // Build response with pagination links ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(paginatedFunctions); // Add pagination metadata Map metadata = new HashMap<>(); metadata.put("size", functions.size()); metadata.put("offset", offset); metadata.put("limit", limit); builder.metadata(metadata); // Add HATEOAS links builder.addLink("self", "/functions?offset=" + offset + "&limit=" + limit); // Add next/prev links if applicable if (endIndex < functions.size()) { builder.addLink("next", "/functions?offset=" + endIndex + "&limit=" + limit); } if (offset > 0) { int prevOffset = Math.max(0, offset - limit); builder.addLink("prev", "/functions?offset=" + prevOffset + "&limit=" + limit); } // Add link to create a new function builder.addLink("create", "/functions", "POST"); sendJsonResponse(exchange, builder.build(), 200); } else if ("POST".equals(exchange.getRequestMethod())) { // Create a new function handleCreateFunction(exchange); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error handling /functions endpoint", e); sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR"); } } /** * Handle requests to the /functions/{name} endpoint */ private void handleFunction(HttpExchange exchange, String path) throws IOException { try { String functionName; // If path is provided, use it; otherwise extract from the request URI if (path != null && path.startsWith("/functions/")) { functionName = path.substring("/functions/".length()); } else { String requestPath = exchange.getRequestURI().getPath(); functionName = requestPath.substring("/functions/".length()); } // Check for nested resources if (functionName.contains("/")) { handleFunctionResource(exchange, functionName); return; } String method = exchange.getRequestMethod(); if ("GET".equals(method)) { // Get function details handleGetFunction(exchange, functionName); } else if ("PATCH".equals(method)) { // Update function handleUpdateFunction(exchange, functionName); } else if ("DELETE".equals(method)) { // Delete function handleDeleteFunction(exchange, functionName); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error handling /functions/{name} endpoint", e); sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR"); } } /** * Handle requests to the /functions/{name} endpoint derived from the path */ private void handleFunctionByPath(HttpExchange exchange) throws IOException { try { String path = exchange.getRequestURI().getPath(); String functionName = path.substring("/functions/".length()); // Check for nested resources if (functionName.contains("/")) { handleFunctionResource(exchange, functionName); return; } String method = exchange.getRequestMethod(); if ("GET".equals(method)) { // Get function details handleGetFunction(exchange, functionName); } else if ("PATCH".equals(method)) { // Update function handleUpdateFunction(exchange, functionName); } else if ("DELETE".equals(method)) { // Delete function handleDeleteFunction(exchange, functionName); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } catch (Exception e) { Msg.error(this, "Error handling /functions/{name} endpoint", e); sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR"); } } /** * Handle requests to function resources like /functions/{name}/decompile */ private void handleFunctionResource(HttpExchange exchange, String functionIdent, String resource) throws IOException { Function function = null; // Try to find function by address first function = findFunctionByAddress(functionIdent); // If not found by address, try by name if (function == null) { function = findFunctionByName(functionIdent); } if (function == null) { sendErrorResponse(exchange, 404, "Function not found: " + functionIdent, "FUNCTION_NOT_FOUND"); return; } if (resource.equals("decompile")) { handleDecompileFunction(exchange, function); } else if (resource.equals("disassembly")) { handleDisassembleFunction(exchange, function); } else if (resource.equals("variables")) { handleFunctionVariables(exchange, function); } else if (resource.startsWith("variables/")) { // Handle variable operations String variableName = resource.substring("variables/".length()); if ("PATCH".equals(exchange.getRequestMethod())) { handleUpdateVariable(exchange, function, variableName); } else { sendErrorResponse(exchange, 405, "Method not allowed for variable operations", "METHOD_NOT_ALLOWED"); } } else { sendErrorResponse(exchange, 404, "Function resource not found: " + resource, "RESOURCE_NOT_FOUND"); } } private void handleFunctionResource(HttpExchange exchange, String functionPath) throws IOException { int slashIndex = functionPath.indexOf('/'); if (slashIndex == -1) { sendErrorResponse(exchange, 404, "Invalid function resource path: " + functionPath, "RESOURCE_NOT_FOUND"); return; } String functionIdent = functionPath.substring(0, slashIndex); String resource = functionPath.substring(slashIndex + 1); handleFunctionResource(exchange, functionIdent, resource); } /** * Handle GET requests to get function details */ public void handleGetFunction(HttpExchange exchange, String functionName) throws IOException { Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED"); return; } Function function = findFunctionByName(functionName); if (function == null) { sendErrorResponse(exchange, 404, "Function not found: " + functionName, "FUNCTION_NOT_FOUND"); return; } // Build function info FunctionInfo info = buildFunctionInfo(function); // Build response with HATEOAS links ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(info); // Add HATEOAS links builder.addLink("self", "/functions/" + functionName); builder.addLink("program", "/programs/current"); builder.addLink("decompile", "/functions/" + functionName + "/decompile"); builder.addLink("disassembly", "/functions/" + functionName + "/disassembly"); builder.addLink("variables", "/functions/" + functionName + "/variables"); // Add xrefs links builder.addLink("xrefs_to", "/programs/current/xrefs?to_addr=" + function.getEntryPoint().toString()); builder.addLink("xrefs_from", "/programs/current/xrefs?from_addr=" + function.getEntryPoint().toString()); sendJsonResponse(exchange, builder.build(), 200); } /** * Handle PATCH requests to update a function */ private void handleUpdateFunction(HttpExchange exchange, String functionName) throws IOException { Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED"); return; } Function function = findFunctionByName(functionName); if (function == null) { sendErrorResponse(exchange, 404, "Function not found: " + functionName, "FUNCTION_NOT_FOUND"); return; } // Parse request body Map params = parseJsonPostParams(exchange); String newName = params.get("name"); String signature = params.get("signature"); String comment = params.get("comment"); // Apply changes boolean changed = false; if (newName != null && !newName.isEmpty() && !newName.equals(function.getName())) { // Rename function try { TransactionHelper.executeInTransaction(program, "Rename Function", () -> { function.setName(newName, ghidra.program.model.symbol.SourceType.USER_DEFINED); return null; }); changed = true; } catch (Exception e) { sendErrorResponse(exchange, 400, "Failed to rename function: " + e.getMessage(), "RENAME_FAILED"); return; } } if (signature != null && !signature.isEmpty()) { // Update function signature using our utility method try { boolean success = TransactionHelper.executeInTransaction(program, "Set Function Signature", () -> { return GhidraUtil.setFunctionSignature(function, signature); }); if (!success) { sendErrorResponse(exchange, 400, "Failed to set function signature: invalid signature format", "SIGNATURE_FAILED"); return; } changed = true; } catch (Exception e) { sendErrorResponse(exchange, 400, "Failed to set function signature: " + e.getMessage(), "SIGNATURE_FAILED"); return; } } if (comment != null) { // Update comment try { TransactionHelper.executeInTransaction(program, "Set Function Comment", () -> { function.setComment(comment); return null; }); changed = true; } catch (Exception e) { sendErrorResponse(exchange, 400, "Failed to set function comment: " + e.getMessage(), "COMMENT_FAILED"); return; } } if (!changed) { sendErrorResponse(exchange, 400, "No changes specified", "NO_CHANGES"); return; } // Return updated function FunctionInfo info = buildFunctionInfo(function); ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(info); // Add HATEOAS links builder.addLink("self", "/functions/" + function.getName()); sendJsonResponse(exchange, builder.build(), 200); } /** * Handle DELETE requests to delete a function */ private void handleDeleteFunction(HttpExchange exchange, String functionName) throws IOException { // This is a placeholder - actual implementation would delete the function sendErrorResponse(exchange, 501, "Function deletion not implemented", "NOT_IMPLEMENTED"); } /** * Handle POST requests to create a new function */ private void handleCreateFunction(HttpExchange exchange) throws IOException { Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED"); return; } // Parse request body Map params = parseJsonPostParams(exchange); String addressStr = params.get("address"); if (addressStr == null || addressStr.isEmpty()) { sendErrorResponse(exchange, 400, "Missing address parameter", "MISSING_PARAMETER"); return; } // Get address AddressFactory addressFactory = program.getAddressFactory(); Address address; try { address = addressFactory.getAddress(addressStr); } catch (Exception e) { sendErrorResponse(exchange, 400, "Invalid address format: " + addressStr, "INVALID_ADDRESS"); return; } if (address == null) { sendErrorResponse(exchange, 400, "Invalid address: " + addressStr, "INVALID_ADDRESS"); return; } // Check if function already exists if (program.getFunctionManager().getFunctionAt(address) != null) { sendErrorResponse(exchange, 409, "Function already exists at address: " + addressStr, "FUNCTION_EXISTS"); return; } // Attempt to disassemble the code at the specified address before creating a function try { TransactionHelper.executeInTransaction(program, "Disassemble Before Function Creation", () -> { // Check if there's already a defined instruction at the address if (program.getListing().getInstructionAt(address) == null) { // Attempt to directly disassemble at the address try { ghidra.app.cmd.disassemble.DisassembleCommand cmd = new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true); cmd.applyTo(program); } catch (Exception ex) { Msg.warn(this, "Basic disassembly failed: " + ex.getMessage()); } } return null; }); } catch (Exception e) { // Log the error but proceed with function creation attempt anyway Msg.warn(this, "Disassembly before function creation failed: " + e.getMessage()); } // Create function Function function; try { function = TransactionHelper.executeInTransaction(program, "Create Function", () -> { return program.getFunctionManager().createFunction(null, address, null, null); }); } catch (Exception e) { // If function creation initially fails, try a different approach try { Msg.info(this, "Initial function creation failed, attempting with code unit clearing"); // Clear any existing data at this location and try disassembling again TransactionHelper.executeInTransaction(program, "Clear and Disassemble", () -> { // Clear existing data at the address program.getListing().clearCodeUnits(address, address, false); // Try disassembling again ghidra.app.cmd.disassemble.DisassembleCommand cmd = new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true); cmd.applyTo(program); return null; }); // Try creating the function again function = TransactionHelper.executeInTransaction(program, "Create Function Retry", () -> { return program.getFunctionManager().createFunction(null, address, null, null); }); } catch (Exception e2) { // Both attempts failed, return the error sendErrorResponse(exchange, 400, "Failed to create function after multiple attempts: " + e.getMessage(), "CREATE_FAILED"); return; } } if (function == null) { sendErrorResponse(exchange, 500, "Failed to create function", "CREATE_FAILED"); return; } // Return created function FunctionInfo info = buildFunctionInfo(function); ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(info); // Add HATEOAS links builder.addLink("self", "/functions/" + function.getName()); sendJsonResponse(exchange, builder.build(), 201); } /** * Handle requests to decompile a function */ public void handleDecompileFunction(HttpExchange exchange, Function function) throws IOException { if ("GET".equals(exchange.getRequestMethod())) { Map params = parseQueryParams(exchange); boolean syntaxTree = Boolean.parseBoolean(params.getOrDefault("syntax_tree", "false")); String style = params.getOrDefault("style", "normalize"); String format = params.getOrDefault("format", "structured"); int timeout = parseIntOrDefault(params.get("timeout"), 30); // Decompile function String decompilation = GhidraUtil.decompileFunction(function); // Create function info Map functionInfo = new HashMap<>(); functionInfo.put("address", function.getEntryPoint().toString()); functionInfo.put("name", function.getName()); // Create the result structure according to GHIDRA_HTTP_API.md Map result = new HashMap<>(); result.put("function", functionInfo); result.put("decompiled", decompilation != null ? decompilation : "// Decompilation failed"); // Add syntax tree if requested if (syntaxTree) { result.put("syntax_tree", "Syntax tree not implemented"); } ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(result); // Path for links (updated to use the correct paths) String functionPath = "/functions/" + function.getEntryPoint().toString(); // Add HATEOAS links builder.addLink("self", functionPath + "/decompile"); builder.addLink("function", functionPath); builder.addLink("disassembly", functionPath + "/disassembly"); builder.addLink("variables", functionPath + "/variables"); builder.addLink("program", "/program"); sendJsonResponse(exchange, builder.build(), 200); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } /** * Handle requests to disassemble a function */ public void handleDisassembleFunction(HttpExchange exchange, Function function) throws IOException { if ("GET".equals(exchange.getRequestMethod())) { List> disassembly = new ArrayList<>(); Program program = function.getProgram(); if (program != null) { try { // Get actual disassembly from the program Address startAddr = function.getEntryPoint(); Address endAddr = function.getBody().getMaxAddress(); ghidra.program.model.listing.Listing listing = program.getListing(); ghidra.program.model.listing.InstructionIterator instrIter = listing.getInstructions(startAddr, true); while (instrIter.hasNext() && disassembly.size() < 100) { ghidra.program.model.listing.Instruction instr = instrIter.next(); // Stop if we've gone past the end of the function if (instr.getAddress().compareTo(endAddr) > 0) { break; } Map instrMap = new HashMap<>(); instrMap.put("address", instr.getAddress().toString()); // Get actual bytes byte[] bytes = new byte[instr.getLength()]; program.getMemory().getBytes(instr.getAddress(), bytes); StringBuilder hexBytes = new StringBuilder(); for (byte b : bytes) { hexBytes.append(String.format("%02X", b & 0xFF)); } instrMap.put("bytes", hexBytes.toString()); // Get mnemonic and operands instrMap.put("mnemonic", instr.getMnemonicString()); instrMap.put("operands", instr.toString().substring(instr.getMnemonicString().length()).trim()); disassembly.add(instrMap); } } catch (Exception e) { Msg.error(this, "Error getting disassembly for function: " + function.getName(), e); } // If we couldn't get real instructions, add placeholder if (disassembly.isEmpty()) { Address addr = function.getEntryPoint(); for (int i = 0; i < 5; i++) { Map instruction = new HashMap<>(); instruction.put("address", addr.toString()); instruction.put("mnemonic", "???"); instruction.put("operands", "???"); instruction.put("bytes", "????"); disassembly.add(instruction); addr = addr.add(2); } } } Map functionInfo = new HashMap<>(); functionInfo.put("address", function.getEntryPoint().toString()); functionInfo.put("name", function.getName()); functionInfo.put("signature", function.getSignature().toString()); Map result = new HashMap<>(); result.put("function", functionInfo); result.put("instructions", disassembly); ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(result); // Update to use the correct paths String functionPath = "/functions/" + function.getEntryPoint().toString(); builder.addLink("self", functionPath + "/disassembly"); builder.addLink("function", functionPath); builder.addLink("decompile", functionPath + "/decompile"); builder.addLink("variables", functionPath + "/variables"); builder.addLink("program", "/program"); sendJsonResponse(exchange, builder.build(), 200); } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } /** * Handle requests to get function variables */ public void handleFunctionVariables(HttpExchange exchange, Function function) throws IOException { if ("GET".equals(exchange.getRequestMethod())) { List> variables = GhidraUtil.getFunctionVariables(function); Map functionInfo = new HashMap<>(); functionInfo.put("address", function.getEntryPoint().toString()); functionInfo.put("name", function.getName()); if (function.getReturnType() != null) { functionInfo.put("returnType", function.getReturnType().getName()); } if (function.getCallingConventionName() != null) { functionInfo.put("callingConvention", function.getCallingConventionName()); } Map result = new HashMap<>(); result.put("function", functionInfo); result.put("variables", variables); // Update to use the correct paths String functionPath = "/functions/" + function.getEntryPoint().toString(); String functionByNamePath = "/functions/by-name/" + function.getName(); ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(result); builder.addLink("self", functionPath + "/variables"); builder.addLink("function", functionPath); builder.addLink("by_name", functionByNamePath); builder.addLink("decompile", functionPath + "/decompile"); builder.addLink("disassembly", functionPath + "/disassembly"); builder.addLink("program", "/program"); sendJsonResponse(exchange, builder.build(), 200); } else if ("PATCH".equals(exchange.getRequestMethod())) { String path = exchange.getRequestURI().getPath(); if (path.contains("/variables/")) { String variableName = path.substring(path.lastIndexOf('/') + 1); handleUpdateVariable(exchange, function, variableName); } else { sendErrorResponse(exchange, 400, "Missing variable name", "MISSING_PARAMETER"); } } else { sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); } } /** * Handle requests to update a function variable */ private void handleUpdateVariable(HttpExchange exchange, Function function, String variableName) throws IOException { try { // Parse the request body to get the update parameters Map params = parseJsonPostParams(exchange); String newName = params.get("name"); String newDataType = params.get("data_type"); if (newName == null && newDataType == null) { sendErrorResponse(exchange, 400, "Missing update parameters - name or data_type required", "MISSING_PARAMETER"); return; } // Use transaction to update variable Program program = getCurrentProgram(); if (program == null) { sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED"); return; } boolean success = TransactionHelper.executeInTransaction(program, "Update Variable", () -> { try { // This requires a decompile operation to get the HighFunction DecompInterface decomp = new DecompInterface(); try { decomp.openProgram(program); DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); if (results.decompileCompleted()) { HighFunction highFunc = results.getHighFunction(); if (highFunc != null) { // Find the variable in the high function for (Iterator symbolIter = highFunc.getLocalSymbolMap().getSymbols(); symbolIter.hasNext();) { HighSymbol symbol = symbolIter.next(); if (symbol.getName().equals(variableName)) { // Rename the variable using HighFunctionDBUtil HighFunctionDBUtil.updateDBVariable(symbol, newName, null, SourceType.USER_DEFINED); return true; } } } } } finally { decomp.dispose(); } return false; } catch (Exception e) { Msg.error(this, "Error updating variable: " + e.getMessage(), e); return false; } }); if (success) { // Create a successful response Map result = new HashMap<>(); result.put("name", newName != null ? newName : variableName); result.put("function", function.getName()); result.put("address", function.getEntryPoint().toString()); result.put("message", "Variable renamed successfully"); ResponseBuilder builder = new ResponseBuilder(exchange, port) .success(true) .result(result); sendJsonResponse(exchange, builder.build(), 200); } else { sendErrorResponse(exchange, 404, "Function resource not found: variables/" + variableName, "RESOURCE_NOT_FOUND"); } } catch (Exception e) { sendErrorResponse(exchange, 500, "Error processing variable update request: " + e.getMessage(), "INTERNAL_ERROR"); } } /** * Helper method to find a function by name */ private Function findFunctionByName(String name) { Program program = getCurrentProgram(); if (program == null) { return null; } for (Function f : program.getFunctionManager().getFunctions(true)) { if (f.getName().equals(name)) { return f; } } return null; } private Function findFunctionByAddress(String addressString) { Program program = getCurrentProgram(); if (program == null) { return null; } try { ghidra.program.model.address.Address address = program.getAddressFactory().getAddress(addressString); return program.getFunctionManager().getFunctionAt(address); } catch (Exception e) { return null; } } /** * Helper method to build a FunctionInfo object from a Function */ private FunctionInfo buildFunctionInfo(Function function) { FunctionInfo.Builder builder = FunctionInfo.builder() .name(function.getName()) .address(function.getEntryPoint().toString()) .signature(function.getSignature().getPrototypeString()); // Add return type if (function.getReturnType() != null) { builder.returnType(function.getReturnType().getName()); } // Add calling convention if (function.getCallingConventionName() != null) { builder.callingConvention(function.getCallingConventionName()); } // Add namespace if (function.getParentNamespace() != null) { builder.namespace(function.getParentNamespace().getName()); } // Add external flag builder.isExternal(function.isExternal()); // Add parameters for (int i = 0; i < function.getParameterCount(); i++) { ghidra.program.model.listing.Parameter param = function.getParameter(i); FunctionInfo.ParameterInfo paramInfo = FunctionInfo.ParameterInfo.builder() .name(param.getName()) .dataType(param.getDataType().getName()) .ordinal(i) .storage(param.getRegister() != null ? param.getRegister().getName() : "stack") .build(); builder.addParameter(paramInfo); } return builder.build(); } }