WIP: Switch to JSON as data exchange format

This commit is contained in:
Teal Bauer 2025-04-03 18:52:47 +02:00
parent 89396469b2
commit 04d088591b
2 changed files with 232 additions and 40 deletions

View File

@ -44,75 +44,194 @@ def get_instance_url(port: int) -> str:
return f"http://{ghidra_host}:{port}"
def safe_get(port: int, endpoint: str, params: dict = None) -> list:
"""Perform a GET request to a specific Ghidra instance"""
def safe_get(port: int, endpoint: str, params: dict = None) -> dict:
"""Perform a GET request to a specific Ghidra instance and return JSON response"""
if params is None:
params = {}
url = f"{get_instance_url(port)}/{endpoint}"
try:
response = requests.get(url, params=params, timeout=5)
response.encoding = 'utf-8'
response = requests.get(
url,
params=params,
headers={'Accept': 'application/json'},
timeout=5
)
if response.ok:
return response.text.splitlines()
elif response.status_code == 404:
# Try falling back to default instance if this was a secondary instance
if port != DEFAULT_GHIDRA_PORT:
return safe_get(DEFAULT_GHIDRA_PORT, endpoint, params)
return [f"Error {response.status_code}: {response.text.strip()}"]
try:
# Always expect JSON response
json_data = response.json()
# If the response has a 'result' field that's a string, extract it
if isinstance(json_data, dict) and 'result' in json_data:
return json_data
# Otherwise, wrap the response in a standard format
return {
"success": True,
"data": json_data,
"timestamp": int(time.time() * 1000)
}
except ValueError:
# If not JSON, wrap the text in our standard format
return {
"success": False,
"error": "Invalid JSON response",
"response": response.text,
"timestamp": int(time.time() * 1000)
}
else:
return [f"Error {response.status_code}: {response.text.strip()}"]
# Try falling back to default instance if this was a secondary instance
if port != DEFAULT_GHIDRA_PORT and response.status_code == 404:
return safe_get(DEFAULT_GHIDRA_PORT, endpoint, params)
try:
error_data = response.json()
return {
"success": False,
"error": error_data.get("error", f"HTTP {response.status_code}"),
"status_code": response.status_code,
"timestamp": int(time.time() * 1000)
}
except ValueError:
return {
"success": False,
"error": response.text.strip(),
"status_code": response.status_code,
"timestamp": int(time.time() * 1000)
}
except requests.exceptions.ConnectionError:
# Instance may be down - try default instance if this was secondary
if port != DEFAULT_GHIDRA_PORT:
return safe_get(DEFAULT_GHIDRA_PORT, endpoint, params)
return ["Error: Failed to connect to Ghidra instance"]
return {
"success": False,
"error": "Failed to connect to Ghidra instance",
"status_code": 503,
"timestamp": int(time.time() * 1000)
}
except Exception as e:
return [f"Request failed: {str(e)}"]
return {
"success": False,
"error": str(e),
"exception": e.__class__.__name__,
"timestamp": int(time.time() * 1000)
}
def safe_put(port: int, endpoint: str, data: dict) -> str:
"""Perform a PUT request to a specific Ghidra instance"""
def safe_put(port: int, endpoint: str, data: dict) -> dict:
"""Perform a PUT request to a specific Ghidra instance with JSON payload"""
try:
url = f"{get_instance_url(port)}/{endpoint}"
response = requests.put(url, data=data, timeout=5)
response.encoding = 'utf-8'
response = requests.put(
url,
json=data,
headers={'Content-Type': 'application/json'},
timeout=5
)
if response.ok:
return response.text.strip()
elif response.status_code == 404 and port != DEFAULT_GHIDRA_PORT:
# Try falling back to default instance
return safe_put(DEFAULT_GHIDRA_PORT, endpoint, data)
try:
return response.json()
except ValueError:
return {
"success": True,
"result": response.text.strip()
}
else:
return f"Error {response.status_code}: {response.text.strip()}"
# Try falling back to default instance if this was a secondary instance
if port != DEFAULT_GHIDRA_PORT and response.status_code == 404:
return safe_put(DEFAULT_GHIDRA_PORT, endpoint, data)
try:
error_data = response.json()
return {
"success": False,
"error": error_data.get("error", f"HTTP {response.status_code}"),
"status_code": response.status_code
}
except ValueError:
return {
"success": False,
"error": response.text.strip(),
"status_code": response.status_code
}
except requests.exceptions.ConnectionError:
if port != DEFAULT_GHIDRA_PORT:
return safe_put(DEFAULT_GHIDRA_PORT, endpoint, data)
return "Error: Failed to connect to Ghidra instance"
return {
"success": False,
"error": "Failed to connect to Ghidra instance",
"status_code": 503
}
except Exception as e:
return f"Request failed: {str(e)}"
return {
"success": False,
"error": str(e),
"exception": e.__class__.__name__
}
def safe_post(port: int, endpoint: str, data: dict | str) -> str:
"""Perform a POST request to a specific Ghidra instance"""
def safe_post(port: int, endpoint: str, data: dict | str) -> dict:
"""Perform a POST request to a specific Ghidra instance with JSON payload"""
try:
url = f"{get_instance_url(port)}/{endpoint}"
if isinstance(data, dict):
response = requests.post(url, data=data, timeout=5)
response = requests.post(
url,
json=data,
headers={'Content-Type': 'application/json'},
timeout=5
)
else:
response = requests.post(url, data=data.encode("utf-8"), timeout=5)
response.encoding = 'utf-8'
response = requests.post(
url,
data=data,
headers={'Content-Type': 'text/plain'},
timeout=5
)
if response.ok:
return response.text.strip()
elif response.status_code == 404 and port != DEFAULT_GHIDRA_PORT:
# Try falling back to default instance
return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
try:
return response.json()
except ValueError:
return {
"success": True,
"result": response.text.strip()
}
else:
return f"Error {response.status_code}: {response.text.strip()}"
# 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()
return {
"success": False,
"error": error_data.get("error", f"HTTP {response.status_code}"),
"status_code": response.status_code
}
except ValueError:
return {
"success": False,
"error": response.text.strip(),
"status_code": response.status_code
}
except requests.exceptions.ConnectionError:
if port != DEFAULT_GHIDRA_PORT:
return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
return "Error: Failed to connect to Ghidra instance"
return {
"success": False,
"error": "Failed to connect to Ghidra instance",
"status_code": 503
}
except Exception as e:
return f"Request failed: {str(e)}"
return {
"success": False,
"error": str(e),
"exception": e.__class__.__name__
}
# Instance management tools
@mcp.tool()

