WIP: Switch to JSON as data exchange format
This commit is contained in:
parent
89396469b2
commit
04d088591b
@ -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()
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user