Switch to JSON as bridge/plugin comm protocol

This commit is contained in:
Teal Bauer 2025-04-04 16:05:22 +02:00
parent 04d088591b
commit cbe5dcc1f3
4 changed files with 228 additions and 149 deletions

View File

@ -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

View File

@ -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,23 +26,29 @@ 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"""
with instances_lock:
if port in active_instances:
return active_instances[port]["url"]
# Auto-register if not found but port is valid
if 8192 <= port <= 65535:
register_instance(port)
if port in active_instances:
return active_instances[port]["url"]
return f"http://{ghidra_host}:{port}"
def safe_get(port: int, endpoint: str, params: dict = None) -> dict:
@ -53,21 +60,21 @@ def safe_get(port: int, endpoint: str, params: dict = None) -> dict:
try:
response = requests.get(
url,
url,
params=params,
headers={'Accept': 'application/json'},
timeout=5
)
if response.ok:
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,
@ -86,7 +93,7 @@ def safe_get(port: int, endpoint: str, params: dict = None) -> dict:
# 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 {
@ -130,7 +137,7 @@ def safe_put(port: int, endpoint: str, data: dict) -> dict:
headers={'Content-Type': 'application/json'},
timeout=5
)
if response.ok:
try:
return response.json()
@ -143,7 +150,7 @@ def safe_put(port: int, endpoint: str, data: dict) -> dict:
# 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 {
@ -176,7 +183,7 @@ 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,
@ -191,7 +198,7 @@ def safe_post(port: int, endpoint: str, data: dict | str) -> dict:
headers={'Content-Type': 'text/plain'},
timeout=5
)
if response.ok:
try:
return response.json()
@ -201,10 +208,10 @@ 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()
return {
@ -241,7 +248,7 @@ def list_instances() -> dict:
return {
"instances": [
{
"port": port,
"port": port,
"url": info["url"],
"project": info.get("project", ""),
"file": info.get("file", "")
@ -255,45 +262,39 @@ def register_instance(port: int, url: str = None) -> str:
"""Register a new Ghidra instance"""
if url is None:
url = f"http://{ghidra_host}:{port}"
# Verify instance is reachable before registering
try:
test_url = f"{url}/instances"
response = requests.get(test_url, timeout=2)
if not response.ok:
return f"Error: Instance at {url} is not responding properly"
# Try to get project info
project_info = {"url": url}
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
if "project" in root_data and root_data["project"]:
project_info["project"] = root_data["project"]
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)
if info_response.ok:
@ -302,7 +303,7 @@ def register_instance(port: int, url: str = None) -> str:
# Extract relevant information
if "project" in info_data and info_data["project"]:
project_info["project"] = info_data["project"]
# Handle file information
file_info = info_data.get("file", {})
if isinstance(file_info, dict) and file_info.get("name"):
@ -318,10 +319,10 @@ def register_instance(port: int, url: str = None) -> str:
except Exception:
# Non-critical, continue with registration even if project info fails
pass
with instances_lock:
active_instances[port] = project_info
return f"Registered instance on port {port} at {url}"
except Exception as e:
return f"Error: Could not connect to instance at {url}: {str(e)}"
@ -338,7 +339,7 @@ def unregister_instance(port: int) -> str:
@mcp.tool()
def discover_instances(host: str = None) -> dict:
"""Auto-discover Ghidra instances by scanning ports (quick discovery with limited range)
Args:
host: Optional host to scan (defaults to configured ghidra_host)
"""
@ -348,11 +349,11 @@ def _discover_instances(port_range, host=None, timeout=0.5) -> dict:
"""Internal function to discover Ghidra instances by scanning ports"""
found_instances = []
scan_host = host if host is not None else ghidra_host
for port in port_range:
if port in active_instances:
continue
url = f"http://{scan_host}:{port}"
try:
test_url = f"{url}/instances"
@ -363,19 +364,24 @@ def _discover_instances(port_range, host=None, timeout=0.5) -> dict:
except requests.exceptions.RequestException:
# Instance not available, just continue
continue
return {
"found": len(found_instances),
"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()
@ -401,12 +407,12 @@ def update_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: st
@mcp.tool()
def list_segments(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
"""List all memory segments in the current program with pagination
Args:
port: Ghidra instance port (default: 8192)
offset: Pagination offset (default: 0)
limit: Maximum number of segments to return (default: 100)
Returns:
List of segment information strings
"""
@ -415,12 +421,12 @@ def list_segments(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int =
@mcp.tool()
def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
"""List all imported symbols with pagination
Args:
port: Ghidra instance port (default: 8192)
offset: Pagination offset (default: 0)
limit: Maximum number of imports to return (default: 100)
Returns:
List of import information strings
"""
@ -429,12 +435,12 @@ def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int =
@mcp.tool()
def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
"""List all exported symbols with pagination
Args:
port: Ghidra instance port (default: 8192)
offset: Pagination offset (default: 0)
limit: Maximum number of exports to return (default: 100)
Returns:
List of export information strings
"""
@ -443,12 +449,12 @@ def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int =
@mcp.tool()
def list_namespaces(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
"""List all namespaces in the current program with pagination
Args:
port: Ghidra instance port (default: 8192)
offset: Pagination offset (default: 0)
limit: Maximum number of namespaces to return (default: 100)
Returns:
List of namespace information strings
"""
@ -457,12 +463,12 @@ def list_namespaces(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int
@mcp.tool()
def list_data_items(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
"""List all defined data items with pagination
Args:
port: Ghidra instance port (default: 8192)
offset: Pagination offset (default: 0)
limit: Maximum number of data items to return (default: 100)
Returns:
List of data item information strings
"""
@ -471,13 +477,13 @@ def list_data_items(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int
@mcp.tool()
def search_functions_by_name(port: int = DEFAULT_GHIDRA_PORT, query: str = "", offset: int = 0, limit: int = 100) -> list:
"""Search for functions by name with pagination
Args:
port: Ghidra instance port (default: 8192)
query: Search string to match against function names
offset: Pagination offset (default: 0)
limit: Maximum number of functions to return (default: 100)
Returns:
List of matching function information strings or error message if query is empty
"""
@ -488,11 +494,11 @@ def search_functions_by_name(port: int = DEFAULT_GHIDRA_PORT, query: str = "", o
@mcp.tool()
def get_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> str:
"""Get function details by its memory address
Args:
port: Ghidra instance port (default: 8192)
address: Memory address of the function (hex string)
Returns:
Multiline string with function details including name, address, and signature
"""
@ -501,10 +507,10 @@ def get_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "")
@mcp.tool()
def get_current_address(port: int = DEFAULT_GHIDRA_PORT) -> str:
"""Get the address currently selected in Ghidra's UI
Args:
port: Ghidra instance port (default: 8192)
Returns:
String containing the current memory address (hex format)
"""
@ -513,35 +519,23 @@ def get_current_address(port: int = DEFAULT_GHIDRA_PORT) -> str:
@mcp.tool()
def get_current_function(port: int = DEFAULT_GHIDRA_PORT) -> str:
"""Get the function currently selected in Ghidra's UI
Args:
port: Ghidra instance port (default: 8192)
Returns:
Multiline string with function details including name, address, and signature
"""
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
Args:
port: Ghidra instance port (default: 8192)
address: Memory address of the function (hex string)
Returns:
Multiline string containing the decompiled pseudocode
"""
@ -550,11 +544,11 @@ def decompile_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str
@mcp.tool()
def disassemble_function(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> list:
"""Get disassembly for a function at a specific address
Args:
port: Ghidra instance port (default: 8192)
address: Memory address of the function (hex string)
Returns:
List of strings showing assembly instructions with addresses and comments
"""
@ -563,12 +557,12 @@ def disassemble_function(port: int = DEFAULT_GHIDRA_PORT, address: str = "") ->
@mcp.tool()
def set_decompiler_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "", comment: str = "") -> str:
"""Add/edit a comment in the decompiler view at a specific address
Args:
port: Ghidra instance port (default: 8192)
address: Memory address to place comment (hex string)
comment: Text of the comment to add
Returns:
Confirmation message or error if failed
"""
@ -577,12 +571,12 @@ def set_decompiler_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "", c
@mcp.tool()
def set_disassembly_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "", comment: str = "") -> str:
"""Add/edit a comment in the disassembly view at a specific address
Args:
port: Ghidra instance port (default: 8192)
address: Memory address to place comment (hex string)
comment: Text of the comment to add
Returns:
Confirmation message or error if failed
"""
@ -591,13 +585,13 @@ def set_disassembly_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "",
@mcp.tool()
def rename_local_variable(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", old_name: str = "", new_name: str = "") -> str:
"""Rename a local variable within a function
Args:
port: Ghidra instance port (default: 8192)
function_address: Memory address of the function (hex string)
old_name: Current name of the variable
new_name: New name for the variable
Returns:
Confirmation message or error if failed
"""
@ -606,12 +600,12 @@ def rename_local_variable(port: int = DEFAULT_GHIDRA_PORT, function_address: str
@mcp.tool()
def rename_function_by_address(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", new_name: str = "") -> str:
"""Rename a function at a specific memory address
Args:
port: Ghidra instance port (default: 8192)
function_address: Memory address of the function (hex string)
new_name: New name for the function
Returns:
Confirmation message or error if failed
"""
@ -620,12 +614,12 @@ def rename_function_by_address(port: int = DEFAULT_GHIDRA_PORT, function_address
@mcp.tool()
def set_function_prototype(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", prototype: str = "") -> str:
"""Update a function's signature/prototype
Args:
port: Ghidra instance port (default: 8192)
function_address: Memory address of the function (hex string)
prototype: New function prototype string (e.g. "int func(int param1)")
Returns:
Confirmation message or error if failed
"""
@ -634,13 +628,13 @@ def set_function_prototype(port: int = DEFAULT_GHIDRA_PORT, function_address: st
@mcp.tool()
def set_local_variable_type(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", variable_name: str = "", new_type: str = "") -> str:
"""Change the data type of a local variable in a function
Args:
port: Ghidra instance port (default: 8192)
function_address: Memory address of the function (hex string)
variable_name: Name of the variable to modify
new_type: New data type for the variable (e.g. "int", "char*")
Returns:
Confirmation message or error if failed
"""
@ -659,7 +653,7 @@ def list_function_variables(port: int = DEFAULT_GHIDRA_PORT, function: str = "")
"""List variables in a specific function"""
if not function:
return "Error: function name is required"
encoded_name = quote(function)
return safe_get(port, f"functions/{encoded_name}/variables", {})
@ -668,7 +662,7 @@ def rename_variable(port: int = DEFAULT_GHIDRA_PORT, function: str = "", name: s
"""Rename a variable in a function"""
if not function or not name or not new_name:
return "Error: function, name, and new_name parameters are required"
encoded_function = quote(function)
encoded_var = quote(name)
return safe_put(port, f"functions/{encoded_function}/variables/{encoded_var}", {"newName": new_name})
@ -678,7 +672,7 @@ def retype_variable(port: int = DEFAULT_GHIDRA_PORT, function: str = "", name: s
"""Change the data type of a variable in a function"""
if not function or not name or not data_type:
return "Error: function, name, and data_type parameters are required"
encoded_function = quote(function)
encoded_var = quote(name)
return safe_put(port, f"functions/{encoded_function}/variables/{encoded_var}", {"dataType": data_type})
@ -692,7 +686,7 @@ def periodic_discovery():
try:
# Use the full discovery range
_discover_instances(FULL_DISCOVERY_RANGE, timeout=0.5)
# Also check if any existing instances are down
with instances_lock:
ports_to_remove = []
@ -704,31 +698,31 @@ def periodic_discovery():
ports_to_remove.append(port)
except requests.exceptions.RequestException:
ports_to_remove.append(port)
# Remove any instances that are down
for port in ports_to_remove:
del active_instances[port]
print(f"Removed unreachable instance on port {port}")
except Exception as e:
print(f"Error in periodic discovery: {e}")
# Sleep for 30 seconds before next scan
time.sleep(30)
if __name__ == "__main__":
# Auto-register default instance
register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}")
# 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()
signal.signal(signal.SIGINT, handle_sigint)
mcp.run()
# # Auto-register default instance
# register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}")
# # 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()
# signal.signal(signal.SIGINT, handle_sigint)
mcp.run(transport="stdio")

View File

@ -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 -->

View File

@ -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;
@ -95,10 +96,12 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
Msg.info(this, "Starting as base instance on port " + port);
}
}
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);
// 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 {
startServer();
@ -201,15 +204,52 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
}
});
// Class resources
// Class resources with detailed logging
server.createContext("/classes", exchange -> {
if ("GET".equals(exchange.getRequestMethod())) {
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));
} else {
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
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);
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 -> {
// 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();
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 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);
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()) {