View File

@ -1100,18 +1100,59 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
}
/**
* Parse post body form params, e.g. oldName=foo&newName=bar
* Parse post body params from form data or simple JSON
*/
private Map<String, String> parsePostParams(HttpExchange exchange) throws IOException {
byte[] body = exchange.getRequestBody().readAllBytes();
String bodyStr = new String(body, StandardCharsets.UTF_8);
Map<String, String> params = new HashMap<>();
// Check if it looks like JSON
if (bodyStr.trim().startsWith("{")) {
try {
// Manual simple JSON parsing for key-value pairs
// This avoids using the JSONParser which might be causing issues
String jsonContent = bodyStr.trim();
// Remove the outer braces
jsonContent = jsonContent.substring(1, jsonContent.length() - 1).trim();
// Split by commas not inside quotes
String[] pairs = jsonContent.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)");
for (String pair : pairs) {
String[] keyValue = pair.split(":", 2);
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
// Remove quotes if present
if (key.startsWith("\"") && key.endsWith("\"")) {
key = key.substring(1, key.length() - 1);
}
if (value.startsWith("\"") && value.endsWith("\"")) {
value = value.substring(1, value.length() - 1);
}
params.put(key, value);
}
}
return params;
} catch (Exception e) {
Msg.error(this, "Failed to parse JSON request body: " + e.getMessage(), e);
// Fall through to form data parsing
}
}
// If JSON parsing fails or it's not JSON, try form data
for (String pair : bodyStr.split("&")) {
String[] kv = pair.split("=");
if (kv.length == 2) {
params.put(kv[0], kv[1]);
}
}
return params;
}
@ -1187,15 +1228,47 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
}
private void sendResponse(HttpExchange exchange, String response) throws IOException {
byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
private void sendResponse(HttpExchange exchange, Object response) throws IOException {
JSONObject json = new JSONObject();
json.put("success", true);
if (response instanceof String) {
json.put("result", response);
} else {
json.put("data", response);
}
json.put("timestamp", System.currentTimeMillis());
json.put("port", this.port);
if (this.isBaseInstance) {
json.put("instanceType", "base");
} else {
json.put("instanceType", "secondary");
}
sendJsonResponse(exchange, json);
}
private void sendJsonResponse(HttpExchange exchange, JSONObject jsonObj) throws IOException {
String json = jsonObj.toJSONString();
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()) {
os.write(bytes);
}
}
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);
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(statusCode, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}
private int findAvailablePort() {
int basePort = 8192;