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]
|
## [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
|
## [1.3.0] - 2025-04-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# /// script
|
# /// script
|
||||||
# requires-python = ">=3.11"
|
# requires-python = ">=3.11"
|
||||||
# dependencies = [
|
# dependencies = [
|
||||||
# "mcp==1.5.0",
|
# "mcp==1.6.0",
|
||||||
# "requests==2.32.3",
|
# "requests==2.32.3",
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
@ -12,6 +12,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from mcp.server.fastmcp import FastMCP
|
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)
|
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)
|
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)
|
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:
|
def get_instance_url(port: int) -> str:
|
||||||
"""Get URL for a Ghidra instance by port"""
|
"""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()
|
"result": response.text.strip()
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# Try falling back to default instance if this was a secondary instance
|
# # Try falling back to default instance if this was a secondary instance
|
||||||
if port != DEFAULT_GHIDRA_PORT and response.status_code == 404:
|
# if port != DEFAULT_GHIDRA_PORT and response.status_code == 404:
|
||||||
return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
|
# return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
error_data = response.json()
|
error_data = response.json()
|
||||||
@ -269,12 +276,10 @@ def register_instance(port: int, url: str = None) -> str:
|
|||||||
try:
|
try:
|
||||||
# Try the root endpoint first
|
# Try the root endpoint first
|
||||||
root_url = f"{url}/"
|
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
|
root_response = requests.get(root_url, timeout=1.5) # Short timeout for root
|
||||||
|
|
||||||
if root_response.ok:
|
if root_response.ok:
|
||||||
try:
|
try:
|
||||||
print(f"Got response from root: {root_response.text}", file=sys.stderr)
|
|
||||||
root_data = root_response.json()
|
root_data = root_response.json()
|
||||||
|
|
||||||
# Extract basic information from root
|
# 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"]:
|
if "file" in root_data and root_data["file"]:
|
||||||
project_info["file"] = root_data["file"]
|
project_info["file"] = root_data["file"]
|
||||||
|
|
||||||
print(f"Root data parsed: {project_info}", file=sys.stderr)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error parsing root info: {e}", file=sys.stderr)
|
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 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"):
|
if not project_info.get("project") and not project_info.get("file"):
|
||||||
info_url = f"{url}/info"
|
info_url = f"{url}/info"
|
||||||
print(f"Trying fallback info from {info_url}", file=sys.stderr)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
info_response = requests.get(info_url, timeout=2)
|
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
|
"instances": found_instances
|
||||||
}
|
}
|
||||||
|
|
||||||
# Updated tool implementations with port parameter
|
|
||||||
from urllib.parse import quote
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_functions(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
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})
|
return safe_get(port, "functions", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
@mcp.tool()
|
@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"))
|
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()
|
@mcp.tool()
|
||||||
def decompile_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> str:
|
def decompile_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> str:
|
||||||
"""Decompile a function at a specific memory address
|
"""Decompile a function at a specific memory address
|
||||||
@ -716,19 +710,19 @@ def periodic_discovery():
|
|||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Auto-register default instance
|
# # Auto-register default instance
|
||||||
register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}")
|
# register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}")
|
||||||
|
|
||||||
# Auto-discover other instances
|
# # Auto-discover other instances
|
||||||
discover_instances()
|
# discover_instances()
|
||||||
|
|
||||||
# Start periodic discovery in background thread
|
# # Start periodic discovery in background thread
|
||||||
discovery_thread = threading.Thread(
|
# discovery_thread = threading.Thread(
|
||||||
target=periodic_discovery,
|
# target=periodic_discovery,
|
||||||
daemon=True,
|
# daemon=True,
|
||||||
name="GhydraMCP-Discovery"
|
# name="GhydraMCP-Discovery"
|
||||||
)
|
# )
|
||||||
discovery_thread.start()
|
# discovery_thread.start()
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, handle_sigint)
|
# signal.signal(signal.SIGINT, handle_sigint)
|
||||||
mcp.run()
|
mcp.run(transport="stdio")
|
||||||
|
|||||||
6
pom.xml
6
pom.xml
@ -23,9 +23,9 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- JSON handling -->
|
<!-- JSON handling -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.googlecode.json-simple</groupId>
|
<groupId>com.google.code.gson</groupId>
|
||||||
<artifactId>json-simple</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
<version>1.1.1</version>
|
<version>2.10.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Ghidra JARs as system-scoped dependencies -->
|
<!-- Ghidra JARs as system-scoped dependencies -->
|
||||||
|
|||||||
@ -46,7 +46,8 @@ import java.util.concurrent.*;
|
|||||||
import java.util.concurrent.atomic.*;
|
import java.util.concurrent.atomic.*;
|
||||||
|
|
||||||
// For JSON response handling
|
// 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.services.CodeViewerService;
|
||||||
import ghidra.app.util.PseudoDisassembler;
|
import ghidra.app.util.PseudoDisassembler;
|
||||||
@ -96,9 +97,11 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log to both console and log file
|
Msg.info(this, "Marker");
|
||||||
Msg.info(this, "GhydraMCPPlugin loaded on port " + port);
|
|
||||||
System.out.println("[GhydraMCP] Plugin loaded on port " + port);
|
// Log to both console and log file
|
||||||
|
Msg.info(this, "GhydraMCPPlugin loaded on port " + port);
|
||||||
|
System.out.println("[GhydraMCP] Plugin loaded on port " + port);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startServer();
|
startServer();
|
||||||
@ -201,15 +204,52 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Class resources
|
// Class resources with detailed logging
|
||||||
server.createContext("/classes", exchange -> {
|
server.createContext("/classes", exchange -> {
|
||||||
if ("GET".equals(exchange.getRequestMethod())) {
|
try {
|
||||||
Map<String, String> qparams = parseQueryParams(exchange);
|
if ("GET".equals(exchange.getRequestMethod())) {
|
||||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
try {
|
||||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
sendResponse(exchange, getAllClassNames(offset, limit));
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
} else {
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -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 -> {
|
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 {
|
try {
|
||||||
String response = "{\n";
|
String response = "{\n";
|
||||||
response += "\"port\": " + port + ",\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 {
|
private void sendResponse(HttpExchange exchange, Object response) throws IOException {
|
||||||
JSONObject json = new JSONObject();
|
JsonObject json = new JsonObject();
|
||||||
json.put("success", true);
|
json.addProperty("success", true);
|
||||||
if (response instanceof String) {
|
if (response instanceof String) {
|
||||||
json.put("result", response);
|
json.addProperty("result", (String)response);
|
||||||
} else {
|
} else {
|
||||||
json.put("data", response);
|
json.addProperty("data", response.toString());
|
||||||
}
|
}
|
||||||
json.put("timestamp", System.currentTimeMillis());
|
json.addProperty("timestamp", System.currentTimeMillis());
|
||||||
json.put("port", this.port);
|
json.addProperty("port", this.port);
|
||||||
if (this.isBaseInstance) {
|
if (this.isBaseInstance) {
|
||||||
json.put("instanceType", "base");
|
json.addProperty("instanceType", "base");
|
||||||
} else {
|
} else {
|
||||||
json.put("instanceType", "secondary");
|
json.addProperty("instanceType", "secondary");
|
||||||
}
|
}
|
||||||
sendJsonResponse(exchange, json);
|
sendJsonResponse(exchange, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendJsonResponse(HttpExchange exchange, JSONObject jsonObj) throws IOException {
|
private void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj) throws IOException {
|
||||||
String json = jsonObj.toJSONString();
|
try {
|
||||||
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
Gson gson = new Gson();
|
||||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
String json = gson.toJson(jsonObj);
|
||||||
exchange.sendResponseHeaders(200, bytes.length);
|
Msg.debug(this, "Sending JSON response: " + json);
|
||||||
try (OutputStream os = exchange.getResponseBody()) {
|
|
||||||
os.write(bytes);
|
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||||
|
exchange.sendResponseHeaders(200, bytes.length);
|
||||||
|
|
||||||
|
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 {
|
private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) throws IOException {
|
||||||
JSONObject error = new JSONObject();
|
JsonObject error = new JsonObject();
|
||||||
error.put("error", message);
|
error.addProperty("error", message);
|
||||||
error.put("status", statusCode);
|
error.addProperty("status", statusCode);
|
||||||
error.put("success", false);
|
error.addProperty("success", false);
|
||||||
byte[] bytes = error.toJSONString().getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
Gson gson = new Gson();
|
||||||
|
byte[] bytes = gson.toJson(error).getBytes(StandardCharsets.UTF_8);
|
||||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||||
try (OutputStream os = exchange.getResponseBody()) {
|
try (OutputStream os = exchange.getResponseBody()) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user