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}" return f"http://{ghidra_host}:{port}"
def safe_get(port: int, endpoint: str, params: dict = None) -> list: def safe_get(port: int, endpoint: str, params: dict = None) -> dict:
"""Perform a GET request to a specific Ghidra instance""" """Perform a GET request to a specific Ghidra instance and return JSON response"""
if params is None: if params is None:
params = {} params = {}
url = f"{get_instance_url(port)}/{endpoint}" url = f"{get_instance_url(port)}/{endpoint}"
try: try:
response = requests.get(url, params=params, timeout=5) response = requests.get(
response.encoding = 'utf-8' url,
params=params,
headers={'Accept': 'application/json'},
timeout=5
)
if response.ok: if response.ok:
return response.text.splitlines() try:
elif response.status_code == 404: # Always expect JSON response
# Try falling back to default instance if this was a secondary instance json_data = response.json()
if port != DEFAULT_GHIDRA_PORT:
return safe_get(DEFAULT_GHIDRA_PORT, endpoint, params) # If the response has a 'result' field that's a string, extract it
return [f"Error {response.status_code}: {response.text.strip()}"] 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: 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: except requests.exceptions.ConnectionError:
# Instance may be down - try default instance if this was secondary # Instance may be down - try default instance if this was secondary
if port != DEFAULT_GHIDRA_PORT: if port != DEFAULT_GHIDRA_PORT:
return safe_get(DEFAULT_GHIDRA_PORT, endpoint, params) 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: 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: def safe_put(port: int, endpoint: str, data: dict) -> dict:
"""Perform a PUT request to a specific Ghidra instance""" """Perform a PUT request to a specific Ghidra instance with JSON payload"""
try: try:
url = f"{get_instance_url(port)}/{endpoint}" url = f"{get_instance_url(port)}/{endpoint}"
response = requests.put(url, data=data, timeout=5) response = requests.put(
response.encoding = 'utf-8' url,
json=data,
headers={'Content-Type': 'application/json'},
timeout=5
)
if response.ok: if response.ok:
return response.text.strip() try:
elif response.status_code == 404 and port != DEFAULT_GHIDRA_PORT: return response.json()
# Try falling back to default instance except ValueError:
return safe_put(DEFAULT_GHIDRA_PORT, endpoint, data) return {
"success": True,
"result": response.text.strip()
}
else: 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: except requests.exceptions.ConnectionError:
if port != DEFAULT_GHIDRA_PORT: if port != DEFAULT_GHIDRA_PORT:
return safe_put(DEFAULT_GHIDRA_PORT, endpoint, data) 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: 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: def safe_post(port: int, endpoint: str, data: dict | str) -> dict:
"""Perform a POST request to a specific Ghidra instance""" """Perform a POST request to a specific Ghidra instance with JSON payload"""
try: try:
url = f"{get_instance_url(port)}/{endpoint}" url = f"{get_instance_url(port)}/{endpoint}"
if isinstance(data, dict): 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: else:
response = requests.post(url, data=data.encode("utf-8"), timeout=5) response = requests.post(
response.encoding = 'utf-8' url,
data=data,
headers={'Content-Type': 'text/plain'},
timeout=5
)
if response.ok: if response.ok:
return response.text.strip() try:
elif response.status_code == 404 and port != DEFAULT_GHIDRA_PORT: return response.json()
# Try falling back to default instance except ValueError:
return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data) return {
"success": True,
"result": response.text.strip()
}
else: 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: except requests.exceptions.ConnectionError:
if port != DEFAULT_GHIDRA_PORT: if port != DEFAULT_GHIDRA_PORT:
return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data) 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: except Exception as e:
return f"Request failed: {str(e)}" return {
"success": False,
"error": str(e),
"exception": e.__class__.__name__
}
# Instance management tools # Instance management tools
@mcp.tool() @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 { private Map<String, String> parsePostParams(HttpExchange exchange) throws IOException {
byte[] body = exchange.getRequestBody().readAllBytes(); byte[] body = exchange.getRequestBody().readAllBytes();
String bodyStr = new String(body, StandardCharsets.UTF_8); String bodyStr = new String(body, StandardCharsets.UTF_8);
Map<String, String> params = new HashMap<>(); 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("&")) { for (String pair : bodyStr.split("&")) {
String[] kv = pair.split("="); String[] kv = pair.split("=");
if (kv.length == 2) { if (kv.length == 2) {
params.put(kv[0], kv[1]); params.put(kv[0], kv[1]);
} }
} }
return params; return params;
} }
@ -1187,15 +1228,47 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
} }
private void sendResponse(HttpExchange exchange, String response) throws IOException { private void sendResponse(HttpExchange exchange, Object response) throws IOException {
byte[] bytes = response.getBytes(StandardCharsets.UTF_8); JSONObject json = new JSONObject();
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8"); 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); exchange.sendResponseHeaders(200, bytes.length);
try (OutputStream os = exchange.getResponseBody()) { try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes); 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() { private int findAvailablePort() {
int basePort = 8192; int basePort = 8192;