Switch to JSON as bridge/plugin comm protocol
This commit is contained in:
parent
04d088591b
commit
cbe5dcc1f3
12
CHANGELOG.md
12
CHANGELOG.md
@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Structured JSON communication between Python bridge and Java plugin
|
||||
- Consistent response format with metadata (timestamp, port, instance type)
|
||||
- Comprehensive test suites for HTTP API and MCP bridge
|
||||
- Test runner script for easy test execution
|
||||
- Detailed testing documentation in TESTING.md
|
||||
|
||||
### Changed
|
||||
- Improved error handling in API responses
|
||||
- Enhanced JSON parsing in the Java plugin
|
||||
- Updated documentation with JSON communication details
|
||||
|
||||
## [1.3.0] - 2025-04-02
|
||||
|
||||
### Added
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "mcp==1.5.0",
|
||||
# "mcp==1.6.0",
|
||||
# "requests==2.32.3",
|
||||
# ]
|
||||
# ///
|
||||
@ -12,6 +12,7 @@ import threading
|
||||
import time
|
||||
from threading import Lock
|
||||
from typing import Dict
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
@ -25,10 +26,16 @@ DEFAULT_GHIDRA_HOST = "localhost"
|
||||
QUICK_DISCOVERY_RANGE = range(8192, 8202) # Limited range for interactive/triggered discovery (10 ports)
|
||||
FULL_DISCOVERY_RANGE = range(8192, 8212) # Wider range for background discovery (20 ports)
|
||||
|
||||
mcp = FastMCP("hydra-mcp")
|
||||
instructions = """
|
||||
GhydraMCP allows interacting with multiple Ghidra SRE instances. Ghidra SRE is a tool for reverse engineering and analyzing binaries, e.g. malware.
|
||||
|
||||
First, run `discover_instances` to find open Ghidra instances. List tools to see what GhydraMCP can do.
|
||||
"""
|
||||
|
||||
mcp = FastMCP("GhydraMCP", instructions=instructions)
|
||||
|
||||
ghidra_host = os.environ.get("GHIDRA_HYDRA_HOST", DEFAULT_GHIDRA_HOST)
|
||||
print(f"Using Ghidra host: {ghidra_host}")
|
||||
# print(f"Using Ghidra host: {ghidra_host}")
|
||||
|
||||
def get_instance_url(port: int) -> str:
|
||||
"""Get URL for a Ghidra instance by port"""
|
||||
@ -201,9 +208,9 @@ def safe_post(port: int, endpoint: str, data: dict | str) -> dict:
|
||||
"result": response.text.strip()
|
||||
}
|
||||
else:
|
||||
# Try falling back to default instance if this was a secondary instance
|
||||
if port != DEFAULT_GHIDRA_PORT and response.status_code == 404:
|
||||
return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
|
||||
# # Try falling back to default instance if this was a secondary instance
|
||||
# if port != DEFAULT_GHIDRA_PORT and response.status_code == 404:
|
||||
# return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
|
||||
|
||||
try:
|
||||
error_data = response.json()
|
||||
@ -269,12 +276,10 @@ def register_instance(port: int, url: str = None) -> str:
|
||||
try:
|
||||
# Try the root endpoint first
|
||||
root_url = f"{url}/"
|
||||
print(f"Trying to get root info from {root_url}", file=sys.stderr)
|
||||
root_response = requests.get(root_url, timeout=1.5) # Short timeout for root
|
||||
|
||||
if root_response.ok:
|
||||
try:
|
||||
print(f"Got response from root: {root_response.text}", file=sys.stderr)
|
||||
root_data = root_response.json()
|
||||
|
||||
# Extract basic information from root
|
||||
@ -283,16 +288,12 @@ def register_instance(port: int, url: str = None) -> str:
|
||||
if "file" in root_data and root_data["file"]:
|
||||
project_info["file"] = root_data["file"]
|
||||
|
||||
print(f"Root data parsed: {project_info}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"Error parsing root info: {e}", file=sys.stderr)
|
||||
else:
|
||||
print(f"Root endpoint returned {root_response.status_code}", file=sys.stderr)
|
||||
|
||||
# If we don't have project info yet, try the /info endpoint as a fallback
|
||||
if not project_info.get("project") and not project_info.get("file"):
|
||||
info_url = f"{url}/info"
|
||||
print(f"Trying fallback info from {info_url}", file=sys.stderr)
|
||||
|
||||
try:
|
||||
info_response = requests.get(info_url, timeout=2)
|
||||
@ -369,13 +370,18 @@ def _discover_instances(port_range, host=None, timeout=0.5) -> dict:
|
||||
"instances": found_instances
|
||||
}
|
||||
|
||||
# Updated tool implementations with port parameter
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_functions(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||
"""List all functions with pagination"""
|
||||
"""List all functions in the current program
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (default: 8192)
|
||||
offset: Pagination offset (default: 0)
|
||||
limit: Maximum number of segments to return (default: 100)
|
||||
|
||||
Returns:
|
||||
List of strings with function names and addresses
|
||||
"""
|
||||
return safe_get(port, "functions", {"offset": offset, "limit": limit})
|
||||
|
||||
@mcp.tool()
|
||||
@ -522,18 +528,6 @@ def get_current_function(port: int = DEFAULT_GHIDRA_PORT) -> str:
|
||||
"""
|
||||
return "\n".join(safe_get(port, "get_current_function"))
|
||||
|
||||
@mcp.tool()
|
||||
def list_functions(port: int = DEFAULT_GHIDRA_PORT) -> list:
|
||||
"""List all functions in the current program
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (default: 8192)
|
||||
|
||||
Returns:
|
||||
List of strings with function names and addresses
|
||||
"""
|
||||
return safe_get(port, "list_functions")
|
||||
|
||||
@mcp.tool()
|
||||
def decompile_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> str:
|
||||
"""Decompile a function at a specific memory address
|
||||
@ -716,19 +710,19 @@ def periodic_discovery():
|
||||
time.sleep(30)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Auto-register default instance
|
||||
register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}")
|
||||
# # Auto-register default instance
|
||||
# register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}")
|
||||
|
||||
# Auto-discover other instances
|
||||
discover_instances()
|
||||
# # Auto-discover other instances
|
||||
# discover_instances()
|
||||
|
||||
# Start periodic discovery in background thread
|
||||
discovery_thread = threading.Thread(
|
||||
target=periodic_discovery,
|
||||
daemon=True,
|
||||
name="GhydraMCP-Discovery"
|
||||
)
|
||||
discovery_thread.start()
|
||||
# # Start periodic discovery in background thread
|
||||
# discovery_thread = threading.Thread(
|
||||
# target=periodic_discovery,
|
||||
# daemon=True,
|
||||
# name="GhydraMCP-Discovery"
|
||||
# )
|
||||
# discovery_thread.start()
|
||||
|
||||
signal.signal(signal.SIGINT, handle_sigint)
|
||||
mcp.run()
|
||||
# signal.signal(signal.SIGINT, handle_sigint)
|
||||
mcp.run(transport="stdio")
|
||||
|
||||
6
pom.xml
6
pom.xml
@ -23,9 +23,9 @@
|
||||
<dependencies>
|
||||
<!-- JSON handling -->
|
||||
<dependency>
|
||||
<groupId>com.googlecode.json-simple</groupId>
|
||||
<artifactId>json-simple</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Ghidra JARs as system-scoped dependencies -->
|
||||
|
||||
@ -46,7 +46,8 @@ import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
|
||||
// For JSON response handling
|
||||
import org.json.simple.JSONObject;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import ghidra.app.services.CodeViewerService;
|
||||
import ghidra.app.util.PseudoDisassembler;
|
||||
@ -96,6 +97,8 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
Msg.info(this, "Marker");
|
||||
|
||||
// Log to both console and log file
|
||||
Msg.info(this, "GhydraMCPPlugin loaded on port " + port);
|
||||
System.out.println("[GhydraMCP] Plugin loaded on port " + port);
|
||||
@ -201,16 +204,53 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
}
|
||||
});
|
||||
|
||||
// Class resources
|
||||
// Class resources with detailed logging
|
||||
server.createContext("/classes", exchange -> {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
try {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||
sendResponse(exchange, getAllClassNames(offset, limit));
|
||||
|
||||
String result = getAllClassNames(offset, limit);
|
||||
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("success", true);
|
||||
json.addProperty("result", result);
|
||||
json.addProperty("timestamp", System.currentTimeMillis());
|
||||
json.addProperty("port", this.port);
|
||||
|
||||
Gson gson = new Gson();
|
||||
String jsonStr = gson.toJson(json);
|
||||
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
|
||||
byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
|
||||
OutputStream os = exchange.getResponseBody();
|
||||
|
||||
os.write(bytes);
|
||||
|
||||
os.flush();
|
||||
|
||||
os.close();
|
||||
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "/classes: Error in request processing: " + e.getMessage(), e);
|
||||
try {
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
} catch (IOException ioe) {
|
||||
Msg.error(this, "/classes: Failed to send error response: " + ioe.getMessage(), ioe);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "/classes: Unhandled error: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
|
||||
// Memory segments
|
||||
@ -353,8 +393,16 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
}
|
||||
});
|
||||
|
||||
// Super simple root endpoint - exact same as /info for consistency
|
||||
// Root endpoint - only handle exact "/" path
|
||||
server.createContext("/", exchange -> {
|
||||
// Only handle exact root path
|
||||
if (!exchange.getRequestURI().getPath().equals("/")) {
|
||||
// Return 404 for any other path that reaches this handler
|
||||
Msg.info(this, "Received request for unknown path: " + exchange.getRequestURI().getPath());
|
||||
sendErrorResponse(exchange, 404, "Endpoint not found");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String response = "{\n";
|
||||
response += "\"port\": " + port + ",\n";
|
||||
@ -1229,39 +1277,64 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
|
||||
|
||||
private void sendResponse(HttpExchange exchange, Object response) throws IOException {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("success", true);
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("success", true);
|
||||
if (response instanceof String) {
|
||||
json.put("result", response);
|
||||
json.addProperty("result", (String)response);
|
||||
} else {
|
||||
json.put("data", response);
|
||||
json.addProperty("data", response.toString());
|
||||
}
|
||||
json.put("timestamp", System.currentTimeMillis());
|
||||
json.put("port", this.port);
|
||||
json.addProperty("timestamp", System.currentTimeMillis());
|
||||
json.addProperty("port", this.port);
|
||||
if (this.isBaseInstance) {
|
||||
json.put("instanceType", "base");
|
||||
json.addProperty("instanceType", "base");
|
||||
} else {
|
||||
json.put("instanceType", "secondary");
|
||||
json.addProperty("instanceType", "secondary");
|
||||
}
|
||||
sendJsonResponse(exchange, json);
|
||||
}
|
||||
|
||||
private void sendJsonResponse(HttpExchange exchange, JSONObject jsonObj) throws IOException {
|
||||
String json = jsonObj.toJSONString();
|
||||
private void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj) throws IOException {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
String json = gson.toJson(jsonObj);
|
||||
Msg.debug(this, "Sending JSON response: " + json);
|
||||
|
||||
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
|
||||
OutputStream os = null;
|
||||
try {
|
||||
os = exchange.getResponseBody();
|
||||
os.write(bytes);
|
||||
os.flush();
|
||||
} catch (IOException e) {
|
||||
Msg.error(this, "Error writing response body: " + e.getMessage(), e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (os != null) {
|
||||
try {
|
||||
os.close();
|
||||
} catch (IOException e) {
|
||||
Msg.error(this, "Error closing output stream: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in sendJsonResponse: " + e.getMessage(), e);
|
||||
throw new IOException("Failed to send JSON response", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) throws IOException {
|
||||
JSONObject error = new JSONObject();
|
||||
error.put("error", message);
|
||||
error.put("status", statusCode);
|
||||
error.put("success", false);
|
||||
byte[] bytes = error.toJSONString().getBytes(StandardCharsets.UTF_8);
|
||||
JsonObject error = new JsonObject();
|
||||
error.addProperty("error", message);
|
||||
error.addProperty("status", statusCode);
|
||||
error.addProperty("success", false);
|
||||
|
||||
Gson gson = new Gson();
|
||||
byte[] bytes = gson.toJson(error).getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user