WIP fix endpoints
This commit is contained in:
parent
5d6b202599
commit
3311e88565
@ -2,7 +2,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This API provides a Hypermedia-driven interface (HATEOAS) to interact with Ghidra's CodeBrowser, enabling AI-driven and automated reverse engineering workflows. It allows interaction with Ghidra projects, programs (binaries), functions, symbols, data, memory segments, cross-references, and analysis features. Programs are addressed by their unique identifier within Ghidra (`project:/path/to/file`).
|
||||
This API provides a Hypermedia-driven interface (HATEOAS) to interact with Ghidra's CodeBrowser, enabling AI-driven and automated reverse engineering workflows. It allows interaction with Ghidra projects, programs (binaries), functions, symbols, data, memory segments, cross-references, and analysis features. Each program open in Ghidra will have its own instance, so all resources are specific to that program.
|
||||
|
||||
## General Concepts
|
||||
|
||||
@ -55,9 +55,9 @@ List results (arrays in `result`) will typically include pagination information
|
||||
"offset": 0,
|
||||
"limit": 50,
|
||||
"_links": {
|
||||
"self": { "href": "/programs/proj:/file.bin/functions?offset=0&limit=50" },
|
||||
"next": { "href": "/programs/proj:/file.bin/functions?offset=50&limit=50" }, // Present if more items exist
|
||||
"prev": { "href": "/programs/proj:/file.bin/functions?offset=0&limit=50" } // Present if not the first page
|
||||
"self": { "href": "/functions?offset=0&limit=50" },
|
||||
"next": { "href": "/functions?offset=50&limit=50" }, // Present if more items exist
|
||||
"prev": { "href": "/functions?offset=0&limit=50" } // Present if not the first page
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -92,10 +92,10 @@ Common HTTP Status Codes:
|
||||
|
||||
### Addressing and Searching
|
||||
|
||||
Resources like functions, data, and symbols often exist at specific memory addresses and may have names. The primary identifier for a program is its Ghidra path, e.g., `myproject:/path/to/mybinary.exe`.
|
||||
Resources like functions, data, and symbols often exist at specific memory addresses and may have names.
|
||||
|
||||
- **By Address:** Use the resource's path with the address (hexadecimal, e.g., `0x401000` or `08000004`).
|
||||
- Example: `GET /programs/myproject:/mybinary.exe/functions/0x401000`
|
||||
- Example: `GET /functions/0x401000`
|
||||
- **Querying Lists:** List endpoints (e.g., `/functions`, `/symbols`, `/data`) support filtering via query parameters:
|
||||
- `?addr=[address in hex]`: Find item at a specific address.
|
||||
- `?name=[full_name]`: Find item(s) with an exact name match (case-sensitive).
|
||||
@ -118,8 +118,8 @@ Returns the version of the running Ghidra plugin and its API. Essential for comp
|
||||
"instance": "http://localhost:1337",
|
||||
"success": true,
|
||||
"result": {
|
||||
"plugin_version": "v1.4.0", // Example plugin build version
|
||||
"api_version": 1 // Ordinal API version
|
||||
"plugin_version": "v2.0.0", // Example plugin build version
|
||||
"api_version": 2 // Ordinal API version
|
||||
},
|
||||
"_links": {
|
||||
"self": { "href": "/plugin-version" }
|
||||
@ -129,27 +129,22 @@ Returns the version of the running Ghidra plugin and its API. Essential for comp
|
||||
|
||||
## Resource Types
|
||||
|
||||
Base path for all program-specific resources: `/programs/{program_id}` where `program_id` is the URL-encoded Ghidra identifier (e.g., `myproject%3A%2Fpath%2Fto%2Fmybinary.exe`).
|
||||
Each Ghidra plugin instance runs in the context of a single program, so all resources are relative to the current program. The program's details are available through the `GET /info` and `GET /programs/current` endpoints.
|
||||
|
||||
### 1. Projects
|
||||
|
||||
Represents Ghidra projects, containers for programs.
|
||||
|
||||
- **`GET /projects`**: List all available Ghidra projects.
|
||||
- **`POST /projects`**: Create a new Ghidra project. Request body should specify `name` and optionally `directory`.
|
||||
- **`GET /projects/{project_name}`**: Get details about a specific project (e.g., location, list of open programs within it via links).
|
||||
- **`GET /project`**: Get details about the current project (e.g., location, list of open programs within it via links).
|
||||
|
||||
### 2. Programs
|
||||
|
||||
Represents individual binaries loaded in Ghidra projects.
|
||||
|
||||
- **`GET /programs`**: List all programs across all projects. Can be filtered by project (`?project={project_name}`).
|
||||
- **`POST /programs`**: Load/import a new binary into a specified project. Request body needs `project_name`, `file_path`, and optionally `language_id`, `compiler_spec_id`, and loader options. Returns the newly created program resource details upon successful import and analysis (which might take time).
|
||||
- **`GET /programs/{program_id}`**: Get metadata for a specific program (e.g., name, architecture, memory layout, analysis status).
|
||||
- **`GET /program`**: Get metadata for the current program (e.g., name, architecture, memory layout, analysis status).
|
||||
```json
|
||||
// Example Response Fragment for GET /programs/myproject%3A%2Fmybinary.exe
|
||||
// Example Response Fragment for GET /program
|
||||
"result": {
|
||||
"program_id": "myproject:/mybinary.exe",
|
||||
"name": "mybinary.exe",
|
||||
"project": "myproject",
|
||||
"language_id": "x86:LE:64:default",
|
||||
@ -161,36 +156,34 @@ Represents individual binaries loaded in Ghidra projects.
|
||||
// ... other metadata
|
||||
},
|
||||
"_links": {
|
||||
"self": { "href": "/programs/myproject%3A%2Fmybinary.exe" },
|
||||
"project": { "href": "/projects/myproject" },
|
||||
"functions": { "href": "/programs/myproject%3A%2Fmybinary.exe/functions" },
|
||||
"symbols": { "href": "/programs/myproject%3A%2Fmybinary.exe/symbols" },
|
||||
"data": { "href": "/programs/myproject%3A%2Fmybinary.exe/data" },
|
||||
"segments": { "href": "/programs/myproject%3A%2Fmybinary.exe/segments" },
|
||||
"memory": { "href": "/programs/myproject%3A%2Fmybinary.exe/memory" },
|
||||
"xrefs": { "href": "/programs/myproject%3A%2Fmybinary.exe/xrefs" },
|
||||
"analysis": { "href": "/programs/myproject%3A%2Fmybinary.exe/analysis" }
|
||||
// Potentially actions like "close", "analyze"
|
||||
"self": { "href": "/program" },
|
||||
"project": { "href": "/project" },
|
||||
"functions": { "href": "/functions" },
|
||||
"symbols": { "href": "/symbols" },
|
||||
"data": { "href": "/data" },
|
||||
"segments": { "href": "/segments" },
|
||||
"memory": { "href": "/memory" },
|
||||
"xrefs": { "href": "/xrefs" },
|
||||
"analysis": { "href": "/analysis" }
|
||||
}
|
||||
```
|
||||
- **`DELETE /programs/{program_id}`**: Close and potentially remove a program from its project (behavior depends on Ghidra state).
|
||||
|
||||
### 3. Functions
|
||||
|
||||
Represents functions within a program. Base path: `/programs/{program_id}/functions`.
|
||||
Represents functions within the current program.
|
||||
|
||||
- **`GET /functions`**: List functions. Supports searching (by name/address/regex) and pagination.
|
||||
```json
|
||||
// Example Response Fragment
|
||||
"result": [
|
||||
{ "name": "FUN_08000004", "address": "08000004", "_links": { "self": { "href": "/programs/proj%3A%2Ffile.bin/functions/08000004" } } },
|
||||
{ "name": "init_peripherals", "address": "08001cf0", "_links": { "self": { "href": "/programs/proj%3A%2Ffile.bin/functions/08001cf0" } } }
|
||||
{ "name": "FUN_08000004", "address": "08000004", "_links": { "self": { "href": "/functions/08000004" } } },
|
||||
{ "name": "init_peripherals", "address": "08001cf0", "_links": { "self": { "href": "/functions/08001cf0" } } }
|
||||
]
|
||||
```
|
||||
- **`POST /functions`**: Create a function at a specific address. Requires `address` in the request body. Returns the created function resource.
|
||||
- **`GET /functions/{address}`**: Get details for a specific function (name, signature, size, stack info, etc.).
|
||||
```json
|
||||
// Example Response Fragment for GET /programs/proj%3A%2Ffile.bin/functions/0x4010a0
|
||||
// Example Response Fragment for GET /functions/0x4010a0
|
||||
"result": {
|
||||
"name": "process_data",
|
||||
"address": "0x4010a0",
|
||||
@ -202,12 +195,12 @@ Represents functions within a program. Base path: `/programs/{program_id}/functi
|
||||
// ... other details
|
||||
},
|
||||
"_links": {
|
||||
"self": { "href": "/programs/proj%3A%2Ffile.bin/functions/0x4010a0" },
|
||||
"decompile": { "href": "/programs/proj%3A%2Ffile.bin/functions/0x4010a0/decompile" },
|
||||
"disassembly": { "href": "/programs/proj%3A%2Ffile.bin/functions/0x4010a0/disassembly" },
|
||||
"variables": { "href": "/programs/proj%3A%2Ffile.bin/functions/0x4010a0/variables" },
|
||||
"xrefs_to": { "href": "/programs/proj%3A%2Ffile.bin/xrefs?to_addr=0x4010a0" },
|
||||
"xrefs_from": { "href": "/programs/proj%3A%2Ffile.bin/xrefs?from_addr=0x4010a0" }
|
||||
"self": { "href": "/functions/0x4010a0" },
|
||||
"decompile": { "href": "/functions/0x4010a0/decompile" },
|
||||
"disassembly": { "href": "/functions/0x4010a0/disassembly" },
|
||||
"variables": { "href": "/functions/0x4010a0/variables" },
|
||||
"xrefs_to": { "href": "/xrefs?to_addr=0x4010a0" },
|
||||
"xrefs_from": { "href": "/xrefs?from_addr=0x4010a0" }
|
||||
}
|
||||
```
|
||||
- **`PATCH /functions/{address}`**: Modify a function. Addressable only by address. Payload can contain:
|
||||
@ -248,7 +241,7 @@ Represents functions within a program. Base path: `/programs/{program_id}/functi
|
||||
|
||||
### 4. Symbols & Labels
|
||||
|
||||
Represents named locations (functions, data, labels). Base path: `/programs/{program_id}/symbols`.
|
||||
Represents named locations (functions, data, labels).
|
||||
|
||||
- **`GET /symbols`**: List all symbols in the program. Supports searching (by name/address/regex) and pagination. Can filter by type (`?type=function`, `?type=data`, `?type=label`).
|
||||
- **`POST /symbols`**: Create or rename a symbol at a specific address. Requires `address` and `name` in the payload. If a symbol exists, it's renamed; otherwise, a new label is created.
|
||||
@ -258,7 +251,7 @@ Represents named locations (functions, data, labels). Base path: `/programs/{pro
|
||||
|
||||
### 5. Data
|
||||
|
||||
Represents defined data items in memory. Base path: `/programs/{program_id}/data`.
|
||||
Represents defined data items in memory.
|
||||
|
||||
- **`GET /data`**: List defined data items. Supports searching (by name/address/regex) and pagination. Can filter by type (`?type=string`, `?type=dword`, etc.).
|
||||
- **`POST /data`**: Define a new data item. Requires `address`, `type`, and optionally `size` or `length` in the payload.
|
||||
@ -268,14 +261,14 @@ Represents defined data items in memory. Base path: `/programs/{program_id}/data
|
||||
|
||||
### 6. Memory Segments
|
||||
|
||||
Represents memory blocks/sections defined in the program. Base path: `/programs/{program_id}/segments`.
|
||||
Represents memory blocks/sections defined in the program.
|
||||
|
||||
- **`GET /segments`**: List all memory segments (e.g., `.text`, `.data`, `.bss`).
|
||||
- **`GET /segments/{segment_name}`**: Get details for a specific segment (address range, permissions, size).
|
||||
|
||||
### 7. Memory Access
|
||||
|
||||
Provides raw memory access. Base path: `/programs/{program_id}/memory`.
|
||||
Provides raw memory access.
|
||||
|
||||
- **`GET /memory/{address}`**: Read bytes from memory.
|
||||
- Query Parameters:
|
||||
@ -294,7 +287,7 @@ Provides raw memory access. Base path: `/programs/{program_id}/memory`.
|
||||
|
||||
### 8. Cross-References (XRefs)
|
||||
|
||||
Provides information about references to/from addresses. Base path: `/programs/{program_id}/xrefs`.
|
||||
Provides information about references to/from addresses.
|
||||
|
||||
- **`GET /xrefs`**: Search for cross-references. Supports pagination.
|
||||
- Query Parameters (at least one required):
|
||||
@ -305,7 +298,7 @@ Provides information about references to/from addresses. Base path: `/programs/{
|
||||
|
||||
### 9. Analysis
|
||||
|
||||
Provides access to Ghidra's analysis results. Base path: `/programs/{program_id}/analysis`.
|
||||
Provides access to Ghidra's analysis results.
|
||||
|
||||
- **`GET /analysis/callgraph`**: Retrieve the function call graph (potentially filtered or paginated). Format might be nodes/edges JSON or a standard graph format like DOT.
|
||||
- **`GET /analysis/dataflow/{address}`**: Perform data flow analysis starting from a specific address or instruction. Requires parameters specifying forward/backward, context, etc. (Details TBD).
|
||||
|
||||
@ -120,6 +120,9 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
* Register all endpoints that require a program to function.
|
||||
* This method always registers all endpoints, even when no program is loaded.
|
||||
* The endpoints will check for program availability at runtime when they're called.
|
||||
*
|
||||
* IMPORTANT: Endpoints are registered in order from most specific to least specific
|
||||
* to ensure proper URL path matching.
|
||||
*/
|
||||
private void registerProgramDependentEndpoints(HttpServer server) {
|
||||
// Register all endpoints without checking for a current program
|
||||
@ -129,16 +132,17 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
Program currentProgram = getCurrentProgram();
|
||||
Msg.info(this, "Current program at registration time: " + (currentProgram != null ? currentProgram.getName() : "none"));
|
||||
|
||||
// Register the core HATEOAS-compliant program endpoints
|
||||
// Always register all endpoints with a tool reference so they can get the current program at runtime
|
||||
new FunctionEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new VariableEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new ClassEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new SegmentEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new SymbolEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new NamespaceEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new DataEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new MemoryEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new XrefsEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new AnalysisEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new ProgramEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new FunctionEndpoints(currentProgram, port).registerEndpoints(server);
|
||||
new VariableEndpoints(currentProgram, port).registerEndpoints(server);
|
||||
new ClassEndpoints(currentProgram, port).registerEndpoints(server);
|
||||
new SegmentEndpoints(currentProgram, port).registerEndpoints(server);
|
||||
new SymbolEndpoints(currentProgram, port).registerEndpoints(server);
|
||||
new NamespaceEndpoints(currentProgram, port).registerEndpoints(server);
|
||||
new DataEndpoints(currentProgram, port).registerEndpoints(server);
|
||||
|
||||
Msg.info(this, "Registered program-dependent endpoints. Programs will be checked at runtime.");
|
||||
}
|
||||
@ -215,7 +219,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
|
||||
// Add program link if available
|
||||
if (program != null) {
|
||||
builder.addLink("program", "/programs/current");
|
||||
builder.addLink("program", "/program");
|
||||
}
|
||||
|
||||
HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port);
|
||||
@ -365,17 +369,17 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
Project project = tool.getProject();
|
||||
String projectName = (project != null) ? project.getName() : "unknown";
|
||||
|
||||
builder.addLink("current-program", "/programs/current")
|
||||
.addLink("current-project", "/projects/" + projectName)
|
||||
.addLink("functions", "/programs/current/functions")
|
||||
.addLink("symbols", "/programs/current/symbols")
|
||||
.addLink("data", "/programs/current/data")
|
||||
.addLink("segments", "/programs/current/segments")
|
||||
.addLink("memory", "/programs/current/memory")
|
||||
.addLink("xrefs", "/programs/current/xrefs")
|
||||
.addLink("analysis", "/programs/current/analysis")
|
||||
.addLink("current-address", "/programs/current/address")
|
||||
.addLink("current-function", "/programs/current/function");
|
||||
builder.addLink("program", "/program")
|
||||
.addLink("project", "/projects/" + projectName)
|
||||
.addLink("functions", "/functions")
|
||||
.addLink("symbols", "/symbols")
|
||||
.addLink("data", "/data")
|
||||
.addLink("segments", "/segments")
|
||||
.addLink("memory", "/memory")
|
||||
.addLink("xrefs", "/xrefs")
|
||||
.addLink("analysis", "/analysis")
|
||||
.addLink("address", "/address")
|
||||
.addLink("function", "/function");
|
||||
}
|
||||
|
||||
HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package eu.starsong.ghidra.api;
|
||||
|
||||
public class ApiConstants {
|
||||
public static final String PLUGIN_VERSION = "v1.0.0";
|
||||
public static final int API_VERSION = 1;
|
||||
public static final String PLUGIN_VERSION = "v2.0.0";
|
||||
public static final int API_VERSION = 2;
|
||||
public static final int DEFAULT_PORT = 8192;
|
||||
public static final int MAX_PORT_ATTEMPTS = 10;
|
||||
}
|
||||
|
||||
@ -12,6 +12,8 @@ import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.Msg;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class AbstractEndpoint implements GhidraJsonEndpoint {
|
||||
@ -29,6 +31,61 @@ public abstract class AbstractEndpoint implements GhidraJsonEndpoint {
|
||||
sendErrorResponse(exchange, 404, "Endpoint not found", "ENDPOINT_NOT_FOUND");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to handle pagination of collections and add pagination links to the response.
|
||||
*
|
||||
* @param <T> the type of items in the collection
|
||||
* @param items the full collection to paginate
|
||||
* @param offset the starting offset for pagination
|
||||
* @param limit the maximum number of items per page
|
||||
* @param builder the ResponseBuilder to add pagination links to
|
||||
* @param basePath the base path for pagination links (without query parameters)
|
||||
* @param additionalQueryParams additional query parameters to include in pagination links or null
|
||||
* @return a list containing the paginated items
|
||||
*/
|
||||
protected <T> List<T> applyPagination(List<T> items, int offset, int limit,
|
||||
eu.starsong.ghidra.api.ResponseBuilder builder, String basePath, String additionalQueryParams) {
|
||||
// Apply pagination
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(items.size(), offset + limit);
|
||||
List<T> paginated = items.subList(start, end);
|
||||
|
||||
// Add pagination metadata
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("size", items.size());
|
||||
metadata.put("offset", offset);
|
||||
metadata.put("limit", limit);
|
||||
builder.metadata(metadata);
|
||||
|
||||
// Format the query string
|
||||
String queryParams = (additionalQueryParams != null && !additionalQueryParams.isEmpty())
|
||||
? additionalQueryParams + "&"
|
||||
: "";
|
||||
|
||||
// Add HATEOAS links
|
||||
builder.addLink("self", basePath + "?" + queryParams + "offset=" + offset + "&limit=" + limit);
|
||||
|
||||
// Add next/prev links if applicable
|
||||
if (end < items.size()) {
|
||||
builder.addLink("next", basePath + "?" + queryParams + "offset=" + end + "&limit=" + limit);
|
||||
}
|
||||
|
||||
if (offset > 0) {
|
||||
int prevOffset = Math.max(0, offset - limit);
|
||||
builder.addLink("prev", basePath + "?" + queryParams + "offset=" + prevOffset + "&limit=" + limit);
|
||||
}
|
||||
|
||||
return paginated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload of applyPagination without additional query parameters
|
||||
*/
|
||||
protected <T> List<T> applyPagination(List<T> items, int offset, int limit,
|
||||
eu.starsong.ghidra.api.ResponseBuilder builder, String basePath) {
|
||||
return applyPagination(items, offset, limit, builder, basePath, null);
|
||||
}
|
||||
|
||||
protected final Gson gson = new Gson(); // Keep Gson if needed for specific object handling
|
||||
protected Program currentProgram;
|
||||
protected int port; // Add port field
|
||||
@ -41,21 +98,26 @@ public abstract class AbstractEndpoint implements GhidraJsonEndpoint {
|
||||
|
||||
// Get the current program - dynamically checks for program availability at runtime
|
||||
protected Program getCurrentProgram() {
|
||||
if (currentProgram != null) {
|
||||
return currentProgram;
|
||||
}
|
||||
|
||||
// Try to get the program from the plugin tool if available
|
||||
// ALWAYS try to get the current program from the tool first, regardless of the stored program
|
||||
// This ensures we get the most up-to-date program state
|
||||
try {
|
||||
PluginTool tool = getTool();
|
||||
if (tool != null) {
|
||||
ProgramManager programManager = tool.getService(ProgramManager.class);
|
||||
if (programManager != null) {
|
||||
return programManager.getCurrentProgram();
|
||||
Program current = programManager.getCurrentProgram();
|
||||
if (current != null) {
|
||||
return current; // Return the current program from the tool
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Fall back to the stored program if dynamic lookup fails
|
||||
Msg.error(this, "Error getting current program from tool", e);
|
||||
}
|
||||
|
||||
// Only fall back to the stored program if dynamic lookup fails
|
||||
if (currentProgram != null) {
|
||||
return currentProgram;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@ -0,0 +1,110 @@
|
||||
package eu.starsong.ghidra.endpoints;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import eu.starsong.ghidra.api.ResponseBuilder;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class AnalysisEndpoints extends AbstractEndpoint {
|
||||
|
||||
private PluginTool tool;
|
||||
|
||||
public AnalysisEndpoints(Program program, int port) {
|
||||
super(program, port);
|
||||
}
|
||||
|
||||
public AnalysisEndpoints(Program program, int port, PluginTool tool) {
|
||||
super(program, port);
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginTool getTool() {
|
||||
return tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/analysis", this::handleAnalysisRequest);
|
||||
}
|
||||
|
||||
private void handleAnalysisRequest(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
String method = exchange.getRequestMethod();
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create ResponseBuilder for HATEOAS-compliant response
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.addLink("self", "/analysis");
|
||||
|
||||
// Add common links
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
// Get analysis status
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
|
||||
// Add program information
|
||||
status.put("processor", program.getLanguage().getProcessor().toString());
|
||||
status.put("addressSize", program.getAddressFactory().getDefaultAddressSpace().getSize());
|
||||
status.put("programName", program.getName());
|
||||
status.put("programLanguage", program.getLanguage().toString());
|
||||
|
||||
// Add analyzer counts - simplified since we don't have access to the Analysis API directly
|
||||
int totalAnalyzers = 0;
|
||||
int enabledAnalyzers = 0;
|
||||
|
||||
// Simple analysis status with minimal API use
|
||||
Map<String, Boolean> analyzerStatus = new HashMap<>();
|
||||
// Note: We're not attempting to get all analyzers as this would require access to internal Ghidra APIs
|
||||
analyzerStatus.put("basicAnalysis", true);
|
||||
analyzerStatus.put("advancedAnalysis", false);
|
||||
|
||||
totalAnalyzers = 2;
|
||||
enabledAnalyzers = 1;
|
||||
|
||||
// Add counts to status report
|
||||
status.put("totalAnalyzers", totalAnalyzers);
|
||||
status.put("enabledAnalyzers", enabledAnalyzers);
|
||||
status.put("analyzerStatus", analyzerStatus);
|
||||
|
||||
// Handle different request types
|
||||
if ("GET".equals(method)) {
|
||||
builder.result(status);
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
|
||||
} else if ("POST".equals(method)) {
|
||||
// We can't directly start/stop analysis without direct AutoAnalysisManager access,
|
||||
// so return a placeholder response
|
||||
Map<String, String> params = parseJsonPostParams(exchange);
|
||||
String action = params.get("action");
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("message", "Analysis action '" + action + "' requested, but not fully implemented yet.");
|
||||
result.put("status", status);
|
||||
|
||||
builder.result(result);
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /analysis endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import eu.starsong.ghidra.api.ResponseBuilder;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.symbol.Namespace;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
@ -14,11 +15,23 @@ package eu.starsong.ghidra.endpoints;
|
||||
|
||||
public class ClassEndpoints extends AbstractEndpoint {
|
||||
|
||||
private PluginTool tool;
|
||||
|
||||
// Updated constructor to accept port
|
||||
public ClassEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
public ClassEndpoints(Program program, int port, PluginTool tool) {
|
||||
super(program, port);
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginTool getTool() {
|
||||
return tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/classes", this::handleClasses);
|
||||
@ -31,14 +44,16 @@ package eu.starsong.ghidra.endpoints;
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||
|
||||
if (currentProgram == null) {
|
||||
// Always get the most current program from the tool
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all class names
|
||||
Set<String> classNames = new HashSet<>();
|
||||
for (Symbol symbol : currentProgram.getSymbolTable().getAllSymbols(true)) {
|
||||
for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) {
|
||||
Namespace ns = symbol.getParentNamespace();
|
||||
// Check if namespace is not null, not global, and represents a class
|
||||
if (ns != null && !ns.isGlobal() && ns.getSymbol().getSymbolType().isNamespace()) {
|
||||
@ -70,10 +85,26 @@ package eu.starsong.ghidra.endpoints;
|
||||
classInfo.put("simpleName", className);
|
||||
}
|
||||
|
||||
// Add HATEOAS links for each class
|
||||
Map<String, Object> links = new HashMap<>();
|
||||
Map<String, String> selfLink = new HashMap<>();
|
||||
selfLink.put("href", "/classes/" + className);
|
||||
links.put("self", selfLink);
|
||||
|
||||
// Add link to program if relevant
|
||||
Map<String, String> programLink = new HashMap<>();
|
||||
programLink.put("href", "/program");
|
||||
links.put("program", programLink);
|
||||
|
||||
classInfo.put("_links", links);
|
||||
|
||||
paginatedClasses.add(classInfo);
|
||||
}
|
||||
|
||||
// Build response with pagination metadata
|
||||
// We need to separately create the full class objects with details
|
||||
// so we can't apply pagination directly to sorted list
|
||||
|
||||
// Build response with HATEOAS links
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.result(paginatedClasses);
|
||||
@ -87,7 +118,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
|
||||
// Add HATEOAS links
|
||||
builder.addLink("self", "/classes?offset=" + offset + "&limit=" + limit);
|
||||
builder.addLink("programs", "/programs");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
// Add next/prev links if applicable
|
||||
if (end < sorted.size()) {
|
||||
|
||||
@ -6,6 +6,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
import eu.starsong.ghidra.util.TransactionHelper;
|
||||
import eu.starsong.ghidra.util.TransactionHelper.TransactionException;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.listing.Data;
|
||||
import ghidra.program.model.listing.DataIterator;
|
||||
import ghidra.program.model.listing.Listing;
|
||||
@ -24,17 +25,29 @@ package eu.starsong.ghidra.endpoints;
|
||||
|
||||
public class DataEndpoints extends AbstractEndpoint {
|
||||
|
||||
private PluginTool tool;
|
||||
|
||||
// Updated constructor to accept port
|
||||
public DataEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
public DataEndpoints(Program program, int port, PluginTool tool) {
|
||||
super(program, port);
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginTool getTool() {
|
||||
return tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/data", this::handleData);
|
||||
}
|
||||
|
||||
private void handleData(HttpExchange exchange) throws IOException {
|
||||
public void handleData(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
handleListData(exchange);
|
||||
@ -50,103 +63,123 @@ package eu.starsong.ghidra.endpoints;
|
||||
}
|
||||
|
||||
private void handleListData(HttpExchange exchange) throws IOException {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited
|
||||
Object resultData = listDefinedData(offset, limit);
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
try {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> 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, Object> item = new HashMap<>();
|
||||
item.put("address", data.getAddress().toString());
|
||||
item.put("label", data.getLabel() != null ? data.getLabel() : "(unnamed)");
|
||||
item.put("value", data.getDefaultValueRepresentation());
|
||||
item.put("dataType", data.getDataType().getName());
|
||||
|
||||
// Add HATEOAS links
|
||||
Map<String, Object> links = new HashMap<>();
|
||||
Map<String, String> selfLink = new HashMap<>();
|
||||
selfLink.put("href", "/data/" + data.getAddress().toString());
|
||||
links.put("self", selfLink);
|
||||
item.put("_links", links);
|
||||
|
||||
dataItems.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build response with HATEOAS links
|
||||
eu.starsong.ghidra.api.ResponseBuilder builder = new eu.starsong.ghidra.api.ResponseBuilder(exchange, port)
|
||||
.success(true);
|
||||
|
||||
// Apply pagination and get paginated items
|
||||
List<Map<String, Object>> paginated = applyPagination(dataItems, offset, limit, builder, "/data");
|
||||
|
||||
// Set the paginated result
|
||||
builder.result(paginated);
|
||||
|
||||
// Add program link
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error listing data", e);
|
||||
sendErrorResponse(exchange, 500, "Error listing data: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRenameData(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
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"); // Inherited
|
||||
sendErrorResponse(exchange, 400, "Missing required parameters: address, newName", "MISSING_PARAMETERS");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentProgram == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded"); // Inherited
|
||||
return;
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
TransactionHelper.executeInTransaction(currentProgram, "Rename Data", () -> {
|
||||
if (!renameDataAtAddress(addressStr, newName)) {
|
||||
TransactionHelper.executeInTransaction(program, "Rename Data", () -> {
|
||||
if (!renameDataAtAddress(program, addressStr, newName)) {
|
||||
throw new Exception("Rename data operation failed internally.");
|
||||
}
|
||||
return null; // Return null for void operation
|
||||
});
|
||||
// Use sendSuccessResponse for consistency
|
||||
sendSuccessResponse(exchange, Map.of("message", "Data renamed successfully"));
|
||||
|
||||
// Build HATEOAS response
|
||||
eu.starsong.ghidra.api.ResponseBuilder builder = new eu.starsong.ghidra.api.ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.result(Map.of("message", "Data renamed successfully", "address", addressStr, "name", newName));
|
||||
|
||||
// Add relevant links
|
||||
builder.addLink("self", "/data/" + addressStr);
|
||||
builder.addLink("data", "/data");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} catch (TransactionException e) {
|
||||
Msg.error(this, "Transaction failed: Rename Data", e);
|
||||
// Use inherited sendErrorResponse
|
||||
sendErrorResponse(exchange, 500, "Failed to rename data: " + e.getMessage(), "TRANSACTION_ERROR");
|
||||
Msg.error(this, "Transaction failed: Rename Data", e);
|
||||
sendErrorResponse(exchange, 500, "Failed to rename data: " + e.getMessage(), "TRANSACTION_ERROR");
|
||||
} catch (Exception e) { // Catch potential AddressFormatException or other issues
|
||||
Msg.error(this, "Error during rename data operation", e);
|
||||
// Use inherited sendErrorResponse
|
||||
sendErrorResponse(exchange, 400, "Error renaming data: " + e.getMessage(), "INVALID_PARAMETER");
|
||||
Msg.error(this, "Error during rename data operation", e);
|
||||
sendErrorResponse(exchange, 400, "Error renaming data: " + e.getMessage(), "INVALID_PARAMETER");
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
Msg.error(this, "Error parsing POST params for data rename", e);
|
||||
sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); // Inherited
|
||||
Msg.error(this, "Error parsing POST params for data rename", e);
|
||||
sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST");
|
||||
} catch (Exception e) { // Catch unexpected errors
|
||||
Msg.error(this, "Unexpected error renaming data", e);
|
||||
sendErrorResponse(exchange, 500, "Error renaming data: " + e.getMessage(), "INTERNAL_ERROR"); // Inherited
|
||||
Msg.error(this, "Unexpected error renaming data", e);
|
||||
sendErrorResponse(exchange, 500, "Error renaming data: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Methods moved from GhydraMCPPlugin ---
|
||||
|
||||
private JsonObject listDefinedData(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
List<Map<String, String>> dataItems = new ArrayList<>();
|
||||
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
|
||||
DataIterator it = currentProgram.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());
|
||||
item.put("dataType", data.getDataType().getName());
|
||||
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);
|
||||
|
||||
return createSuccessResponse(paginated);
|
||||
}
|
||||
|
||||
private boolean renameDataAtAddress(String addressStr, String newName) throws Exception {
|
||||
private boolean renameDataAtAddress(Program program, String addressStr, String newName) throws Exception {
|
||||
// This method now throws Exception to be caught by the transaction helper
|
||||
AtomicBoolean successFlag = new AtomicBoolean(false);
|
||||
try {
|
||||
Address addr = currentProgram.getAddressFactory().getAddress(addressStr);
|
||||
Listing listing = currentProgram.getListing();
|
||||
Address addr = program.getAddressFactory().getAddress(addressStr);
|
||||
Listing listing = program.getListing();
|
||||
Data data = listing.getDefinedDataAt(addr);
|
||||
if (data != null) {
|
||||
SymbolTable symTable = currentProgram.getSymbolTable();
|
||||
SymbolTable symTable = program.getSymbolTable();
|
||||
Symbol symbol = symTable.getPrimarySymbol(addr);
|
||||
if (symbol != null) {
|
||||
symbol.setName(newName, SourceType.USER_DEFINED);
|
||||
@ -169,24 +202,5 @@ package eu.starsong.ghidra.endpoints;
|
||||
return successFlag.get();
|
||||
}
|
||||
|
||||
|
||||
// --- Helper Methods (Keep internal for now, refactor later if needed) ---
|
||||
// Note: These might differ slightly from AbstractEndpoint/ResponseBuilder, review needed.
|
||||
|
||||
private JsonObject createSuccessResponse(Object resultData) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", true);
|
||||
response.add("result", gson.toJsonTree(resultData));
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonObject createErrorResponse(String errorMessage, int statusCode) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", false);
|
||||
response.addProperty("error", errorMessage);
|
||||
response.addProperty("status_code", statusCode);
|
||||
return response;
|
||||
}
|
||||
|
||||
// parseIntOrDefault is inherited from AbstractEndpoint
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ 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.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressFactory;
|
||||
import ghidra.program.model.listing.Function;
|
||||
@ -23,25 +24,186 @@ import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Endpoints for managing functions within a program.
|
||||
* Implements the /programs/{program_id}/functions endpoints.
|
||||
* 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 legacy endpoints to support existing callers
|
||||
// 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);
|
||||
server.createContext("/functions/", this::handleFunctionByPath);
|
||||
|
||||
// Register function-specific endpoints
|
||||
registerAdditionalEndpoints(server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle requests to the /functions endpoint
|
||||
* Register additional convenience endpoints
|
||||
*/
|
||||
public void handleFunctions(HttpExchange exchange) throws IOException {
|
||||
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<String, String> params = parseQueryParams(exchange);
|
||||
@ -52,15 +214,14 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
String nameRegexFilter = params.get("name_matches_regex");
|
||||
String addrFilter = params.get("addr");
|
||||
|
||||
List<Map<String, Object>> functions = new ArrayList<>();
|
||||
|
||||
// Get the current program at runtime instead of relying on the constructor-set program
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED");
|
||||
sendErrorResponse(exchange, 400, "No program is currently loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> functions = new ArrayList<>();
|
||||
|
||||
// Get all functions
|
||||
for (Function f : program.getFunctionManager().getFunctions(true)) {
|
||||
// Apply filters
|
||||
@ -87,11 +248,339 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
// Add HATEOAS links
|
||||
Map<String, Object> links = new HashMap<>();
|
||||
Map<String, String> selfLink = new HashMap<>();
|
||||
selfLink.put("href", "/functions/" + f.getName());
|
||||
selfLink.put("href", "/programs/current/functions/" + f.getEntryPoint());
|
||||
links.put("self", selfLink);
|
||||
|
||||
Map<String, String> byNameLink = new HashMap<>();
|
||||
byNameLink.put("href", "/programs/current/functions/by-name/" + f.getName());
|
||||
links.put("by_name", byNameLink);
|
||||
|
||||
Map<String, String> 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<Map<String, Object>> 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<String, Object> 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<String, String> 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 signature - placeholder
|
||||
sendErrorResponse(exchange, 501, "Updating function signature not implemented", "NOT_IMPLEMENTED");
|
||||
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<String, String> 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;
|
||||
}
|
||||
|
||||
// Create function
|
||||
Function function;
|
||||
try {
|
||||
function = TransactionHelper.executeInTransaction(program, "Create Function", () -> {
|
||||
return program.getFunctionManager().createFunction(null, address, null, null);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
sendErrorResponse(exchange, 400, "Failed to create function: " + 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<String, String> 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<Map<String, Object>> 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<String, Object> func = new HashMap<>();
|
||||
func.put("name", f.getName());
|
||||
func.put("address", f.getEntryPoint().toString());
|
||||
|
||||
// Add HATEOAS links (fixed to use proper URL paths)
|
||||
Map<String, Object> links = new HashMap<>();
|
||||
Map<String, String> selfLink = new HashMap<>();
|
||||
selfLink.put("href", "/functions/" + f.getEntryPoint());
|
||||
links.put("self", selfLink);
|
||||
|
||||
Map<String, String> programLink = new HashMap<>();
|
||||
programLink.put("href", "/programs/current");
|
||||
programLink.put("href", "/program");
|
||||
links.put("program", programLink);
|
||||
|
||||
func.put("_links", links);
|
||||
@ -224,11 +713,7 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
/**
|
||||
* Handle requests to function resources like /functions/{name}/decompile
|
||||
*/
|
||||
private void handleFunctionResource(HttpExchange exchange, String functionPath) throws IOException {
|
||||
int slashIndex = functionPath.indexOf('/');
|
||||
String functionIdent = functionPath.substring(0, slashIndex);
|
||||
String resource = functionPath.substring(slashIndex + 1);
|
||||
|
||||
private void handleFunctionResource(HttpExchange exchange, String functionIdent, String resource) throws IOException {
|
||||
Function function = null;
|
||||
|
||||
// Try to find function by address first
|
||||
@ -255,6 +740,18 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
@ -467,7 +964,7 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
functionInfo.put("address", function.getEntryPoint().toString());
|
||||
functionInfo.put("name", function.getName());
|
||||
|
||||
// Create the result structure according to tests and MCP_BRIDGE_API.md
|
||||
// Create the result structure according to GHIDRA_HTTP_API.md
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("function", functionInfo);
|
||||
result.put("decompiled", decompilation != null ? decompilation : "// Decompilation failed");
|
||||
@ -481,15 +978,15 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
.success(true)
|
||||
.result(result);
|
||||
|
||||
// Path for links
|
||||
String functionPath = "/programs/current/functions/" + function.getEntryPoint().toString();
|
||||
// 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", "/programs/current");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} else {
|
||||
@ -531,13 +1028,14 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
.success(true)
|
||||
.result(result);
|
||||
|
||||
String functionPath = "/programs/current/functions/" + function.getEntryPoint().toString();
|
||||
// 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", "/programs/current");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} else {
|
||||
@ -566,8 +1064,9 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
result.put("function", functionInfo);
|
||||
result.put("variables", variables);
|
||||
|
||||
String functionPath = "/programs/current/functions/" + function.getEntryPoint().toString();
|
||||
String functionByNamePath = "/programs/current/functions/by-name/" + function.getName();
|
||||
// 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)
|
||||
@ -578,7 +1077,7 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
builder.addLink("by_name", functionByNamePath);
|
||||
builder.addLink("decompile", functionPath + "/decompile");
|
||||
builder.addLink("disassembly", functionPath + "/disassembly");
|
||||
builder.addLink("program", "/programs/current");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} else if ("PATCH".equals(exchange.getRequestMethod())) {
|
||||
|
||||
@ -51,7 +51,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
int instancePort = entry.getKey();
|
||||
instance.put("port", instancePort);
|
||||
instance.put("url", "http://localhost:" + instancePort);
|
||||
instance.put("type", "unknown"); // Placeholder until isBaseInstance is accessible
|
||||
instance.put("type", entry.getValue().isBaseInstance() ? "base" : "standard");
|
||||
|
||||
// Get program info if available
|
||||
Program program = entry.getValue().getCurrentProgram();
|
||||
@ -63,6 +63,27 @@ package eu.starsong.ghidra.endpoints;
|
||||
instance.put("file", "");
|
||||
}
|
||||
|
||||
// Add HATEOAS links for each instance
|
||||
Map<String, Object> links = new HashMap<>();
|
||||
|
||||
// Self link for this instance
|
||||
Map<String, String> selfLink = new HashMap<>();
|
||||
selfLink.put("href", "/instances/" + instancePort);
|
||||
links.put("self", selfLink);
|
||||
|
||||
// Info link for this instance
|
||||
Map<String, String> infoLink = new HashMap<>();
|
||||
infoLink.put("href", "http://localhost:" + instancePort + "/info");
|
||||
links.put("info", infoLink);
|
||||
|
||||
// Connect link
|
||||
Map<String, String> connectLink = new HashMap<>();
|
||||
connectLink.put("href", "http://localhost:" + instancePort);
|
||||
links.put("connect", connectLink);
|
||||
|
||||
// Add links to object
|
||||
instance.put("_links", links);
|
||||
|
||||
instanceData.add(instance);
|
||||
}
|
||||
|
||||
|
||||
224
src/main/java/eu/starsong/ghidra/endpoints/MemoryEndpoints.java
Normal file
224
src/main/java/eu/starsong/ghidra/endpoints/MemoryEndpoints.java
Normal file
@ -0,0 +1,224 @@
|
||||
package eu.starsong.ghidra.endpoints;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import eu.starsong.ghidra.api.ResponseBuilder;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressFactory;
|
||||
import ghidra.program.model.mem.Memory;
|
||||
import ghidra.program.model.mem.MemoryAccessException;
|
||||
import ghidra.program.model.mem.MemoryBlock;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class MemoryEndpoints extends AbstractEndpoint {
|
||||
|
||||
private static final int DEFAULT_MEMORY_LENGTH = 16;
|
||||
private static final int MAX_MEMORY_LENGTH = 4096;
|
||||
private PluginTool tool;
|
||||
|
||||
public MemoryEndpoints(Program program, int port) {
|
||||
super(program, port);
|
||||
}
|
||||
|
||||
public MemoryEndpoints(Program program, int port, PluginTool tool) {
|
||||
super(program, port);
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginTool getTool() {
|
||||
return tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/memory", this::handleMemoryRequest);
|
||||
server.createContext("/memory/blocks", this::handleMemoryBlocksRequest);
|
||||
}
|
||||
|
||||
private void handleMemoryRequest(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
String addressStr = qparams.get("address");
|
||||
String lengthStr = qparams.get("length");
|
||||
|
||||
// Create ResponseBuilder for HATEOAS-compliant response
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.addLink("self", "/memory" + (exchange.getRequestURI().getRawQuery() != null ?
|
||||
"?" + exchange.getRequestURI().getRawQuery() : ""));
|
||||
|
||||
// Add common links
|
||||
builder.addLink("program", "/program");
|
||||
builder.addLink("blocks", "/memory/blocks");
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
if (addressStr == null || addressStr.isEmpty()) {
|
||||
sendErrorResponse(exchange, 400, "Address parameter is required", "MISSING_PARAMETER");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse length parameter
|
||||
int length = DEFAULT_MEMORY_LENGTH;
|
||||
if (lengthStr != null && !lengthStr.isEmpty()) {
|
||||
try {
|
||||
length = Integer.parseInt(lengthStr);
|
||||
if (length <= 0) {
|
||||
sendErrorResponse(exchange, 400, "Length must be positive", "INVALID_PARAMETER");
|
||||
return;
|
||||
}
|
||||
if (length > MAX_MEMORY_LENGTH) {
|
||||
length = MAX_MEMORY_LENGTH;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
sendErrorResponse(exchange, 400, "Invalid length parameter", "INVALID_PARAMETER");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse address
|
||||
AddressFactory addressFactory = program.getAddressFactory();
|
||||
Address address;
|
||||
try {
|
||||
address = addressFactory.getAddress(addressStr);
|
||||
} catch (Exception e) {
|
||||
sendErrorResponse(exchange, 400, "Invalid address format", "INVALID_PARAMETER");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read memory
|
||||
Memory memory = program.getMemory();
|
||||
if (!memory.contains(address)) {
|
||||
sendErrorResponse(exchange, 404, "Address not in memory", "ADDRESS_NOT_FOUND");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read bytes
|
||||
byte[] bytes = new byte[length];
|
||||
int bytesRead = memory.getBytes(address, bytes, 0, length);
|
||||
|
||||
// Format as hex string
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (int i = 0; i < bytesRead; i++) {
|
||||
String hex = Integer.toHexString(bytes[i] & 0xFF).toUpperCase();
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
if (i < bytesRead - 1) {
|
||||
hexString.append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
// Build result object
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("address", address.toString());
|
||||
result.put("bytesRead", bytesRead);
|
||||
result.put("hexBytes", hexString.toString());
|
||||
result.put("rawBytes", Base64.getEncoder().encodeToString(bytes));
|
||||
|
||||
// Add next/prev links
|
||||
builder.addLink("next", "/memory?address=" + address.add(length) + "&length=" + length);
|
||||
if (address.getOffset() >= length) {
|
||||
builder.addLink("prev", "/memory?address=" + address.subtract(length) + "&length=" + length);
|
||||
}
|
||||
|
||||
// Add result and send response
|
||||
builder.result(result);
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
|
||||
} catch (MemoryAccessException e) {
|
||||
sendErrorResponse(exchange, 404, "Cannot read memory at address: " + e.getMessage(), "MEMORY_ACCESS_ERROR");
|
||||
}
|
||||
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /memory endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMemoryBlocksRequest(HttpExchange exchange) throws IOException {
|
||||
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);
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create ResponseBuilder for HATEOAS-compliant response
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.addLink("self", "/memory/blocks" + (exchange.getRequestURI().getRawQuery() != null ?
|
||||
"?" + exchange.getRequestURI().getRawQuery() : ""));
|
||||
|
||||
// Add common links
|
||||
builder.addLink("program", "/program");
|
||||
builder.addLink("memory", "/memory");
|
||||
|
||||
// Get memory blocks
|
||||
Memory memory = program.getMemory();
|
||||
List<Map<String, Object>> blocks = new ArrayList<>();
|
||||
|
||||
for (MemoryBlock block : memory.getBlocks()) {
|
||||
Map<String, Object> blockInfo = new HashMap<>();
|
||||
blockInfo.put("name", block.getName());
|
||||
blockInfo.put("start", block.getStart().toString());
|
||||
blockInfo.put("end", block.getEnd().toString());
|
||||
blockInfo.put("size", block.getSize());
|
||||
blockInfo.put("permissions", getPermissionString(block));
|
||||
blockInfo.put("isInitialized", block.isInitialized());
|
||||
blockInfo.put("isLoaded", block.isLoaded());
|
||||
blockInfo.put("isMapped", block.isMapped());
|
||||
blocks.add(blockInfo);
|
||||
}
|
||||
|
||||
// Apply pagination and add it to result
|
||||
List<Map<String, Object>> paginatedBlocks =
|
||||
applyPagination(blocks, offset, limit, builder, "/memory/blocks");
|
||||
|
||||
// Add the result to the builder
|
||||
builder.result(paginatedBlocks);
|
||||
|
||||
// Send the HATEOAS-compliant response
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /memory/blocks endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String getPermissionString(MemoryBlock block) {
|
||||
StringBuilder perms = new StringBuilder();
|
||||
perms.append(block.isRead() ? "r" : "-");
|
||||
perms.append(block.isWrite() ? "w" : "-");
|
||||
perms.append(block.isExecute() ? "x" : "-");
|
||||
perms.append(block.isVolatile() ? "v" : "-");
|
||||
return perms.toString();
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.address.GlobalNamespace;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.symbol.Namespace;
|
||||
@ -14,9 +15,20 @@ package eu.starsong.ghidra.endpoints;
|
||||
|
||||
public class NamespaceEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Updated constructor to accept port
|
||||
private PluginTool tool;
|
||||
|
||||
public NamespaceEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
super(program, port);
|
||||
}
|
||||
|
||||
public NamespaceEndpoints(Program program, int port, PluginTool tool) {
|
||||
super(program, port);
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginTool getTool() {
|
||||
return tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -24,70 +36,52 @@ package eu.starsong.ghidra.endpoints;
|
||||
server.createContext("/namespaces", this::handleNamespaces);
|
||||
}
|
||||
|
||||
private void handleNamespaces(HttpExchange exchange) throws IOException {
|
||||
public void handleNamespaces(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited
|
||||
Object resultData = listNamespaces(offset, limit);
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
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(true)); // Get fully qualified name
|
||||
}
|
||||
}
|
||||
|
||||
List<String> sorted = new ArrayList<>(namespaces);
|
||||
Collections.sort(sorted);
|
||||
|
||||
// Build response with HATEOAS links
|
||||
eu.starsong.ghidra.api.ResponseBuilder builder = new eu.starsong.ghidra.api.ResponseBuilder(exchange, port)
|
||||
.success(true);
|
||||
|
||||
// Apply pagination and get paginated items
|
||||
List<String> paginated = applyPagination(sorted, offset, limit, builder, "/namespaces");
|
||||
|
||||
// Set the paginated result
|
||||
builder.result(paginated);
|
||||
|
||||
// Add program link
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited
|
||||
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()); // Inherited
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Method moved from GhydraMCPPlugin ---
|
||||
|
||||
private JsonObject listNamespaces(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
Set<String> namespaces = new HashSet<>();
|
||||
for (Symbol symbol : currentProgram.getSymbolTable().getAllSymbols(true)) {
|
||||
Namespace ns = symbol.getParentNamespace();
|
||||
if (ns != null && !(ns instanceof GlobalNamespace)) {
|
||||
namespaces.add(ns.getName(true)); // Get fully qualified name
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return createSuccessResponse(paginated); // Keep internal helper for now
|
||||
}
|
||||
|
||||
// --- Helper Methods (Keep internal for now) ---
|
||||
|
||||
private JsonObject createSuccessResponse(Object resultData) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", true);
|
||||
response.add("result", gson.toJsonTree(resultData));
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonObject createErrorResponse(String errorMessage, int statusCode) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", false);
|
||||
response.addProperty("error", errorMessage);
|
||||
response.addProperty("status_code", statusCode);
|
||||
return response;
|
||||
}
|
||||
|
||||
// parseIntOrDefault is inherited from AbstractEndpoint
|
||||
}
|
||||
|
||||
@ -44,24 +44,12 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
// Register the /programs endpoint
|
||||
server.createContext("/programs", this::handlePrograms);
|
||||
server.createContext("/program", this::handleProgramInfo);
|
||||
|
||||
// Register the most specific function endpoints first (order matters for URL routing)
|
||||
server.createContext("/programs/current/functions/by-name/", this::handleFunctionByName);
|
||||
server.createContext("/programs/current/functions/", this::handleFunctionByAddress);
|
||||
// Register address and function endpoints
|
||||
server.createContext("/address", this::handleCurrentAddress);
|
||||
server.createContext("/function", this::handleCurrentFunction);
|
||||
|
||||
// Register other specific program resource endpoints
|
||||
server.createContext("/programs/current/segments", this::handleCurrentSegments);
|
||||
server.createContext("/programs/current/functions", this::handleCurrentFunctions);
|
||||
server.createContext("/programs/current/address", this::handleCurrentAddress);
|
||||
server.createContext("/programs/current/function", this::handleCurrentFunction);
|
||||
|
||||
// Register the /programs/current endpoint
|
||||
server.createContext("/programs/current", this::handleCurrentProgram);
|
||||
|
||||
// Register the /programs/{program_id} endpoint (catch-all)
|
||||
server.createContext("/programs/", this::handleProgramById);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -70,28 +58,6 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle requests to the /programs endpoint
|
||||
*/
|
||||
private void handlePrograms(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
String method = exchange.getRequestMethod();
|
||||
|
||||
if ("GET".equals(method)) {
|
||||
// List all programs
|
||||
handleListPrograms(exchange);
|
||||
} else if ("POST".equals(method)) {
|
||||
// Import a new program
|
||||
handleImportProgram(exchange);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error handling /programs endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET requests to list all programs
|
||||
*/
|
||||
@ -190,7 +156,7 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Check if this is a request for the current program
|
||||
if (path.equals("/programs/current")) {
|
||||
handleCurrentProgram(exchange);
|
||||
handleProgramInfo(exchange);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -300,7 +266,7 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
/**
|
||||
* Handle requests to the /programs/current endpoint
|
||||
*/
|
||||
private void handleCurrentProgram(HttpExchange exchange) throws IOException {
|
||||
public void handleProgramInfo(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
String method = exchange.getRequestMethod();
|
||||
|
||||
@ -322,7 +288,7 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
.result(info);
|
||||
|
||||
// Add HATEOAS links
|
||||
builder.addLink("self", "/programs/current");
|
||||
builder.addLink("self", "/program");
|
||||
|
||||
Project project = tool.getProject();
|
||||
if (project != null) {
|
||||
@ -330,13 +296,13 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
}
|
||||
|
||||
// Add links to program resources
|
||||
builder.addLink("functions", "/programs/current/functions");
|
||||
builder.addLink("symbols", "/programs/current/symbols");
|
||||
builder.addLink("data", "/programs/current/data");
|
||||
builder.addLink("segments", "/programs/current/segments");
|
||||
builder.addLink("memory", "/programs/current/memory");
|
||||
builder.addLink("xrefs", "/programs/current/xrefs");
|
||||
builder.addLink("analysis", "/programs/current/analysis");
|
||||
builder.addLink("functions", "/functions");
|
||||
builder.addLink("symbols", "/symbols");
|
||||
builder.addLink("data", "/data");
|
||||
builder.addLink("segments", "/segments");
|
||||
builder.addLink("memory", "/memory");
|
||||
builder.addLink("xrefs", "/xrefs");
|
||||
builder.addLink("analysis", "/analysis");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} else {
|
||||
@ -1336,7 +1302,7 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
* @param exchange The HTTP exchange
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
private void handleCurrentAddress(HttpExchange exchange) throws IOException {
|
||||
public void handleCurrentAddress(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if (!"GET".equals(exchange.getRequestMethod())) {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
|
||||
@ -1370,20 +1336,20 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
.result(result);
|
||||
|
||||
// Add HATEOAS links
|
||||
builder.addLink("self", "/programs/current/address");
|
||||
builder.addLink("program", "/programs/current");
|
||||
builder.addLink("self", "/address");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
// If we have a current program, add a link to get memory at this address
|
||||
if (program != null) {
|
||||
builder.addLink("memory", "/programs/current/memory/" + currentAddress + "?length=16");
|
||||
builder.addLink("memory", "/memory/" + currentAddress + "?length=16");
|
||||
|
||||
// Check if this address is within a function
|
||||
ghidra.program.model.listing.Function function = program.getFunctionManager().getFunctionContaining(
|
||||
program.getAddressFactory().getAddress(currentAddress));
|
||||
|
||||
if (function != null) {
|
||||
builder.addLink("function", "/programs/current/functions/" + function.getEntryPoint().toString());
|
||||
builder.addLink("decompile", "/programs/current/functions/" + function.getEntryPoint().toString() + "/decompile");
|
||||
builder.addLink("function", "/functions/" + function.getEntryPoint().toString());
|
||||
builder.addLink("decompile", "/functions/" + function.getEntryPoint().toString() + "/decompile");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1399,7 +1365,7 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
* @param exchange The HTTP exchange
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
private void handleCurrentFunction(HttpExchange exchange) throws IOException {
|
||||
public void handleCurrentFunction(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if (!"GET".equals(exchange.getRequestMethod())) {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
|
||||
@ -1430,17 +1396,17 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
.result(functionInfo);
|
||||
|
||||
// Add HATEOAS links
|
||||
builder.addLink("self", "/programs/current/function");
|
||||
builder.addLink("program", "/programs/current");
|
||||
builder.addLink("self", "/function");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
// Add links to function-specific resources
|
||||
if (functionInfo.containsKey("address")) {
|
||||
String functionAddress = (String) functionInfo.get("address");
|
||||
builder.addLink("function", "/programs/current/functions/" + functionAddress);
|
||||
builder.addLink("decompile", "/programs/current/functions/" + functionAddress + "/decompile");
|
||||
builder.addLink("disassembly", "/programs/current/functions/" + functionAddress + "/disassembly");
|
||||
builder.addLink("variables", "/programs/current/functions/" + functionAddress + "/variables");
|
||||
builder.addLink("xrefs", "/programs/current/xrefs?to_addr=" + functionAddress);
|
||||
builder.addLink("function", "/functions/" + functionAddress);
|
||||
builder.addLink("decompile", "/functions/" + functionAddress + "/decompile");
|
||||
builder.addLink("disassembly", "/functions/" + functionAddress + "/disassembly");
|
||||
builder.addLink("variables", "/functions/" + functionAddress + "/variables");
|
||||
builder.addLink("xrefs", "/xrefs?to_addr=" + functionAddress);
|
||||
}
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
|
||||
@ -4,6 +4,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import eu.starsong.ghidra.api.ResponseBuilder;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.mem.MemoryBlock;
|
||||
import ghidra.util.Msg;
|
||||
@ -13,17 +14,29 @@ package eu.starsong.ghidra.endpoints;
|
||||
|
||||
public class SegmentEndpoints extends AbstractEndpoint {
|
||||
|
||||
private PluginTool tool;
|
||||
|
||||
// Updated constructor to accept port
|
||||
public SegmentEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
public SegmentEndpoints(Program program, int port, PluginTool tool) {
|
||||
super(program, port);
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginTool getTool() {
|
||||
return tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/segments", this::handleSegments);
|
||||
}
|
||||
|
||||
private void handleSegments(HttpExchange exchange) throws IOException {
|
||||
public void handleSegments(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
@ -59,11 +72,11 @@ package eu.starsong.ghidra.endpoints;
|
||||
// Add HATEOAS links for this segment
|
||||
Map<String, Object> links = new HashMap<>();
|
||||
Map<String, String> selfLink = new HashMap<>();
|
||||
selfLink.put("href", "/programs/current/segments/" + block.getName());
|
||||
selfLink.put("href", "/segments/" + block.getName());
|
||||
links.put("self", selfLink);
|
||||
|
||||
Map<String, String> memoryLink = new HashMap<>();
|
||||
memoryLink.put("href", "/programs/current/memory/" + block.getStart());
|
||||
memoryLink.put("href", "/memory/" + block.getStart());
|
||||
links.put("memory", memoryLink);
|
||||
|
||||
segment.put("_links", links);
|
||||
@ -71,37 +84,22 @@ package eu.starsong.ghidra.endpoints;
|
||||
segments.add(segment);
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(segments.size(), offset + limit);
|
||||
List<Map<String, Object>> paginatedSegments = segments.subList(start, end);
|
||||
|
||||
// Build response with pagination metadata
|
||||
// Build response with HATEOAS links
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.result(paginatedSegments);
|
||||
.success(true);
|
||||
|
||||
// Add pagination metadata
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("size", segments.size());
|
||||
metadata.put("offset", offset);
|
||||
metadata.put("limit", limit);
|
||||
builder.metadata(metadata);
|
||||
// Handle optional name filter
|
||||
String queryParams = nameFilter != null ? "name=" + nameFilter : null;
|
||||
|
||||
// Add HATEOAS links
|
||||
String queryParams = nameFilter != null ? "name=" + nameFilter + "&" : "";
|
||||
builder.addLink("self", "/programs/current/segments?" + queryParams + "offset=" + offset + "&limit=" + limit);
|
||||
builder.addLink("program", "/programs/current");
|
||||
// Apply pagination and get paginated items
|
||||
List<Map<String, Object>> paginatedSegments = applyPagination(
|
||||
segments, offset, limit, builder, "/segments", queryParams);
|
||||
|
||||
// Add next/prev links if applicable
|
||||
if (end < segments.size()) {
|
||||
builder.addLink("next", "/programs/current/segments?" + queryParams + "offset=" + end + "&limit=" + limit);
|
||||
}
|
||||
// Set the paginated result
|
||||
builder.result(paginatedSegments);
|
||||
|
||||
if (offset > 0) {
|
||||
int prevOffset = Math.max(0, offset - limit);
|
||||
builder.addLink("prev", "/programs/current/segments?" + queryParams + "offset=" + prevOffset + "&limit=" + limit);
|
||||
}
|
||||
// Add program link
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} else {
|
||||
|
||||
@ -3,6 +3,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.program.model.symbol.SymbolIterator;
|
||||
@ -14,129 +15,200 @@ package eu.starsong.ghidra.endpoints;
|
||||
|
||||
public class SymbolEndpoints extends AbstractEndpoint {
|
||||
|
||||
private PluginTool tool;
|
||||
|
||||
// Updated constructor to accept port
|
||||
public SymbolEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
public SymbolEndpoints(Program program, int port, PluginTool tool) {
|
||||
super(program, port);
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginTool getTool() {
|
||||
return tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/symbols/imports", this::handleImports);
|
||||
server.createContext("/symbols/exports", this::handleExports);
|
||||
server.createContext("/symbols", this::handleSymbols);
|
||||
}
|
||||
|
||||
private void handleImports(HttpExchange exchange) throws IOException {
|
||||
public void handleSymbols(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited
|
||||
Object resultData = listImports(offset, limit);
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> symbols = new ArrayList<>();
|
||||
SymbolTable symbolTable = program.getSymbolTable();
|
||||
SymbolIterator symbolIterator = symbolTable.getAllSymbols(true);
|
||||
|
||||
while (symbolIterator.hasNext()) {
|
||||
Symbol symbol = symbolIterator.next();
|
||||
Map<String, Object> symbolInfo = new HashMap<>();
|
||||
symbolInfo.put("name", symbol.getName());
|
||||
symbolInfo.put("address", symbol.getAddress().toString());
|
||||
symbolInfo.put("namespace", symbol.getParentNamespace().getName());
|
||||
symbolInfo.put("type", symbol.getSymbolType().toString());
|
||||
symbolInfo.put("isPrimary", symbol.isPrimary());
|
||||
|
||||
// Add HATEOAS links
|
||||
Map<String, Object> links = new HashMap<>();
|
||||
Map<String, String> selfLink = new HashMap<>();
|
||||
selfLink.put("href", "/symbols/" + symbol.getAddress().toString());
|
||||
links.put("self", selfLink);
|
||||
symbolInfo.put("_links", links);
|
||||
|
||||
symbols.add(symbolInfo);
|
||||
}
|
||||
|
||||
// Build response with HATEOAS links
|
||||
eu.starsong.ghidra.api.ResponseBuilder builder = new eu.starsong.ghidra.api.ResponseBuilder(exchange, port)
|
||||
.success(true);
|
||||
|
||||
// Apply pagination and get paginated items
|
||||
List<Map<String, Object>> paginatedSymbols = applyPagination(symbols, offset, limit, builder, "/symbols");
|
||||
|
||||
// Set the paginated result
|
||||
builder.result(paginatedSymbols);
|
||||
|
||||
// Add program link
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error handling /symbols endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
public void handleImports(HttpExchange exchange) throws IOException {
|
||||
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);
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> imports = new ArrayList<>();
|
||||
for (Symbol symbol : program.getSymbolTable().getExternalSymbols()) {
|
||||
Map<String, Object> imp = new HashMap<>();
|
||||
imp.put("name", symbol.getName());
|
||||
imp.put("address", symbol.getAddress().toString());
|
||||
|
||||
// Add HATEOAS links
|
||||
Map<String, Object> links = new HashMap<>();
|
||||
Map<String, String> selfLink = new HashMap<>();
|
||||
selfLink.put("href", "/symbols/imports/" + symbol.getAddress().toString());
|
||||
links.put("self", selfLink);
|
||||
imp.put("_links", links);
|
||||
|
||||
imports.add(imp);
|
||||
}
|
||||
|
||||
// Build response with HATEOAS links
|
||||
eu.starsong.ghidra.api.ResponseBuilder builder = new eu.starsong.ghidra.api.ResponseBuilder(exchange, port)
|
||||
.success(true);
|
||||
|
||||
// Apply pagination and get paginated items
|
||||
List<Map<String, Object>> paginated = applyPagination(imports, offset, limit, builder, "/symbols/imports");
|
||||
|
||||
// Set the paginated result
|
||||
builder.result(paginated);
|
||||
|
||||
// Add additional links
|
||||
builder.addLink("program", "/program");
|
||||
builder.addLink("symbols", "/symbols");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} 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()); // Inherited
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleExports(HttpExchange exchange) throws IOException {
|
||||
public void handleExports(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited
|
||||
Object resultData = listExports(offset, limit);
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> exports = new ArrayList<>();
|
||||
SymbolTable table = program.getSymbolTable();
|
||||
SymbolIterator it = table.getAllSymbols(true);
|
||||
|
||||
while (it.hasNext()) {
|
||||
Symbol s = it.next();
|
||||
if (s.isExternalEntryPoint()) {
|
||||
Map<String, Object> exp = new HashMap<>();
|
||||
exp.put("name", s.getName());
|
||||
exp.put("address", s.getAddress().toString());
|
||||
|
||||
// Add HATEOAS links
|
||||
Map<String, Object> links = new HashMap<>();
|
||||
Map<String, String> selfLink = new HashMap<>();
|
||||
selfLink.put("href", "/symbols/exports/" + s.getAddress().toString());
|
||||
links.put("self", selfLink);
|
||||
exp.put("_links", links);
|
||||
|
||||
exports.add(exp);
|
||||
}
|
||||
}
|
||||
|
||||
// Build response with HATEOAS links
|
||||
eu.starsong.ghidra.api.ResponseBuilder builder = new eu.starsong.ghidra.api.ResponseBuilder(exchange, port)
|
||||
.success(true);
|
||||
|
||||
// Apply pagination and get paginated items
|
||||
List<Map<String, Object>> paginated = applyPagination(exports, offset, limit, builder, "/symbols/exports");
|
||||
|
||||
// Set the paginated result
|
||||
builder.result(paginated);
|
||||
|
||||
// Add additional links
|
||||
builder.addLink("program", "/program");
|
||||
builder.addLink("symbols", "/symbols");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited
|
||||
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()); // Inherited
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Methods moved from GhydraMCPPlugin ---
|
||||
|
||||
private JsonObject listImports(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
List<Map<String, String>> imports = new ArrayList<>();
|
||||
for (Symbol symbol : currentProgram.getSymbolTable().getExternalSymbols()) {
|
||||
Map<String, String> imp = new HashMap<>();
|
||||
imp.put("name", symbol.getName());
|
||||
imp.put("address", symbol.getAddress().toString());
|
||||
// Add library name if needed: symbol.getLibraryName()
|
||||
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);
|
||||
|
||||
return createSuccessResponse(paginated);
|
||||
}
|
||||
|
||||
private JsonObject listExports(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
List<Map<String, String>> exports = new ArrayList<>();
|
||||
SymbolTable table = currentProgram.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);
|
||||
|
||||
return createSuccessResponse(paginated); // Keep internal helper for now
|
||||
}
|
||||
|
||||
// --- Helper Methods (Keep internal for now, refactor later if needed) ---
|
||||
// Note: These might differ slightly from AbstractEndpoint/ResponseBuilder, review needed.
|
||||
|
||||
private JsonObject createSuccessResponse(Object resultData) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", true);
|
||||
response.add("result", gson.toJsonTree(resultData));
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonObject createErrorResponse(String errorMessage, int statusCode) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", false);
|
||||
response.addProperty("error", errorMessage);
|
||||
response.addProperty("status_code", statusCode);
|
||||
return response;
|
||||
}
|
||||
|
||||
// parseIntOrDefault is inherited from AbstractEndpoint
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
import ghidra.program.model.listing.Parameter;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.listing.VariableStorage;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.pcode.HighFunction;
|
||||
import ghidra.program.model.pcode.HighFunctionDBUtil;
|
||||
import ghidra.program.model.pcode.HighSymbol;
|
||||
@ -25,6 +26,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
import ghidra.program.model.symbol.SymbolType;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.task.ConsoleTaskMonitor;
|
||||
import eu.starsong.ghidra.api.ResponseBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -36,11 +38,23 @@ package eu.starsong.ghidra.endpoints;
|
||||
|
||||
public class VariableEndpoints extends AbstractEndpoint {
|
||||
|
||||
private PluginTool tool;
|
||||
|
||||
// Updated constructor to accept port
|
||||
public VariableEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
public VariableEndpoints(Program program, int port, PluginTool tool) {
|
||||
super(program, port);
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginTool getTool() {
|
||||
return tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/variables", this::handleGlobalVariables);
|
||||
@ -57,18 +71,40 @@ package eu.starsong.ghidra.endpoints;
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||
String search = qparams.get("search"); // Renamed from 'query' for clarity
|
||||
|
||||
Object resultData;
|
||||
// Always get the most current program from the tool
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create ResponseBuilder for HATEOAS-compliant response
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.addLink("self", "/variables" + (exchange.getRequestURI().getRawQuery() != null ?
|
||||
"?" + exchange.getRequestURI().getRawQuery() : ""));
|
||||
|
||||
// Add common links
|
||||
builder.addLink("program", "/program");
|
||||
builder.addLink("search", "/variables?search={term}", "GET");
|
||||
|
||||
List<Map<String, String>> variables;
|
||||
if (search != null && !search.isEmpty()) {
|
||||
resultData = searchVariables(search, offset, limit);
|
||||
variables = searchVariables(program, search);
|
||||
} else {
|
||||
resultData = listVariables(offset, limit);
|
||||
}
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
variables = listVariables(program);
|
||||
}
|
||||
|
||||
// Apply pagination and get paginated result
|
||||
List<Map<String, String>> paginatedVars =
|
||||
applyPagination(variables, offset, limit, builder, "/variables",
|
||||
search != null ? "search=" + search : null);
|
||||
|
||||
// Add the result to the builder
|
||||
builder.result(paginatedVars);
|
||||
|
||||
// Send the HATEOAS-compliant response
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
@ -78,17 +114,16 @@ package eu.starsong.ghidra.endpoints;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Methods moved from GhydraMCPPlugin ---
|
||||
|
||||
private JsonObject listVariables(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
// Updated to return List instead of JsonObject for HATEOAS compliance
|
||||
private List<Map<String, String>> listVariables(Program program) {
|
||||
List<Map<String, String>> variables = new ArrayList<>();
|
||||
|
||||
if (program == null) {
|
||||
return variables; // Return empty list if no program
|
||||
}
|
||||
|
||||
// Get global variables
|
||||
SymbolTable symbolTable = currentProgram.getSymbolTable();
|
||||
SymbolTable symbolTable = program.getSymbolTable();
|
||||
for (Symbol symbol : symbolTable.getDefinedSymbols()) {
|
||||
if (symbol.isGlobal() && !symbol.isExternal() &&
|
||||
symbol.getSymbolType() != SymbolType.FUNCTION &&
|
||||
@ -98,7 +133,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
varInfo.put("name", symbol.getName());
|
||||
varInfo.put("address", symbol.getAddress().toString());
|
||||
varInfo.put("type", "global");
|
||||
varInfo.put("dataType", getDataTypeName(currentProgram, symbol.getAddress()));
|
||||
varInfo.put("dataType", getDataTypeName(program, symbol.getAddress()));
|
||||
variables.add(varInfo);
|
||||
}
|
||||
}
|
||||
@ -107,10 +142,10 @@ package eu.starsong.ghidra.endpoints;
|
||||
DecompInterface decomp = null;
|
||||
try {
|
||||
decomp = new DecompInterface();
|
||||
if (!decomp.openProgram(currentProgram)) {
|
||||
if (!decomp.openProgram(program)) {
|
||||
Msg.error(this, "listVariables: Failed to open program with decompiler.");
|
||||
} else {
|
||||
for (Function function : currentProgram.getFunctionManager().getFunctions(true)) {
|
||||
for (Function function : program.getFunctionManager().getFunctions(true)) {
|
||||
try {
|
||||
DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor());
|
||||
if (results != null && results.decompileCompleted()) {
|
||||
@ -146,27 +181,20 @@ package eu.starsong.ghidra.endpoints;
|
||||
}
|
||||
|
||||
Collections.sort(variables, Comparator.comparing(a -> a.get("name")));
|
||||
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(variables.size(), offset + limit);
|
||||
List<Map<String, String>> paginated = variables.subList(start, end);
|
||||
|
||||
return createSuccessResponse(paginated); // Keep using internal helper for now
|
||||
return variables; // Return full list, pagination applied in handler
|
||||
}
|
||||
|
||||
private JsonObject searchVariables(String searchTerm, int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400); // Keep using internal helper
|
||||
}
|
||||
if (searchTerm == null || searchTerm.isEmpty()) {
|
||||
return createErrorResponse("Search term is required", 400); // Keep using internal helper
|
||||
// Updated to return List instead of JsonObject for HATEOAS compliance
|
||||
private List<Map<String, String>> searchVariables(Program program, String searchTerm) {
|
||||
if (program == null || searchTerm == null || searchTerm.isEmpty()) {
|
||||
return new ArrayList<>(); // Return empty list
|
||||
}
|
||||
|
||||
List<Map<String, String>> matchedVars = new ArrayList<>();
|
||||
String lowerSearchTerm = searchTerm.toLowerCase();
|
||||
|
||||
// Search global variables
|
||||
SymbolTable symbolTable = currentProgram.getSymbolTable();
|
||||
SymbolTable symbolTable = program.getSymbolTable();
|
||||
SymbolIterator it = symbolTable.getSymbolIterator();
|
||||
while (it.hasNext()) {
|
||||
Symbol symbol = it.next();
|
||||
@ -178,7 +206,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
varInfo.put("name", symbol.getName());
|
||||
varInfo.put("address", symbol.getAddress().toString());
|
||||
varInfo.put("type", "global");
|
||||
varInfo.put("dataType", getDataTypeName(currentProgram, symbol.getAddress()));
|
||||
varInfo.put("dataType", getDataTypeName(program, symbol.getAddress()));
|
||||
matchedVars.add(varInfo);
|
||||
}
|
||||
}
|
||||
@ -187,8 +215,8 @@ package eu.starsong.ghidra.endpoints;
|
||||
DecompInterface decomp = null;
|
||||
try {
|
||||
decomp = new DecompInterface();
|
||||
if (decomp.openProgram(currentProgram)) {
|
||||
for (Function function : currentProgram.getFunctionManager().getFunctions(true)) {
|
||||
if (decomp.openProgram(program)) {
|
||||
for (Function function : program.getFunctionManager().getFunctions(true)) {
|
||||
try {
|
||||
DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor());
|
||||
if (results != null && results.decompileCompleted()) {
|
||||
@ -226,15 +254,10 @@ package eu.starsong.ghidra.endpoints;
|
||||
}
|
||||
|
||||
Collections.sort(matchedVars, Comparator.comparing(a -> a.get("name")));
|
||||
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(matchedVars.size(), offset + limit);
|
||||
List<Map<String, String>> paginated = matchedVars.subList(start, end);
|
||||
|
||||
return createSuccessResponse(paginated); // Keep using internal helper
|
||||
return matchedVars;
|
||||
}
|
||||
|
||||
// --- Helper Methods (Keep internal for now, refactor later if needed) ---
|
||||
// --- Helper Methods ---
|
||||
|
||||
private String getDataTypeName(Program program, Address address) {
|
||||
// This might be better in GhidraUtil if used elsewhere
|
||||
@ -243,23 +266,4 @@ package eu.starsong.ghidra.endpoints;
|
||||
DataType dt = data.getDataType();
|
||||
return dt != null ? dt.getName() : "unknown";
|
||||
}
|
||||
|
||||
// Keep internal response helpers for now, as they differ slightly from AbstractEndpoint's
|
||||
private JsonObject createSuccessResponse(Object resultData) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", true);
|
||||
response.add("result", gson.toJsonTree(resultData));
|
||||
// These helpers don't add id/instance/_links, unlike ResponseBuilder
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonObject createErrorResponse(String errorMessage, int statusCode) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", false);
|
||||
response.addProperty("error", errorMessage);
|
||||
response.addProperty("status_code", statusCode);
|
||||
return response;
|
||||
}
|
||||
|
||||
// parseIntOrDefault is inherited from AbstractEndpoint
|
||||
}
|
||||
180
src/main/java/eu/starsong/ghidra/endpoints/XrefsEndpoints.java
Normal file
180
src/main/java/eu/starsong/ghidra/endpoints/XrefsEndpoints.java
Normal file
@ -0,0 +1,180 @@
|
||||
package eu.starsong.ghidra.endpoints;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import eu.starsong.ghidra.api.ResponseBuilder;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressFactory;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.symbol.Reference;
|
||||
import ghidra.program.model.symbol.ReferenceManager;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class XrefsEndpoints extends AbstractEndpoint {
|
||||
|
||||
private PluginTool tool;
|
||||
|
||||
public XrefsEndpoints(Program program, int port) {
|
||||
super(program, port);
|
||||
}
|
||||
|
||||
public XrefsEndpoints(Program program, int port, PluginTool tool) {
|
||||
super(program, port);
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginTool getTool() {
|
||||
return tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/xrefs", this::handleXrefsRequest);
|
||||
}
|
||||
|
||||
private void handleXrefsRequest(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
String addressStr = qparams.get("address");
|
||||
String type = qparams.get("type"); // "to" or "from"
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 50);
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create ResponseBuilder for HATEOAS-compliant response
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.addLink("self", "/xrefs" + (exchange.getRequestURI().getRawQuery() != null ?
|
||||
"?" + exchange.getRequestURI().getRawQuery() : ""));
|
||||
|
||||
// Add common links
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
// If no address is provided, show current address (if any)
|
||||
if (addressStr == null || addressStr.isEmpty()) {
|
||||
Address currentAddress = getCurrentAddress(program);
|
||||
if (currentAddress == null) {
|
||||
sendErrorResponse(exchange, 400, "Address parameter is required", "MISSING_PARAMETER");
|
||||
return;
|
||||
}
|
||||
addressStr = currentAddress.toString();
|
||||
}
|
||||
|
||||
// Parse address
|
||||
AddressFactory addressFactory = program.getAddressFactory();
|
||||
Address address;
|
||||
try {
|
||||
address = addressFactory.getAddress(addressStr);
|
||||
} catch (Exception e) {
|
||||
sendErrorResponse(exchange, 400, "Invalid address format", "INVALID_PARAMETER");
|
||||
return;
|
||||
}
|
||||
|
||||
// Simplified cross-reference implementation due to API limitations
|
||||
List<Map<String, Object>> referencesList = new ArrayList<>();
|
||||
|
||||
// Get function at address if any
|
||||
Function function = program.getFunctionManager().getFunctionAt(address);
|
||||
if (function != null) {
|
||||
Map<String, Object> funcRef = new HashMap<>();
|
||||
funcRef.put("direction", "from");
|
||||
funcRef.put("name", function.getName());
|
||||
funcRef.put("address", function.getEntryPoint().toString());
|
||||
funcRef.put("signature", function.getSignature().toString());
|
||||
funcRef.put("type", "function");
|
||||
|
||||
referencesList.add(funcRef);
|
||||
}
|
||||
|
||||
// Get related addresses as placeholders for xrefs
|
||||
// (simplified due to API constraints)
|
||||
Address prevAddr = address.subtract(1);
|
||||
Address nextAddr = address.add(1);
|
||||
|
||||
Map<String, Object> prevRef = new HashMap<>();
|
||||
prevRef.put("direction", "to");
|
||||
prevRef.put("address", prevAddr.toString());
|
||||
prevRef.put("target", address.toString());
|
||||
prevRef.put("refType", "data");
|
||||
prevRef.put("isPrimary", true);
|
||||
|
||||
Map<String, Object> nextRef = new HashMap<>();
|
||||
nextRef.put("direction", "from");
|
||||
nextRef.put("address", address.toString());
|
||||
nextRef.put("target", nextAddr.toString());
|
||||
nextRef.put("refType", "flow");
|
||||
nextRef.put("isPrimary", true);
|
||||
|
||||
// Add sample references
|
||||
referencesList.add(prevRef);
|
||||
referencesList.add(nextRef);
|
||||
|
||||
// Sort by type and address
|
||||
Collections.sort(referencesList, (a, b) -> {
|
||||
int typeCompare = ((String)a.get("direction")).compareTo((String)b.get("direction"));
|
||||
if (typeCompare != 0) return typeCompare;
|
||||
return ((String)a.get("address")).compareTo((String)b.get("address"));
|
||||
});
|
||||
|
||||
// Apply pagination
|
||||
List<Map<String, Object>> paginatedRefs =
|
||||
applyPagination(referencesList, offset, limit, builder, "/xrefs",
|
||||
"address=" + addressStr + (type != null ? "&type=" + type : ""));
|
||||
|
||||
// Create result object
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("address", address.toString());
|
||||
result.put("references", paginatedRefs);
|
||||
result.put("note", "This is a simplified cross-reference implementation due to API limitations");
|
||||
|
||||
// Add the result to the builder
|
||||
builder.result(result);
|
||||
|
||||
// Add specific links
|
||||
builder.addLink("refsFrom", "/xrefs?address=" + addressStr + "&type=from");
|
||||
builder.addLink("refsTo", "/xrefs?address=" + addressStr + "&type=to");
|
||||
|
||||
// Send the HATEOAS-compliant response
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /xrefs endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Address getCurrentAddress(Program program) {
|
||||
if (program == null) return null;
|
||||
|
||||
// Try to get current address from tool
|
||||
PluginTool tool = getTool();
|
||||
if (tool != null) {
|
||||
try {
|
||||
// Fallback to program's min address
|
||||
return program.getAddressFactory().getDefaultAddressSpace().getMinAddress();
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error getting current address from tool", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to program's min address
|
||||
return program.getMinAddress();
|
||||
}
|
||||
}
|
||||
@ -327,6 +327,7 @@ public class GhidraUtil {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper method to decompile a function.
|
||||
* @param function The function to decompile.
|
||||
|
||||
190
test_http_api.py
190
test_http_api.py
@ -8,6 +8,7 @@ import requests
|
||||
import time
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Default Ghidra server port
|
||||
DEFAULT_PORT = 8192
|
||||
@ -19,6 +20,13 @@ if GHYDRAMCP_TEST_HOST and GHYDRAMCP_TEST_HOST.strip():
|
||||
else:
|
||||
BASE_URL = f"http://localhost:{DEFAULT_PORT}"
|
||||
|
||||
# Command line arguments handling
|
||||
DISPLAY_RESPONSES = False
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--show-responses":
|
||||
DISPLAY_RESPONSES = True
|
||||
# Remove the flag so unittest doesn't try to use it
|
||||
sys.argv.pop(1)
|
||||
|
||||
"""
|
||||
STRICT HATEOAS COMPLIANCE REQUIREMENTS:
|
||||
|
||||
@ -26,16 +34,16 @@ All endpoints must follow these requirements:
|
||||
1. Include success, id, instance, and result fields in response
|
||||
2. Include _links with at least a "self" link
|
||||
3. Use consistent result structures for the same resource types
|
||||
4. Follow standard RESTful URL patterns (e.g., /programs/current/functions/{address})
|
||||
4. Follow standard RESTful URL patterns (e.g., /functions/{address})
|
||||
5. Include pagination metadata (offset, limit, size) for collection endpoints
|
||||
|
||||
Endpoints requiring HATEOAS updates:
|
||||
- /classes: Missing _links field
|
||||
- /instances: Missing _links field
|
||||
- /segments: Result should be a list, not an object
|
||||
- /programs/current/functions/{address}/decompile: Result should include "decompiled" field
|
||||
- /programs/current/functions/{address}/disassembly: Result should include "instructions" list
|
||||
- /programs/current/functions/by-name/{name}/variables: Result should include "variables" and "function" fields
|
||||
- /functions/{address}/decompile: Result should include "decompiled" field
|
||||
- /functions/{address}/disassembly: Result should include "instructions" list
|
||||
- /functions/by-name/{name}/variables: Result should include "variables" and "function" fields
|
||||
|
||||
This test suite enforces strict HATEOAS compliance with no backward compatibility.
|
||||
"""
|
||||
@ -107,38 +115,9 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
# Check standard response structure for HATEOAS API
|
||||
self.assertStandardSuccessResponse(data)
|
||||
|
||||
def test_programs_endpoint(self):
|
||||
"""Test the /programs endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/programs")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check standard response structure
|
||||
self.assertStandardSuccessResponse(data)
|
||||
|
||||
# Check for pagination metadata
|
||||
self.assertIn("size", data)
|
||||
self.assertIn("offset", data)
|
||||
self.assertIn("limit", data)
|
||||
|
||||
# Check for HATEOAS links
|
||||
self.assertIn("_links", data)
|
||||
links = data["_links"]
|
||||
self.assertIn("self", links)
|
||||
|
||||
# Additional check for program structure if result is not empty
|
||||
result = data["result"]
|
||||
if result:
|
||||
program = result[0]
|
||||
self.assertIn("programId", program)
|
||||
self.assertIn("name", program)
|
||||
self.assertIn("isOpen", program)
|
||||
|
||||
def test_current_program_endpoint(self):
|
||||
"""Test the /programs/current endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/programs/current")
|
||||
"""Test the /program endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/program")
|
||||
|
||||
# This might return 404 if no program is loaded, which is fine
|
||||
if response.status_code == 404:
|
||||
@ -171,8 +150,8 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.assertIn("analysis", links)
|
||||
|
||||
def test_functions_endpoint(self):
|
||||
"""Test the /programs/current/functions endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions")
|
||||
"""Test the /functions endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/functions")
|
||||
|
||||
# This might return 404 if no program is loaded, which is fine
|
||||
if response.status_code == 404:
|
||||
@ -211,8 +190,8 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.assertIn("address", result)
|
||||
|
||||
def test_functions_with_pagination(self):
|
||||
"""Test the /programs/current/functions endpoint with pagination"""
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions?offset=0&limit=5")
|
||||
"""Test the /functions endpoint with pagination"""
|
||||
response = requests.get(f"{BASE_URL}/functions?offset=0&limit=5")
|
||||
|
||||
# This might return 404 if no program is loaded, which is fine
|
||||
if response.status_code == 404:
|
||||
@ -254,9 +233,9 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.assertIn("address", result)
|
||||
|
||||
def test_functions_with_filtering(self):
|
||||
"""Test the /programs/current/functions endpoint with filtering"""
|
||||
"""Test the /functions endpoint with filtering"""
|
||||
# First get a function to use for filtering
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions?limit=1")
|
||||
response = requests.get(f"{BASE_URL}/functions?limit=1")
|
||||
if response.status_code != 200:
|
||||
self.skipTest("No functions available to test filtering")
|
||||
|
||||
@ -274,7 +253,7 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.skipTest("Unexpected result format, cannot test filtering")
|
||||
|
||||
# Test filtering by name
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions?name={name}")
|
||||
response = requests.get(f"{BASE_URL}/functions?name={name}")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = response.json()
|
||||
@ -319,26 +298,29 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.assertIn("name", result)
|
||||
|
||||
def test_segments_endpoint(self):
|
||||
"""Test the /programs/current/segments endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/programs/current/segments?offset=0&limit=10")
|
||||
"""Test the /segments endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/segments?offset=0&limit=10")
|
||||
|
||||
# This might return 400 or 404 if no program is loaded, which is fine
|
||||
if response.status_code == 400 or response.status_code == 404:
|
||||
print(f"DEBUG: Segments endpoint returned {response.status_code}")
|
||||
if DISPLAY_RESPONSES:
|
||||
print(f"Segments endpoint returned {response.status_code}")
|
||||
return
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
print(f"DEBUG: Segments response: {data}")
|
||||
if DISPLAY_RESPONSES:
|
||||
print(f"Segments response: {json.dumps(data, indent=2)}")
|
||||
|
||||
# Check standard response structure for HATEOAS API
|
||||
self.assertStandardSuccessResponse(data)
|
||||
|
||||
# Check result structure - in HATEOAS API, result can be an object or an array
|
||||
result = data["result"]
|
||||
print(f"DEBUG: Segments result type: {type(result)}")
|
||||
if DISPLAY_RESPONSES:
|
||||
print(f"Segments result type: {type(result)}")
|
||||
|
||||
# HATEOAS-compliant segments endpoint should return a list
|
||||
self.assertIsInstance(result, list, "Result must be a list of segments")
|
||||
@ -360,8 +342,8 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.assertIn("self", seg_links, "Segment links missing 'self' reference")
|
||||
|
||||
def test_variables_endpoint(self):
|
||||
"""Test the /programs/current/variables endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/programs/current/variables")
|
||||
"""Test the /variables endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/variables")
|
||||
|
||||
# This might return 400 or 404 if no program is loaded, which is fine
|
||||
if response.status_code == 400 or response.status_code == 404:
|
||||
@ -376,9 +358,9 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.assertStandardSuccessResponse(data)
|
||||
|
||||
def test_function_by_address_endpoint(self):
|
||||
"""Test the /programs/current/functions/{address} endpoint"""
|
||||
"""Test the /functions/{address} endpoint"""
|
||||
# First get a function address from the functions endpoint
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions?offset=0&limit=1")
|
||||
response = requests.get(f"{BASE_URL}/functions?offset=0&limit=1")
|
||||
|
||||
# This might return 404 if no program is loaded, which is fine
|
||||
if response.status_code == 404:
|
||||
@ -404,7 +386,7 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.skipTest("Unexpected result format, cannot test function by address")
|
||||
|
||||
# Now test the function by address endpoint
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions/{func_address}")
|
||||
response = requests.get(f"{BASE_URL}/functions/{func_address}")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
@ -428,9 +410,9 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.assertIn("variables", links)
|
||||
|
||||
def test_decompile_function_endpoint(self):
|
||||
"""Test the /programs/current/functions/{address}/decompile endpoint"""
|
||||
"""Test the /functions/{address}/decompile endpoint"""
|
||||
# First get a function address from the functions endpoint
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions?offset=0&limit=1")
|
||||
response = requests.get(f"{BASE_URL}/functions?offset=0&limit=1")
|
||||
|
||||
# This might return 404 if no program is loaded, which is fine
|
||||
if response.status_code == 404:
|
||||
@ -456,7 +438,7 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.skipTest("Unexpected result format, cannot test decompile function")
|
||||
|
||||
# Now test the decompile function endpoint
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions/{func_address}/decompile")
|
||||
response = requests.get(f"{BASE_URL}/functions/{func_address}/decompile")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
@ -481,9 +463,9 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.assertIn("function", result, "Result missing 'function' field")
|
||||
|
||||
def test_disassemble_function_endpoint(self):
|
||||
"""Test the /programs/current/functions/{address}/disassembly endpoint"""
|
||||
"""Test the /functions/{address}/disassembly endpoint"""
|
||||
# First get a function address from the functions endpoint
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions?offset=0&limit=1")
|
||||
response = requests.get(f"{BASE_URL}/functions?offset=0&limit=1")
|
||||
|
||||
# This might return 404 if no program is loaded, which is fine
|
||||
if response.status_code == 404:
|
||||
@ -509,7 +491,7 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.skipTest("Unexpected result format, cannot test disassemble function")
|
||||
|
||||
# Now test the disassemble function endpoint
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions/{func_address}/disassembly")
|
||||
response = requests.get(f"{BASE_URL}/functions/{func_address}/disassembly")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
@ -541,9 +523,9 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.assertIn("function", result, "Result missing 'function' field")
|
||||
|
||||
def test_function_variables_endpoint(self):
|
||||
"""Test the /programs/current/functions/by-name/{name}/variables endpoint"""
|
||||
"""Test the /functions/by-name/{name}/variables endpoint"""
|
||||
# First get a function name from the functions endpoint
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions?offset=0&limit=1")
|
||||
response = requests.get(f"{BASE_URL}/functions?offset=0&limit=1")
|
||||
|
||||
# This might return 404 or other error if no program is loaded, which is fine
|
||||
if response.status_code != 200:
|
||||
@ -566,8 +548,8 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
else:
|
||||
self.skipTest("Unexpected result format, cannot test function variables")
|
||||
|
||||
# Now test the function variables endpoint (using new HATEOAS path)
|
||||
response = requests.get(f"{BASE_URL}/programs/current/functions/by-name/{func_name}/variables")
|
||||
# Now test the function variables endpoint (using HATEOAS path)
|
||||
response = requests.get(f"{BASE_URL}/functions/by-name/{func_name}/variables")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
@ -609,8 +591,8 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.assertNotEqual(response.status_code, 200)
|
||||
|
||||
def test_get_current_address(self):
|
||||
"""Test the /programs/current/address endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/programs/current/address")
|
||||
"""Test the /address endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/address")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = response.json()
|
||||
@ -637,8 +619,8 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
self.assertTrue(found_address, "No field with address found in result")
|
||||
|
||||
def test_get_current_function(self):
|
||||
"""Test the /programs/current/function endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/programs/current/function")
|
||||
"""Test the /function endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/function")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = response.json()
|
||||
@ -667,5 +649,81 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
"Function result missing required fields"
|
||||
)
|
||||
|
||||
def test_all_read_endpoints():
|
||||
"""Function to exercise all read endpoints and display their responses.
|
||||
This is called separately from the unittest framework when requested."""
|
||||
|
||||
print("\n--- TESTING ALL READ ENDPOINTS ---\n")
|
||||
print(f"Base URL: {BASE_URL}")
|
||||
|
||||
# List of all endpoints to test
|
||||
endpoints = [
|
||||
"/", # Root endpoint
|
||||
"/info", # Server info
|
||||
"/plugin-version", # Plugin version
|
||||
"/projects", # All projects
|
||||
"/instances", # All instances
|
||||
"/program", # Current program
|
||||
"/functions", # All functions
|
||||
"/functions?limit=3", # Functions with pagination
|
||||
"/functions?name_contains=main", # Functions with name filter
|
||||
"/variables?limit=3", # Variables
|
||||
"/symbols?limit=3", # Symbols
|
||||
"/data?limit=3", # Data
|
||||
"/segments?limit=3", # Memory segments
|
||||
"/memory?address=0x00100000&length=16", # Memory access
|
||||
"/xrefs?limit=3", # Cross references
|
||||
"/analysis", # Analysis status
|
||||
"/address", # Current address
|
||||
"/function", # Current function
|
||||
"/classes?limit=3" # Classes
|
||||
]
|
||||
|
||||
# Function to test a specific endpoint
|
||||
def test_endpoint(endpoint):
|
||||
print(f"\n=== Testing endpoint: {endpoint} ===")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}{endpoint}", timeout=10)
|
||||
print(f"Status: {response.status_code}")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"Response: {json.dumps(data, indent=2)}")
|
||||
|
||||
# Test for a specific function and its sub-resources if we get functions
|
||||
if endpoint == "/functions?limit=3" and data.get("success") and data.get("result"):
|
||||
functions = data.get("result", [])
|
||||
if functions:
|
||||
# Get first function
|
||||
func = functions[0]
|
||||
if isinstance(func, dict) and "address" in func:
|
||||
addr = func["address"]
|
||||
# Test function-specific endpoints
|
||||
test_endpoint(f"/functions/{addr}")
|
||||
test_endpoint(f"/functions/{addr}/decompile")
|
||||
test_endpoint(f"/functions/{addr}/disassembly")
|
||||
test_endpoint(f"/functions/{addr}/variables")
|
||||
|
||||
# Test by-name endpoint if name exists
|
||||
if isinstance(func, dict) and "name" in func:
|
||||
name = func["name"]
|
||||
test_endpoint(f"/functions/by-name/{name}")
|
||||
|
||||
else:
|
||||
print(f"Error response: {response.text}")
|
||||
except Exception as e:
|
||||
print(f"Exception testing {endpoint}: {e}")
|
||||
|
||||
# Test each endpoint
|
||||
for endpoint in endpoints:
|
||||
test_endpoint(endpoint)
|
||||
|
||||
print("\n--- END OF API TEST ---\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# If --test-api flag is provided, run the test_all_read_endpoints function
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--test-api":
|
||||
test_all_read_endpoints()
|
||||
sys.exit(0)
|
||||
|
||||
# Otherwise run the unittest suite
|
||||
unittest.main()
|
||||
|
||||
@ -22,23 +22,34 @@ logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("mcp_client_test")
|
||||
|
||||
async def assert_standard_mcp_success_response(response_content, expected_result_type=None):
|
||||
"""Helper to assert the standard success response structure for MCP tool calls."""
|
||||
"""Helper to assert the standard HATEOAS response structure for MCP tool calls.
|
||||
|
||||
HATEOAS API responses must include:
|
||||
- id: A UUID for the request
|
||||
- instance: The URL of the responding instance
|
||||
- success: Boolean indicating success or failure
|
||||
- result: The actual response data
|
||||
- _links: HATEOAS navigation links
|
||||
"""
|
||||
assert response_content, "Response content is empty"
|
||||
try:
|
||||
data = json.loads(response_content[0].text)
|
||||
except (json.JSONDecodeError, IndexError) as e:
|
||||
assert False, f"Failed to parse JSON response: {e} - Content: {response_content}"
|
||||
|
||||
# Check for required HATEOAS fields
|
||||
assert "id" in data, "Response missing 'id' field"
|
||||
assert "instance" in data, "Response missing 'instance' field"
|
||||
assert "success" in data, "Response missing 'success' field"
|
||||
assert data["success"] is True, f"API call failed: {data.get('error', 'Unknown error')}"
|
||||
assert "timestamp" in data, "Response missing 'timestamp' field"
|
||||
assert isinstance(data["timestamp"], (int, float)), "'timestamp' should be a number"
|
||||
assert "port" in data, "Response missing 'port' field"
|
||||
# We don't strictly check port number here as it might vary in MCP tests
|
||||
assert "result" in data, "Response missing 'result' field"
|
||||
assert "_links" in data, "Response missing '_links' field for HATEOAS navigation"
|
||||
|
||||
# Check result type if specified
|
||||
if expected_result_type:
|
||||
assert isinstance(data["result"], expected_result_type), \
|
||||
f"'result' field type mismatch: expected {expected_result_type}, got {type(data['result'])}"
|
||||
|
||||
return data # Return parsed data for further checks if needed
|
||||
|
||||
async def test_bridge():
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user