diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/APIError.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/APIError.java new file mode 100644 index 0000000..db7e1d3 --- /dev/null +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/APIError.java @@ -0,0 +1,15 @@ +package ai.reveng.toolkit.ghidra.core.services.api; + +import org.json.JSONObject; + +public record APIError( + String code, + String message +) { + public static APIError fromJSONObject(JSONObject json) { + return new APIError( + json.getString("code"), + json.getString("message") + ); + } +} diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/APIVersion.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/APIVersion.java new file mode 100644 index 0000000..b803719 --- /dev/null +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/APIVersion.java @@ -0,0 +1,6 @@ +package ai.reveng.toolkit.ghidra.core.services.api; + +public enum APIVersion { + V1, + V2 +} diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java index a86604f..8c7fe42 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java @@ -15,6 +15,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.net.http.HttpTimeoutException; import java.nio.file.Path; import java.time.Duration; import java.util.*; @@ -36,7 +37,6 @@ public class TypedApiImplementation implements TypedApiInterface { private final HttpClient httpClient; private String baseUrl; - private String apiVersion; private String apiKey; Map headers; @@ -47,7 +47,6 @@ public TypedApiImplementation(String baseUrl, String apiKey) { .connectTimeout(Duration.ofSeconds(5)) .version(HTTP_1_1) // by default the client would attempt HTTP2.0 which leads to weird issues .build(); - this.apiVersion = "v1"; headers = new HashMap<>(); headers.put("Authorization", this.apiKey); headers.put("User-Agent", "REAIT Java Proxy"); @@ -67,7 +66,7 @@ public List recentAnalyses(AnalysisStatus status, AnalysisScope parameters.put("scope", scope.name()); parameters.put("n", number); - HttpRequest.Builder requestBuilder = requestBuilderForEndpoint("analyse/recent"); + HttpRequest.Builder requestBuilder = requestBuilderForEndpoint(APIVersion.V1, "analyse/recent"); requestBuilder .method("GET",HttpRequest.BodyPublishers.ofString(parameters.toString())) .header("Content-Type", "application/json"); @@ -106,7 +105,7 @@ public BinaryHash upload(Path binPath) throws FileNotFoundException { byte[] requestBody = Bytes.concat(bodyStart.getBytes(), fileBytes, bodyEnd.getBytes()); // Create HttpRequest - var request = requestBuilderForEndpoint("upload") + var request = requestBuilderForEndpoint(APIVersion.V1, "upload") .POST(HttpRequest.BodyPublishers.ofByteArray(requestBody)) .header("Content-Type", "multipart/form-data; boundary=" + boundary) .build(); @@ -149,7 +148,7 @@ public List search( JSONObject json = sendRequest( - requestBuilderForEndpoint("search") + requestBuilderForEndpoint(APIVersion.V1, "search") .method("GET", HttpRequest.BodyPublishers.ofString(parameters.toString())) .header("Content-Type", "application/json" ) .build()); @@ -157,6 +156,10 @@ public List search( return mapJSONArray(json.getJSONArray("query_results"), AnalysisResult::fromJSONObject); } + private V2Response sendVersion2Request(HttpRequest request){ + return V2Response.fromJSONObject(sendRequest(request)); + } + private JSONObject sendRequest(HttpRequest request) throws APIAuthenticationException { HttpResponse response = null; @@ -179,41 +182,9 @@ private JSONObject sendRequest(HttpRequest request) throws APIAuthenticationExce } } - - @Override - public BinaryID analyse(BinaryHash binHash, - Long baseAddress, - List functionBounds, ModelName modelName) { - JSONObject parameters = new JSONObject(); - // Probably no point making this configurable for now - parameters.put("model_name", modelName.modelName()); -// parameters.put("platform_options", "Auto"); -// parameters.put("isa_options", "Auto"); -// parameters.put("file_options", "Auto"); -// parameters.put("dynamic_execution", false); -// parameters.put("command_line_args", ""); -// parameters.put("priority", 0); - - // Make configurable later -// parameters.put("tags", new JSONArray()); -// parameters.put("binary_scope", AnalysisScope.PRIVATE.name()); - - // Actual arguments - parameters.put("sha_256_hash", binHash.sha256()); -// parameters.put("debug_hash", ""); // ??? - - - var request = requestBuilderForEndpoint("analyse/") - .POST(HttpRequest.BodyPublishers.ofString(parameters.toString())) - .header("Content-Type", "application/json" ) - .build(); - var jsonResponse = sendRequest(request); - return new BinaryID(jsonResponse.getInt("binary_id")); - } - @Override public BinaryID analyse(AnalysisOptionsBuilder builder) { - var request = requestBuilderForEndpoint("analyse/") + var request = requestBuilderForEndpoint(APIVersion.V1, "analyse/") .POST(HttpRequest.BodyPublishers.ofString(builder.toJSON().toString())) .header("Content-Type", "application/json" ) .build(); @@ -238,7 +209,7 @@ public List annSymbolsForBinary(BinaryID binID, } - var request = requestBuilderForEndpoint("ann/symbol/" + binID.value()) + var request = requestBuilderForEndpoint(APIVersion.V1, "ann/symbol/" + binID.value()) .POST(HttpRequest.BodyPublishers.ofString(params.toString())) .header("Content-Type", "application/json" ) .build(); @@ -258,7 +229,7 @@ public List annSymbolsForFunctions(List fID, params.put("debug_mode", false); params.put("function_id_list", fID.stream().map(FunctionID::value).toList()); - var request = requestBuilderForEndpoint("ann/symbol/batch") + var request = requestBuilderForEndpoint(APIVersion.V1, "ann/symbol/batch") .POST(HttpRequest.BodyPublishers.ofString(params.toString())) .header("Content-Type", "application/json" ) .build(); @@ -269,7 +240,7 @@ public List annSymbolsForFunctions(List fID, @Override public AnalysisStatus status(BinaryID binaryID) { - var request = requestBuilderForEndpoint("analyse/status/" + binaryID.value()) + var request = requestBuilderForEndpoint(APIVersion.V1, "analyse/status/" + binaryID.value()) .GET() .build(); return AnalysisStatus.valueOf(sendRequest(request).getString("status")); @@ -277,7 +248,7 @@ public AnalysisStatus status(BinaryID binaryID) { @Override public List getFunctionInfo(BinaryID binaryID) { - var request = requestBuilderForEndpoint("analyse/functions/" + binaryID.value()) + var request = requestBuilderForEndpoint(APIVersion.V1, "analyse/functions/" + binaryID.value()) .GET() .build(); @@ -301,7 +272,7 @@ public String healthMessage(){ @Override public List collectionQuickSearch(ModelName modelName) { - var request = requestBuilderForEndpoint("collections/quick/search?model_name=" + modelName.modelName()) + var request = requestBuilderForEndpoint(APIVersion.V1, "collections/quick/search?model_name=" + modelName.modelName()) .build(); var response = sendRequest(request); var result = new ArrayList(); @@ -310,7 +281,7 @@ public List collectionQuickSearch(ModelName modelName) { @Override public List collectionQuickSearch(String searchTerm) { - var request = requestBuilderForEndpoint("collections/quick/search?search_term=" + searchTerm) + var request = requestBuilderForEndpoint(APIVersion.V1, "collections/quick/search?search_term=" + searchTerm) .build(); var response = sendRequest(request); return mapJSONArray( @@ -320,7 +291,7 @@ public List collectionQuickSearch(String searchTerm) { @Override public String getAnalysisLogs(BinaryID binID) { - var request = requestBuilderForEndpoint("logs/" + binID.value()) + var request = requestBuilderForEndpoint(APIVersion.V1, "logs/" + binID.value()) .build(); var response = sendRequest(request); return response.getString("logs"); @@ -329,7 +300,7 @@ public String getAnalysisLogs(BinaryID binID) { public JSONObject health(){ URI uri; try { - uri = new URI(baseUrl + apiVersion); + uri = new URI(baseUrl + "v1"); } catch (URISyntaxException e) { throw new RuntimeException(e); } @@ -344,27 +315,37 @@ public JSONObject health(){ } } - private HttpRequest.Builder requestBuilderForEndpoint(String endpoint){ + private HttpRequest.Builder requestBuilderForEndpoint(APIVersion version, String endpoint){ URI uri; + String apiVersionPath; + if (version == APIVersion.V1){ + apiVersionPath = "v1"; + } else if (version == APIVersion.V2){ + apiVersionPath = "v2"; + } else { + throw new RuntimeException("Unknown API version"); + } + try { - uri = new URI(baseUrl + apiVersion + "/" + endpoint); + uri = new URI(baseUrl + apiVersionPath + "/" + endpoint); } catch (URISyntaxException e) { throw new RuntimeException(e); } var requestBuilder = HttpRequest.newBuilder(uri); headers.forEach(requestBuilder::header); + requestBuilder.timeout(Duration.ofSeconds(1)); return requestBuilder; } @Override public List models(){ - JSONObject jsonResponse = sendRequest(requestBuilderForEndpoint("models").GET().build()); + JSONObject jsonResponse = sendRequest(requestBuilderForEndpoint(APIVersion.V1, "models").GET().build()); return mapJSONArray(jsonResponse.getJSONArray("models"), o -> new ModelName(o.getString("model_name"))); } @Override public void authenticate() throws InvalidAPIInfoException { - var request = requestBuilderForEndpoint("authenticate") + var request = requestBuilderForEndpoint(APIVersion.V1, "authenticate") .build(); try { sendRequest(request); diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java index 0addc78..3557a9f 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java @@ -12,6 +12,13 @@ /** * Service for interacting with the RevEngAi API * This is a generic Java Interface and should not use any Ghidra specific classes + * + * It aims to stick close to the API functions themselves. + * E.g. if a feature is implemented via two API calls, it should be implemented as two methods here. + * + * Wrapping this feature into one conceptual method should then happen inside the {@link ai.reveng.toolkit.ghidra.core.services.api.GhidraRevengService} + * + * */ public interface TypedApiInterface { // Analysis @@ -21,10 +28,6 @@ List search( Collection collection, AnalysisStatus state); - BinaryID analyse(BinaryHash binHash, - Long baseAddress, - List functionBounds, ModelName modelName); - BinaryID analyse(AnalysisOptionsBuilder binHash); diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/V2Response.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/V2Response.java new file mode 100644 index 0000000..fc7228c --- /dev/null +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/V2Response.java @@ -0,0 +1,45 @@ +package ai.reveng.toolkit.ghidra.core.services.api; + + +import org.json.JSONObject; + +import java.util.List; + +import static ai.reveng.toolkit.ghidra.core.services.api.Utils.mapJSONArray; + +/** + * Structured Response from any V2 Endpoint + * { + * "status": true, + * "data": { + * "queued": true, + * "reference": "404f60e6-7b1d-4adf-951c-710925422bd8" + * }, + * "message": null, + * "errors": null, + * "meta": { + * "pagination": null + * } + * } + * + */ +public record V2Response( + boolean status, + JSONObject data, + String message, + List errors, + JSONObject meta + +) { + + + public static V2Response fromJSONObject(JSONObject json) { + return new V2Response( + json.getBoolean("status"), + !json.isNull("data") ? json.getJSONObject("data") : null, + !json.isNull("message") ? json.getString("message") : null, + !json.isNull("errors") ? mapJSONArray(json.getJSONArray("errors"), APIError::fromJSONObject) : null, + json.getJSONObject("meta") + ); + } +}