diff --git a/CHANGELOG.md b/CHANGELOG.md index a6daa99..424bbe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/bridge_mcp_hydra.py b/bridge_mcp_hydra.py index 6a17a27..f30e828 100644 --- a/bridge_mcp_hydra.py +++ b/bridge_mcp_hydra.py @@ -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") diff --git a/pom.xml b/pom.xml index 033978c..1c0bbee 100644 --- a/pom.xml +++ b/pom.xml @@ -23,9 +23,9 @@ - com.googlecode.json-simple - json-simple - 1.1.1 + com.google.code.gson + gson + 2.10.1 diff --git a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java index dcfed31..5c2c19d 100644 --- a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java +++ b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java @@ -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 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 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()) {