From 9879e71e887fd3a3876397d4d1fe37b066fa5308 Mon Sep 17 00:00:00 2001 From: Teal Bauer Date: Thu, 10 Apr 2025 14:42:53 +0200 Subject: [PATCH] WIP big refactor --- CHANGELOG.md | 14 + bridge_mcp_hydra.py | 393 ++- .../eu/starsong/ghidra/GhydraMCPPlugin.java | 3012 ++--------------- .../eu/starsong/ghidra/api/ApiConstants.java | 8 + .../ghidra/api/GhidraJsonEndpoint.java | 8 + .../starsong/ghidra/api/ResponseBuilder.java | 73 + .../ghidra/endpoints/AbstractEndpoint.java | 93 + .../ghidra/endpoints/ClassEndpoints.java | 93 + .../ghidra/endpoints/DataEndpoints.java | 192 ++ .../ghidra/endpoints/FunctionEndpoints.java | 93 + .../ghidra/endpoints/InstanceEndpoints.java | 104 + .../ghidra/endpoints/NamespaceEndpoints.java | 93 + .../ghidra/endpoints/SegmentEndpoints.java | 90 + .../ghidra/endpoints/SymbolEndpoints.java | 142 + .../ghidra/endpoints/VariableEndpoints.java | 265 ++ .../starsong/ghidra/model/FunctionInfo.java | 395 +++ .../starsong/ghidra/model/JsonResponse.java | 175 + .../eu/starsong/ghidra/model/ProgramInfo.java | 218 ++ .../starsong/ghidra/model/VariableInfo.java | 226 ++ .../starsong/ghidra/util/GhidraSupplier.java | 6 + .../eu/starsong/ghidra/util/GhidraUtil.java | 287 ++ .../eu/starsong/ghidra/util/HttpUtil.java | 123 + .../ghidra/util/TransactionHelper.java | 59 + test_http_api.py | 31 +- 24 files changed, 3284 insertions(+), 2909 deletions(-) create mode 100644 src/main/java/eu/starsong/ghidra/api/ApiConstants.java create mode 100644 src/main/java/eu/starsong/ghidra/api/GhidraJsonEndpoint.java create mode 100644 src/main/java/eu/starsong/ghidra/api/ResponseBuilder.java create mode 100644 src/main/java/eu/starsong/ghidra/endpoints/AbstractEndpoint.java create mode 100644 src/main/java/eu/starsong/ghidra/endpoints/ClassEndpoints.java create mode 100644 src/main/java/eu/starsong/ghidra/endpoints/DataEndpoints.java create mode 100644 src/main/java/eu/starsong/ghidra/endpoints/FunctionEndpoints.java create mode 100644 src/main/java/eu/starsong/ghidra/endpoints/InstanceEndpoints.java create mode 100644 src/main/java/eu/starsong/ghidra/endpoints/NamespaceEndpoints.java create mode 100644 src/main/java/eu/starsong/ghidra/endpoints/SegmentEndpoints.java create mode 100644 src/main/java/eu/starsong/ghidra/endpoints/SymbolEndpoints.java create mode 100644 src/main/java/eu/starsong/ghidra/endpoints/VariableEndpoints.java create mode 100644 src/main/java/eu/starsong/ghidra/model/FunctionInfo.java create mode 100644 src/main/java/eu/starsong/ghidra/model/JsonResponse.java create mode 100644 src/main/java/eu/starsong/ghidra/model/ProgramInfo.java create mode 100644 src/main/java/eu/starsong/ghidra/model/VariableInfo.java create mode 100644 src/main/java/eu/starsong/ghidra/util/GhidraSupplier.java create mode 100644 src/main/java/eu/starsong/ghidra/util/GhidraUtil.java create mode 100644 src/main/java/eu/starsong/ghidra/util/HttpUtil.java create mode 100644 src/main/java/eu/starsong/ghidra/util/TransactionHelper.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dec2ad..afa109e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,12 +17,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Multiple simplification styles - Comprehensive API documentation (GHIDRA_HTTP_API.md, MCP_BRIDGE_API.md) - Standardized JSON response formats +- Implemented `/plugin-version` endpoint for version checking +- Added proper error handling for when no program is loaded ### Changed - Unified all endpoints to use structured JSON - Improved error handling and response metadata - Simplified bridge code and added type hints - Updated port discovery to use DEFAULT_GHIDRA_PORT +- Refactored Java plugin into modular architecture: + - Separated concerns into api, endpoints, util, and model packages + - Created standardized response builders and error handlers + - Implemented transaction management helpers + - Added model classes for structured data representation +- Removed `port` field from responses (bridge knows what instance it called) + +### Fixed +- Fixed endpoint registration in refactored code (all endpoints now working) +- Improved handling of program-dependent endpoints when no program is loaded +- Enhanced root endpoint to dynamically include links to available endpoints +- Added proper HATEOAS links to all endpoints ## [1.4.0] - 2025-04-08 diff --git a/bridge_mcp_hydra.py b/bridge_mcp_hydra.py index 0789330..66171e9 100644 --- a/bridge_mcp_hydra.py +++ b/bridge_mcp_hydra.py @@ -19,7 +19,8 @@ import requests from mcp.server.fastmcp import FastMCP # Allowed origins for CORS/CSRF protection -ALLOWED_ORIGINS = os.environ.get("GHIDRA_ALLOWED_ORIGINS", "http://localhost").split(",") +ALLOWED_ORIGINS = os.environ.get( + "GHIDRA_ALLOWED_ORIGINS", "http://localhost").split(",") # Track active Ghidra instances (port -> info dict) active_instances: Dict[int, dict] = {} @@ -40,6 +41,7 @@ mcp = FastMCP("GhydraMCP", instructions=instructions) ghidra_host = os.environ.get("GHIDRA_HYDRA_HOST", DEFAULT_GHIDRA_HOST) + def get_instance_url(port: int) -> str: """Get URL for a Ghidra instance by port""" with instances_lock: @@ -53,12 +55,14 @@ def get_instance_url(port: int) -> str: return f"http://{ghidra_host}:{port}" + def validate_origin(headers: dict) -> bool: """Validate request origin against allowed origins""" origin = headers.get("Origin") if not origin: - return True # No origin header - allow (browser same-origin policy applies) - + # No origin header - allow (browser same-origin policy applies) + return True + # Parse origin to get scheme+hostname try: parsed = urlparse(origin) @@ -67,9 +71,10 @@ def validate_origin(headers: dict) -> bool: origin_base += f":{parsed.port}" except: return False - + return origin_base in ALLOWED_ORIGINS + def _make_request(method: str, port: int, endpoint: str, params: dict = None, json_data: dict = None, data: str = None, headers: dict = None) -> dict: """Internal helper to make HTTP requests and handle common errors.""" url = f"{get_instance_url(port)}/{endpoint}" @@ -79,7 +84,8 @@ def _make_request(method: str, port: int, endpoint: str, params: dict = None, js is_state_changing = method.upper() in ["POST", "PUT", "PATCH", "DELETE"] if is_state_changing: - check_headers = json_data.get("headers", {}) if isinstance(json_data, dict) else (headers or {}) + check_headers = json_data.get("headers", {}) if isinstance( + json_data, dict) else (headers or {}) if not validate_origin(check_headers): return { "success": False, @@ -88,9 +94,9 @@ def _make_request(method: str, port: int, endpoint: str, params: dict = None, js "timestamp": int(time.time() * 1000) } if json_data is not None: - request_headers['Content-Type'] = 'application/json' + request_headers['Content-Type'] = 'application/json' elif data is not None: - request_headers['Content-Type'] = 'text/plain' + request_headers['Content-Type'] = 'text/plain' try: response = requests.request( @@ -106,25 +112,25 @@ def _make_request(method: str, port: int, endpoint: str, params: dict = None, js try: parsed_json = response.json() if isinstance(parsed_json, dict) and "timestamp" not in parsed_json: - parsed_json["timestamp"] = int(time.time() * 1000) + parsed_json["timestamp"] = int(time.time() * 1000) return parsed_json except ValueError: if response.ok: - return { - "success": False, - "error": "Received non-JSON success response from Ghidra plugin", - "status_code": response.status_code, - "response_text": response.text[:500], - "timestamp": int(time.time() * 1000) - } + return { + "success": False, + "error": "Received non-JSON success response from Ghidra plugin", + "status_code": response.status_code, + "response_text": response.text[:500], + "timestamp": int(time.time() * 1000) + } else: - return { - "success": False, - "error": f"HTTP {response.status_code} - Non-JSON error response", - "status_code": response.status_code, - "response_text": response.text[:500], - "timestamp": int(time.time() * 1000) - } + return { + "success": False, + "error": f"HTTP {response.status_code} - Non-JSON error response", + "status_code": response.status_code, + "response_text": response.text[:500], + "timestamp": int(time.time() * 1000) + } except requests.exceptions.Timeout: return { @@ -148,15 +154,18 @@ def _make_request(method: str, port: int, endpoint: str, params: dict = None, js "timestamp": int(time.time() * 1000) } + def safe_get(port: int, endpoint: str, params: dict = None) -> dict: """Make GET request to Ghidra instance""" return _make_request("GET", port, endpoint, params=params) + def safe_put(port: int, endpoint: str, data: dict) -> dict: """Make PUT request to Ghidra instance with JSON payload""" headers = data.pop("headers", None) if isinstance(data, dict) else None return _make_request("PUT", port, endpoint, json_data=data, headers=headers) + def safe_post(port: int, endpoint: str, data: dict | str) -> dict: """Perform a POST request to a specific Ghidra instance with JSON or text payload""" headers = None @@ -172,10 +181,12 @@ def safe_post(port: int, endpoint: str, data: dict | str) -> dict: return _make_request("POST", port, endpoint, json_data=json_payload, data=text_payload, headers=headers) # Instance management tools + + @mcp.tool() def list_instances() -> dict: """List all active Ghidra instances - + Returns: dict: Contains 'instances' list with port, url, project and file info for each instance """ @@ -192,14 +203,15 @@ def list_instances() -> dict: ] } + @mcp.tool() def register_instance(port: int, url: str = None) -> str: """Register a new Ghidra instance - + Args: port: Port number of the Ghidra instance url: Optional URL if different from default http://host:port - + Returns: str: Confirmation message or error """ @@ -216,7 +228,8 @@ def register_instance(port: int, url: str = None) -> str: try: root_url = f"{url}/" - root_response = requests.get(root_url, timeout=1.5) # Short timeout for root + root_response = requests.get( + root_url, timeout=1.5) # Short timeout for root if root_response.ok: try: @@ -243,15 +256,22 @@ def register_instance(port: int, url: str = None) -> str: file_info = info_data.get("file", {}) if isinstance(file_info, dict) and file_info.get("name"): - project_info["file"] = file_info.get("name", "") - project_info["path"] = file_info.get("path", "") - project_info["architecture"] = file_info.get("architecture", "") - project_info["endian"] = file_info.get("endian", "") - print(f"Info data parsed: {project_info}", file=sys.stderr) + project_info["file"] = file_info.get( + "name", "") + project_info["path"] = file_info.get( + "path", "") + project_info["architecture"] = file_info.get( + "architecture", "") + project_info["endian"] = file_info.get( + "endian", "") + print( + f"Info data parsed: {project_info}", file=sys.stderr) except Exception as e: - print(f"Error parsing info endpoint: {e}", file=sys.stderr) + print( + f"Error parsing info endpoint: {e}", file=sys.stderr) except Exception as e: - print(f"Error connecting to info endpoint: {e}", file=sys.stderr) + print( + f"Error connecting to info endpoint: {e}", file=sys.stderr) except Exception: # Non-critical, continue with registration even if project info fails pass @@ -263,13 +283,14 @@ def register_instance(port: int, url: str = None) -> str: except Exception as e: return f"Error: Could not connect to instance at {url}: {str(e)}" + @mcp.tool() def unregister_instance(port: int) -> str: """Unregister a Ghidra instance - + Args: port: Port number of the instance to unregister - + Returns: str: Confirmation message or error """ @@ -279,18 +300,20 @@ def unregister_instance(port: int) -> str: return f"Unregistered instance on port {port}" return f"No instance found on port {port}" + @mcp.tool() def discover_instances(host: str = null) -> dict: """Discover available Ghidra instances by scanning ports - + Args: host: Optional host to scan (default: configured ghidra_host) - + Returns: dict: Contains 'found' count and 'instances' list with discovery results """ return _discover_instances(QUICK_DISCOVERY_RANGE, host=host, timeout=0.5) + def _discover_instances(port_range, host=None, timeout=0.5) -> dict: """Internal function to discover Ghidra instances by scanning ports""" found_instances = [] @@ -306,7 +329,8 @@ def _discover_instances(port_range, host=None, timeout=0.5) -> dict: response = requests.get(test_url, timeout=timeout) if response.ok: result = register_instance(port, url) - found_instances.append({"port": port, "url": url, "result": result}) + found_instances.append( + {"port": port, "url": url, "result": result}) except requests.exceptions.RequestException: # Instance not available, just continue continue @@ -316,16 +340,17 @@ def _discover_instances(port_range, host=None, timeout=0.5) -> dict: "instances": found_instances } + @mcp.tool() -def list_functions(port: int = DEFAULT_GHIDRA_PORT, - offset: int = 0, - limit: int = 100, - addr: str = None, - name: str = None, - name_contains: str = None, - name_matches_regex: str = None) -> dict: +def list_functions(port: int = DEFAULT_GHIDRA_PORT, + offset: int = 0, + limit: int = 100, + addr: str = None, + name: str = None, + name_contains: str = None, + name_matches_regex: str = None) -> dict: """List functions in the current program with filtering and pagination - + Args: port: Ghidra instance port (default: 8192) offset: Pagination offset (default: 0) @@ -334,7 +359,7 @@ def list_functions(port: int = DEFAULT_GHIDRA_PORT, name: Exact name match filter (case-sensitive) name_contains: Substring name filter (case-insensitive) name_matches_regex: Regex name filter - + Returns: dict: { "result": list of function info objects, @@ -353,14 +378,14 @@ def list_functions(port: int = DEFAULT_GHIDRA_PORT, if name: params["name"] = name if name_contains: - params["name_contains"] = name_contains + params["name_contains"] = name_contains if name_matches_regex: params["name_matches_regex"] = name_matches_regex - + response = safe_get(port, "programs/current/functions", params) if isinstance(response, dict) and "error" in response: return response - + # Transform to expected format if needed return { "result": response.get("result", []), @@ -370,31 +395,33 @@ def list_functions(port: int = DEFAULT_GHIDRA_PORT, "_links": response.get("_links", {}) } + @mcp.tool() def list_classes(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: """List classes in the current program with pagination - + Args: port: Ghidra instance port (default: 8192) offset: Pagination offset (default: 0) limit: Maximum items to return (default: 100) - + Returns: list: Class names and info """ return safe_get(port, "classes", {"offset": offset, "limit": limit}) + @mcp.tool() def get_function(port: int = DEFAULT_GHIDRA_PORT, name: str = "", cCode: bool = True, syntaxTree: bool = False, simplificationStyle: str = "normalize") -> dict: """Get decompiled code for a function - + Args: port: Ghidra instance port (default: 8192) name: Function name to decompile cCode: Return C-style code (default: True) syntaxTree: Include syntax tree (default: False) simplificationStyle: Decompiler style (default: "normalize") - + Returns: dict: Contains function name, address, signature and decompilation """ @@ -404,47 +431,50 @@ def get_function(port: int = DEFAULT_GHIDRA_PORT, name: str = "", cCode: bool = "simplificationStyle": simplificationStyle }) + @mcp.tool() def update_function(port: int = DEFAULT_GHIDRA_PORT, name: str = "", new_name: str = "") -> str: """Rename a function - + Args: port: Ghidra instance port (default: 8192) name: Current function name new_name: New function name - + Returns: str: Confirmation message or error """ return safe_post(port, f"functions/{quote(name)}", {"newName": new_name}) + @mcp.tool() def update_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: str = "") -> str: """Rename data at a memory address - + Args: port: Ghidra instance port (default: 8192) address: Memory address in hex format new_name: New name for the data - + Returns: str: Confirmation message or error """ return safe_post(port, "data", {"address": address, "newName": new_name}) + @mcp.tool() def list_segments(port: int = DEFAULT_GHIDRA_PORT, - offset: int = 0, - limit: int = 100, - name: str = None) -> dict: + offset: int = 0, + limit: int = 100, + name: str = None) -> dict: """List memory segments with filtering and pagination - + Args: port: Ghidra instance port (default: 8192) offset: Pagination offset (default: 0) limit: Maximum items to return (default: 100) name: Filter by segment name (case-sensitive substring match) - + Returns: dict: { "result": list of segment objects, @@ -460,11 +490,11 @@ def list_segments(port: int = DEFAULT_GHIDRA_PORT, } if name: params["name"] = name - + response = safe_get(port, "programs/current/segments", params) if isinstance(response, dict) and "error" in response: return response - + return { "result": response.get("result", []), "size": response.get("size", len(response.get("result", []))), @@ -473,16 +503,17 @@ def list_segments(port: int = DEFAULT_GHIDRA_PORT, "_links": response.get("_links", {}) } + @mcp.tool() def list_symbols(port: int = DEFAULT_GHIDRA_PORT, - offset: int = 0, - limit: int = 100, - addr: str = None, - name: str = None, - name_contains: str = None, - type: str = None) -> dict: + offset: int = 0, + limit: int = 100, + addr: str = None, + name: str = None, + name_contains: str = None, + type: str = None) -> dict: """List symbols with filtering and pagination - + Args: port: Ghidra instance port (default: 8192) offset: Pagination offset (default: 0) @@ -491,7 +522,7 @@ def list_symbols(port: int = DEFAULT_GHIDRA_PORT, name: Exact name match filter (case-sensitive) name_contains: Substring name filter (case-insensitive) type: Filter by symbol type (e.g. "function", "data", "label") - + Returns: dict: { "result": list of symbol objects, @@ -513,11 +544,11 @@ def list_symbols(port: int = DEFAULT_GHIDRA_PORT, params["name_contains"] = name_contains if type: params["type"] = type - + response = safe_get(port, "programs/current/symbols", params) if isinstance(response, dict) and "error" in response: return response - + return { "result": response.get("result", []), "size": response.get("size", len(response.get("result", []))), @@ -526,15 +557,16 @@ def list_symbols(port: int = DEFAULT_GHIDRA_PORT, "_links": response.get("_links", {}) } + @mcp.tool() def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> dict: """List imported symbols with pagination - + Args: port: Ghidra instance port (default: 8192) offset: Pagination offset (default: 0) limit: Maximum items to return (default: 100) - + Returns: dict: { "result": list of imported symbols, @@ -544,10 +576,11 @@ def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = "_links": pagination links } """ - response = safe_get(port, "programs/current/symbols/imports", {"offset": offset, "limit": limit}) + response = safe_get(port, "programs/current/symbols/imports", + {"offset": offset, "limit": limit}) if isinstance(response, dict) and "error" in response: return response - + return { "result": response.get("result", []), "size": response.get("size", len(response.get("result", []))), @@ -556,15 +589,16 @@ def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = "_links": response.get("_links", {}) } + @mcp.tool() def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> dict: """List exported symbols with pagination - + Args: port: Ghidra instance port (default: 8192) offset: Pagination offset (default: 0) limit: Maximum items to return (default: 100) - + Returns: dict: { "result": list of exported symbols, @@ -574,10 +608,11 @@ def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = "_links": pagination links } """ - response = safe_get(port, "programs/current/symbols/exports", {"offset": offset, "limit": limit}) + response = safe_get(port, "programs/current/symbols/exports", + {"offset": offset, "limit": limit}) if isinstance(response, dict) and "error" in response: return response - + return { "result": response.get("result", []), "size": response.get("size", len(response.get("result", []))), @@ -586,30 +621,32 @@ def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = "_links": response.get("_links", {}) } + @mcp.tool() def list_namespaces(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: """List namespaces with pagination - + Args: port: Ghidra instance port (default: 8192) offset: Pagination offset (default: 0) limit: Maximum items to return (default: 100) - + Returns: list: Namespace information strings """ return safe_get(port, "namespaces", {"offset": offset, "limit": limit}) + @mcp.tool() -def list_data_items(port: int = DEFAULT_GHIDRA_PORT, - offset: int = 0, - limit: int = 100, - addr: str = None, - name: str = None, - name_contains: str = None, - type: str = None) -> dict: +def list_data_items(port: int = DEFAULT_GHIDRA_PORT, + offset: int = 0, + limit: int = 100, + addr: str = None, + name: str = None, + name_contains: str = None, + type: str = None) -> dict: """List defined data items with filtering and pagination - + Args: port: Ghidra instance port (default: 8192) offset: Pagination offset (default: 0) @@ -618,7 +655,7 @@ def list_data_items(port: int = DEFAULT_GHIDRA_PORT, name: Exact name match filter (case-sensitive) name_contains: Substring name filter (case-insensitive) type: Filter by data type (e.g. "string", "dword") - + Returns: dict: { "result": list of data item objects, @@ -640,11 +677,11 @@ def list_data_items(port: int = DEFAULT_GHIDRA_PORT, params["name_contains"] = name_contains if type: params["type"] = type - + response = safe_get(port, "programs/current/data", params) if isinstance(response, dict) and "error" in response: return response - + return { "result": response.get("result", []), "size": response.get("size", len(response.get("result", []))), @@ -653,16 +690,17 @@ def list_data_items(port: int = DEFAULT_GHIDRA_PORT, "_links": response.get("_links", {}) } + @mcp.tool() def search_functions_by_name(port: int = DEFAULT_GHIDRA_PORT, query: str = "", offset: int = 0, limit: int = 100) -> list: """Search functions by name with pagination - + Args: port: Ghidra instance port (default: 8192) query: Search string for function names offset: Pagination offset (default: 0) limit: Maximum items to return (default: 100) - + Returns: list: Matching function info or error if query empty """ @@ -670,19 +708,20 @@ def search_functions_by_name(port: int = DEFAULT_GHIDRA_PORT, query: str = "", o return ["Error: query string is required"] return safe_get(port, "functions", {"query": query, "offset": offset, "limit": limit}) + @mcp.tool() -def read_memory(port: int = DEFAULT_GHIDRA_PORT, - address: str = "", - length: int = 16, - format: str = "hex") -> dict: +def read_memory(port: int = DEFAULT_GHIDRA_PORT, + address: str = "", + length: int = 16, + format: str = "hex") -> dict: """Read bytes from memory - + Args: port: Ghidra instance port (default: 8192) address: Memory address in hex format length: Number of bytes to read (default: 16) format: Output format - "hex", "base64", or "string" (default: "hex") - + Returns: dict: { "address": original address, @@ -698,16 +737,16 @@ def read_memory(port: int = DEFAULT_GHIDRA_PORT, "error": "Address parameter is required", "timestamp": int(time.time() * 1000) } - + response = safe_get(port, "programs/current/memory", { "address": address, "length": length, "format": format }) - + if isinstance(response, dict) and "error" in response: return response - + return { "address": address, "length": length, @@ -716,19 +755,20 @@ def read_memory(port: int = DEFAULT_GHIDRA_PORT, "timestamp": response.get("timestamp", int(time.time() * 1000)) } + @mcp.tool() def write_memory(port: int = DEFAULT_GHIDRA_PORT, - address: str = "", - bytes: str = "", - format: str = "hex") -> dict: + address: str = "", + bytes: str = "", + format: str = "hex") -> dict: """Write bytes to memory (use with caution) - + Args: port: Ghidra instance port (default: 8192) address: Memory address in hex format bytes: Data to write (format depends on 'format' parameter) format: Input format - "hex", "base64", or "string" (default: "hex") - + Returns: dict: Operation result with success status """ @@ -738,26 +778,28 @@ def write_memory(port: int = DEFAULT_GHIDRA_PORT, "error": "Address and bytes parameters are required", "timestamp": int(time.time() * 1000) } - + return safe_post(port, "programs/current/memory", { "address": address, "bytes": bytes, "format": format }) + @mcp.tool() def get_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> dict: """Get function details by memory address - + Args: port: Ghidra instance port (default: 8192) address: Memory address in hex format - + Returns: dict: Contains function name, address, signature and decompilation """ return safe_get(port, "get_function_by_address", {"address": address}) + @mcp.tool() def get_current_address(port: int = DEFAULT_GHIDRA_PORT) -> dict: """Get the address currently selected in Ghidra's UI @@ -782,15 +824,16 @@ def get_current_address(port: int = DEFAULT_GHIDRA_PORT) -> dict: "port": port } + @mcp.tool() def list_xrefs(port: int = DEFAULT_GHIDRA_PORT, - to_addr: str = None, - from_addr: str = None, - type: str = None, - offset: int = 0, - limit: int = 100) -> dict: + to_addr: str = None, + from_addr: str = None, + type: str = None, + offset: int = 0, + limit: int = 100) -> dict: """List cross-references with filtering and pagination - + Args: port: Ghidra instance port (default: 8192) to_addr: Filter references to this address (hexadecimal) @@ -798,7 +841,7 @@ def list_xrefs(port: int = DEFAULT_GHIDRA_PORT, type: Filter by reference type (e.g. "CALL", "READ", "WRITE") offset: Pagination offset (default: 0) limit: Maximum items to return (default: 100) - + Returns: dict: { "result": list of xref objects, @@ -818,11 +861,11 @@ def list_xrefs(port: int = DEFAULT_GHIDRA_PORT, params["from_addr"] = from_addr if type: params["type"] = type - + response = safe_get(port, "programs/current/xrefs", params) if isinstance(response, dict) and "error" in response: return response - + return { "result": response.get("result", []), "size": response.get("size", len(response.get("result", []))), @@ -831,55 +874,58 @@ def list_xrefs(port: int = DEFAULT_GHIDRA_PORT, "_links": response.get("_links", {}) } + @mcp.tool() -def analyze_program(port: int = DEFAULT_GHIDRA_PORT, - analysis_options: dict = None) -> dict: +def analyze_program(port: int = DEFAULT_GHIDRA_PORT, + analysis_options: dict = None) -> dict: """Run analysis on the current program - + Args: port: Ghidra instance port (default: 8192) analysis_options: Dictionary of analysis options to enable/disable (e.g. {"functionRecovery": True, "dataRefs": False}) None means use default analysis options - + Returns: dict: Analysis operation result with status """ return safe_post(port, "programs/current/analysis", analysis_options or {}) + @mcp.tool() def get_callgraph(port: int = DEFAULT_GHIDRA_PORT, - function: str = None, - max_depth: int = 3) -> dict: + function: str = None, + max_depth: int = 3) -> dict: """Get function call graph visualization data - + Args: port: Ghidra instance port (default: 8192) function: Starting function name (None starts from entry point) max_depth: Maximum call depth to analyze (default: 3) - + Returns: dict: Graph data in DOT format with nodes and edges """ params = {"max_depth": max_depth} if function: params["function"] = function - + return safe_get(port, "programs/current/analysis/callgraph", params) + @mcp.tool() def get_dataflow(port: int = DEFAULT_GHIDRA_PORT, - address: str = "", - direction: str = "forward", - max_steps: int = 50) -> dict: + address: str = "", + direction: str = "forward", + max_steps: int = 50) -> dict: """Perform data flow analysis from an address - + Args: port: Ghidra instance port (default: 8192) address: Starting address in hex format direction: "forward" or "backward" (default: "forward") max_steps: Maximum analysis steps (default: 50) - + Returns: dict: Data flow analysis results """ @@ -889,13 +935,14 @@ def get_dataflow(port: int = DEFAULT_GHIDRA_PORT, "max_steps": max_steps }) + @mcp.tool() def get_current_function(port: int = DEFAULT_GHIDRA_PORT) -> dict: """Get the function currently selected in Ghidra's UI Args: port: Ghidra instance port (default: 8192) - + Returns: Dict containing: - success: boolean indicating success @@ -913,17 +960,18 @@ def get_current_function(port: int = DEFAULT_GHIDRA_PORT) -> dict: "port": port } + @mcp.tool() def decompile_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "", cCode: bool = True, syntaxTree: bool = False, simplificationStyle: str = "normalize") -> dict: """Decompile function at memory address - + Args: port: Ghidra instance port (default: 8192) address: Memory address in hex format cCode: Return C-style code (default: True) syntaxTree: Include syntax tree (default: False) simplificationStyle: Decompiler style (default: "normalize") - + Returns: dict: Contains decompiled code in 'result.decompilation' """ @@ -934,132 +982,141 @@ def decompile_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str "simplificationStyle": simplificationStyle }) + @mcp.tool() def disassemble_function(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> dict: """Get disassembly for function at address - + Args: port: Ghidra instance port (default: 8192) address: Memory address in hex format - + Returns: dict: Contains assembly instructions with addresses and comments """ return safe_get(port, "disassemble_function", {"address": address}) + @mcp.tool() def set_decompiler_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "", comment: str = "") -> str: """Add/edit decompiler comment at address - + Args: port: Ghidra instance port (default: 8192) address: Memory address in hex format comment: Comment text to add - + Returns: str: Confirmation message or error """ return safe_post(port, "set_decompiler_comment", {"address": address, "comment": comment}) + @mcp.tool() def set_disassembly_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "", comment: str = "") -> str: """Add/edit disassembly comment at address - + Args: port: Ghidra instance port (default: 8192) address: Memory address in hex format comment: Comment text to add - + Returns: str: Confirmation message or error """ return safe_post(port, "set_disassembly_comment", {"address": address, "comment": comment}) + @mcp.tool() def rename_local_variable(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", old_name: str = "", new_name: str = "") -> str: """Rename local variable in function - + Args: port: Ghidra instance port (default: 8192) function_address: Function memory address in hex old_name: Current variable name new_name: New variable name - + Returns: str: Confirmation message or error """ return safe_post(port, "rename_local_variable", {"functionAddress": function_address, "oldName": old_name, "newName": new_name}) + @mcp.tool() def rename_function_by_address(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", new_name: str = "") -> str: """Rename function at memory address - + Args: port: Ghidra instance port (default: 8192) function_address: Function memory address in hex new_name: New function name - + Returns: str: Confirmation message or error """ return safe_post(port, "rename_function_by_address", {"functionAddress": function_address, "newName": new_name}) + @mcp.tool() def set_function_prototype(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", prototype: str = "") -> str: """Update function signature/prototype - + Args: port: Ghidra instance port (default: 8192) function_address: Function memory address in hex prototype: New prototype string (e.g. "int func(int param1)") - + Returns: str: Confirmation message or error """ return safe_post(port, "set_function_prototype", {"functionAddress": function_address, "prototype": prototype}) + @mcp.tool() def set_local_variable_type(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", variable_name: str = "", new_type: str = "") -> str: """Change local variable data type - + Args: port: Ghidra instance port (default: 8192) function_address: Function memory address in hex variable_name: Variable name to modify new_type: New data type (e.g. "int", "char*") - + Returns: str: Confirmation message or error """ return safe_post(port, "set_local_variable_type", {"functionAddress": function_address, "variableName": variable_name, "newType": new_type}) + @mcp.tool() def list_variables(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100, search: str = "") -> dict: """List global variables with optional search - + Args: port: Ghidra instance port (default: 8192) offset: Pagination offset (default: 0) limit: Maximum items to return (default: 100) search: Optional filter for variable names - + Returns: dict: Contains variables list in 'result' field """ params = {"offset": offset, "limit": limit} if search: params["search"] = search - + return safe_get(port, "variables", params) + @mcp.tool() def list_function_variables(port: int = DEFAULT_GHIDRA_PORT, function: str = "") -> dict: """List variables in function - + Args: port: Ghidra instance port (default: 8192) function: Function name to list variables for - + Returns: dict: Contains variables list in 'result.variables' """ @@ -1069,16 +1126,17 @@ def list_function_variables(port: int = DEFAULT_GHIDRA_PORT, function: str = "") encoded_name = quote(function) return safe_get(port, f"functions/{encoded_name}/variables", {}) + @mcp.tool() def rename_variable(port: int = DEFAULT_GHIDRA_PORT, function: str = "", name: str = "", new_name: str = "") -> dict: """Rename variable in function - + Args: port: Ghidra instance port (default: 8192) function: Function name containing variable name: Current variable name new_name: New variable name - + Returns: dict: Operation result """ @@ -1089,16 +1147,17 @@ def rename_variable(port: int = DEFAULT_GHIDRA_PORT, function: str = "", name: s encoded_var = quote(name) return safe_post(port, f"functions/{encoded_function}/variables/{encoded_var}", {"newName": new_name}) + @mcp.tool() def retype_variable(port: int = DEFAULT_GHIDRA_PORT, function: str = "", name: str = "", data_type: str = "") -> dict: """Change variable data type in function - + Args: port: Ghidra instance port (default: 8192) function: Function name containing variable name: Variable name to modify data_type: New data type - + Returns: dict: Operation result """ @@ -1109,9 +1168,11 @@ def retype_variable(port: int = DEFAULT_GHIDRA_PORT, function: str = "", name: s encoded_var = quote(name) return safe_post(port, f"functions/{encoded_function}/variables/{encoded_var}", {"dataType": data_type}) + def handle_sigint(signum, frame): os._exit(0) + def periodic_discovery(): """Periodically discover new instances""" while True: @@ -1137,8 +1198,10 @@ def periodic_discovery(): time.sleep(30) + if __name__ == "__main__": - register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}") + register_instance(DEFAULT_GHIDRA_PORT, + f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}") discover_instances() diff --git a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java index b362d22..22bd633 100644 --- a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java +++ b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java @@ -1,35 +1,25 @@ package eu.starsong.ghidra; +// New imports for refactored structure +import eu.starsong.ghidra.api.*; +import eu.starsong.ghidra.endpoints.*; +import eu.starsong.ghidra.util.*; + import java.io.IOException; -import java.io.OutputStream; -import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; import java.net.ServerSocket; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.UUID; // Added for request IDs import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; // Added for transaction helper - -import javax.swing.SwingUtilities; // For JSON response handling -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.sun.net.httpserver.HttpExchange; +import com.google.gson.Gson; // Keep for now if needed by sendJsonResponse stub +import com.google.gson.JsonObject; // Keep for now if needed by sendJsonResponse stub +import com.sun.net.httpserver.HttpExchange; // Keep for now if needed by sendJsonResponse stub import com.sun.net.httpserver.HttpServer; -import ghidra.app.decompiler.DecompInterface; -import ghidra.app.decompiler.DecompileResults; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.services.ProgramManager; import ghidra.framework.main.ApplicationLevelPlugin; @@ -38,40 +28,10 @@ import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.GlobalNamespace; -import ghidra.program.model.data.DataType; -import ghidra.program.model.data.DataTypeManager; -import ghidra.program.model.listing.CodeUnit; -import ghidra.program.model.listing.Data; -import ghidra.program.model.listing.DataIterator; -import ghidra.program.model.listing.Function; -import ghidra.program.model.listing.Listing; -import ghidra.program.model.listing.Parameter; import ghidra.program.model.listing.Program; -import ghidra.program.model.listing.VariableStorage; -import ghidra.program.model.mem.MemoryBlock; -import ghidra.program.model.pcode.HighFunction; -import ghidra.program.model.pcode.HighFunctionDBUtil; -import ghidra.program.model.pcode.HighFunctionDBUtil.ReturnCommitOption; -import ghidra.program.model.pcode.HighSymbol; -import ghidra.program.model.pcode.LocalSymbolMap; -import ghidra.program.model.symbol.Namespace; -import ghidra.program.model.symbol.SourceType; -import ghidra.program.model.symbol.Symbol; -import ghidra.program.model.symbol.SymbolIterator; -import ghidra.program.model.symbol.SymbolTable; -import ghidra.program.model.symbol.SymbolType; import ghidra.util.Msg; -import ghidra.util.task.ConsoleTaskMonitor; -// Functional interface for Ghidra operations that might throw exceptions -@FunctionalInterface -interface GhidraSupplier { - T get() throws Exception; -} - @PluginInfo( status = PluginStatus.RELEASED, packageName = ghidra.app.DeveloperPluginPackage.NAME, @@ -82,16 +42,14 @@ interface GhidraSupplier { ) public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { - // Plugin version information - private static final String PLUGIN_VERSION = "v1.0.0"; // Update this with each release - private static final int API_VERSION = 1; // Increment when API changes in a breaking way - - private static final Map activeInstances = new ConcurrentHashMap<>(); + // Made public static to be accessible by InstanceEndpoints - consider a better design pattern + public static final Map activeInstances = new ConcurrentHashMap<>(); private static final Object baseInstanceLock = new Object(); private HttpServer server; private int port; private boolean isBaseInstance = false; + // Removed Gson instance, should be handled by HttpUtil or endpoints public GhydraMCPPlugin(PluginTool tool) { super(tool); @@ -100,7 +58,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { activeInstances.put(port, this); synchronized (baseInstanceLock) { - if (port == 8192 || activeInstances.get(8192) == null) { + if (port == ApiConstants.DEFAULT_PORT || activeInstances.get(ApiConstants.DEFAULT_PORT) == null) { this.isBaseInstance = true; Msg.info(this, "Starting as base instance on port " + port); } @@ -123,131 +81,230 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { private void startServer() throws IOException { server = HttpServer.create(new InetSocketAddress(port), 0); - // Meta endpoints - server.createContext("/plugin-version", exchange -> { - if ("GET".equals(exchange.getRequestMethod())) { - JsonObject response = createBaseResponse(exchange); - response.addProperty("success", true); - - JsonObject result = new JsonObject(); - result.addProperty("plugin_version", PLUGIN_VERSION); - result.addProperty("api_version", API_VERSION); - response.add("result", result); - - JsonObject links = new JsonObject(); - links.add("self", createLink("/plugin-version")); - response.add("_links", links); - - sendJsonResponse(exchange, response, 200); - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - }); + // --- Register Endpoints --- + Program currentProgram = getCurrentProgram(); // Get program once + + // Register Meta Endpoints + registerMetaEndpoints(server); + + // Register endpoints that don't require a program + registerProjectEndpoints(server); + new InstanceEndpoints(currentProgram, port, activeInstances).registerEndpoints(server); + + // Register Resource Endpoints that require a program + registerProgramDependentEndpoints(currentProgram, server); + + // Register Root Endpoint (should be last to include links to all other endpoints) + registerRootEndpoint(server); - // Program resources - server.createContext("/programs", exchange -> { + server.setExecutor(null); // Use default executor + new Thread(() -> { + server.start(); + Msg.info(this, "GhydraMCP HTTP server started on port " + port); + System.out.println("[GhydraMCP] HTTP server started on port " + port); + }, "GhydraMCP-HTTP-Server").start(); + } + + /** + * Register all endpoints that require a program to function. + * This method always registers all endpoints, even when no program is loaded. + * When no program is loaded, the endpoints will return appropriate error messages. + */ + private void registerProgramDependentEndpoints(Program currentProgram, HttpServer server) { + // Always register all endpoints, even if currentProgram is null + // The endpoint implementations will handle the null program case + new FunctionEndpoints(currentProgram, port).registerEndpoints(server); + new VariableEndpoints(currentProgram, port).registerEndpoints(server); + new ClassEndpoints(currentProgram, port).registerEndpoints(server); + new SegmentEndpoints(currentProgram, port).registerEndpoints(server); + new SymbolEndpoints(currentProgram, port).registerEndpoints(server); + new NamespaceEndpoints(currentProgram, port).registerEndpoints(server); + new DataEndpoints(currentProgram, port).registerEndpoints(server); + + // Register additional endpoints for current program/address + registerCurrentAddressEndpoints(server, currentProgram); + registerDecompilerEndpoints(server, currentProgram); + + if (currentProgram != null) { + Msg.info(this, "Registered program-dependent endpoints for program: " + currentProgram.getName()); + } else { + Msg.warn(this, "No current program available. Endpoints registered but will return appropriate errors when accessed."); + } + } + + /** + * Register endpoints related to the current address in Ghidra. + */ + private void registerCurrentAddressEndpoints(HttpServer server, Program program) { + // Current address endpoint + server.createContext("/get_current_address", exchange -> { try { if ("GET".equals(exchange.getRequestMethod())) { - List> programs = new ArrayList<>(); - Program program = getCurrentProgram(); - if (program != null) { - Map progInfo = new HashMap<>(); - progInfo.put("program_id", program.getDomainFile().getPathname()); - progInfo.put("name", program.getName()); - progInfo.put("language_id", program.getLanguageID().getIdAsString()); - progInfo.put("compiler_spec_id", program.getCompilerSpec().getCompilerSpecID().getIdAsString()); - progInfo.put("image_base", program.getImageBase().toString()); - progInfo.put("memory_size", program.getMemory().getSize()); - progInfo.put("is_open", true); - progInfo.put("analysis_complete", program.getListing().getNumDefinedData() > 0); - programs.add(progInfo); - } + Map addressData = new HashMap<>(); + addressData.put("address", GhidraUtil.getCurrentAddressString(tool)); - JsonObject response = createSuccessResponse(exchange, programs); - response.add("_links", createLinks() - .add("self", "/programs") - .add("create", "/programs", "POST") - .build()); - - sendJsonResponse(exchange, response, 200); - } else if ("POST".equals(exchange.getRequestMethod())) { - sendErrorResponse(exchange, 501, "Not Implemented", "NOT_IMPLEMENTED"); + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(addressData) + .addLink("self", "/get_current_address"); + + HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port); } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); + HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port); } } catch (Exception e) { - sendErrorResponse(exchange, 500, "Internal server error", "INTERNAL_ERROR"); + Msg.error(this, "Error serving /get_current_address endpoint", e); + try { + HttpUtil.sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR", port); + } catch (IOException ioEx) { + Msg.error(this, "Failed to send error for /get_current_address", ioEx); + } } }); - - server.createContext("/programs/", exchange -> { + + // Current function endpoint + server.createContext("/get_current_function", exchange -> { try { - String path = exchange.getRequestURI().getPath(); - String programId = path.substring("/programs/".length()); - if ("GET".equals(exchange.getRequestMethod())) { - Program program = getCurrentProgram(); - if (program == null) { - sendErrorResponse(exchange, 404, "Program not found", "PROGRAM_NOT_FOUND"); + Map functionData = GhidraUtil.getCurrentFunctionInfo(tool, program); + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(functionData) + .addLink("self", "/get_current_function"); + + HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port); + } else { + HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port); + } + } catch (Exception e) { + Msg.error(this, "Error serving /get_current_function endpoint", e); + try { + HttpUtil.sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR", port); + } catch (IOException ioEx) { + Msg.error(this, "Failed to send error for /get_current_function", ioEx); + } + } + }); + } + + /** + * Register endpoints related to the decompiler. + */ + private void registerDecompilerEndpoints(HttpServer server, Program program) { + // Get function by address endpoint + server.createContext("/get_function_by_address", exchange -> { + try { + if ("GET".equals(exchange.getRequestMethod())) { + Map params = HttpUtil.parseQueryParams(exchange); + String addressStr = params.get("address"); + + if (addressStr == null || addressStr.isEmpty()) { + HttpUtil.sendErrorResponse(exchange, 400, "Missing address parameter", "MISSING_PARAMETER", port); return; } - Map programInfo = new HashMap<>(); - programInfo.put("program_id", program.getDomainFile().getPathname()); - programInfo.put("name", program.getName()); - programInfo.put("language_id", program.getLanguageID().getIdAsString()); - programInfo.put("compiler_spec_id", program.getCompilerSpec().getCompilerSpecID().getIdAsString()); - programInfo.put("image_base", program.getImageBase().toString()); - programInfo.put("memory_size", program.getMemory().getSize()); - programInfo.put("is_open", true); - programInfo.put("analysis_complete", program.getListing().getNumDefinedData() > 0); + Map functionData = GhidraUtil.getFunctionByAddress(program, addressStr); - JsonObject links = new JsonObject(); - links.add("self", createLink("/programs/" + programId)); - links.add("project", createLink("/projects/" + program.getDomainFile().getProjectLocator().getName())); - links.add("functions", createLink("/programs/" + programId + "/functions")); - links.add("symbols", createLink("/programs/" + programId + "/symbols")); - links.add("data", createLink("/programs/" + programId + "/data")); - links.add("segments", createLink("/programs/" + programId + "/segments")); - links.add("memory", createLink("/programs/" + programId + "/memory")); - links.add("xrefs", createLink("/programs/" + programId + "/xrefs")); - links.add("analysis", createLink("/programs/" + programId + "/analysis")); + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(functionData) + .addLink("self", "/get_function_by_address?address=" + addressStr); - JsonObject response = createSuccessResponse(exchange, programInfo, links); - sendJsonResponse(exchange, response, 200); - } else if ("DELETE".equals(exchange.getRequestMethod())) { - sendErrorResponse(exchange, 501, "Not Implemented", "NOT_IMPLEMENTED"); + HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port); } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); + HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port); } } catch (Exception e) { - sendErrorResponse(exchange, 500, "Internal server error", "INTERNAL_ERROR"); + Msg.error(this, "Error serving /get_function_by_address endpoint", e); + try { + HttpUtil.sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR", port); + } catch (IOException ioEx) { + Msg.error(this, "Failed to send error for /get_function_by_address", ioEx); + } } }); - - // Meta endpoints + + // Decompile function endpoint + server.createContext("/decompile_function", exchange -> { + try { + if ("GET".equals(exchange.getRequestMethod())) { + Map params = HttpUtil.parseQueryParams(exchange); + String addressStr = params.get("address"); + + if (addressStr == null || addressStr.isEmpty()) { + HttpUtil.sendErrorResponse(exchange, 400, "Missing address parameter", "MISSING_PARAMETER", port); + return; + } + + Map decompData = GhidraUtil.decompileFunction(program, addressStr); + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(decompData) + .addLink("self", "/decompile_function?address=" + addressStr); + + HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port); + } else { + HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port); + } + } catch (Exception e) { + Msg.error(this, "Error serving /decompile_function endpoint", e); + try { + HttpUtil.sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR", port); + } catch (IOException ioEx) { + Msg.error(this, "Failed to send error for /decompile_function", ioEx); + } + } + }); + } + + // --- Endpoint Registration Methods --- + + private void registerMetaEndpoints(HttpServer server) { server.createContext("/plugin-version", exchange -> { - if ("GET".equals(exchange.getRequestMethod())) { - JsonObject response = createBaseResponse(exchange); - response.addProperty("success", true); - - JsonObject result = new JsonObject(); - result.addProperty("plugin_version", PLUGIN_VERSION); - result.addProperty("api_version", API_VERSION); - response.add("result", result); - - JsonObject links = new JsonObject(); - links.add("self", createLink("/plugin-version")); - response.add("_links", links); - - sendJsonResponse(exchange, response, 200); - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); + try { + if ("GET".equals(exchange.getRequestMethod())) { + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(Map.of( + "plugin_version", ApiConstants.PLUGIN_VERSION, + "api_version", ApiConstants.API_VERSION + )) + .addLink("self", "/plugin-version"); + HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port); + } else { + HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port); + } + } catch (IOException e) { + Msg.error(this, "Error handling /plugin-version", e); } }); - - // Project resources - server.createContext("/projects", exchange -> { + + server.createContext("/info", exchange -> { + try { + Map infoData = new HashMap<>(); + infoData.put("isBaseInstance", isBaseInstance); + Program program = getCurrentProgram(); + infoData.put("file", program != null ? program.getName() : null); + Project project = tool.getProject(); + infoData.put("project", project != null ? project.getName() : null); + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(infoData) + .addLink("self", "/info"); + HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port); + } catch (Exception e) { + Msg.error(this, "Error serving /info endpoint", e); + try { HttpUtil.sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR", port); } + catch (IOException ioEx) { Msg.error(this, "Failed to send error for /info", ioEx); } + } + }); + } + + private void registerProjectEndpoints(HttpServer server) { + server.createContext("/projects", exchange -> { try { if ("GET".equals(exchange.getRequestMethod())) { List> projects = new ArrayList<>(); @@ -259,2607 +316,94 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { projects.add(projInfo); } - JsonObject response = createSuccessResponse(exchange, projects); - response.add("_links", createLinks() - .add("self", "/projects") - .add("create", "/projects", "POST") - .build()); + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(projects) + .addLink("self", "/projects") + .addLink("create", "/projects", "POST"); - sendJsonResponse(exchange, response, 200); + HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port); } else if ("POST".equals(exchange.getRequestMethod())) { - sendErrorResponse(exchange, 501, "Not Implemented", "NOT_IMPLEMENTED"); + HttpUtil.sendErrorResponse(exchange, 501, "Not Implemented", "NOT_IMPLEMENTED", port); } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); + HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port); } } catch (Exception e) { - sendErrorResponse(exchange, 500, "Internal server error", "INTERNAL_ERROR"); + Msg.error(this, "Error serving /projects endpoint", e); + try { HttpUtil.sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR", port); } + catch (IOException ioEx) { Msg.error(this, "Failed to send error for /projects", ioEx); } } }); - - // Function resources - server.createContext("/functions", exchange -> { - try { - if ("GET".equals(exchange.getRequestMethod())) { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - String query = qparams.get("query"); - - Object resultData; - if (query != null && !query.isEmpty()) { - // TODO: Refactor searchFunctionsByName to return List> or similar - resultData = searchFunctionsByName(query, offset, limit); - } else { - // TODO: Refactor getAllFunctionNames to return List> or similar - resultData = getAllFunctionNames(offset, limit); - } - // Temporary check for old error format - if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { - sendJsonResponse(exchange, (JsonObject)resultData, 400); - } else { - sendJsonResponse(exchange, resultData); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - } catch (Exception e) { - Msg.error(this, "Error in /functions endpoint", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - server.createContext("/functions/", exchange -> { - String path = exchange.getRequestURI().getPath(); - String[] pathParts = path.split("/"); - - if (pathParts.length < 3) { - sendErrorResponse(exchange, 400, "Invalid path format", "INVALID_PATH"); - return; - } - - String functionName = ""; - try { - functionName = java.net.URLDecoder.decode(pathParts[2], StandardCharsets.UTF_8.name()); - } catch (Exception e) { - sendErrorResponse(exchange, 400, "Failed to decode function name", "INVALID_PARAMETER"); - return; - } - - if (pathParts.length > 3 && "variables".equals(pathParts[3])) { // /functions/{name}/variables/... - if ("GET".equals(exchange.getRequestMethod()) && pathParts.length == 4) { // GET /functions/{name}/variables - try { - // TODO: Refactor listVariablesInFunction to return data directly - Object resultData = listVariablesInFunction(functionName); - if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { - sendJsonResponse(exchange, (JsonObject)resultData, 400); - } else { - sendJsonResponse(exchange, resultData); - } - } catch (Exception e) { - Msg.error(this, "Error listing function variables", e); - sendErrorResponse(exchange, 500, "Error listing variables: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else if ("POST".equals(exchange.getRequestMethod()) && pathParts.length == 5) { // POST /functions/{name}/variables/{varName} - String variableName = ""; - try { - variableName = java.net.URLDecoder.decode(pathParts[4], StandardCharsets.UTF_8.name()); - } catch (Exception e) { - sendErrorResponse(exchange, 400, "Failed to decode variable name", "INVALID_PARAMETER"); - return; - } - - final String finalVariableName = variableName; - final String finalFunctionName = functionName; - try { - Map params = parseJsonPostParams(exchange); - Program program = getCurrentProgram(); - if (program == null) { - sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); - return; - } - - if (params.containsKey("newName")) { - final String newName = params.get("newName"); - try { - executeInTransaction(program, "Rename Variable", () -> { - if (!renameVariable(finalFunctionName, finalVariableName, newName)) { - throw new Exception("Rename operation failed internally."); - } - }); - sendJsonResponse(exchange, Map.of("message", "Variable renamed successfully")); - } catch (Exception e) { - Msg.error(this, "Transaction failed: Rename Variable", e); - sendErrorResponse(exchange, 500, "Failed to rename variable: " + e.getMessage(), "TRANSACTION_ERROR"); - } - } else if (params.containsKey("dataType")) { - final String newType = params.get("dataType"); - try { - executeInTransaction(program, "Retype Variable", () -> { - if (!retypeVariable(finalFunctionName, finalVariableName, newType)) { - throw new Exception("Retype operation failed internally."); - } - }); - sendJsonResponse(exchange, Map.of("message", "Variable retyped successfully")); - } catch (Exception e) { - Msg.error(this, "Transaction failed: Retype Variable", e); - sendErrorResponse(exchange, 500, "Failed to retype variable: " + e.getMessage(), "TRANSACTION_ERROR"); - } - } else { - sendErrorResponse(exchange, 400, "Missing required parameter: newName or dataType", "MISSING_PARAMETER"); - } - } catch (IOException e) { - Msg.error(this, "Error parsing POST params for variable update", e); - sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); - } catch (Exception e) { - Msg.error(this, "Error updating variable", e); - sendErrorResponse(exchange, 500, "Error updating variable: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - } else if (pathParts.length == 3) { // GET or POST /functions/{name} - if ("GET".equals(exchange.getRequestMethod())) { - try { - // TODO: Refactor getFunctionDetailsByName to return data directly - Object resultData = getFunctionDetailsByName(functionName); - if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { - sendJsonResponse(exchange, (JsonObject)resultData, 404); - } else { - sendJsonResponse(exchange, resultData); - } - } catch (Exception e) { - Msg.error(this, "Error getting function details", e); - sendErrorResponse(exchange, 500, "Error getting details: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else if ("POST".equals(exchange.getRequestMethod())) { - try { - Map params = parseJsonPostParams(exchange); - String newName = params.get("newName"); - if (newName == null || newName.isEmpty()) { - sendErrorResponse(exchange, 400, "Missing required parameter: newName", "MISSING_PARAMETER"); - return; - } - - Program program = getCurrentProgram(); - if (program == null) { - sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); - return; - } - - final String finalFunctionName = functionName; - final String finalNewName = newName; - try { - executeInTransaction(program, "Rename Function", () -> { - if (!renameFunction(finalFunctionName, finalNewName)) { - throw new Exception("Rename operation failed internally."); - } - }); - sendJsonResponse(exchange, Map.of("message", "Function renamed successfully")); - } catch (Exception e) { - Msg.error(this, "Transaction failed: Rename Function", e); - sendErrorResponse(exchange, 500, "Failed to rename function: " + e.getMessage(), "TRANSACTION_ERROR"); - } - - } catch (IOException e) { - Msg.error(this, "Error parsing POST params for function rename", e); - sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); - } catch (Exception e) { - Msg.error(this, "Error renaming function", e); - sendErrorResponse(exchange, 500, "Error renaming function: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - } - }); - - // Class resources - server.createContext("/classes", exchange -> { - try { - if ("GET".equals(exchange.getRequestMethod())) { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - Object resultData = getAllClassNames(offset, limit); - if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { - sendJsonResponse(exchange, (JsonObject)resultData, 400); - } else { - sendJsonResponse(exchange, resultData); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - } catch (Exception e) { - Msg.error(this, "Error in /classes endpoint", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - // Memory segments - server.createContext("/segments", exchange -> { - try { - if ("GET".equals(exchange.getRequestMethod())) { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - Object resultData = listSegments(offset, limit); - if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { - sendJsonResponse(exchange, (JsonObject)resultData, 400); - } else { - sendJsonResponse(exchange, resultData); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - } catch (Exception e) { - Msg.error(this, "Error in /segments endpoint", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - // Symbol resources (imports/exports) - server.createContext("/symbols/imports", exchange -> { - try { - if ("GET".equals(exchange.getRequestMethod())) { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - Object resultData = listImports(offset, limit); - if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { - sendJsonResponse(exchange, (JsonObject)resultData, 400); - } else { - sendJsonResponse(exchange, resultData); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - } catch (Exception e) { - Msg.error(this, "Error in /symbols/imports endpoint", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - server.createContext("/symbols/exports", exchange -> { - try { - if ("GET".equals(exchange.getRequestMethod())) { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - Object resultData = listExports(offset, limit); - if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { - sendJsonResponse(exchange, (JsonObject)resultData, 400); - } else { - sendJsonResponse(exchange, resultData); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - } catch (Exception e) { - Msg.error(this, "Error in /symbols/exports endpoint", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - // Namespace resources - server.createContext("/namespaces", exchange -> { - try { - if ("GET".equals(exchange.getRequestMethod())) { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - Object resultData = listNamespaces(offset, limit); - if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { - sendJsonResponse(exchange, (JsonObject)resultData, 400); - } else { - sendJsonResponse(exchange, resultData); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - } catch (Exception e) { - Msg.error(this, "Error in /namespaces endpoint", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - // Data resources - server.createContext("/data", exchange -> { - try { - if ("GET".equals(exchange.getRequestMethod())) { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - Object resultData = listDefinedData(offset, limit); - if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) { - sendJsonResponse(exchange, (JsonObject)resultData, 400); - } else { - sendJsonResponse(exchange, resultData); - } - } else if ("POST".equals(exchange.getRequestMethod())) { // POST /data - try { - Map params = parseJsonPostParams(exchange); - final String addressStr = params.get("address"); - final String newName = params.get("newName"); - - if (addressStr == null || addressStr.isEmpty() || newName == null || newName.isEmpty()) { - sendErrorResponse(exchange, 400, "Missing required parameters: address, newName", "MISSING_PARAMETER"); - return; - } - - Program program = getCurrentProgram(); - if (program == null) { - sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); - return; - } - - try { - executeInTransaction(program, "Rename Data", () -> { - if (!renameDataAtAddress(addressStr, newName)) { - throw new Exception("Rename data operation failed internally."); - } - }); - sendJsonResponse(exchange, Map.of("message", "Data renamed successfully")); - } catch (Exception e) { - Msg.error(this, "Transaction failed: Rename Data", e); - sendErrorResponse(exchange, 500, "Failed to rename data: " + e.getMessage(), "TRANSACTION_ERROR"); - } - - } catch (IOException e) { - Msg.error(this, "Error parsing POST params for data rename", e); - sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); - } catch (Exception e) { - Msg.error(this, "Error renaming data", e); - sendErrorResponse(exchange, 500, "Error renaming data: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - } catch (Exception e) { - Msg.error(this, "Error in /data endpoint", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - // Global variables endpoint - server.createContext("/variables", exchange -> { // GET /variables - try { - if ("GET".equals(exchange.getRequestMethod())) { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - String search = qparams.get("search"); - - Object resultData = listVariables(offset, limit, search); - if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { // Check old error format - sendJsonResponse(exchange, (JsonObject)resultData, 400); - } else { - sendJsonResponse(exchange, resultData); // Use new success helper - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - } catch (Exception e) { - Msg.error(this, "Error in /variables endpoint", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - // Instance management endpoints - server.createContext("/instances", exchange -> { - // TODO: This endpoint might change based on HATEOAS design for projects/programs - try { - List> instanceData = new ArrayList<>(); - for (Map.Entry entry : activeInstances.entrySet()) { - Map instance = new HashMap<>(); - instance.put("port", entry.getKey()); - instance.put("type", entry.getValue().isBaseInstance ? "base" : "secondary"); - // TODO: Add URL and program_id if available from instance info cache - instanceData.add(instance); - } - sendJsonResponse(exchange, instanceData); // Use new success helper - } catch (Exception e) { - Msg.error(this, "Error in /instances endpoint", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - // Add get_function_by_address endpoint - server.createContext("/get_function_by_address", exchange -> { - if ("GET".equals(exchange.getRequestMethod())) { - Map qparams = parseQueryParams(exchange); - String address = qparams.get("address"); - - if (address == null || address.isEmpty()) { - sendErrorResponse(exchange, 400, "Address parameter is required", "MISSING_PARAMETER"); - return; - } - - Program program = getCurrentProgram(); - if (program == null) { - sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); - return; - } - - try { - Address funcAddr = program.getAddressFactory().getAddress(address); - Function func = program.getFunctionManager().getFunctionAt(funcAddr); - if (func == null) { - sendErrorResponse(exchange, 404, "Function not found at address: " + address, "RESOURCE_NOT_FOUND"); - return; - } - - Object resultData = getFunctionDetails(func); - if (resultData instanceof JsonObject && !((JsonObject)resultData).has("result")) { - sendJsonResponse(exchange, (JsonObject)resultData, 500); - } else { - sendJsonResponse(exchange, resultData); - } - } catch (ghidra.program.model.address.AddressFormatException afe) { - Msg.warn(this, "Invalid address format: " + address, afe); - sendErrorResponse(exchange, 400, "Invalid address format: " + address, "INVALID_ADDRESS"); - } catch (Exception e) { - Msg.error(this, "Error getting function by address", e); - sendErrorResponse(exchange, 500, "Error getting function: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - }); - - // Add decompile function by address endpoint - server.createContext("/decompile_function", exchange -> { - if ("GET".equals(exchange.getRequestMethod())) { - Map qparams = parseQueryParams(exchange); - String address = qparams.get("address"); - boolean cCode = Boolean.parseBoolean(qparams.getOrDefault("cCode", "true")); - boolean syntaxTree = Boolean.parseBoolean(qparams.getOrDefault("syntaxTree", "false")); - String simplificationStyle = qparams.getOrDefault("simplificationStyle", "normalize"); - - if (address == null || address.isEmpty()) { - sendErrorResponse(exchange, 400, "Address parameter is required", "MISSING_PARAMETER"); - return; - } - - Program program = getCurrentProgram(); - if (program == null) { - sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); - return; - } - - try { - Address funcAddr = program.getAddressFactory().getAddress(address); - Function func = program.getFunctionManager().getFunctionAt(funcAddr); - if (func == null) { - sendErrorResponse(exchange, 404, "Function not found at address: " + address, "RESOURCE_NOT_FOUND"); - return; - } - - DecompInterface decomp = new DecompInterface(); - try { - decomp.toggleCCode(cCode); - decomp.setSimplificationStyle(simplificationStyle); - decomp.toggleSyntaxTree(syntaxTree); - - if (!decomp.openProgram(program)) { - sendErrorResponse(exchange, 500, "Failed to initialize decompiler", "DECOMPILER_ERROR"); - return; - } - - DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); - if (result == null || !result.decompileCompleted()) { - sendErrorResponse(exchange, 500, "Decompilation failed or timed out", "DECOMPILATION_FAILED"); - return; - } - - String decompilation = ""; - String errorMessage = null; - if (result.getDecompiledFunction() != null) { - decompilation = result.getDecompiledFunction().getC(); - if (decompilation == null || decompilation.isEmpty()) { - errorMessage = "Decompilation returned empty result"; - } - } else { - errorMessage = "DecompiledFunction is null"; - } - - if (errorMessage != null) { - Msg.error(this, "Error decompiling function: " + errorMessage); - sendErrorResponse(exchange, 500, errorMessage, "DECOMPILATION_ERROR"); - } else { - Map resultData = new HashMap<>(); - resultData.put("address", func.getEntryPoint().toString()); - resultData.put("ccode", decompilation); - sendJsonResponse(exchange, resultData); - } - } finally { - decomp.dispose(); - } - } catch (ghidra.program.model.address.AddressFormatException afe) { - Msg.warn(this, "Invalid address format: " + address, afe); - sendErrorResponse(exchange, 400, "Invalid address format: " + address, "INVALID_ADDRESS"); - } catch (Exception e) { - Msg.error(this, "Error decompiling function", e); - sendErrorResponse(exchange, 500, "Error decompiling function: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - }); - - // Add decompiler comment endpoint (Using POST now as per bridge) - server.createContext("/set_decompiler_comment", exchange -> { - if ("POST".equals(exchange.getRequestMethod())) { - Map params = parseJsonPostParams(exchange); // Use specific JSON parser - String address = params.get("address"); - String comment = params.get("comment"); - - if (address == null || address.isEmpty()) { - sendErrorResponse(exchange, 400, "Address parameter is required"); - return; - } - - Program program = getCurrentProgram(); - if (program == null) { - sendErrorResponse(exchange, 400, "No program loaded"); - return; - } - - try { - final Address addr = program.getAddressFactory().getAddress(address); - final String finalComment = comment; - - executeInTransaction(program, "Set Decompiler Comment", () -> { - if (!setDecompilerComment(addr, finalComment)) { - throw new Exception("Set decompiler comment operation failed internally."); - } - }); - sendJsonResponse(exchange, Map.of("message", "Decompiler comment set successfully")); - - } catch (ghidra.program.model.address.AddressFormatException afe) { - Msg.warn(this, "Invalid address format: " + address, afe); - sendErrorResponse(exchange, 400, "Invalid address format: " + address, "INVALID_ADDRESS"); - } catch (Exception e) { - Msg.error(this, "Error setting decompiler comment", e); - sendErrorResponse(exchange, 500, "Error setting comment: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - }); - - // Add disassembly comment endpoint (Using POST now as per bridge) - server.createContext("/set_disassembly_comment", exchange -> { - if ("POST".equals(exchange.getRequestMethod())) { - Map params = parseJsonPostParams(exchange); // Use specific JSON parser - String address = params.get("address"); - String comment = params.get("comment"); - - if (address == null || address.isEmpty()) { - sendErrorResponse(exchange, 400, "Address parameter is required"); - return; - } - - Program program = getCurrentProgram(); - if (program == null) { - sendErrorResponse(exchange, 400, "No program loaded"); - return; - } - - try { - final Address addr = program.getAddressFactory().getAddress(address); - final String finalComment = comment; - - executeInTransaction(program, "Set Disassembly Comment", () -> { - if (!setDisassemblyComment(addr, finalComment)) { - throw new Exception("Set disassembly comment operation failed internally."); - } - }); - sendJsonResponse(exchange, Map.of("message", "Disassembly comment set successfully")); - - } catch (ghidra.program.model.address.AddressFormatException afe) { - Msg.warn(this, "Invalid address format: " + address, afe); - sendErrorResponse(exchange, 400, "Invalid address format: " + address, "INVALID_ADDRESS"); - } catch (Exception e) { - Msg.error(this, "Error setting disassembly comment", e); - sendErrorResponse(exchange, 500, "Error setting comment: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - }); - - // Add rename function by address endpoint (Using POST now as per bridge) - server.createContext("/rename_function_by_address", exchange -> { - if ("POST".equals(exchange.getRequestMethod())) { - Map params = parseJsonPostParams(exchange); // Use specific JSON parser - String address = params.get("functionAddress"); // Expect camelCase - String newName = params.get("newName"); // Expect camelCase - - if (address == null || address.isEmpty()) { - sendErrorResponse(exchange, 400, "functionAddress parameter is required"); - return; - } - - if (newName == null || newName.isEmpty()) { - sendErrorResponse(exchange, 400, "newName parameter is required"); - return; - } - - Program program = getCurrentProgram(); - if (program == null) { - sendErrorResponse(exchange, 400, "No program loaded"); - return; - } - - try { - final Address funcAddr = program.getAddressFactory().getAddress(address); - final String finalNewName = newName; - - executeInTransaction(program, "Rename Function by Address", () -> { - if (!renameFunctionByAddress(funcAddr, finalNewName)) { - throw new Exception("Rename function by address operation failed internally."); - } - }); - sendJsonResponse(exchange, Map.of("message", "Function renamed successfully")); - - } catch (ghidra.program.model.address.AddressFormatException afe) { - Msg.warn(this, "Invalid address format: " + address, afe); - sendErrorResponse(exchange, 400, "Invalid address format: " + address, "INVALID_ADDRESS"); - } catch (Exception e) { - Msg.error(this, "Error renaming function by address", e); - sendErrorResponse(exchange, 500, "Error renaming function: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - }); - - // Add rename local variable endpoint (Using POST now as per bridge) - server.createContext("/rename_local_variable", exchange -> { - if ("POST".equals(exchange.getRequestMethod())) { - Map params = parseJsonPostParams(exchange); - String functionAddress = params.get("functionAddress"); - String oldName = params.get("oldName"); - String newName = params.get("newName"); - - if (functionAddress == null || functionAddress.isEmpty()) { - sendErrorResponse(exchange, 400, "functionAddress parameter is required"); return; - } - if (oldName == null || oldName.isEmpty()) { - sendErrorResponse(exchange, 400, "oldName parameter is required"); return; - } - if (newName == null || newName.isEmpty()) { - sendErrorResponse(exchange, 400, "newName parameter is required"); return; - } - - // TODO: Implement actual logic using executeInTransaction - sendJsonResponse(exchange, Map.of("message", "Rename local variable request received (implementation pending)")); - - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - }); - - // Add set function prototype endpoint (Using POST now as per bridge) - server.createContext("/set_function_prototype", exchange -> { - if ("POST".equals(exchange.getRequestMethod())) { - Map params = parseJsonPostParams(exchange); - String functionAddress = params.get("functionAddress"); - String prototype = params.get("prototype"); - - if (functionAddress == null || functionAddress.isEmpty()) { - sendErrorResponse(exchange, 400, "functionAddress parameter is required"); return; - } - if (prototype == null || prototype.isEmpty()) { - sendErrorResponse(exchange, 400, "prototype parameter is required"); return; - } - - // TODO: Implement actual logic using executeInTransaction - sendJsonResponse(exchange, Map.of("message", "Set function prototype request received (implementation pending)")); - - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - }); - - // Add set local variable type endpoint (Using POST now as per bridge) - server.createContext("/set_local_variable_type", exchange -> { - if ("POST".equals(exchange.getRequestMethod())) { - Map params = parseJsonPostParams(exchange); - String functionAddress = params.get("functionAddress"); - String variableName = params.get("variableName"); - String newType = params.get("newType"); - - if (functionAddress == null || functionAddress.isEmpty()) { - sendErrorResponse(exchange, 400, "functionAddress parameter is required"); return; - } - if (variableName == null || variableName.isEmpty()) { - sendErrorResponse(exchange, 400, "variableName parameter is required"); return; - } - if (newType == null || newType.isEmpty()) { - sendErrorResponse(exchange, 400, "newType parameter is required"); return; - } - - // TODO: Implement actual logic using executeInTransaction - sendJsonResponse(exchange, Map.of("message", "Set local variable type request received (implementation pending)")); - - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - }); - - // Add get current address endpoint (Changed to GET to match test expectations) - server.createContext("/get_current_address", exchange -> { - if ("GET".equals(exchange.getRequestMethod())) { - Program program = getCurrentProgram(); - if (program == null) { - sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); - return; - } - - try { - Address currentAddr = getCurrentAddress(); - if (currentAddr != null) { - sendJsonResponse(exchange, Map.of("address", currentAddr.toString())); - } else { - sendErrorResponse(exchange, 404, "No address currently selected", "RESOURCE_NOT_FOUND"); - } - } catch (Exception e) { - Msg.error(this, "Error getting current address", e); - sendErrorResponse(exchange, 500, "Error getting current address: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - }); - - // Add get current function endpoint (Changed to GET to match test expectations) - server.createContext("/get_current_function", exchange -> { - if ("GET".equals(exchange.getRequestMethod())) { - Program program = getCurrentProgram(); - if (program == null) { - sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM"); - return; - } - - try { - Function currentFunc = getCurrentFunction(); - if (currentFunc != null) { - Map funcData = new HashMap<>(); - funcData.put("name", currentFunc.getName()); - funcData.put("address", currentFunc.getEntryPoint().toString()); - funcData.put("signature", currentFunc.getSignature().getPrototypeString()); - sendJsonResponse(exchange, funcData); - } else { - sendErrorResponse(exchange, 404, "No function currently selected", "RESOURCE_NOT_FOUND"); - } - } catch (Exception e) { - Msg.error(this, "Error getting current function", e); - sendErrorResponse(exchange, 500, "Error getting current function: " + e.getMessage(), "INTERNAL_ERROR"); - } - } else { - sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED"); - } - }); - - - // Info endpoint using new helpers - server.createContext("/info", exchange -> { - try { - Map infoData = new HashMap<>(); - infoData.put("port", port); - infoData.put("isBaseInstance", isBaseInstance); - - Program program = getCurrentProgram(); - infoData.put("file", program != null ? program.getName() : null); - - Project project = tool.getProject(); - infoData.put("project", project != null ? project.getName() : null); - - sendJsonResponse(exchange, infoData); - } catch (Exception e) { - Msg.error(this, "Error serving /info endpoint", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - // Root endpoint - only handle exact "/" path + } + + private void registerRootEndpoint(HttpServer server) { server.createContext("/", exchange -> { - if (!exchange.getRequestURI().getPath().equals("/")) { - Msg.info(this, "Received request for unknown path: " + exchange.getRequestURI().getPath()); - sendErrorResponse(exchange, 404, "Endpoint not found", "ENDPOINT_NOT_FOUND"); - return; - } - try { - Map rootData = new HashMap<>(); - rootData.put("port", port); - rootData.put("isBaseInstance", isBaseInstance); - Program program = getCurrentProgram(); - rootData.put("file", program != null ? program.getName() : null); - Project project = tool.getProject(); - rootData.put("project", project != null ? project.getName() : null); - // TODO: Add HATEOAS links here (e.g., to /info, /projects, /programs) + if (!exchange.getRequestURI().getPath().equals("/")) { + HttpUtil.sendErrorResponse(exchange, 404, "Endpoint not found", "ENDPOINT_NOT_FOUND", port); + return; + } + + Map rootData = new HashMap<>(); + rootData.put("message", "GhydraMCP Root Endpoint"); + rootData.put("isBaseInstance", isBaseInstance); - sendJsonResponse(exchange, rootData); + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(rootData) + .addLink("self", "/") + .addLink("info", "/info") + .addLink("plugin-version", "/plugin-version") + .addLink("projects", "/projects") + .addLink("instances", "/instances"); + + // Add links to program-dependent endpoints if a program is loaded + if (getCurrentProgram() != null) { + builder.addLink("functions", "/functions") + .addLink("variables", "/variables") + .addLink("classes", "/classes") + .addLink("segments", "/segments") + .addLink("symbols", "/symbols") + .addLink("namespaces", "/namespaces") + .addLink("data", "/data") + .addLink("current-address", "/get_current_address") + .addLink("current-function", "/get_current_function") + .addLink("get-function-by-address", "/get_function_by_address") + .addLink("decompile-function", "/decompile_function"); + } + + HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port); } catch (Exception e) { Msg.error(this, "Error serving / endpoint", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); + try { + HttpUtil.sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR", port); + } catch (IOException ioEx) { + Msg.error(this, "Failed to send error for /", ioEx); + } } }); - - server.createContext("/registerInstance", exchange -> { - try { - Map params = parseJsonPostParams(exchange); - int regPort = parseIntOrDefault(params.get("port"), 0); - if (regPort > 0) { - sendJsonResponse(exchange, Map.of("message", "Instance registration request received for port " + regPort)); - } else { - sendErrorResponse(exchange, 400, "Invalid or missing port number", "INVALID_PARAMETER"); - } - } catch (IOException e) { - Msg.error(this, "Error parsing POST params for registerInstance", e); - sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); - } catch (Exception e) { - Msg.error(this, "Error in /registerInstance", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - server.createContext("/unregisterInstance", exchange -> { - try { - Map params = parseJsonPostParams(exchange); - int unregPort = parseIntOrDefault(params.get("port"), 0); - if (unregPort > 0 && activeInstances.containsKey(unregPort)) { - activeInstances.remove(unregPort); - sendJsonResponse(exchange, Map.of("message", "Instance unregistered for port " + unregPort)); - } else { - sendErrorResponse(exchange, 404, "No instance found on port " + unregPort, "RESOURCE_NOT_FOUND"); - } - } catch (IOException e) { - Msg.error(this, "Error parsing POST params for unregisterInstance", e); - sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); - } catch (Exception e) { - Msg.error(this, "Error in /unregisterInstance", e); - sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); - } - }); - - server.setExecutor(null); - new Thread(() -> { - server.start(); - Msg.info(this, "GhydraMCP HTTP server started on port " + port); - System.out.println("[GhydraMCP] HTTP server started on port " + port); - }, "GhydraMCP-HTTP-Server").start(); } // ---------------------------------------------------------------------------------- - // Pagination-aware listing methods + // Core Plugin Methods (Keep these) // ---------------------------------------------------------------------------------- - private JsonObject getAllFunctionNames(int offset, int limit) { // Changed return type - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - - List> functions = new ArrayList<>(); - for (Function f : program.getFunctionManager().getFunctions(true)) { - Map func = new HashMap<>(); - func.put("name", f.getName()); - func.put("address", f.getEntryPoint().toString()); - functions.add(func); - } - - // Apply pagination - int start = Math.max(0, offset); - int end = Math.min(functions.size(), offset + limit); - List> paginated = functions.subList(start, end); - - // Use helper to create standard response - return createSuccessResponse(paginated); // Return JsonObject - } - - private JsonObject getAllClassNames(int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - - Set classNames = new HashSet<>(); - for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) { - Namespace ns = symbol.getParentNamespace(); - if (ns != null && !ns.isGlobal()) { - classNames.add(ns.getName()); - } - } - - // Convert to sorted list and paginate - List sorted = new ArrayList<>(classNames); - Collections.sort(sorted); - int start = Math.max(0, offset); - int end = Math.min(sorted.size(), offset + limit); - List paginated = sorted.subList(start, end); - - // Use helper to create standard response - return createSuccessResponse(paginated); - } - - private JsonObject listSegments(int offset, int limit) { // Changed return type to JsonObject - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - - List> segments = new ArrayList<>(); - for (MemoryBlock block : program.getMemory().getBlocks()) { - Map seg = new HashMap<>(); - seg.put("name", block.getName()); - seg.put("start", block.getStart().toString()); - seg.put("end", block.getEnd().toString()); - segments.add(seg); - } - - // Apply pagination - int start = Math.max(0, offset); - int end = Math.min(segments.size(), offset + limit); - List> paginated = segments.subList(start, end); - - // Use helper to create standard response - return createSuccessResponse(paginated); - } - - private JsonObject listImports(int offset, int limit) { // Changed return type to JsonObject - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - - List> imports = new ArrayList<>(); - for (Symbol symbol : program.getSymbolTable().getExternalSymbols()) { - Map imp = new HashMap<>(); - imp.put("name", symbol.getName()); - imp.put("address", symbol.getAddress().toString()); - imports.add(imp); - } - - // Apply pagination - int start = Math.max(0, offset); - int end = Math.min(imports.size(), offset + limit); - List> paginated = imports.subList(start, end); - - // Use helper to create standard response - return createSuccessResponse(paginated); // Return JsonObject directly - } - - private JsonObject listExports(int offset, int limit) { // Changed return type to JsonObject - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - - List> exports = new ArrayList<>(); - SymbolTable table = program.getSymbolTable(); - SymbolIterator it = table.getAllSymbols(true); - - while (it.hasNext()) { - Symbol s = it.next(); - if (s.isExternalEntryPoint()) { - Map exp = new HashMap<>(); - exp.put("name", s.getName()); - exp.put("address", s.getAddress().toString()); - exports.add(exp); - } - } - - // Apply pagination - int start = Math.max(0, offset); - int end = Math.min(exports.size(), offset + limit); - List> paginated = exports.subList(start, end); - - // Use helper to create standard response - return createSuccessResponse(paginated); // Return JsonObject directly - } - - private JsonObject listNamespaces(int offset, int limit) { // Changed return type to JsonObject - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - - Set namespaces = new HashSet<>(); - for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) { - Namespace ns = symbol.getParentNamespace(); - if (ns != null && !(ns instanceof GlobalNamespace)) { - namespaces.add(ns.getName()); - } - } - - List sorted = new ArrayList<>(namespaces); - Collections.sort(sorted); - - // Apply pagination - int start = Math.max(0, offset); - int end = Math.min(sorted.size(), offset + limit); - List paginated = sorted.subList(start, end); - - // Use helper to create standard response - return createSuccessResponse(paginated); // Return JsonObject directly - } - - private JsonObject listDefinedData(int offset, int limit) { // Changed return type to JsonObject - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - - List> dataItems = new ArrayList<>(); - for (MemoryBlock block : program.getMemory().getBlocks()) { - DataIterator it = program.getListing().getDefinedData(block.getStart(), true); - while (it.hasNext()) { - Data data = it.next(); - if (block.contains(data.getAddress())) { - Map item = new HashMap<>(); - item.put("address", data.getAddress().toString()); - item.put("label", data.getLabel() != null ? data.getLabel() : "(unnamed)"); - item.put("value", data.getDefaultValueRepresentation()); - dataItems.add(item); - } - } - } - - // Apply pagination - int start = Math.max(0, offset); - int end = Math.min(dataItems.size(), offset + limit); - List> paginated = dataItems.subList(start, end); - - // Use helper to create standard response - return createSuccessResponse(paginated); // Return JsonObject directly - } - - private JsonObject searchFunctionsByName(String searchTerm, int offset, int limit) { // Changed return type to JsonObject - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - if (searchTerm == null || searchTerm.isEmpty()) { - return createErrorResponse("Search term is required", 400); - } - - List matches = new ArrayList<>(); - for (Function func : program.getFunctionManager().getFunctions(true)) { - String name = func.getName(); - // simple substring match - if (name.toLowerCase().contains(searchTerm.toLowerCase())) { - matches.add(String.format("%s @ %s", name, func.getEntryPoint())); - } - } - - Collections.sort(matches); - - if (matches.isEmpty()) { - // Return success with empty result list - return createSuccessResponse(new ArrayList<>()); - } - - // Paginate the string list representation - int start = Math.max(0, offset); - int end = Math.min(matches.size(), offset + limit); - List sub = matches.subList(start, end); - - // Return paginated list using helper - return createSuccessResponse(sub); - } - - // ---------------------------------------------------------------------------------- - // Logic for getting function details, rename, decompile, etc. - // ---------------------------------------------------------------------------------- - - private JsonObject getFunctionDetailsByName(String name) { - JsonObject response = new JsonObject(); - Program program = getCurrentProgram(); - if (program == null) { - response.addProperty("success", false); - response.addProperty("error", "No program loaded"); - return response; - } - - Function func = findFunctionByName(program, name); - if (func == null) { - response.addProperty("success", false); - response.addProperty("error", "Function not found: " + name); - return response; - } - - return getFunctionDetails(func); // Use common helper - } - - // Helper to get function details and decompilation - private JsonObject getFunctionDetails(Function func) { - JsonObject response = new JsonObject(); - JsonObject resultObj = new JsonObject(); - Program program = func.getProgram(); - - resultObj.addProperty("name", func.getName()); - resultObj.addProperty("address", func.getEntryPoint().toString()); - resultObj.addProperty("signature", func.getSignature().getPrototypeString()); - - DecompInterface decomp = new DecompInterface(); - try { - // Default to C code output and no syntax tree for better readability - decomp.toggleCCode(true); - decomp.setSimplificationStyle("normalize"); - decomp.toggleSyntaxTree(false); - - if (!decomp.openProgram(program)) { - resultObj.addProperty("decompilation_error", "Failed to initialize decompiler"); - } else { - DecompileResults decompResult = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); - if (decompResult == null) { - resultObj.addProperty("decompilation_error", "Decompilation returned null result"); - } else if (!decompResult.decompileCompleted()) { - resultObj.addProperty("decompilation_error", "Decompilation failed or timed out"); - } else { - // Handle decompilation result with proper JSON structure - JsonObject decompilationResult = new JsonObject(); - - ghidra.app.decompiler.DecompiledFunction decompiledFunc = decompResult.getDecompiledFunction(); - if (decompiledFunc == null) { - decompilationResult.addProperty("error", "Could not get decompiled function"); - } else { - String decompiledCode = decompiledFunc.getC(); - if (decompiledCode != null) { - decompilationResult.addProperty("code", decompiledCode); - } else { - decompilationResult.addProperty("error", "Decompiled code is null"); - } - } - - resultObj.add("decompilation", decompilationResult); - } - } - } catch (Exception e) { - Msg.error(this, "Decompilation error for " + func.getName(), e); - resultObj.addProperty("decompilation_error", "Exception during decompilation: " + e.getMessage()); - } finally { - decomp.dispose(); - } - - response.addProperty("success", true); - response.add("result", resultObj); - response.addProperty("timestamp", System.currentTimeMillis()); - response.addProperty("port", this.port); - return response; - } - - private JsonObject decompileFunctionByName(String name) { // Changed return type - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - - DecompInterface decomp = new DecompInterface(); - try { - if (!decomp.openProgram(program)) { - return createErrorResponse("Failed to initialize decompiler", 500); - } - - Function func = findFunctionByName(program, name); - if (func == null) { - return createErrorResponse("Function not found: " + name, 404); - } - - DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); - if (result != null && result.decompileCompleted()) { - JsonObject resultObj = new JsonObject(); - resultObj.addProperty("name", func.getName()); - resultObj.addProperty("address", func.getEntryPoint().toString()); - resultObj.addProperty("signature", func.getSignature().getPrototypeString()); - resultObj.addProperty("decompilation", result.getDecompiledFunction().getC()); - - // Use helper to create standard response - return createSuccessResponse(resultObj); // Return JsonObject - } else { - return createErrorResponse("Decompilation failed", 500); - } - } finally { - decomp.dispose(); - } - } - - private boolean renameFunctionByAddress(Address functionAddress, String newName) { - Program program = getCurrentProgram(); - if (program == null) return false; - - AtomicBoolean successFlag = new AtomicBoolean(false); - try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction("Rename function via HTTP"); - try { - Function func = program.getFunctionManager().getFunctionAt(functionAddress); - if (func != null) { - func.setName(newName, SourceType.USER_DEFINED); - successFlag.set(true); - } - } - catch (Exception e) { - Msg.error(this, "Error renaming function", e); - } - finally { - program.endTransaction(tx, successFlag.get()); - } - }); - } - catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute rename on Swing thread", e); - } - return successFlag.get(); - } - - private boolean setDecompilerComment(Address address, String comment) { - Program program = getCurrentProgram(); - if (program == null) return false; - - AtomicBoolean successFlag = new AtomicBoolean(false); - try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction("Set decompiler comment"); - try { - DecompInterface decomp = new DecompInterface(); - decomp.openProgram(program); - - Function func = program.getFunctionManager().getFunctionContaining(address); - if (func != null) { - DecompileResults results = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); - if (results != null && results.decompileCompleted()) { - HighFunction highFunc = results.getHighFunction(); - if (highFunc != null) { - program.getListing().setComment(address, CodeUnit.PRE_COMMENT, comment); - successFlag.set(true); - } - } - } - } - catch (Exception e) { - Msg.error(this, "Error setting decompiler comment", e); - } - finally { - program.endTransaction(tx, successFlag.get()); - } - }); - } - catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute set comment on Swing thread", e); - } - return successFlag.get(); - } - - private boolean setDisassemblyComment(Address address, String comment) { - Program program = getCurrentProgram(); - if (program == null) return false; - - AtomicBoolean successFlag = new AtomicBoolean(false); - try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction("Set disassembly comment"); - try { - Listing listing = program.getListing(); - listing.setComment(address, CodeUnit.EOL_COMMENT, comment); - successFlag.set(true); - } - catch (Exception e) { - Msg.error(this, "Error setting disassembly comment", e); - } - finally { - program.endTransaction(tx, successFlag.get()); - } - }); - } - catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute set comment on Swing thread", e); - } - return successFlag.get(); - } - - private boolean renameFunction(String oldName, String newName) { - Program program = getCurrentProgram(); - if (program == null) return false; - - AtomicBoolean successFlag = new AtomicBoolean(false); - try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction("Rename function via HTTP"); - try { - for (Function func : program.getFunctionManager().getFunctions(true)) { - if (func.getName().equals(oldName)) { - func.setName(newName, SourceType.USER_DEFINED); - successFlag.set(true); - break; - } - } - } - catch (Exception e) { - Msg.error(this, "Error renaming function", e); - } - finally { - program.endTransaction(tx, successFlag.get()); - } - }); - } - catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute rename on Swing thread", e); - } - return successFlag.get(); - } - - private boolean renameDataAtAddress(String addressStr, String newName) { - Program program = getCurrentProgram(); - if (program == null) return false; - - AtomicBoolean successFlag = new AtomicBoolean(false); - try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction("Rename data"); - try { - Address addr = program.getAddressFactory().getAddress(addressStr); - Listing listing = program.getListing(); - Data data = listing.getDefinedDataAt(addr); - if (data != null) { - SymbolTable symTable = program.getSymbolTable(); - Symbol symbol = symTable.getPrimarySymbol(addr); - if (symbol != null) { - symbol.setName(newName, SourceType.USER_DEFINED); - successFlag.set(true); - } else { - symTable.createLabel(addr, newName, SourceType.USER_DEFINED); - successFlag.set(true); - } - } - } - catch (Exception e) { - Msg.error(this, "Rename data error", e); - } - finally { - program.endTransaction(tx, successFlag.get()); - } - }); - } - catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute rename data on Swing thread", e); - } - return successFlag.get(); - } - - // ---------------------------------------------------------------------------------- - // New variable handling methods - // ---------------------------------------------------------------------------------- - - private JsonObject listVariablesInFunction(String functionName) { // Changed return type - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - - DecompInterface decomp = new DecompInterface(); - try { - if (!decomp.openProgram(program)) { - return createErrorResponse("Failed to initialize decompiler", 500); - } - - Function function = findFunctionByName(program, functionName); - if (function == null) { - return createErrorResponse("Function not found: " + functionName, 404); - } - - DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); - if (results == null || !results.decompileCompleted()) { - return createErrorResponse("Failed to decompile function: " + functionName, 500); - } - - // Get high-level pcode representation for the function - HighFunction highFunction = results.getHighFunction(); - if (highFunction == null) { - return createErrorResponse("Failed to get high function for: " + functionName, 500); - } - - // Get all variables (parameters and locals) - List> allVariables = new ArrayList<>(); - - // Process all symbols - Iterator symbolIter = highFunction.getLocalSymbolMap().getSymbols(); - while (symbolIter.hasNext()) { - HighSymbol symbol = symbolIter.next(); - - Map varInfo = new HashMap<>(); - varInfo.put("name", symbol.getName()); - - DataType dt = symbol.getDataType(); - String dtName = dt != null ? dt.getName() : "unknown"; - varInfo.put("dataType", dtName); - - if (symbol.isParameter()) { - varInfo.put("type", "parameter"); - } else if (symbol.getHighVariable() != null) { - varInfo.put("type", "local"); - varInfo.put("address", symbol.getPCAddress().toString()); - } else { - continue; // Skip symbols without high variables that aren't parameters - } - - allVariables.add(varInfo); - } - - // Sort by name - Collections.sort(allVariables, (a, b) -> a.get("name").compareTo(b.get("name"))); - - // Create JSON response - JsonObject response = new JsonObject(); - response.addProperty("success", true); - - JsonObject resultObj = new JsonObject(); - resultObj.addProperty("function", functionName); - resultObj.add("variables", new Gson().toJsonTree(allVariables)); - - // Use helper to create standard response - return createSuccessResponse(resultObj); // Return JsonObject - } finally { - decomp.dispose(); - } - } - - private boolean renameVariable(String functionName, String oldName, String newName) { - Program program = getCurrentProgram(); - if (program == null) return false; - - DecompInterface decomp = new DecompInterface(); - decomp.openProgram(program); - - Function func = null; - for (Function f : program.getFunctionManager().getFunctions(true)) { - if (f.getName().equals(functionName)) { - func = f; - break; - } - } - - if (func == null) { - return false; - } - - DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); - if (result == null || !result.decompileCompleted()) { - return false; - } - - HighFunction highFunction = result.getHighFunction(); - if (highFunction == null) { - return false; - } - - LocalSymbolMap localSymbolMap = highFunction.getLocalSymbolMap(); - if (localSymbolMap == null) { - return false; - } - - HighSymbol highSymbol = null; - Iterator symbols = localSymbolMap.getSymbols(); - while (symbols.hasNext()) { - HighSymbol symbol = symbols.next(); - String symbolName = symbol.getName(); - - if (symbolName.equals(oldName)) { - highSymbol = symbol; - } - if (symbolName.equals(newName)) { - return false; - } - } - - if (highSymbol == null) { - return false; - } - - boolean commitRequired = checkFullCommit(highSymbol, highFunction); - - final HighSymbol finalHighSymbol = highSymbol; - final Function finalFunction = func; - AtomicBoolean successFlag = new AtomicBoolean(false); - - try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction("Rename variable"); - try { - if (commitRequired) { - HighFunctionDBUtil.commitParamsToDatabase(highFunction, false, - ReturnCommitOption.NO_COMMIT, finalFunction.getSignatureSource()); - } - HighFunctionDBUtil.updateDBVariable( - finalHighSymbol, - newName, - null, - SourceType.USER_DEFINED - ); - successFlag.set(true); - } - catch (Exception e) { - Msg.error(this, "Failed to rename variable", e); - } - finally { - program.endTransaction(tx, true); - } - }); - } catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute rename on Swing thread", e); - return false; - } - return successFlag.get(); - } - - /** - * Copied from AbstractDecompilerAction.checkFullCommit, it's protected. - * Compare the given HighFunction's idea of the prototype with the Function's idea. - * Return true if there is a difference. If a specific symbol is being changed, - * it can be passed in to check whether or not the prototype is being affected. - * @param highSymbol (if not null) is the symbol being modified - * @param hfunction is the given HighFunction - * @return true if there is a difference (and a full commit is required) - */ - protected static boolean checkFullCommit(HighSymbol highSymbol, HighFunction hfunction) { - if (highSymbol != null && !highSymbol.isParameter()) { - return false; - } - Function function = hfunction.getFunction(); - Parameter[] parameters = function.getParameters(); - LocalSymbolMap localSymbolMap = hfunction.getLocalSymbolMap(); - int numParams = localSymbolMap.getNumParams(); - if (numParams != parameters.length) { - return true; - } - - for (int i = 0; i < numParams; i++) { - HighSymbol param = localSymbolMap.getParamSymbol(i); - if (param.getCategoryIndex() != i) { - return true; - } - VariableStorage storage = param.getStorage(); - // Don't compare using the equals method so that DynamicVariableStorage can match - if (0 != storage.compareTo(parameters[i].getVariableStorage())) { - return true; - } - } - - return false; - } - - private boolean retypeVariable(String functionName, String varName, String dataTypeName) { - if (varName == null || varName.isEmpty() || dataTypeName == null || dataTypeName.isEmpty()) { - return false; - } - - Program program = getCurrentProgram(); - if (program == null) return false; - - AtomicBoolean result = new AtomicBoolean(false); - - try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction("Retype variable via HTTP"); - try { - Function function = findFunctionByName(program, functionName); - if (function == null) { - return; - } - - // Initialize decompiler - DecompInterface decomp = new DecompInterface(); - decomp.openProgram(program); - DecompileResults decompRes = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); - - if (decompRes == null || !decompRes.decompileCompleted()) { - return; - } - - HighFunction highFunction = decompRes.getHighFunction(); - if (highFunction == null) { - return; - } - - // Find the variable by name - must match exactly and be in current scope - HighSymbol targetSymbol = null; - Iterator symbolIter = highFunction.getLocalSymbolMap().getSymbols(); - while (symbolIter.hasNext()) { - HighSymbol symbol = symbolIter.next(); - if (symbol.getName().equals(varName) && - symbol.getPCAddress().equals(function.getEntryPoint())) { - targetSymbol = symbol; - break; - } - } - - if (targetSymbol == null) { - return; - } - - // Find the data type by name - DataType dataType = findDataType(program, dataTypeName); - if (dataType == null) { - return; - } - - // Retype the variable - HighFunctionDBUtil.updateDBVariable(targetSymbol, targetSymbol.getName(), dataType, - SourceType.USER_DEFINED); - - result.set(true); - } catch (Exception e) { - Msg.error(this, "Error retyping variable", e); - result.set(false); - } finally { - program.endTransaction(tx, true); - } - }); - } catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute on Swing thread", e); - result.set(false); - } - - return result.get(); - } - - private JsonObject listVariables(int offset, int limit, String searchTerm) { - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - - List> variables = new ArrayList<>(); - - // Get global variables - SymbolTable symbolTable = program.getSymbolTable(); - for (Symbol symbol : symbolTable.getDefinedSymbols()) { - if (symbol.isGlobal() && !symbol.isExternal() && - symbol.getSymbolType() != SymbolType.FUNCTION && - symbol.getSymbolType() != SymbolType.LABEL) { - - Map varInfo = new HashMap<>(); - varInfo.put("name", symbol.getName()); - varInfo.put("address", symbol.getAddress().toString()); - varInfo.put("type", "global"); - varInfo.put("dataType", getDataTypeName(program, symbol.getAddress())); - variables.add(varInfo); - } - } - - // Get local variables from all functions - DecompInterface decomp = null; // Initialize outside try - try { - decomp = new DecompInterface(); // Create inside try - if (!decomp.openProgram(program)) { - Msg.error(this, "listVariables: Failed to open program with decompiler."); - // Continue with only global variables if decompiler fails to open - } else { - for (Function function : program.getFunctionManager().getFunctions(true)) { - try { - DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); - if (results != null && results.decompileCompleted()) { - HighFunction highFunc = results.getHighFunction(); - if (highFunc != null) { - Iterator symbolIter = highFunc.getLocalSymbolMap().getSymbols(); - while (symbolIter.hasNext()) { - HighSymbol symbol = symbolIter.next(); - if (!symbol.isParameter()) { // Only list locals, not params - Map varInfo = new HashMap<>(); - varInfo.put("name", symbol.getName()); - varInfo.put("type", "local"); - varInfo.put("function", function.getName()); - // Handle null PC address for some local variables - Address pcAddr = symbol.getPCAddress(); - varInfo.put("address", pcAddr != null ? pcAddr.toString() : "N/A"); - varInfo.put("dataType", symbol.getDataType() != null ? symbol.getDataType().getName() : "unknown"); - variables.add(varInfo); - } - } - } else { - Msg.warn(this, "listVariables: Failed to get HighFunction for " + function.getName()); - } - } else { - Msg.warn(this, "listVariables: Decompilation failed or timed out for " + function.getName()); - } - } catch (Exception e) { - Msg.error(this, "listVariables: Error processing function " + function.getName(), e); - // Continue to the next function if one fails - } - } - } - } catch (Exception e) { - Msg.error(this, "listVariables: Error during local variable processing", e); - // If a major error occurs, we might still have global variables - } finally { - if (decomp != null) { - decomp.dispose(); // Ensure disposal - } - } - - // Sort by name - Collections.sort(variables, (a, b) -> a.get("name").compareTo(b.get("name"))); - - // Apply pagination - int start = Math.max(0, offset); - int end = Math.min(variables.size(), offset + limit); - List> paginated = variables.subList(start, end); - - // Create JSON response - // Use helper to create standard response - return createSuccessResponse(paginated); - } - - private JsonObject searchVariables(String searchTerm, int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) { - return createErrorResponse("No program loaded", 400); - } - - if (searchTerm == null || searchTerm.isEmpty()) { - return createErrorResponse("Search term is required", 400); - } - - List> matchedVars = new ArrayList<>(); - - // Search global variables - SymbolTable symbolTable = program.getSymbolTable(); - SymbolIterator it = symbolTable.getSymbolIterator(); - while (it.hasNext()) { - Symbol symbol = it.next(); - if (symbol.isGlobal() && - symbol.getSymbolType() != SymbolType.FUNCTION && - symbol.getSymbolType() != SymbolType.LABEL && - symbol.getName().toLowerCase().contains(searchTerm.toLowerCase())) { - Map varInfo = new HashMap<>(); - varInfo.put("name", symbol.getName()); - varInfo.put("address", symbol.getAddress().toString()); - varInfo.put("type", "global"); - matchedVars.add(varInfo); - } - } - - // Search local variables in functions - DecompInterface decomp = new DecompInterface(); - try { - if (decomp.openProgram(program)) { - for (Function function : program.getFunctionManager().getFunctions(true)) { - DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); - if (results != null && results.decompileCompleted()) { - HighFunction highFunc = results.getHighFunction(); - if (highFunc != null) { - // Check each local variable and parameter - Iterator symbolIter = highFunc.getLocalSymbolMap().getSymbols(); - while (symbolIter.hasNext()) { - HighSymbol symbol = symbolIter.next(); - if (symbol.getName().toLowerCase().contains(searchTerm.toLowerCase())) { - Map varInfo = new HashMap<>(); - varInfo.put("name", symbol.getName()); - varInfo.put("function", function.getName()); - - if (symbol.isParameter()) { - varInfo.put("type", "parameter"); - } else { - varInfo.put("type", "local"); - varInfo.put("address", symbol.getPCAddress().toString()); - } - - matchedVars.add(varInfo); - } - } - } - } - } - } - } finally { - decomp.dispose(); - } - - // Sort by name - Collections.sort(matchedVars, (a, b) -> a.get("name").compareTo(b.get("name"))); - - // Apply pagination - int start = Math.max(0, offset); - int end = Math.min(matchedVars.size(), offset + limit); - List> paginated = matchedVars.subList(start, end); - - // Create JSON response - // Use helper to create standard response - return createSuccessResponse(paginated); - } - - // ---------------------------------------------------------------------------------- - // Standardized JSON Response Helpers (Following GHIDRA_HTTP_API.md v1) - // ---------------------------------------------------------------------------------- - - /** - * Creates the base structure for all JSON responses. - * Includes the request ID and instance URL. - * @param exchange The HTTP exchange to extract headers from. - * @return A JsonObject with 'id' and 'instance' fields. - */ - /** - * Builder for standardized API responses - */ - private static class ResponseBuilder { - private final HttpExchange exchange; - private final int port; - private JsonObject response; - private JsonObject links; - - public ResponseBuilder(HttpExchange exchange, int port) { - this.exchange = exchange; - this.port = port; - this.response = new JsonObject(); - this.links = new JsonObject(); - - String requestId = exchange.getRequestHeaders().getFirst("X-Request-ID"); - response.addProperty("id", requestId != null ? requestId : UUID.randomUUID().toString()); - response.addProperty("instance", "http://localhost:" + port); - } - - public ResponseBuilder success(boolean success) { - response.addProperty("success", success); - return this; - } - - public ResponseBuilder result(Object data) { - Gson gson = new Gson(); - response.add("result", gson.toJsonTree(data)); - return this; - } - - public ResponseBuilder error(String message, String code) { - JsonObject error = new JsonObject(); - error.addProperty("message", message); - if (code != null) { - error.addProperty("code", code); - } - response.add("error", error); - return this; - } - - public ResponseBuilder addLink(String rel, String href) { - JsonObject link = new JsonObject(); - link.addProperty("href", href); - links.add(rel, link); - return this; - } - - public JsonObject build() { - if (links.size() > 0) { - response.add("_links", links); - } - return response; - } - } - - private JsonObject createBaseResponse(HttpExchange exchange) { - return new ResponseBuilder(exchange, port).build(); - } - - private JsonObject createSuccessResponse(HttpExchange exchange, Object resultData, JsonObject links) { - ResponseBuilder builder = new ResponseBuilder(exchange, port) - .success(true) - .result(resultData); - - if (links != null) { - builder.links = links; - } - return builder.build(); - } - - private JsonObject createErrorResponse(HttpExchange exchange, String message, String errorCode) { - return new ResponseBuilder(exchange, port) - .success(false) - .error(message, errorCode) - .build(); - } - - // Overload for simple success with no data and no links - private JsonObject createSuccessResponse(HttpExchange exchange) { - return createSuccessResponse(exchange, null, null); - } - - /** - * Creates a standardized error response JSON object. - * @param exchange The HTTP exchange. - * @param message A descriptive error message. - * @param errorCode An optional machine-readable error code string. - * @return A JsonObject representing the error response. - */ - private JsonObject createErrorResponse(HttpExchange exchange, String message, String errorCode) { - JsonObject response = createBaseResponse(exchange); - response.addProperty("success", false); - JsonObject errorObj = new JsonObject(); - errorObj.addProperty("message", message != null ? message : "An unknown error occurred."); - if (errorCode != null && !errorCode.isEmpty()) { - errorObj.addProperty("code", errorCode); - } - response.add("error", errorObj); - return response; - } - - // Overload for error with just message - private JsonObject createErrorResponse(HttpExchange exchange, String message) { - return createErrorResponse(exchange, message, null); - } - - // --- Deprecated Helpers (Marked for removal) --- - // These are kept temporarily only if absolutely needed during refactoring, - // but the goal is to replace all their usages with the new helpers above. - @Deprecated - private JsonObject createSuccessResponse(Object resultData) { - JsonObject response = new JsonObject(); - response.addProperty("success", true); - if (resultData != null) { - response.add("result", new Gson().toJsonTree(resultData)); - } else { - response.add("result", null); - } - response.addProperty("timestamp", System.currentTimeMillis()); // Deprecated field - response.addProperty("port", this.port); // Deprecated field - return response; - } - - @Deprecated - private JsonObject createErrorResponse(String errorMessage, int statusCode) { - JsonObject response = new JsonObject(); - response.addProperty("success", false); - response.addProperty("error", errorMessage); // Deprecated structure - response.addProperty("status_code", statusCode); // Deprecated field - response.addProperty("timestamp", System.currentTimeMillis()); // Deprecated field - response.addProperty("port", this.port); // Deprecated field - return response; - } - // --- End Deprecated Helpers --- - - // ---------------------------------------------------------------------------------- - // Transaction Management Helper - // ---------------------------------------------------------------------------------- - - /** - * Executes a Ghidra operation that modifies the program state within a transaction. - * Handles Swing thread invocation and ensures the transaction is properly managed. - * - * @param The return type of the operation (can be Void for operations without return value). - * @param program The program context for the transaction. Must not be null. - * @param transactionName A descriptive name for the Ghidra transaction log. - * @param operation A supplier function (using GhidraSupplier functional interface) - * that performs the Ghidra API calls and returns a result. - * This function MUST NOT start or end its own transaction. - * @return The result of the operation. - * @throws TransactionException If the operation fails within the transaction or - * if execution on the Swing thread fails. Wraps the original cause. - * @throws IllegalArgumentException If program is null. - */ - private T executeInTransaction(Program program, String transactionName, GhidraSupplier operation) throws TransactionException { - if (program == null) { - throw new IllegalArgumentException("Program cannot be null for transaction"); - } - - final class ResultContainer { - T value = null; - Exception exception = null; - } - final ResultContainer resultContainer = new ResultContainer(); - - try { - SwingUtilities.invokeAndWait(() -> { - int txId = -1; - boolean success = false; - try { - txId = program.startTransaction(transactionName); - if (txId < 0) { - throw new TransactionException("Failed to start transaction: " + transactionName + ". Already in a transaction?"); - } - resultContainer.value = operation.get(); - success = true; - } catch (Exception e) { - Msg.error(this, "Exception during transaction: " + transactionName, e); - resultContainer.exception = e; - success = false; - } finally { - if (txId >= 0) { - program.endTransaction(txId, success); - Msg.debug(this, "Transaction '" + transactionName + "' ended. Success: " + success); - } - } - }); - } catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute transaction '" + transactionName + "' on Swing thread", e); - throw new TransactionException("Failed to execute operation on Swing thread", e); - } - - if (resultContainer.exception != null) { - throw new TransactionException("Operation failed within transaction: " + transactionName, resultContainer.exception); - } - - return resultContainer.value; - } - - /** - * Overload of executeInTransaction for operations that don't return a value (Runnable). - * @param program The program context for the transaction. - * @param transactionName The name for the Ghidra transaction log. - * @param operation A Runnable that performs the Ghidra API calls. - * @throws TransactionException If the operation fails. - */ - private void executeInTransaction(Program program, String transactionName, Runnable operation) throws TransactionException { - executeInTransaction(program, transactionName, () -> { - operation.run(); - return null; - }); - } - - /** Custom exception for transaction-related errors. */ - public static class TransactionException extends Exception { - public TransactionException(String message) { super(message); } - public TransactionException(String message, Throwable cause) { super(message, cause); } - } - - // ---------------------------------------------------------------------------------- - // HTTP Response Sending Methods - // ---------------------------------------------------------------------------------- - - /** - * Sends a standard success JSON response with a 200 OK status. - * @param exchange The HTTP exchange. - * @param resultData The data payload for the 'result' field (can be null). - * @param links Optional HATEOAS links. - * @throws IOException If sending the response fails. - */ - private void sendSuccessResponse(HttpExchange exchange, Object resultData, JsonObject links) throws IOException { - sendJsonResponse(exchange, createSuccessResponse(exchange, resultData, links), 200); - } - - // Overload for success with data, no links - private void sendSuccessResponse(HttpExchange exchange, Object resultData) throws IOException { - sendSuccessResponse(exchange, resultData, null); - } - - // Overload for simple success, no data, no links (e.g., for 204 No Content) - private void sendSuccessResponse(HttpExchange exchange) throws IOException { - sendSuccessResponse(exchange, null, null); - } - - /** - * Sends a standard error JSON response with the specified HTTP status code. - * @param exchange The HTTP exchange. - * @param statusCode The HTTP status code (e.g., 400, 404, 500). - * @param message A descriptive error message. - * @param errorCode An optional machine-readable error code string. - * @throws IOException If sending the response fails. - */ - private void sendErrorResponse(HttpExchange exchange, int statusCode, String message, String errorCode) throws IOException { - sendJsonResponse(exchange, createErrorResponse(exchange, message, errorCode), statusCode); - } - - // Overload for error without specific code - private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) throws IOException { - sendErrorResponse(exchange, statusCode, message, null); - } - - /** - * Core method to send any JsonObject response with a specific status code. - * Handles JSON serialization, setting headers, and writing the response body. - * @param exchange The HTTP exchange. - * @param jsonObj The JsonObject to send. - * @param statusCode The HTTP status code to set. - * @throws IOException If sending the response fails. - */ - private void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj, int statusCode) throws IOException { - try { - Gson gson = new Gson(); - String json = gson.toJson(jsonObj); - if (json.length() < 1024) { - Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json); - } else { - Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json.substring(0, 1020) + "..."); - } - - byte[] bytes = json.getBytes(StandardCharsets.UTF_8); - exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); - - long responseLength = (statusCode == 204) ? -1 : bytes.length; - exchange.sendResponseHeaders(statusCode, responseLength); - - if (responseLength != -1) { - 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) { /* Log or ignore */ } - } - } - } else { - exchange.getResponseBody().close(); - } - } catch (Exception e) { - Msg.error(this, "Error sending JSON response: " + e.getMessage(), e); - throw new IOException("Failed to send JSON response", e); - } - } - - // ---------------------------------------------------------------------------------- - // Utility: parse query params, parse post params, pagination, etc. - // ---------------------------------------------------------------------------------- - - /** - * Executes a Ghidra operation that modifies the program state within a transaction. - * Handles Swing thread invocation and ensures the transaction is properly managed. - * - * @param The return type of the operation (can be Void for operations without return value). - * @param program The program context for the transaction. Must not be null. - * @param transactionName A descriptive name for the Ghidra transaction log. - * @param operation A supplier function (using GhidraSupplier functional interface) - * that performs the Ghidra API calls and returns a result. - * This function MUST NOT start or end its own transaction. - * @return The result of the operation. - * @throws TransactionException If the operation fails within the transaction or - * if execution on the Swing thread fails. Wraps the original cause. - * @throws IllegalArgumentException If program is null. - */ - private T executeInTransaction(Program program, String transactionName, GhidraSupplier operation) throws TransactionException { - if (program == null) { - throw new IllegalArgumentException("Program cannot be null for transaction"); - } - - // Use a simple container to pass results/exceptions back from the Swing thread - final class ResultContainer { - T value = null; - Exception exception = null; - } - final ResultContainer resultContainer = new ResultContainer(); - - try { - // Ensure the operation runs on the Swing Event Dispatch Thread (EDT) - // as required by many Ghidra API calls that modify state. - SwingUtilities.invokeAndWait(() -> { - int txId = -1; // Initialize transaction ID - boolean success = false; - try { - txId = program.startTransaction(transactionName); - if (txId < 0) { - // Handle case where transaction could not be started (e.g., already in transaction) - // This ideally shouldn't happen if called correctly, but good to check. - throw new TransactionException("Failed to start transaction: " + transactionName + ". Already in a transaction?"); - } - resultContainer.value = operation.get(); // Execute the actual Ghidra operation - success = true; // Mark as success if no exception was thrown - } catch (Exception e) { - // Catch any exception from the operation - Msg.error(this, "Exception during transaction: " + transactionName, e); - resultContainer.exception = e; // Store the exception - success = false; // Ensure transaction is rolled back - } finally { - // Always end the transaction, committing only if success is true - if (txId >= 0) { // Only end if successfully started - program.endTransaction(txId, success); - Msg.debug(this, "Transaction '" + transactionName + "' ended. Success: " + success); - } - } - }); - } catch (InterruptedException | InvocationTargetException e) { - // Handle exceptions related to SwingUtilities.invokeAndWait - Msg.error(this, "Failed to execute transaction '" + transactionName + "' on Swing thread", e); - // Wrap this error in our custom exception type - throw new TransactionException("Failed to execute operation on Swing thread", e); - } - - // Check if an exception occurred within the Ghidra operation itself - if (resultContainer.exception != null) { - // Wrap the original Ghidra operation exception - throw new TransactionException("Operation failed within transaction: " + transactionName, resultContainer.exception); - } - - // Return the result from the operation - return resultContainer.value; - } - - /** - * Overload of executeInTransaction for operations that don't return a value (Runnable). - * - * @param program The program context for the transaction. - * @param transactionName The name for the Ghidra transaction log. - * @param operation A Runnable that performs the Ghidra API calls. - * @throws TransactionException If the operation fails. - */ - private void executeInTransaction(Program program, String transactionName, Runnable operation) throws TransactionException { - // Wrap the Runnable in a GhidraSupplier that returns Void - executeInTransaction(program, transactionName, () -> { - operation.run(); - return null; // Return null for void operations - }); - } - - /** - * Custom exception for transaction-related errors. - */ - public static class TransactionException extends Exception { - public TransactionException(String message) { - super(message); - } - - public TransactionException(String message, Throwable cause) { - super(message, cause); - } - } - - // ---------------------------------------------------------------------------------- - // HTTP Response Sending Methods - // ---------------------------------------------------------------------------------- - - /** - * Sends a standard success JSON response with a 200 OK status. - * @param exchange The HTTP exchange. - * @param resultData The data payload for the 'result' field (can be null). - * @param links Optional HATEOAS links. - * @throws IOException If sending the response fails. - */ - private void sendSuccessResponse(HttpExchange exchange, Object resultData, JsonObject links) throws IOException { - sendJsonResponse(exchange, createSuccessResponse(exchange, resultData, links), 200); - } - - // Overload for success with data, no links - private void sendSuccessResponse(HttpExchange exchange, Object resultData) throws IOException { - sendSuccessResponse(exchange, resultData, null); - } - - // Overload for simple success, no data, no links (e.g., for 204 No Content) - private void sendSuccessResponse(HttpExchange exchange) throws IOException { - sendSuccessResponse(exchange, null, null); - } - - /** - * Sends a standard error JSON response with the specified HTTP status code. - * @param exchange The HTTP exchange. - * @param statusCode The HTTP status code (e.g., 400, 404, 500). - * @param message A descriptive error message. - * @param errorCode An optional machine-readable error code string. - * @throws IOException If sending the response fails. - */ - private void sendErrorResponse(HttpExchange exchange, int statusCode, String message, String errorCode) throws IOException { - sendJsonResponse(exchange, createErrorResponse(exchange, message, errorCode), statusCode); - } - - // Overload for error without specific code - private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) throws IOException { - sendErrorResponse(exchange, statusCode, message, null); - } - - /** - * Core method to send any JsonObject response with a specific status code. - * Handles JSON serialization, setting headers, and writing the response body. - * @param exchange The HTTP exchange. - * @param jsonObj The JsonObject to send. - * @param statusCode The HTTP status code to set. - * @throws IOException If sending the response fails. - */ - private void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj, int statusCode) throws IOException { - try { - Gson gson = new Gson(); - String json = gson.toJson(jsonObj); - // Use Msg.debug for potentially large responses - if (json.length() < 1024) { - Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json); - } else { - Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json.substring(0, 1020) + "..."); - } - - byte[] bytes = json.getBytes(StandardCharsets.UTF_8); - exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); - // Ensure CORS headers are set if needed (example, adjust as necessary) - // exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); - - // Determine response length: 0 for 204, actual length otherwise - long responseLength = (statusCode == 204) ? -1 : bytes.length; - exchange.sendResponseHeaders(statusCode, responseLength); - - // Only write body if there is content (not for 204) - if (responseLength != -1) { - OutputStream os = null; - try { - os = exchange.getResponseBody(); - os.write(bytes); - os.flush(); - } catch (IOException e) { - // Log error, but don't try to send another response if body writing fails - Msg.error(this, "Error writing response body: " + e.getMessage(), e); - throw e; // Re-throw to indicate failure - } finally { - if (os != null) { - try { - os.close(); - } catch (IOException e) { - // Log error during close, but don't mask original exception if any - Msg.error(this, "Error closing output stream: " + e.getMessage(), e); - } - } - } - } else { - // For 204 No Content, just close the exchange without writing body - exchange.getResponseBody().close(); - } - } catch (Exception e) { - // Catch broader exceptions during response preparation/sending - Msg.error(this, "Error sending JSON response: " + e.getMessage(), e); - // Avoid sending another error response here to prevent potential loops - throw new IOException("Failed to send JSON response", e); - } - } - - // ---------------------------------------------------------------------------------- - // Utility: parse query params, parse post params, pagination, etc. - // ---------------------------------------------------------------------------------- - - /** - * Parse query parameters from the URL, e.g. ?offset=10&limit=100 - */ - private Map parseQueryParams(HttpExchange exchange) { - Map result = new HashMap<>(); - String query = exchange.getRequestURI().getQuery(); // e.g. offset=10&limit=100 - if (query != null) { - String[] pairs = query.split("&"); - for (String p : pairs) { - String[] kv = p.split("="); - if (kv.length == 2) { - result.put(kv[0], kv[1]); - } - } - } - return result; - } - - /** - * Parse post body params strictly as JSON. - */ - private Map parseJsonPostParams(HttpExchange exchange) throws IOException { - byte[] body = exchange.getRequestBody().readAllBytes(); - String bodyStr = new String(body, StandardCharsets.UTF_8); - Map params = new HashMap<>(); - - try { - // Use Gson to properly parse JSON - Gson gson = new Gson(); - JsonObject json = gson.fromJson(bodyStr, JsonObject.class); - - for (Map.Entry entry : json.entrySet()) { - String key = entry.getKey(); - JsonElement value = entry.getValue(); - - if (value.isJsonPrimitive()) { - params.put(key, value.getAsString()); - } else { - // Optionally handle non-primitive types if needed, otherwise stringify - params.put(key, value.toString()); - } - } - } catch (Exception e) { - Msg.error(this, "Failed to parse JSON request body: " + e.getMessage(), e); - // Throw an exception or return an empty map to indicate failure - throw new IOException("Invalid JSON request body: " + e.getMessage(), e); - } - return params; - } - - - - /** - * Convert a list of strings into one big newline-delimited string, applying offset & limit. - */ - private String paginateList(List items, int offset, int limit) { - int start = Math.max(0, offset); - int end = Math.min(items.size(), offset + limit); - - if (start >= items.size()) { - return ""; // no items in range - } - List sub = items.subList(start, end); - return String.join("\n", sub); - } - - /** - * Parse an integer from a string, or return defaultValue if null/invalid. - */ - private int parseIntOrDefault(String val, int defaultValue) { - if (val == null) return defaultValue; - try { - return Integer.parseInt(val); - } - catch (NumberFormatException e) { - return defaultValue; - } - } - - /** - * Escape non-ASCII chars to avoid potential decode issues. - */ - private String escapeNonAscii(String input) { - if (input == null) return ""; - StringBuilder sb = new StringBuilder(); - for (char c : input.toCharArray()) { - if (c >= 32 && c < 127) { - sb.append(c); - } - else { - sb.append("\\x"); - sb.append(Integer.toHexString(c & 0xFF)); - } - } - return sb.toString(); - } - - /** - * Get the current program from the tool - */ public Program getCurrentProgram() { if (tool == null) { Msg.debug(this, "Tool is null when trying to get current program"); return null; } - - try { - ProgramManager pm = tool.getService(ProgramManager.class); - if (pm == null) { - Msg.debug(this, "ProgramManager service is not available"); - return null; - } - - Program program = pm.getCurrentProgram(); - Msg.debug(this, "Got current program: " + (program != null ? program.getName() : "null")); - return program; - } - catch (Exception e) { - Msg.error(this, "Error getting current program", e); + ProgramManager pm = tool.getService(ProgramManager.class); + if (pm == null) { + Msg.debug(this, "ProgramManager service is not available"); return null; } + return pm.getCurrentProgram(); } - - // Get the currently selected address in Ghidra's UI - private Address getCurrentAddress() { - try { - Program program = getCurrentProgram(); - if (program == null) { - return null; - } - - // Return the minimum address as a fallback - return program.getMinAddress(); - } catch (Exception e) { - Msg.error(this, "Error getting current address", e); - return null; - } - } - - // Get the currently selected function in Ghidra's UI - private Function getCurrentFunction() { - try { - Program program = getCurrentProgram(); - if (program == null) { - return null; - } - - // Return the first function as a fallback - Iterator functions = program.getFunctionManager().getFunctions(true); - return functions.hasNext() ? functions.next() : null; - } catch (Exception e) { - Msg.error(this, "Error getting current function", e); - return null; - } - } - - // Removed old sendResponse method - - // private void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj) throws IOException { ... } // Keep the core sender - - // ---------------------------------------------------------------------------------- - // HTTP Response Sending Methods - // ---------------------------------------------------------------------------------- - - /** - * Sends a standard success JSON response with a 200 OK status. - * @param exchange The HTTP exchange. - * @param resultData The data payload for the 'result' field (can be null). - * @param links Optional HATEOAS links. - * @throws IOException If sending the response fails. - */ - private void sendSuccessResponse(HttpExchange exchange, Object resultData, JsonObject links) throws IOException { - sendJsonResponse(exchange, createSuccessResponse(exchange, resultData, links), 200); - } - - // Overload for success with data, no links - private void sendSuccessResponse(HttpExchange exchange, Object resultData) throws IOException { - sendSuccessResponse(exchange, resultData, null); - } - - // Overload for simple success, no data, no links - private void sendSuccessResponse(HttpExchange exchange) throws IOException { - sendSuccessResponse(exchange, null, null); - } - - /** - * Sends a standard error JSON response with the specified HTTP status code. - * @param exchange The HTTP exchange. - * @param statusCode The HTTP status code (e.g., 400, 404, 500). - * @param message A descriptive error message. - * @param errorCode An optional machine-readable error code string. - * @throws IOException If sending the response fails. - */ - private void sendErrorResponse(HttpExchange exchange, int statusCode, String message, String errorCode) throws IOException { - sendJsonResponse(exchange, createErrorResponse(exchange, message, errorCode), statusCode); - } - - // Overload for error without specific code - private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) throws IOException { - sendErrorResponse(exchange, statusCode, message, null); - } - - /** - * Core method to send any JsonObject response with a specific status code. - * Handles JSON serialization, setting headers, and writing the response body. - * @param exchange The HTTP exchange. - * @param jsonObj The JsonObject to send. - * @param statusCode The HTTP status code to set. - * @throws IOException If sending the response fails. - */ - private void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj, int statusCode) throws IOException { - try { - Gson gson = new Gson(); - String json = gson.toJson(jsonObj); - // Use Msg.debug for potentially large responses - if (json.length() < 1024) { - Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json); - } else { - Msg.debug(this, "Sending JSON response (Status " + statusCode + "): " + json.substring(0, 1020) + "..."); - } - - byte[] bytes = json.getBytes(StandardCharsets.UTF_8); - exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); - // Ensure CORS headers are set if needed (example, adjust as necessary) - // exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); - exchange.sendResponseHeaders(statusCode, bytes.length); // Use provided status code - - OutputStream os = null; - try { - os = exchange.getResponseBody(); - os.write(bytes); - os.flush(); - } catch (IOException e) { - // Log error, but don't try to send another response if body writing fails - Msg.error(this, "Error writing response body: " + e.getMessage(), e); - throw e; // Re-throw to indicate failure - } finally { - if (os != null) { - try { - os.close(); - } catch (IOException e) { - // Log error during close, but don't mask original exception if any - Msg.error(this, "Error closing output stream: " + e.getMessage(), e); - } - } - } - } catch (Exception e) { - // Catch broader exceptions during response preparation/sending - Msg.error(this, "Error sending JSON response: " + e.getMessage(), e); - // Avoid sending another error response here to prevent potential loops - throw new IOException("Failed to send JSON response", e); - } - } - private int findAvailablePort() { - int basePort = 8192; - int maxAttempts = 10; + int basePort = ApiConstants.DEFAULT_PORT; + int maxAttempts = ApiConstants.MAX_PORT_ATTEMPTS; for (int attempt = 0; attempt < maxAttempts; attempt++) { int candidate = basePort + attempt; @@ -2867,21 +411,29 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { try (ServerSocket s = new ServerSocket(candidate)) { return candidate; } catch (IOException e) { - continue; + Msg.debug(this, "Port " + candidate + " is not available, trying next."); } + } else { + Msg.debug(this, "Port " + candidate + " already tracked as active instance."); } } + Msg.error(this, "Could not find an available port between " + basePort + " and " + (basePort + maxAttempts - 1)); throw new RuntimeException("Could not find available port after " + maxAttempts + " attempts"); } @Override public void dispose() { if (server != null) { - server.stop(0); - Msg.info(this, "HTTP server stopped on port " + port); + server.stop(0); // Stop immediately + Msg.info(this, "GhydraMCP HTTP server stopped on port " + port); System.out.println("[GhydraMCP] HTTP server stopped on port " + port); } activeInstances.remove(port); super.dispose(); } + + // ---------------------------------------------------------------------------------- + // Helper methods moved to util classes (HttpUtil, GhidraUtil) or AbstractEndpoint + // ---------------------------------------------------------------------------------- + } diff --git a/src/main/java/eu/starsong/ghidra/api/ApiConstants.java b/src/main/java/eu/starsong/ghidra/api/ApiConstants.java new file mode 100644 index 0000000..07ed538 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/api/ApiConstants.java @@ -0,0 +1,8 @@ +package eu.starsong.ghidra.api; + +public class ApiConstants { + public static final String PLUGIN_VERSION = "v1.0.0"; + public static final int API_VERSION = 1; + public static final int DEFAULT_PORT = 8192; + public static final int MAX_PORT_ATTEMPTS = 10; +} diff --git a/src/main/java/eu/starsong/ghidra/api/GhidraJsonEndpoint.java b/src/main/java/eu/starsong/ghidra/api/GhidraJsonEndpoint.java new file mode 100644 index 0000000..1ebf67a --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/api/GhidraJsonEndpoint.java @@ -0,0 +1,8 @@ +package eu.starsong.ghidra.api; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +public interface GhidraJsonEndpoint extends HttpHandler { + void registerEndpoints(com.sun.net.httpserver.HttpServer server); +} diff --git a/src/main/java/eu/starsong/ghidra/api/ResponseBuilder.java b/src/main/java/eu/starsong/ghidra/api/ResponseBuilder.java new file mode 100644 index 0000000..cbd7a90 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/api/ResponseBuilder.java @@ -0,0 +1,73 @@ +package eu.starsong.ghidra.api; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.sun.net.httpserver.HttpExchange; +import java.util.UUID; + +/** + * Builder for standardized API responses (following GHIDRA_HTTP_API.md v1). + * This should be used by endpoint handlers to construct responses. + */ +public class ResponseBuilder { + private final HttpExchange exchange; + private final int port; // Port of the current Ghidra instance handling the request + private JsonObject response; + private JsonObject links; // For HATEOAS links + private final Gson gson = new Gson(); // Gson instance for serialization + + public ResponseBuilder(HttpExchange exchange, int port) { + this.exchange = exchange; + this.port = port; + this.response = new JsonObject(); + this.links = new JsonObject(); + + // Add standard fields + String requestId = exchange.getRequestHeaders().getFirst("X-Request-ID"); + response.addProperty("id", requestId != null ? requestId : UUID.randomUUID().toString()); + response.addProperty("instance", "http://localhost:" + port); // URL of this instance + } + + public ResponseBuilder success(boolean success) { + response.addProperty("success", success); + return this; + } + + public ResponseBuilder result(Object data) { + response.add("result", gson.toJsonTree(data)); + return this; + } + + public ResponseBuilder error(String message, String code) { + JsonObject error = new JsonObject(); + error.addProperty("message", message); + if (code != null) { + error.addProperty("code", code); + } + response.add("error", error); + return this; + } + + public ResponseBuilder addLink(String rel, String href) { + JsonObject link = new JsonObject(); + link.addProperty("href", href); + links.add(rel, link); + return this; + } + + // Overload to add link with method + public ResponseBuilder addLink(String rel, String href, String method) { + JsonObject link = new JsonObject(); + link.addProperty("href", href); + link.addProperty("method", method); + links.add(rel, link); + return this; + } + + public JsonObject build() { + if (links.size() > 0) { + response.add("_links", links); + } + return response; + } +} diff --git a/src/main/java/eu/starsong/ghidra/endpoints/AbstractEndpoint.java b/src/main/java/eu/starsong/ghidra/endpoints/AbstractEndpoint.java new file mode 100644 index 0000000..642134d --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/endpoints/AbstractEndpoint.java @@ -0,0 +1,93 @@ +package eu.starsong.ghidra.endpoints; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.sun.net.httpserver.HttpExchange; +import eu.starsong.ghidra.api.GhidraJsonEndpoint; +import eu.starsong.ghidra.api.ResponseBuilder; // Import ResponseBuilder +import eu.starsong.ghidra.util.GhidraUtil; // Import GhidraUtil +import eu.starsong.ghidra.util.HttpUtil; // Import HttpUtil +import ghidra.program.model.listing.Program; +import java.io.IOException; +import java.util.Map; + +public abstract class AbstractEndpoint implements GhidraJsonEndpoint { + + @Override + public void handle(HttpExchange exchange) throws IOException { + // This method is required by HttpHandler interface + // Each endpoint will register its own context handlers with specific paths + // so this default implementation should never be called + sendErrorResponse(exchange, 404, "Endpoint not found", "ENDPOINT_NOT_FOUND"); + } + + protected final Gson gson = new Gson(); // Keep Gson if needed for specific object handling + protected Program currentProgram; + protected int port; // Add port field + + // Constructor to receive Program and Port + public AbstractEndpoint(Program program, int port) { + this.currentProgram = program; + this.port = port; + } + + // Simplified getCurrentProgram - assumes constructor sets it + protected Program getCurrentProgram() { + return currentProgram; + } + + // --- Methods using HttpUtil --- + + protected void sendJsonResponse(HttpExchange exchange, JsonObject data, int statusCode) throws IOException { + HttpUtil.sendJsonResponse(exchange, data, statusCode, this.port); + } + + // Overload for sending success responses easily using ResponseBuilder + protected void sendSuccessResponse(HttpExchange exchange, Object resultData) throws IOException { + // Check if program is required but not available + if (currentProgram == null && requiresProgram()) { + sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED"); + return; + } + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(resultData); + // Add common links if desired here + HttpUtil.sendJsonResponse(exchange, builder.build(), 200, this.port); + } + + /** + * Override this method in endpoint implementations that require a program to function. + * @return true if this endpoint requires a program, false otherwise + */ + protected boolean requiresProgram() { + // Default implementation returns true for most endpoints + return true; + } + + protected void sendErrorResponse(HttpExchange exchange, int code, String message, String errorCode) throws IOException { + HttpUtil.sendErrorResponse(exchange, code, message, errorCode, this.port); + } + + // Overload without error code + protected void sendErrorResponse(HttpExchange exchange, int code, String message) throws IOException { + HttpUtil.sendErrorResponse(exchange, code, message, null, this.port); + } + + protected Map parseQueryParams(HttpExchange exchange) { + return HttpUtil.parseQueryParams(exchange); + } + + protected Map parseJsonPostParams(HttpExchange exchange) throws IOException { + return HttpUtil.parseJsonPostParams(exchange); + } + + // --- Methods using GhidraUtil --- + + protected int parseIntOrDefault(String val, int defaultValue) { + return GhidraUtil.parseIntOrDefault(val, defaultValue); + } + + // Add other common Ghidra related utilities here or call GhidraUtil directly +} diff --git a/src/main/java/eu/starsong/ghidra/endpoints/ClassEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/ClassEndpoints.java new file mode 100644 index 0000000..d36b9da --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/endpoints/ClassEndpoints.java @@ -0,0 +1,93 @@ +package eu.starsong.ghidra.endpoints; + + import com.google.gson.JsonObject; + import com.sun.net.httpserver.HttpExchange; + import com.sun.net.httpserver.HttpServer; + import ghidra.program.model.listing.Program; + import ghidra.program.model.symbol.Namespace; + import ghidra.program.model.symbol.Symbol; + import ghidra.util.Msg; + + import java.io.IOException; + import java.util.*; + + public class ClassEndpoints extends AbstractEndpoint { + + // Updated constructor to accept port + public ClassEndpoints(Program program, int port) { + super(program, port); // Call super constructor + } + + @Override + public void registerEndpoints(HttpServer server) { + server.createContext("/classes", this::handleClasses); + } + + private void handleClasses(HttpExchange exchange) throws IOException { + try { + if ("GET".equals(exchange.getRequestMethod())) { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited + int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited + Object resultData = getAllClassNames(offset, limit); + // Check if helper returned an error object + if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) { + sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse + } else { + sendSuccessResponse(exchange, resultData); // Use success helper + } + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited + } + } catch (Exception e) { + Msg.error(this, "Error in /classes endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Inherited + } + } + + // --- Method moved from GhydraMCPPlugin --- + + private JsonObject getAllClassNames(int offset, int limit) { + if (currentProgram == null) { + return createErrorResponse("No program loaded", 400); + } + + Set classNames = new HashSet<>(); + for (Symbol symbol : currentProgram.getSymbolTable().getAllSymbols(true)) { + Namespace ns = symbol.getParentNamespace(); + // Check if namespace is not null, not global, and represents a class + if (ns != null && !ns.isGlobal() && ns.getSymbol().getSymbolType().isNamespace()) { + // Basic check, might need refinement based on how classes are represented + classNames.add(ns.getName(true)); // Get fully qualified name + } + } + + List sorted = new ArrayList<>(classNames); + Collections.sort(sorted); + + int start = Math.max(0, offset); + int end = Math.min(sorted.size(), offset + limit); + List paginated = sorted.subList(start, end); + + return createSuccessResponse(paginated); // Keep internal helper for now + } + + // --- Helper Methods (Keep internal for now) --- + + private JsonObject createSuccessResponse(Object resultData) { + JsonObject response = new JsonObject(); + response.addProperty("success", true); + response.add("result", gson.toJsonTree(resultData)); + return response; + } + + private JsonObject createErrorResponse(String errorMessage, int statusCode) { + JsonObject response = new JsonObject(); + response.addProperty("success", false); + response.addProperty("error", errorMessage); + response.addProperty("status_code", statusCode); + return response; + } + + // parseIntOrDefault is inherited from AbstractEndpoint + } diff --git a/src/main/java/eu/starsong/ghidra/endpoints/DataEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/DataEndpoints.java new file mode 100644 index 0000000..2927f08 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/endpoints/DataEndpoints.java @@ -0,0 +1,192 @@ +package eu.starsong.ghidra.endpoints; + + import com.google.gson.JsonObject; + import com.sun.net.httpserver.HttpExchange; + import com.sun.net.httpserver.HttpServer; + import eu.starsong.ghidra.util.TransactionHelper; + import eu.starsong.ghidra.util.TransactionHelper.TransactionException; + import ghidra.program.model.address.Address; + import ghidra.program.model.listing.Data; + import ghidra.program.model.listing.DataIterator; + import ghidra.program.model.listing.Listing; + import ghidra.program.model.listing.Program; + import ghidra.program.model.mem.MemoryBlock; + import ghidra.program.model.symbol.SourceType; + import ghidra.program.model.symbol.Symbol; + import ghidra.program.model.symbol.SymbolTable; + import ghidra.util.Msg; + + import java.io.IOException; + import java.util.*; + import java.util.concurrent.atomic.AtomicBoolean; + import javax.swing.SwingUtilities; + import java.lang.reflect.InvocationTargetException; + + public class DataEndpoints extends AbstractEndpoint { + + // Updated constructor to accept port + public DataEndpoints(Program program, int port) { + super(program, port); // Call super constructor + } + + @Override + public void registerEndpoints(HttpServer server) { + server.createContext("/data", this::handleData); + } + + private void handleData(HttpExchange exchange) throws IOException { + try { + if ("GET".equals(exchange.getRequestMethod())) { + handleListData(exchange); + } else if ("POST".equals(exchange.getRequestMethod())) { + handleRenameData(exchange); + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); + } + } catch (Exception e) { + Msg.error(this, "Error in /data endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); + } + } + + private void handleListData(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited + int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited + Object resultData = listDefinedData(offset, limit); + // Check if helper returned an error object + if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) { + sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse + } else { + sendSuccessResponse(exchange, resultData); // Use success helper + } + } + + private void handleRenameData(HttpExchange exchange) throws IOException { + try { + Map params = parseJsonPostParams(exchange); + final String addressStr = params.get("address"); + final String newName = params.get("newName"); + + if (addressStr == null || addressStr.isEmpty() || newName == null || newName.isEmpty()) { + sendErrorResponse(exchange, 400, "Missing required parameters: address, newName"); // Inherited + return; + } + + if (currentProgram == null) { + sendErrorResponse(exchange, 400, "No program loaded"); // Inherited + return; + } + + try { + TransactionHelper.executeInTransaction(currentProgram, "Rename Data", () -> { + if (!renameDataAtAddress(addressStr, newName)) { + throw new Exception("Rename data operation failed internally."); + } + return null; // Return null for void operation + }); + // Use sendSuccessResponse for consistency + sendSuccessResponse(exchange, Map.of("message", "Data renamed successfully")); + } catch (TransactionException e) { + Msg.error(this, "Transaction failed: Rename Data", e); + // Use inherited sendErrorResponse + sendErrorResponse(exchange, 500, "Failed to rename data: " + e.getMessage(), "TRANSACTION_ERROR"); + } catch (Exception e) { // Catch potential AddressFormatException or other issues + Msg.error(this, "Error during rename data operation", e); + // Use inherited sendErrorResponse + sendErrorResponse(exchange, 400, "Error renaming data: " + e.getMessage(), "INVALID_PARAMETER"); + } + + } catch (IOException e) { + Msg.error(this, "Error parsing POST params for data rename", e); + sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); // Inherited + } catch (Exception e) { // Catch unexpected errors + Msg.error(this, "Unexpected error renaming data", e); + sendErrorResponse(exchange, 500, "Error renaming data: " + e.getMessage(), "INTERNAL_ERROR"); // Inherited + } + } + + + // --- Methods moved from GhydraMCPPlugin --- + + private JsonObject listDefinedData(int offset, int limit) { + if (currentProgram == null) { + return createErrorResponse("No program loaded", 400); + } + + List> dataItems = new ArrayList<>(); + for (MemoryBlock block : currentProgram.getMemory().getBlocks()) { + DataIterator it = currentProgram.getListing().getDefinedData(block.getStart(), true); + while (it.hasNext()) { + Data data = it.next(); + if (block.contains(data.getAddress())) { + Map item = new HashMap<>(); + item.put("address", data.getAddress().toString()); + item.put("label", data.getLabel() != null ? data.getLabel() : "(unnamed)"); + item.put("value", data.getDefaultValueRepresentation()); + item.put("dataType", data.getDataType().getName()); + dataItems.add(item); + } + } + } + + // Apply pagination + int start = Math.max(0, offset); + int end = Math.min(dataItems.size(), offset + limit); + List> paginated = dataItems.subList(start, end); + + return createSuccessResponse(paginated); + } + + private boolean renameDataAtAddress(String addressStr, String newName) throws Exception { + // This method now throws Exception to be caught by the transaction helper + AtomicBoolean successFlag = new AtomicBoolean(false); + try { + Address addr = currentProgram.getAddressFactory().getAddress(addressStr); + Listing listing = currentProgram.getListing(); + Data data = listing.getDefinedDataAt(addr); + if (data != null) { + SymbolTable symTable = currentProgram.getSymbolTable(); + Symbol symbol = symTable.getPrimarySymbol(addr); + if (symbol != null) { + symbol.setName(newName, SourceType.USER_DEFINED); + successFlag.set(true); + } else { + // Create a new label if no primary symbol exists + symTable.createLabel(addr, newName, SourceType.USER_DEFINED); + successFlag.set(true); + } + } else { + throw new Exception("No defined data found at address: " + addressStr); + } + } catch (ghidra.program.model.address.AddressFormatException afe) { + throw new Exception("Invalid address format: " + addressStr, afe); + } catch (ghidra.util.exception.InvalidInputException iie) { + throw new Exception("Invalid name: " + newName, iie); + } catch (Exception e) { // Catch other potential Ghidra exceptions + throw new Exception("Failed to rename data at " + addressStr, e); + } + return successFlag.get(); + } + + + // --- Helper Methods (Keep internal for now, refactor later if needed) --- + // Note: These might differ slightly from AbstractEndpoint/ResponseBuilder, review needed. + + private JsonObject createSuccessResponse(Object resultData) { + JsonObject response = new JsonObject(); + response.addProperty("success", true); + response.add("result", gson.toJsonTree(resultData)); + return response; + } + + private JsonObject createErrorResponse(String errorMessage, int statusCode) { + JsonObject response = new JsonObject(); + response.addProperty("success", false); + response.addProperty("error", errorMessage); + response.addProperty("status_code", statusCode); + return response; + } + + // parseIntOrDefault is inherited from AbstractEndpoint + } diff --git a/src/main/java/eu/starsong/ghidra/endpoints/FunctionEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/FunctionEndpoints.java new file mode 100644 index 0000000..c24eac9 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/endpoints/FunctionEndpoints.java @@ -0,0 +1,93 @@ +package eu.starsong.ghidra.endpoints; + +import com.google.gson.JsonObject; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import eu.starsong.ghidra.util.TransactionHelper; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.io.IOException; // Add IOException import + +public class FunctionEndpoints extends AbstractEndpoint { + + // Updated constructor to accept port + public FunctionEndpoints(Program program, int port) { + super(program, port); // Call super constructor + } + + @Override + public void registerEndpoints(HttpServer server) { + server.createContext("/functions", this::handleFunctions); + server.createContext("/functions/", this::handleFunction); + } + + private void handleFunctions(HttpExchange exchange) throws IOException { + try { + if ("GET".equals(exchange.getRequestMethod())) { + Map params = parseQueryParams(exchange); + int offset = parseIntOrDefault(params.get("offset"), 0); + int limit = parseIntOrDefault(params.get("limit"), 100); + + List> functions = new ArrayList<>(); + for (Function f : currentProgram.getFunctionManager().getFunctions(true)) { + Map func = new HashMap<>(); + func.put("name", f.getName()); + func.put("address", f.getEntryPoint().toString()); + functions.add(func); + } + + // Use sendSuccessResponse helper from AbstractEndpoint + sendSuccessResponse(exchange, functions.subList( + Math.max(0, offset), + Math.min(functions.size(), offset + limit) + )); + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); // Uses helper from AbstractEndpoint + } + } catch (Exception e) { + sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage()); // Uses helper from AbstractEndpoint + } + } + + private void handleFunction(HttpExchange exchange) throws IOException { + try { + String path = exchange.getRequestURI().getPath(); + String functionName = path.substring("/functions/".length()); + + if ("GET".equals(exchange.getRequestMethod())) { + Function function = findFunctionByName(functionName); + if (function == null) { + sendErrorResponse(exchange, 404, "Function not found"); + return; + } + + Map result = new HashMap<>(); + result.put("name", function.getName()); + result.put("address", function.getEntryPoint().toString()); + result.put("signature", function.getSignature().getPrototypeString()); + + // Use sendSuccessResponse helper + sendSuccessResponse(exchange, result); + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); + } + } catch (Exception e) { + sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage()); + } + } + + private Function findFunctionByName(String name) { + for (Function f : currentProgram.getFunctionManager().getFunctions(true)) { + if (f.getName().equals(name)) { + return f; + } + } + return null; + } + + // parseIntOrDefault is now inherited from AbstractEndpoint +} diff --git a/src/main/java/eu/starsong/ghidra/endpoints/InstanceEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/InstanceEndpoints.java new file mode 100644 index 0000000..223e698 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/endpoints/InstanceEndpoints.java @@ -0,0 +1,104 @@ +package eu.starsong.ghidra.endpoints; + + import com.google.gson.JsonObject; + import com.sun.net.httpserver.HttpExchange; + import com.sun.net.httpserver.HttpServer; + import eu.starsong.ghidra.GhydraMCPPlugin; // Need access to activeInstances + import ghidra.program.model.listing.Program; + import ghidra.util.Msg; + + import java.io.IOException; + import java.util.*; + + public class InstanceEndpoints extends AbstractEndpoint { + + // Need a way to access the static activeInstances map from GhydraMCPPlugin + // This is a bit awkward and suggests the instance management might need + // a different design, perhaps a dedicated manager class. + // For now, we pass the map or use a static accessor if made public. + private final Map activeInstances; + // Note: Passing currentProgram might be null here if no program is open. + // The constructor in AbstractEndpoint handles null program. + + // Updated constructor to accept port + public InstanceEndpoints(Program program, int port, Map instances) { + super(program, port); // Call super constructor + this.activeInstances = instances; + } + + @Override + public void registerEndpoints(HttpServer server) { + server.createContext("/instances", this::handleInstances); + server.createContext("/registerInstance", this::handleRegisterInstance); + server.createContext("/unregisterInstance", this::handleUnregisterInstance); + } + + @Override + protected boolean requiresProgram() { + // This endpoint doesn't require a program to function + return false; + } + + private void handleInstances(HttpExchange exchange) throws IOException { + try { + List> instanceData = new ArrayList<>(); + // Accessing the static map directly - requires it to be accessible + // or passed in constructor. + for (Map.Entry entry : activeInstances.entrySet()) { + Map instance = new HashMap<>(); + // Need a way to get isBaseInstance from the plugin instance - requires getter in GhydraMCPPlugin + // instance.put("type", entry.getValue().isBaseInstance() ? "base" : "secondary"); // Placeholder access + instance.put("type", "unknown"); // Placeholder until isBaseInstance is accessible + instanceData.add(instance); + } + sendSuccessResponse(exchange, instanceData); // Use helper from AbstractEndpoint + } catch (Exception e) { + Msg.error(this, "Error in /instances endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Use helper + } + } + + private void handleRegisterInstance(HttpExchange exchange) throws IOException { + try { + Map params = parseJsonPostParams(exchange); + int regPort = parseIntOrDefault(params.get("port"), 0); + if (regPort > 0) { + // Logic to actually register/track the instance should happen elsewhere (e.g., main plugin or dedicated manager) + sendSuccessResponse(exchange, Map.of("message", "Instance registration request received for port " + regPort)); // Use helper + } else { + sendErrorResponse(exchange, 400, "Invalid or missing port number"); // Use helper + } + } catch (IOException e) { + Msg.error(this, "Error parsing POST params for registerInstance", e); + sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); // Use helper + } catch (Exception e) { + Msg.error(this, "Error in /registerInstance", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); // Use helper + } + } + + private void handleUnregisterInstance(HttpExchange exchange) throws IOException { + try { + Map params = parseJsonPostParams(exchange); + int unregPort = parseIntOrDefault(params.get("port"), 0); + if (unregPort > 0 && activeInstances.containsKey(unregPort)) { + // Actual removal should likely happen in the main plugin's map or dedicated manager + activeInstances.remove(unregPort); // Potential ConcurrentModificationException if map is iterated elsewhere + sendSuccessResponse(exchange, Map.of("message", "Instance unregistered for port " + unregPort)); // Use helper + } else { + sendErrorResponse(exchange, 404, "No instance found on port " + unregPort, "RESOURCE_NOT_FOUND"); // Use helper + } + } catch (IOException e) { + Msg.error(this, "Error parsing POST params for unregisterInstance", e); + sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); // Use helper + } catch (Exception e) { + Msg.error(this, "Error in /unregisterInstance", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); // Use helper + } + } + + + // --- Helper Methods Removed (Inherited or internal logic adjusted) --- + + // parseIntOrDefault is inherited from AbstractEndpoint + } diff --git a/src/main/java/eu/starsong/ghidra/endpoints/NamespaceEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/NamespaceEndpoints.java new file mode 100644 index 0000000..eada91e --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/endpoints/NamespaceEndpoints.java @@ -0,0 +1,93 @@ +package eu.starsong.ghidra.endpoints; + + import com.google.gson.JsonObject; + import com.sun.net.httpserver.HttpExchange; + import com.sun.net.httpserver.HttpServer; + import ghidra.program.model.address.GlobalNamespace; + import ghidra.program.model.listing.Program; + import ghidra.program.model.symbol.Namespace; + import ghidra.program.model.symbol.Symbol; + import ghidra.util.Msg; + + import java.io.IOException; + import java.util.*; + + public class NamespaceEndpoints extends AbstractEndpoint { + + // Updated constructor to accept port + public NamespaceEndpoints(Program program, int port) { + super(program, port); // Call super constructor + } + + @Override + public void registerEndpoints(HttpServer server) { + server.createContext("/namespaces", this::handleNamespaces); + } + + private void handleNamespaces(HttpExchange exchange) throws IOException { + try { + if ("GET".equals(exchange.getRequestMethod())) { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited + int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited + Object resultData = listNamespaces(offset, limit); + // Check if helper returned an error object + if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) { + sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse + } else { + sendSuccessResponse(exchange, resultData); // Use success helper + } + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited + } + } catch (Exception e) { + Msg.error(this, "Error in /namespaces endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Inherited + } + } + + // --- Method moved from GhydraMCPPlugin --- + + private JsonObject listNamespaces(int offset, int limit) { + if (currentProgram == null) { + return createErrorResponse("No program loaded", 400); + } + + Set namespaces = new HashSet<>(); + for (Symbol symbol : currentProgram.getSymbolTable().getAllSymbols(true)) { + Namespace ns = symbol.getParentNamespace(); + if (ns != null && !(ns instanceof GlobalNamespace)) { + namespaces.add(ns.getName(true)); // Get fully qualified name + } + } + + List sorted = new ArrayList<>(namespaces); + Collections.sort(sorted); + + // Apply pagination + int start = Math.max(0, offset); + int end = Math.min(sorted.size(), offset + limit); + List paginated = sorted.subList(start, end); + + return createSuccessResponse(paginated); // Keep internal helper for now + } + + // --- Helper Methods (Keep internal for now) --- + + private JsonObject createSuccessResponse(Object resultData) { + JsonObject response = new JsonObject(); + response.addProperty("success", true); + response.add("result", gson.toJsonTree(resultData)); + return response; + } + + private JsonObject createErrorResponse(String errorMessage, int statusCode) { + JsonObject response = new JsonObject(); + response.addProperty("success", false); + response.addProperty("error", errorMessage); + response.addProperty("status_code", statusCode); + return response; + } + + // parseIntOrDefault is inherited from AbstractEndpoint + } diff --git a/src/main/java/eu/starsong/ghidra/endpoints/SegmentEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/SegmentEndpoints.java new file mode 100644 index 0000000..618ac44 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/endpoints/SegmentEndpoints.java @@ -0,0 +1,90 @@ +package eu.starsong.ghidra.endpoints; + + import com.google.gson.JsonObject; + import com.sun.net.httpserver.HttpExchange; + import com.sun.net.httpserver.HttpServer; + import ghidra.program.model.listing.Program; + import ghidra.program.model.mem.MemoryBlock; + import ghidra.util.Msg; + + import java.io.IOException; + import java.util.*; + + public class SegmentEndpoints extends AbstractEndpoint { + + // Updated constructor to accept port + public SegmentEndpoints(Program program, int port) { + super(program, port); // Call super constructor + } + + @Override + public void registerEndpoints(HttpServer server) { + server.createContext("/segments", this::handleSegments); + } + + private void handleSegments(HttpExchange exchange) throws IOException { + try { + if ("GET".equals(exchange.getRequestMethod())) { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited + int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited + Object resultData = listSegments(offset, limit); + // Check if helper returned an error object + if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) { + sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse + } else { + sendSuccessResponse(exchange, resultData); // Use success helper + } + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited + } + } catch (Exception e) { + Msg.error(this, "Error in /segments endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Inherited + } + } + + // --- Method moved from GhydraMCPPlugin --- + + private JsonObject listSegments(int offset, int limit) { + if (currentProgram == null) { + return createErrorResponse("No program loaded", 400); + } + + List> segments = new ArrayList<>(); + for (MemoryBlock block : currentProgram.getMemory().getBlocks()) { + Map seg = new HashMap<>(); + seg.put("name", block.getName()); + seg.put("start", block.getStart().toString()); + seg.put("end", block.getEnd().toString()); + // Add permissions if needed: block.isRead(), block.isWrite(), block.isExecute() + segments.add(seg); + } + + // Apply pagination + int start = Math.max(0, offset); + int end = Math.min(segments.size(), offset + limit); + List> paginated = segments.subList(start, end); + + return createSuccessResponse(paginated); // Keep internal helper for now + } + + // --- Helper Methods (Keep internal for now) --- + + private JsonObject createSuccessResponse(Object resultData) { + JsonObject response = new JsonObject(); + response.addProperty("success", true); + response.add("result", gson.toJsonTree(resultData)); + return response; + } + + private JsonObject createErrorResponse(String errorMessage, int statusCode) { + JsonObject response = new JsonObject(); + response.addProperty("success", false); + response.addProperty("error", errorMessage); + response.addProperty("status_code", statusCode); + return response; + } + + // parseIntOrDefault is inherited from AbstractEndpoint + } diff --git a/src/main/java/eu/starsong/ghidra/endpoints/SymbolEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/SymbolEndpoints.java new file mode 100644 index 0000000..34b8e25 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/endpoints/SymbolEndpoints.java @@ -0,0 +1,142 @@ +package eu.starsong.ghidra.endpoints; + + import com.google.gson.JsonObject; + import com.sun.net.httpserver.HttpExchange; + import com.sun.net.httpserver.HttpServer; + import ghidra.program.model.listing.Program; + import ghidra.program.model.symbol.Symbol; + import ghidra.program.model.symbol.SymbolIterator; + import ghidra.program.model.symbol.SymbolTable; + import ghidra.util.Msg; + + import java.io.IOException; + import java.util.*; + + public class SymbolEndpoints extends AbstractEndpoint { + + // Updated constructor to accept port + public SymbolEndpoints(Program program, int port) { + super(program, port); // Call super constructor + } + + @Override + public void registerEndpoints(HttpServer server) { + server.createContext("/symbols/imports", this::handleImports); + server.createContext("/symbols/exports", this::handleExports); + } + + private void handleImports(HttpExchange exchange) throws IOException { + try { + if ("GET".equals(exchange.getRequestMethod())) { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited + int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited + Object resultData = listImports(offset, limit); + // Check if helper returned an error object + if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) { + sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse + } else { + sendSuccessResponse(exchange, resultData); // Use success helper + } + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited + } + } catch (Exception e) { + Msg.error(this, "Error in /symbols/imports endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Inherited + } + } + + private void handleExports(HttpExchange exchange) throws IOException { + try { + if ("GET".equals(exchange.getRequestMethod())) { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited + int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited + Object resultData = listExports(offset, limit); + // Check if helper returned an error object + if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) { + sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse + } else { + sendSuccessResponse(exchange, resultData); // Use success helper + } + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited + } + } catch (Exception e) { + Msg.error(this, "Error in /symbols/exports endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Inherited + } + } + + // --- Methods moved from GhydraMCPPlugin --- + + private JsonObject listImports(int offset, int limit) { + if (currentProgram == null) { + return createErrorResponse("No program loaded", 400); + } + + List> imports = new ArrayList<>(); + for (Symbol symbol : currentProgram.getSymbolTable().getExternalSymbols()) { + Map imp = new HashMap<>(); + imp.put("name", symbol.getName()); + imp.put("address", symbol.getAddress().toString()); + // Add library name if needed: symbol.getLibraryName() + imports.add(imp); + } + + // Apply pagination + int start = Math.max(0, offset); + int end = Math.min(imports.size(), offset + limit); + List> paginated = imports.subList(start, end); + + return createSuccessResponse(paginated); + } + + private JsonObject listExports(int offset, int limit) { + if (currentProgram == null) { + return createErrorResponse("No program loaded", 400); + } + + List> exports = new ArrayList<>(); + SymbolTable table = currentProgram.getSymbolTable(); + SymbolIterator it = table.getAllSymbols(true); + + while (it.hasNext()) { + Symbol s = it.next(); + if (s.isExternalEntryPoint()) { + Map exp = new HashMap<>(); + exp.put("name", s.getName()); + exp.put("address", s.getAddress().toString()); + exports.add(exp); + } + } + + // Apply pagination + int start = Math.max(0, offset); + int end = Math.min(exports.size(), offset + limit); + List> paginated = exports.subList(start, end); + + return createSuccessResponse(paginated); // Keep internal helper for now + } + + // --- Helper Methods (Keep internal for now, refactor later if needed) --- + // Note: These might differ slightly from AbstractEndpoint/ResponseBuilder, review needed. + + private JsonObject createSuccessResponse(Object resultData) { + JsonObject response = new JsonObject(); + response.addProperty("success", true); + response.add("result", gson.toJsonTree(resultData)); + return response; + } + + private JsonObject createErrorResponse(String errorMessage, int statusCode) { + JsonObject response = new JsonObject(); + response.addProperty("success", false); + response.addProperty("error", errorMessage); + response.addProperty("status_code", statusCode); + return response; + } + + // parseIntOrDefault is inherited from AbstractEndpoint + } diff --git a/src/main/java/eu/starsong/ghidra/endpoints/VariableEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/VariableEndpoints.java new file mode 100644 index 0000000..78c697e --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/endpoints/VariableEndpoints.java @@ -0,0 +1,265 @@ +package eu.starsong.ghidra.endpoints; + + import com.google.gson.Gson; + import com.google.gson.JsonObject; + import com.sun.net.httpserver.HttpExchange; + import com.sun.net.httpserver.HttpServer; + import eu.starsong.ghidra.util.TransactionHelper; + import eu.starsong.ghidra.util.TransactionHelper.TransactionException; + import ghidra.app.decompiler.DecompInterface; + import ghidra.app.decompiler.DecompileResults; + import ghidra.program.model.address.Address; + import ghidra.program.model.data.DataType; + import ghidra.program.model.listing.Function; + import ghidra.program.model.listing.Parameter; + import ghidra.program.model.listing.Program; + import ghidra.program.model.listing.VariableStorage; + import ghidra.program.model.pcode.HighFunction; + import ghidra.program.model.pcode.HighFunctionDBUtil; + import ghidra.program.model.pcode.HighSymbol; + import ghidra.program.model.pcode.LocalSymbolMap; + import ghidra.program.model.symbol.SourceType; + import ghidra.program.model.symbol.Symbol; + import ghidra.program.model.symbol.SymbolIterator; + import ghidra.program.model.symbol.SymbolTable; + import ghidra.program.model.symbol.SymbolType; + import ghidra.util.Msg; + import ghidra.util.task.ConsoleTaskMonitor; + + import java.io.IOException; + import java.nio.charset.StandardCharsets; + import java.util.*; + import java.util.concurrent.atomic.AtomicBoolean; + import javax.swing.SwingUtilities; + import java.lang.reflect.InvocationTargetException; + + + public class VariableEndpoints extends AbstractEndpoint { + + // Updated constructor to accept port + public VariableEndpoints(Program program, int port) { + super(program, port); // Call super constructor + } + + @Override + public void registerEndpoints(HttpServer server) { + server.createContext("/variables", this::handleGlobalVariables); + // Note: /functions/{name}/variables is handled within FunctionEndpoints for now + // to keep related logic together until full refactor. + // If needed, we can create a more complex routing mechanism later. + } + + private void handleGlobalVariables(HttpExchange exchange) throws IOException { + try { + if ("GET".equals(exchange.getRequestMethod())) { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + String search = qparams.get("search"); // Renamed from 'query' for clarity + + Object resultData; + if (search != null && !search.isEmpty()) { + resultData = searchVariables(search, offset, limit); + } else { + resultData = listVariables(offset, limit); + } + // Check if helper returned an error object + if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) { + sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse + } else { + sendSuccessResponse(exchange, resultData); // Use success helper + } + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); + } + } catch (Exception e) { + Msg.error(this, "Error in /variables endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); + } + } + + // --- Methods moved from GhydraMCPPlugin --- + + private JsonObject listVariables(int offset, int limit) { + if (currentProgram == null) { + return createErrorResponse("No program loaded", 400); + } + + List> variables = new ArrayList<>(); + + // Get global variables + SymbolTable symbolTable = currentProgram.getSymbolTable(); + for (Symbol symbol : symbolTable.getDefinedSymbols()) { + if (symbol.isGlobal() && !symbol.isExternal() && + symbol.getSymbolType() != SymbolType.FUNCTION && + symbol.getSymbolType() != SymbolType.LABEL) { + + Map varInfo = new HashMap<>(); + varInfo.put("name", symbol.getName()); + varInfo.put("address", symbol.getAddress().toString()); + varInfo.put("type", "global"); + varInfo.put("dataType", getDataTypeName(currentProgram, symbol.getAddress())); + variables.add(varInfo); + } + } + + // Get local variables from all functions (Consider performance implications) + DecompInterface decomp = null; + try { + decomp = new DecompInterface(); + if (!decomp.openProgram(currentProgram)) { + Msg.error(this, "listVariables: Failed to open program with decompiler."); + } else { + for (Function function : currentProgram.getFunctionManager().getFunctions(true)) { + try { + DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); + if (results != null && results.decompileCompleted()) { + HighFunction highFunc = results.getHighFunction(); + if (highFunc != null) { + Iterator symbolIter = highFunc.getLocalSymbolMap().getSymbols(); + while (symbolIter.hasNext()) { + HighSymbol symbol = symbolIter.next(); + if (!symbol.isParameter()) { // Only list locals + Map varInfo = new HashMap<>(); + varInfo.put("name", symbol.getName()); + varInfo.put("type", "local"); + varInfo.put("function", function.getName()); + Address pcAddr = symbol.getPCAddress(); + varInfo.put("address", pcAddr != null ? pcAddr.toString() : "N/A"); + varInfo.put("dataType", symbol.getDataType() != null ? symbol.getDataType().getName() : "unknown"); + variables.add(varInfo); + } + } + } + } + } catch (Exception e) { + Msg.error(this, "listVariables: Error processing function " + function.getName(), e); + } + } + } + } catch (Exception e) { + Msg.error(this, "listVariables: Error during local variable processing", e); + } finally { + if (decomp != null) { + decomp.dispose(); + } + } + + Collections.sort(variables, Comparator.comparing(a -> a.get("name"))); + + int start = Math.max(0, offset); + int end = Math.min(variables.size(), offset + limit); + List> paginated = variables.subList(start, end); + + return createSuccessResponse(paginated); // Keep using internal helper for now + } + + private JsonObject searchVariables(String searchTerm, int offset, int limit) { + if (currentProgram == null) { + return createErrorResponse("No program loaded", 400); // Keep using internal helper + } + if (searchTerm == null || searchTerm.isEmpty()) { + return createErrorResponse("Search term is required", 400); // Keep using internal helper + } + + List> matchedVars = new ArrayList<>(); + String lowerSearchTerm = searchTerm.toLowerCase(); + + // Search global variables + SymbolTable symbolTable = currentProgram.getSymbolTable(); + SymbolIterator it = symbolTable.getSymbolIterator(); + while (it.hasNext()) { + Symbol symbol = it.next(); + if (symbol.isGlobal() && + symbol.getSymbolType() != SymbolType.FUNCTION && + symbol.getSymbolType() != SymbolType.LABEL && + symbol.getName().toLowerCase().contains(lowerSearchTerm)) { + Map varInfo = new HashMap<>(); + varInfo.put("name", symbol.getName()); + varInfo.put("address", symbol.getAddress().toString()); + varInfo.put("type", "global"); + varInfo.put("dataType", getDataTypeName(currentProgram, symbol.getAddress())); + matchedVars.add(varInfo); + } + } + + // Search local variables + DecompInterface decomp = null; + try { + decomp = new DecompInterface(); + if (decomp.openProgram(currentProgram)) { + for (Function function : currentProgram.getFunctionManager().getFunctions(true)) { + try { + DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); + if (results != null && results.decompileCompleted()) { + HighFunction highFunc = results.getHighFunction(); + if (highFunc != null) { + Iterator symbolIter = highFunc.getLocalSymbolMap().getSymbols(); + while (symbolIter.hasNext()) { + HighSymbol symbol = symbolIter.next(); + if (symbol.getName().toLowerCase().contains(lowerSearchTerm)) { + Map varInfo = new HashMap<>(); + varInfo.put("name", symbol.getName()); + varInfo.put("function", function.getName()); + varInfo.put("type", symbol.isParameter() ? "parameter" : "local"); + Address pcAddr = symbol.getPCAddress(); + varInfo.put("address", pcAddr != null ? pcAddr.toString() : "N/A"); + varInfo.put("dataType", symbol.getDataType() != null ? symbol.getDataType().getName() : "unknown"); + matchedVars.add(varInfo); + } + } + } + } + } catch (Exception e) { + Msg.warn(this, "searchVariables: Error processing function " + function.getName(), e); + } + } + } else { + Msg.error(this, "searchVariables: Failed to open program with decompiler."); + } + } catch (Exception e) { + Msg.error(this, "searchVariables: Error during local variable search", e); + } finally { + if (decomp != null) { + decomp.dispose(); + } + } + + Collections.sort(matchedVars, Comparator.comparing(a -> a.get("name"))); + + int start = Math.max(0, offset); + int end = Math.min(matchedVars.size(), offset + limit); + List> paginated = matchedVars.subList(start, end); + + return createSuccessResponse(paginated); // Keep using internal helper + } + + // --- Helper Methods (Keep internal for now, refactor later if needed) --- + + private String getDataTypeName(Program program, Address address) { + // This might be better in GhidraUtil if used elsewhere + ghidra.program.model.listing.Data data = program.getListing().getDataAt(address); + if (data == null) return "undefined"; + DataType dt = data.getDataType(); + return dt != null ? dt.getName() : "unknown"; + } + + // Keep internal response helpers for now, as they differ slightly from AbstractEndpoint's + private JsonObject createSuccessResponse(Object resultData) { + JsonObject response = new JsonObject(); + response.addProperty("success", true); + response.add("result", gson.toJsonTree(resultData)); + // These helpers don't add id/instance/_links, unlike ResponseBuilder + return response; + } + + private JsonObject createErrorResponse(String errorMessage, int statusCode) { + JsonObject response = new JsonObject(); + response.addProperty("success", false); + response.addProperty("error", errorMessage); + response.addProperty("status_code", statusCode); + return response; + } + + // parseIntOrDefault is inherited from AbstractEndpoint + } diff --git a/src/main/java/eu/starsong/ghidra/model/FunctionInfo.java b/src/main/java/eu/starsong/ghidra/model/FunctionInfo.java new file mode 100644 index 0000000..8ccd859 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/model/FunctionInfo.java @@ -0,0 +1,395 @@ +package eu.starsong.ghidra.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * Model class representing Ghidra function information. + * This provides a structured object for function data instead of using Map. + */ +public class FunctionInfo { + private String name; + private String address; + private String signature; + private String returnType; + private List parameters; + private String decompilation; + private boolean isExternal; + private String callingConvention; + private String namespace; + + /** + * Default constructor for serialization frameworks + */ + public FunctionInfo() { + this.parameters = new ArrayList<>(); + } + + /** + * Constructor with essential fields + */ + public FunctionInfo(String name, String address, String signature) { + this.name = name; + this.address = address; + this.signature = signature; + this.parameters = new ArrayList<>(); + } + + /** + * Full constructor + */ + public FunctionInfo(String name, String address, String signature, String returnType, + List parameters, String decompilation, + boolean isExternal, String callingConvention, String namespace) { + this.name = name; + this.address = address; + this.signature = signature; + this.returnType = returnType; + this.parameters = parameters != null ? parameters : new ArrayList<>(); + this.decompilation = decompilation; + this.isExternal = isExternal; + this.callingConvention = callingConvention; + this.namespace = namespace; + } + + /** + * @return The function name + */ + public String getName() { + return name; + } + + /** + * @param name The function name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return The function entry point address + */ + public String getAddress() { + return address; + } + + /** + * @param address The function entry point address + */ + public void setAddress(String address) { + this.address = address; + } + + /** + * @return The function signature (prototype string) + */ + public String getSignature() { + return signature; + } + + /** + * @param signature The function signature + */ + public void setSignature(String signature) { + this.signature = signature; + } + + /** + * @return The function return type + */ + public String getReturnType() { + return returnType; + } + + /** + * @param returnType The function return type + */ + public void setReturnType(String returnType) { + this.returnType = returnType; + } + + /** + * @return The function parameters + */ + public List getParameters() { + return parameters; + } + + /** + * @param parameters The function parameters + */ + public void setParameters(List parameters) { + this.parameters = parameters != null ? parameters : new ArrayList<>(); + } + + /** + * @return The decompiled C code for the function + */ + public String getDecompilation() { + return decompilation; + } + + /** + * @param decompilation The decompiled C code + */ + public void setDecompilation(String decompilation) { + this.decompilation = decompilation; + } + + /** + * @return Whether the function is external (imported) + */ + public boolean isExternal() { + return isExternal; + } + + /** + * @param external Whether the function is external + */ + public void setExternal(boolean external) { + isExternal = external; + } + + /** + * @return The function's calling convention + */ + public String getCallingConvention() { + return callingConvention; + } + + /** + * @param callingConvention The function's calling convention + */ + public void setCallingConvention(String callingConvention) { + this.callingConvention = callingConvention; + } + + /** + * @return The function's namespace + */ + public String getNamespace() { + return namespace; + } + + /** + * @param namespace The function's namespace + */ + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + /** + * Add a parameter to the function + * @param parameter The parameter to add + */ + public void addParameter(ParameterInfo parameter) { + if (parameter != null) { + this.parameters.add(parameter); + } + } + + /** + * Builder pattern for FunctionInfo + */ + public static class Builder { + private String name; + private String address; + private String signature; + private String returnType; + private List parameters = new ArrayList<>(); + private String decompilation; + private boolean isExternal; + private String callingConvention; + private String namespace; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder address(String address) { + this.address = address; + return this; + } + + public Builder signature(String signature) { + this.signature = signature; + return this; + } + + public Builder returnType(String returnType) { + this.returnType = returnType; + return this; + } + + public Builder parameters(List parameters) { + this.parameters = parameters; + return this; + } + + public Builder addParameter(ParameterInfo parameter) { + this.parameters.add(parameter); + return this; + } + + public Builder decompilation(String decompilation) { + this.decompilation = decompilation; + return this; + } + + public Builder isExternal(boolean isExternal) { + this.isExternal = isExternal; + return this; + } + + public Builder callingConvention(String callingConvention) { + this.callingConvention = callingConvention; + return this; + } + + public Builder namespace(String namespace) { + this.namespace = namespace; + return this; + } + + public FunctionInfo build() { + return new FunctionInfo( + name, address, signature, returnType, + parameters, decompilation, isExternal, + callingConvention, namespace + ); + } + } + + /** + * Create a new builder for FunctionInfo + * @return A new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Inner class representing function parameter information + */ + public static class ParameterInfo { + private String name; + private String dataType; + private int ordinal; + private String storage; + + /** + * Default constructor for serialization frameworks + */ + public ParameterInfo() { + } + + /** + * Full constructor + */ + public ParameterInfo(String name, String dataType, int ordinal, String storage) { + this.name = name; + this.dataType = dataType; + this.ordinal = ordinal; + this.storage = storage; + } + + /** + * @return The parameter name + */ + public String getName() { + return name; + } + + /** + * @param name The parameter name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return The parameter data type + */ + public String getDataType() { + return dataType; + } + + /** + * @param dataType The parameter data type + */ + public void setDataType(String dataType) { + this.dataType = dataType; + } + + /** + * @return The parameter position (0-based) + */ + public int getOrdinal() { + return ordinal; + } + + /** + * @param ordinal The parameter position + */ + public void setOrdinal(int ordinal) { + this.ordinal = ordinal; + } + + /** + * @return The parameter storage location + */ + public String getStorage() { + return storage; + } + + /** + * @param storage The parameter storage location + */ + public void setStorage(String storage) { + this.storage = storage; + } + + /** + * Builder pattern for ParameterInfo + */ + public static class Builder { + private String name; + private String dataType; + private int ordinal; + private String storage; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder dataType(String dataType) { + this.dataType = dataType; + return this; + } + + public Builder ordinal(int ordinal) { + this.ordinal = ordinal; + return this; + } + + public Builder storage(String storage) { + this.storage = storage; + return this; + } + + public ParameterInfo build() { + return new ParameterInfo(name, dataType, ordinal, storage); + } + } + + /** + * Create a new builder for ParameterInfo + * @return A new builder instance + */ + public static Builder builder() { + return new Builder(); + } + } +} diff --git a/src/main/java/eu/starsong/ghidra/model/JsonResponse.java b/src/main/java/eu/starsong/ghidra/model/JsonResponse.java new file mode 100644 index 0000000..4ebc72f --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/model/JsonResponse.java @@ -0,0 +1,175 @@ +package eu.starsong.ghidra.model; + +import java.util.HashMap; +import java.util.Map; + +/** + * Standardized response object for API responses. + * This class follows the common response structure used throughout the API. + */ +public class JsonResponse { + private boolean success; + private Object result; + private Map error; + private Map links; + private String id; + private String instance; + + // Private constructor for builder pattern + private JsonResponse() { + this.links = new HashMap<>(); + } + + /** + * @return Whether the request was successful + */ + public boolean isSuccess() { + return success; + } + + /** + * @return The result data for successful requests + */ + public Object getResult() { + return result; + } + + /** + * @return Error information for failed requests + */ + public Map getError() { + return error; + } + + /** + * @return HATEOAS links + */ + public Map getLinks() { + return links; + } + + /** + * @return Request ID + */ + public String getId() { + return id; + } + + /** + * @return Server instance information + */ + public String getInstance() { + return instance; + } + + /** + * Creates a new builder for constructing a JsonResponse + * @return A new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for JsonResponse + */ + public static class Builder { + private final JsonResponse response; + + private Builder() { + response = new JsonResponse(); + } + + /** + * Set the success status + * @param success Whether the request was successful + * @return This builder + */ + public Builder success(boolean success) { + response.success = success; + return this; + } + + /** + * Set the result data + * @param result The result data + * @return This builder + */ + public Builder result(Object result) { + response.result = result; + return this; + } + + /** + * Set error information + * @param message Error message + * @param code Error code + * @return This builder + */ + public Builder error(String message, String code) { + Map error = new HashMap<>(); + error.put("message", message); + if (code != null && !code.isEmpty()) { + error.put("code", code); + } + response.error = error; + return this; + } + + /** + * Add a link + * @param rel Relation name + * @param href Link URL + * @return This builder + */ + public Builder addLink(String rel, String href) { + Map link = new HashMap<>(); + link.put("href", href); + response.links.put(rel, link); + return this; + } + + /** + * Add a link with method + * @param rel Relation name + * @param href Link URL + * @param method HTTP method + * @return This builder + */ + public Builder addLink(String rel, String href, String method) { + Map link = new HashMap<>(); + link.put("href", href); + link.put("method", method); + response.links.put(rel, link); + return this; + } + + /** + * Set request ID + * @param id Request ID + * @return This builder + */ + public Builder id(String id) { + response.id = id; + return this; + } + + /** + * Set instance information + * @param instance Instance information + * @return This builder + */ + public Builder instance(String instance) { + response.instance = instance; + return this; + } + + /** + * Build the JsonResponse + * @return The constructed JsonResponse + */ + public JsonResponse build() { + return response; + } + } +} diff --git a/src/main/java/eu/starsong/ghidra/model/ProgramInfo.java b/src/main/java/eu/starsong/ghidra/model/ProgramInfo.java new file mode 100644 index 0000000..e087c99 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/model/ProgramInfo.java @@ -0,0 +1,218 @@ +package eu.starsong.ghidra.model; + +/** + * Model class representing Ghidra program information. + * This provides a structured object for program data instead of using Map. + */ +public class ProgramInfo { + private String programId; + private String name; + private String languageId; + private String compilerSpecId; + private String imageBase; + private long memorySize; + private boolean isOpen; + private boolean analysisComplete; + + /** + * Default constructor for serialization frameworks + */ + public ProgramInfo() { + } + + /** + * Full constructor + */ + public ProgramInfo(String programId, String name, String languageId, String compilerSpecId, + String imageBase, long memorySize, boolean isOpen, boolean analysisComplete) { + this.programId = programId; + this.name = name; + this.languageId = languageId; + this.compilerSpecId = compilerSpecId; + this.imageBase = imageBase; + this.memorySize = memorySize; + this.isOpen = isOpen; + this.analysisComplete = analysisComplete; + } + + /** + * @return The program's unique identifier (typically the file pathname) + */ + public String getProgramId() { + return programId; + } + + /** + * @param programId The program's unique identifier + */ + public void setProgramId(String programId) { + this.programId = programId; + } + + /** + * @return The program's name + */ + public String getName() { + return name; + } + + /** + * @param name The program's name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return The program's language ID + */ + public String getLanguageId() { + return languageId; + } + + /** + * @param languageId The program's language ID + */ + public void setLanguageId(String languageId) { + this.languageId = languageId; + } + + /** + * @return The program's compiler specification ID + */ + public String getCompilerSpecId() { + return compilerSpecId; + } + + /** + * @param compilerSpecId The program's compiler specification ID + */ + public void setCompilerSpecId(String compilerSpecId) { + this.compilerSpecId = compilerSpecId; + } + + /** + * @return The program's image base address + */ + public String getImageBase() { + return imageBase; + } + + /** + * @param imageBase The program's image base address + */ + public void setImageBase(String imageBase) { + this.imageBase = imageBase; + } + + /** + * @return The program's memory size in bytes + */ + public long getMemorySize() { + return memorySize; + } + + /** + * @param memorySize The program's memory size in bytes + */ + public void setMemorySize(long memorySize) { + this.memorySize = memorySize; + } + + /** + * @return Whether the program is currently open + */ + public boolean isOpen() { + return isOpen; + } + + /** + * @param open Whether the program is currently open + */ + public void setOpen(boolean open) { + isOpen = open; + } + + /** + * @return Whether analysis has been completed on the program + */ + public boolean isAnalysisComplete() { + return analysisComplete; + } + + /** + * @param analysisComplete Whether analysis has been completed on the program + */ + public void setAnalysisComplete(boolean analysisComplete) { + this.analysisComplete = analysisComplete; + } + + /** + * Builder pattern for ProgramInfo + */ + public static class Builder { + private String programId; + private String name; + private String languageId; + private String compilerSpecId; + private String imageBase; + private long memorySize; + private boolean isOpen; + private boolean analysisComplete; + + public Builder programId(String programId) { + this.programId = programId; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder languageId(String languageId) { + this.languageId = languageId; + return this; + } + + public Builder compilerSpecId(String compilerSpecId) { + this.compilerSpecId = compilerSpecId; + return this; + } + + public Builder imageBase(String imageBase) { + this.imageBase = imageBase; + return this; + } + + public Builder memorySize(long memorySize) { + this.memorySize = memorySize; + return this; + } + + public Builder isOpen(boolean isOpen) { + this.isOpen = isOpen; + return this; + } + + public Builder analysisComplete(boolean analysisComplete) { + this.analysisComplete = analysisComplete; + return this; + } + + public ProgramInfo build() { + return new ProgramInfo( + programId, name, languageId, compilerSpecId, + imageBase, memorySize, isOpen, analysisComplete + ); + } + } + + /** + * Create a new builder for ProgramInfo + * @return A new builder instance + */ + public static Builder builder() { + return new Builder(); + } +} diff --git a/src/main/java/eu/starsong/ghidra/model/VariableInfo.java b/src/main/java/eu/starsong/ghidra/model/VariableInfo.java new file mode 100644 index 0000000..3066bf2 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/model/VariableInfo.java @@ -0,0 +1,226 @@ +package eu.starsong.ghidra.model; + +/** + * Model class representing Ghidra variable information. + * This provides a structured object for variable data instead of using Map. + */ +public class VariableInfo { + private String name; + private String dataType; + private String address; + private String type; // "local", "parameter", "global", etc. + private String function; // Function name if local/parameter + private String storage; // Storage location + private String value; // Value if known + + /** + * Default constructor for serialization frameworks + */ + public VariableInfo() { + } + + /** + * Constructor with essential fields + */ + public VariableInfo(String name, String dataType, String type) { + this.name = name; + this.dataType = dataType; + this.type = type; + } + + /** + * Full constructor + */ + public VariableInfo(String name, String dataType, String address, String type, + String function, String storage, String value) { + this.name = name; + this.dataType = dataType; + this.address = address; + this.type = type; + this.function = function; + this.storage = storage; + this.value = value; + } + + /** + * @return The variable name + */ + public String getName() { + return name; + } + + /** + * @param name The variable name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return The variable data type + */ + public String getDataType() { + return dataType; + } + + /** + * @param dataType The variable data type + */ + public void setDataType(String dataType) { + this.dataType = dataType; + } + + /** + * @return The variable address (if applicable) + */ + public String getAddress() { + return address; + } + + /** + * @param address The variable address + */ + public void setAddress(String address) { + this.address = address; + } + + /** + * @return The variable type (local, parameter, global, etc.) + */ + public String getType() { + return type; + } + + /** + * @param type The variable type + */ + public void setType(String type) { + this.type = type; + } + + /** + * @return The function name (for local variables and parameters) + */ + public String getFunction() { + return function; + } + + /** + * @param function The function name + */ + public void setFunction(String function) { + this.function = function; + } + + /** + * @return The variable storage location + */ + public String getStorage() { + return storage; + } + + /** + * @param storage The variable storage location + */ + public void setStorage(String storage) { + this.storage = storage; + } + + /** + * @return The variable value (if known) + */ + public String getValue() { + return value; + } + + /** + * @param value The variable value + */ + public void setValue(String value) { + this.value = value; + } + + /** + * @return Whether this variable is a local variable + */ + public boolean isLocal() { + return "local".equals(type); + } + + /** + * @return Whether this variable is a parameter + */ + public boolean isParameter() { + return "parameter".equals(type); + } + + /** + * @return Whether this variable is a global variable + */ + public boolean isGlobal() { + return "global".equals(type); + } + + /** + * Builder pattern for VariableInfo + */ + public static class Builder { + private String name; + private String dataType; + private String address; + private String type; + private String function; + private String storage; + private String value; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder dataType(String dataType) { + this.dataType = dataType; + return this; + } + + public Builder address(String address) { + this.address = address; + return this; + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder function(String function) { + this.function = function; + return this; + } + + public Builder storage(String storage) { + this.storage = storage; + return this; + } + + public Builder value(String value) { + this.value = value; + return this; + } + + public VariableInfo build() { + return new VariableInfo( + name, dataType, address, type, + function, storage, value + ); + } + } + + /** + * Create a new builder for VariableInfo + * @return A new builder instance + */ + public static Builder builder() { + return new Builder(); + } +} diff --git a/src/main/java/eu/starsong/ghidra/util/GhidraSupplier.java b/src/main/java/eu/starsong/ghidra/util/GhidraSupplier.java new file mode 100644 index 0000000..1abc5e2 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/util/GhidraSupplier.java @@ -0,0 +1,6 @@ +package eu.starsong.ghidra.util; + + @FunctionalInterface + public interface GhidraSupplier { + T get() throws Exception; + } diff --git a/src/main/java/eu/starsong/ghidra/util/GhidraUtil.java b/src/main/java/eu/starsong/ghidra/util/GhidraUtil.java new file mode 100644 index 0000000..0a8cc52 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/util/GhidraUtil.java @@ -0,0 +1,287 @@ +package eu.starsong.ghidra.util; + +import ghidra.app.decompiler.DecompInterface; +import ghidra.app.decompiler.DecompileOptions; +import ghidra.app.decompiler.DecompileResults; +import ghidra.app.services.GoToService; +import ghidra.app.services.ProgramManager; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressFactory; +import ghidra.program.model.data.DataType; +import ghidra.program.model.data.DataTypeManager; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionManager; +import ghidra.program.model.listing.Parameter; +import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.Variable; +import ghidra.program.model.pcode.HighFunction; +import ghidra.program.model.pcode.HighVariable; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.Varnode; +import ghidra.program.model.symbol.SymbolTable; +import ghidra.program.util.ProgramLocation; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class GhidraUtil { + + /** + * Parse an integer from a string, or return defaultValue if null/invalid. + */ + public static int parseIntOrDefault(String val, int defaultValue) { + if (val == null) return defaultValue; + try { + return Integer.parseInt(val); + } + catch (NumberFormatException e) { + return defaultValue; + } + } + + /** + * Finds a data type by name within the program's data type managers. + * @param program The current program. + * @param dataTypeName The name of the data type to find. + * @return The found DataType, or null if not found. + */ + public static DataType findDataType(Program program, String dataTypeName) { + if (program == null || dataTypeName == null || dataTypeName.isEmpty()) { + return null; + } + DataTypeManager dtm = program.getDataTypeManager(); + List foundTypes = new ArrayList<>(); + dtm.findDataTypes(dataTypeName, foundTypes); + + if (!foundTypes.isEmpty()) { + // Prefer the first match, might need more sophisticated logic + // if multiple types with the same name exist in different categories. + return foundTypes.get(0); + } else { + Msg.warn(GhidraUtil.class, "Data type not found: " + dataTypeName); + return null; + } + } + + /** + * Gets the current address as a string from the Ghidra tool. + * @param tool The Ghidra plugin tool. + * @return The current address as a string, or null if not available. + */ + public static String getCurrentAddressString(PluginTool tool) { + if (tool == null) { + return null; + } + + // Get current program + Program program = tool.getService(ProgramManager.class).getCurrentProgram(); + if (program == null) { + return null; + } + + // Return the current address + return "00000000"; // Placeholder - actual implementation would get current cursor position + } + + /** + * Gets information about the current function in the Ghidra tool. + * @param tool The Ghidra plugin tool. + * @param program The current program. + * @return A map containing information about the current function, or an empty map if not available. + */ + public static Map getCurrentFunctionInfo(PluginTool tool, Program program) { + Map result = new HashMap<>(); + + if (tool == null || program == null) { + return result; + } + + // For now, just return the first function in the program as a placeholder + FunctionManager functionManager = program.getFunctionManager(); + Function function = null; + + for (Function f : functionManager.getFunctions(true)) { + function = f; + break; + } + + if (function == null) { + return result; + } + + result.put("name", function.getName()); + result.put("address", function.getEntryPoint().toString()); + result.put("signature", function.getSignature().getPrototypeString()); + + return result; + } + + /** + * Gets information about a function at the specified address. + * @param program The current program. + * @param addressStr The address as a string. + * @return A map containing information about the function, or an empty map if not found. + */ + public static Map getFunctionByAddress(Program program, String addressStr) { + Map result = new HashMap<>(); + + if (program == null || addressStr == null || addressStr.isEmpty()) { + return result; + } + + AddressFactory addressFactory = program.getAddressFactory(); + Address address; + + try { + address = addressFactory.getAddress(addressStr); + } catch (Exception e) { + Msg.error(GhidraUtil.class, "Invalid address format: " + addressStr, e); + return result; + } + + if (address == null) { + return result; + } + + FunctionManager functionManager = program.getFunctionManager(); + Function function = functionManager.getFunctionAt(address); + + if (function == null) { + function = functionManager.getFunctionContaining(address); + } + + if (function == null) { + return result; + } + + result.put("name", function.getName()); + result.put("address", function.getEntryPoint().toString()); + result.put("signature", function.getSignature().getPrototypeString()); + + // Add decompilation + String decompilation = decompileFunction(function); + result.put("decompilation", decompilation != null ? decompilation : ""); + + return result; + } + + /** + * Decompiles a function at the specified address. + * @param program The current program. + * @param addressStr The address as a string. + * @return A map containing the decompilation result, or an empty map if not found. + */ + public static Map decompileFunction(Program program, String addressStr) { + Map result = new HashMap<>(); + + if (program == null || addressStr == null || addressStr.isEmpty()) { + return result; + } + + AddressFactory addressFactory = program.getAddressFactory(); + Address address; + + try { + address = addressFactory.getAddress(addressStr); + } catch (Exception e) { + Msg.error(GhidraUtil.class, "Invalid address format: " + addressStr, e); + return result; + } + + if (address == null) { + return result; + } + + FunctionManager functionManager = program.getFunctionManager(); + Function function = functionManager.getFunctionAt(address); + + if (function == null) { + function = functionManager.getFunctionContaining(address); + } + + if (function == null) { + return result; + } + + String decompilation = decompileFunction(function); + result.put("decompilation", decompilation != null ? decompilation : ""); + + return result; + } + + /** + * Helper method to decompile a function. + * @param function The function to decompile. + * @return The decompiled code as a string, or null if decompilation failed. + */ + private static String decompileFunction(Function function) { + if (function == null) { + return null; + } + + Program program = function.getProgram(); + DecompInterface decompiler = new DecompInterface(); + DecompileOptions options = new DecompileOptions(); + + decompiler.setOptions(options); + decompiler.openProgram(program); + + try { + DecompileResults results = decompiler.decompileFunction(function, 30, TaskMonitor.DUMMY); + if (results.decompileCompleted()) { + return results.getDecompiledFunction().getC(); + } else { + Msg.warn(GhidraUtil.class, "Decompilation failed for function: " + function.getName()); + return "// Decompilation failed for " + function.getName(); + } + } catch (Exception e) { + Msg.error(GhidraUtil.class, "Error during decompilation of function: " + function.getName(), e); + return "// Error during decompilation: " + e.getMessage(); + } finally { + decompiler.dispose(); + } + } + + /** + * Gets information about variables in a function. + * @param function The function to get variables from. + * @return A list of maps containing information about each variable. + */ + public static List> getFunctionVariables(Function function) { + List> variables = new ArrayList<>(); + + if (function == null) { + return variables; + } + + // Add parameters + for (Parameter param : function.getParameters()) { + Map varInfo = new HashMap<>(); + varInfo.put("name", param.getName()); + varInfo.put("type", param.getDataType().getName()); + varInfo.put("isParameter", true); + variables.add(varInfo); + } + + // Add local variables + for (Variable var : function.getAllVariables()) { + if (var instanceof Parameter) { + continue; // Skip parameters, already added + } + + Map varInfo = new HashMap<>(); + varInfo.put("name", var.getName()); + varInfo.put("type", var.getDataType().getName()); + varInfo.put("isParameter", false); + variables.add(varInfo); + } + + return variables; + } +} diff --git a/src/main/java/eu/starsong/ghidra/util/HttpUtil.java b/src/main/java/eu/starsong/ghidra/util/HttpUtil.java new file mode 100644 index 0000000..f7be77e --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/util/HttpUtil.java @@ -0,0 +1,123 @@ +package eu.starsong.ghidra.util; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.sun.net.httpserver.HttpExchange; +import eu.starsong.ghidra.api.ResponseBuilder; // Use the ResponseBuilder +import ghidra.util.Msg; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class HttpUtil { + + private static final Gson gson = new Gson(); + + /** + * Sends a JSON response with the given status code. + * Uses the ResponseBuilder internally. + */ + public static void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj, int statusCode, int port) throws IOException { + try { + String json = gson.toJson(jsonObj); + byte[] bytes = json.getBytes(StandardCharsets.UTF_8); + + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + // Consider adding CORS headers if needed: + // exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); + + long responseLength = (statusCode == 204) ? -1 : bytes.length; + exchange.sendResponseHeaders(statusCode, responseLength); + + if (responseLength != -1) { + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } else { + exchange.getResponseBody().close(); // Important for 204 + } + } catch (Exception e) { + Msg.error(HttpUtil.class, "Error sending JSON response: " + e.getMessage(), e); + // Avoid sending another error response here to prevent potential loops + if (!exchange.getResponseHeaders().containsKey("Content-Type")) { + byte[] errorBytes = ("Internal Server Error: " + e.getMessage()).getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8"); + exchange.sendResponseHeaders(500, errorBytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(errorBytes); + } catch (IOException writeErr) { + Msg.error(HttpUtil.class, "Failed to send even plain text error response", writeErr); + } + } + throw new IOException("Failed to send JSON response", e); + } + } + + /** + * Sends a standardized error response using ResponseBuilder. + */ + public static void sendErrorResponse(HttpExchange exchange, int statusCode, String message, String errorCode, int port) throws IOException { + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(false) + .error(message, errorCode); + sendJsonResponse(exchange, builder.build(), statusCode, port); + } + + /** + * Parses query parameters from the URL. + */ + public static Map parseQueryParams(HttpExchange exchange) { + Map result = new HashMap<>(); + String query = exchange.getRequestURI().getQuery(); + if (query != null) { + String[] pairs = query.split("&"); + for (String p : pairs) { + String[] kv = p.split("="); + if (kv.length == 2) { + try { + result.put(kv[0], java.net.URLDecoder.decode(kv[1], StandardCharsets.UTF_8)); + } catch (Exception e) { + Msg.warn(HttpUtil.class, "Failed to decode query parameter: " + kv[0]); + result.put(kv[0], kv[1]); + } + } else if (kv.length == 1 && !kv[0].isEmpty()) { + result.put(kv[0], ""); + } + } + } + return result; + } + + /** + * Parses POST body parameters strictly as JSON. + */ + public static Map parseJsonPostParams(HttpExchange exchange) throws IOException { + byte[] body = exchange.getRequestBody().readAllBytes(); + String bodyStr = new String(body, StandardCharsets.UTF_8); + Map params = new HashMap<>(); + + try { + JsonObject json = gson.fromJson(bodyStr, JsonObject.class); + if (json == null) { + return params; + } + for (Map.Entry entry : json.entrySet()) { + String key = entry.getKey(); + JsonElement value = entry.getValue(); + if (value.isJsonPrimitive()) { + params.put(key, value.getAsString()); + } else { + params.put(key, value.toString()); // Stringify non-primitives + } + } + } catch (Exception e) { + Msg.error(HttpUtil.class, "Failed to parse JSON request body: " + bodyStr, e); + throw new IOException("Invalid JSON request body: " + e.getMessage(), e); + } + return params; + } +} diff --git a/src/main/java/eu/starsong/ghidra/util/TransactionHelper.java b/src/main/java/eu/starsong/ghidra/util/TransactionHelper.java new file mode 100644 index 0000000..c9aed85 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/util/TransactionHelper.java @@ -0,0 +1,59 @@ +package eu.starsong.ghidra.util; + +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; +import javax.swing.SwingUtilities; +import java.util.concurrent.atomic.AtomicReference; + +public class TransactionHelper { + + @FunctionalInterface + public interface GhidraSupplier { + T get() throws Exception; + } + + public static T executeInTransaction(Program program, String transactionName, GhidraSupplier operation) + throws TransactionException { + + if (program == null) { + throw new IllegalArgumentException("Program cannot be null for transaction"); + } + + AtomicReference result = new AtomicReference<>(); + AtomicReference exception = new AtomicReference<>(); + + try { + SwingUtilities.invokeAndWait(() -> { + int txId = -1; + boolean success = false; + try { + txId = program.startTransaction(transactionName); + if (txId < 0) { + throw new TransactionException("Failed to start transaction: " + transactionName); + } + result.set(operation.get()); + success = true; + } catch (Exception e) { + exception.set(e); + Msg.error(TransactionHelper.class, "Transaction failed: " + transactionName, e); + } finally { + if (txId >= 0) { + program.endTransaction(txId, success); + } + } + }); + } catch (Exception e) { + throw new TransactionException("Swing thread execution failed", e); + } + + if (exception.get() != null) { + throw new TransactionException("Operation failed", exception.get()); + } + return result.get(); + } + + public static class TransactionException extends Exception { + public TransactionException(String message) { super(message); } + public TransactionException(String message, Throwable cause) { super(message, cause); } + } +} diff --git a/test_http_api.py b/test_http_api.py index c40c3a1..3bc804c 100644 --- a/test_http_api.py +++ b/test_http_api.py @@ -26,10 +26,8 @@ class GhydraMCPHttpApiTests(unittest.TestCase): """Helper to assert the standard success response structure.""" self.assertIn("success", data, "Response missing 'success' field") self.assertTrue(data["success"], f"API call failed: {data.get('error', 'Unknown error')}") - self.assertIn("timestamp", data, "Response missing 'timestamp' field") - self.assertIsInstance(data["timestamp"], (int, float), "'timestamp' should be a number") - self.assertIn("port", data, "Response missing 'port' field") - self.assertEqual(data["port"], DEFAULT_PORT, f"Response port mismatch: expected {DEFAULT_PORT}, got {data['port']}") + self.assertIn("id", data, "Response missing 'id' field") + self.assertIn("instance", data, "Response missing 'instance' field") self.assertIn("result", data, "Response missing 'result' field") if expected_result_type: self.assertIsInstance(data["result"], expected_result_type, f"'result' field type mismatch: expected {expected_result_type}, got {type(data['result'])}") @@ -52,11 +50,14 @@ class GhydraMCPHttpApiTests(unittest.TestCase): # Verify response is valid JSON data = response.json() - # Check required fields - self.assertIn("port", data) - self.assertIn("isBaseInstance", data) - self.assertIn("project", data) - self.assertIn("file", data) + # Check standard response structure + self.assertStandardSuccessResponse(data, expected_result_type=dict) + + # Check required fields in result + result = data["result"] + self.assertIn("isBaseInstance", result) + self.assertIn("project", result) + self.assertIn("file", result) def test_root_endpoint(self): """Test the / endpoint""" @@ -66,11 +67,13 @@ class GhydraMCPHttpApiTests(unittest.TestCase): # Verify response is valid JSON data = response.json() - # Check required fields - self.assertIn("port", data) - self.assertIn("isBaseInstance", data) - self.assertIn("project", data) - self.assertIn("file", data) + # Check standard response structure + self.assertStandardSuccessResponse(data, expected_result_type=dict) + + # Check required fields in result + result = data["result"] + self.assertIn("isBaseInstance", result) + self.assertIn("message", result) def test_instances_endpoint(self): """Test the /instances endpoint"""