Skip to content

Commit

Permalink
Support mixed API version usage
Browse files Browse the repository at this point in the history
  • Loading branch information
fmagin committed Nov 22, 2024
1 parent bf0e7ba commit e122ed2
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -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")
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ai.reveng.toolkit.ghidra.core.services.api;

public enum APIVersion {
V1,
V2
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -36,7 +37,6 @@ public class TypedApiImplementation implements TypedApiInterface {

private final HttpClient httpClient;
private String baseUrl;
private String apiVersion;
private String apiKey;
Map<String, String> headers;

Expand All @@ -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");
Expand All @@ -67,7 +66,7 @@ public List<AnalysisResult> 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");
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -149,14 +148,18 @@ public List<AnalysisResult> search(


JSONObject json = sendRequest(
requestBuilderForEndpoint("search")
requestBuilderForEndpoint(APIVersion.V1, "search")
.method("GET", HttpRequest.BodyPublishers.ofString(parameters.toString()))
.header("Content-Type", "application/json" )
.build());

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<String> response = null;

Expand All @@ -179,41 +182,9 @@ private JSONObject sendRequest(HttpRequest request) throws APIAuthenticationExce
}
}


@Override
public BinaryID analyse(BinaryHash binHash,
Long baseAddress,
List<FunctionBoundary> 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();
Expand All @@ -238,7 +209,7 @@ public List<FunctionMatch> 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();
Expand All @@ -258,7 +229,7 @@ public List<FunctionMatch> annSymbolsForFunctions(List<FunctionID> 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();
Expand All @@ -269,15 +240,15 @@ public List<FunctionMatch> annSymbolsForFunctions(List<FunctionID> 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"));
}

@Override
public List<FunctionInfo> getFunctionInfo(BinaryID binaryID) {
var request = requestBuilderForEndpoint("analyse/functions/" + binaryID.value())
var request = requestBuilderForEndpoint(APIVersion.V1, "analyse/functions/" + binaryID.value())
.GET()
.build();

Expand All @@ -301,7 +272,7 @@ public String healthMessage(){

@Override
public List<Collection> 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<Collection>();
Expand All @@ -310,7 +281,7 @@ public List<Collection> collectionQuickSearch(ModelName modelName) {

@Override
public List<Collection> 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(
Expand All @@ -320,7 +291,7 @@ public List<Collection> 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");
Expand All @@ -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);
}
Expand All @@ -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<ModelName> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,10 +28,6 @@ List<AnalysisResult> search(
Collection collection,
AnalysisStatus state);

BinaryID analyse(BinaryHash binHash,
Long baseAddress,
List<FunctionBoundary> functionBounds, ModelName modelName);

BinaryID analyse(AnalysisOptionsBuilder binHash);


Expand Down
Original file line number Diff line number Diff line change
@@ -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<APIError> 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")
);
}
}

0 comments on commit e122ed2

Please sign in to comment.