mirror of
https://github.com/radareorg/r4ghidra.git
synced 2026-11-07 06:36:02 -10:00
Support mixed URI and POST body HTTP commands
This commit is contained in:
1 parent
9a422cc037
commit
a7dfaa7712
7 files changed
+223
-44
No files matched your search
@@ -55,6 +55,9 @@ jobs:
|
||||
- name: Test binary project compatibility
|
||||
run: python3 scripts/test-project.py
|
||||
|
||||
- name: Test HTTP commands
|
||||
run: python3 scripts/test-http.py
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -8,6 +8,9 @@ all:
|
||||
test-project:
|
||||
python3 scripts/test-project.py
|
||||
|
||||
test-http:
|
||||
python3 scripts/test-http.py
|
||||
|
||||
oops:
|
||||
analyzeHeadless . Test.gpr -import $(TESTBIN) -postScript $(SCRIPT) $(FCNADDR) -deleteProject
|
||||
r2 -caf -i ghidra-output.r2 $(TESTBIN)
|
||||
|
||||
@@ -4,83 +4,81 @@ import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.MalformedInputException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
/** HTTP handler that processes radare2 commands using the new REPL implementation */
|
||||
public class R4GhidraHttpHandler implements HttpHandler {
|
||||
|
||||
private R2REPLImpl repl;
|
||||
private Map<String, R2CommandHandler> commandRegistry;
|
||||
|
||||
/** Create a new HTTP handler using the shared R4Ghidra command registry. */
|
||||
public R4GhidraHttpHandler(){
|
||||
public R4GhidraHttpHandler() {
|
||||
|
||||
repl = new R2REPLImpl();
|
||||
repl.registerCommands (R4CommandInitializer.getCommandHandlers());
|
||||
repl.registerCommands(R4CommandInitializer.getCommandHandlers());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
// Support POST requests (body contains the command) and fallback to GET/query/path
|
||||
String method = exchange.getRequestMethod();
|
||||
String cmd = null;
|
||||
|
||||
if ("POST".equalsIgnoreCase(method)) {
|
||||
// Read the entire request body as the command (assume UTF-8)
|
||||
java.io.InputStream is = exchange.getRequestBody();
|
||||
try {
|
||||
byte[] body = is.readAllBytes();
|
||||
CharsetDecoder charsetDecoder = StandardCharsets.UTF_8.newDecoder();
|
||||
CharBuffer decodedCharBuffer = charsetDecoder.decode(ByteBuffer.wrap(body));
|
||||
cmd = decodedCharBuffer.toString().trim();
|
||||
} catch(MalformedInputException mie){
|
||||
sendErrorResponse(400, exchange, "Invalid UTF-8 encoding!".getBytes());
|
||||
return;
|
||||
}finally {
|
||||
is.close();
|
||||
}
|
||||
} else if ("GET".equalsIgnoreCase(method)){
|
||||
// Extract command from query string or path (existing behavior)
|
||||
cmd = exchange.getRequestURI().getQuery();
|
||||
if (cmd == null) {
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
if (path.length() > 5) {
|
||||
cmd = path.substring(5);
|
||||
} else {
|
||||
cmd = "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sendErrorResponse(400, exchange, "Invalid request".getBytes());
|
||||
if (!"GET".equalsIgnoreCase(method) && !"POST".equalsIgnoreCase(method)) {
|
||||
sendErrorResponse(400, exchange, "Invalid request".getBytes(StandardCharsets.UTF_8));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == null || cmd.isEmpty()) {
|
||||
sendErrorResponse(400, exchange, "Empty request".getBytes());
|
||||
// Use the same URI command for GET and POST, retaining query-string precedence.
|
||||
String cmd = exchange.getRequestURI().getQuery();
|
||||
if (cmd == null) {
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
cmd = path.length() > 5 ? path.substring(5) : "";
|
||||
}
|
||||
|
||||
if ("POST".equalsIgnoreCase(method)) {
|
||||
// The raw UTF-8 body either completes the URI command or supplies the whole command.
|
||||
try (InputStream is = exchange.getRequestBody()) {
|
||||
String body =
|
||||
StandardCharsets.UTF_8
|
||||
.newDecoder()
|
||||
.decode(ByteBuffer.wrap(is.readAllBytes()))
|
||||
.toString();
|
||||
if (!body.isEmpty()) {
|
||||
cmd += (cmd.isEmpty() ? "" : " ") + body;
|
||||
}
|
||||
} catch (MalformedInputException mie) {
|
||||
sendErrorResponse(
|
||||
400, exchange, "Invalid UTF-8 encoding!".getBytes(StandardCharsets.UTF_8));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd.trim().isEmpty()) {
|
||||
sendErrorResponse(400, exchange, "Empty request".getBytes(StandardCharsets.UTF_8));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute the command using our REPL implementation
|
||||
String result = repl.executeCommand(cmd);
|
||||
sendResponse(exchange, result.getBytes());
|
||||
sendResponse(exchange, result.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
// Handle any unexpected exceptions
|
||||
sendErrorResponse(500, exchange, ("Error executing command: " + e.getMessage()).getBytes());
|
||||
sendErrorResponse(
|
||||
500,
|
||||
exchange,
|
||||
("Error executing command: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private void setResponseHeaders(HttpExchange exchange){
|
||||
Headers headers=exchange.getResponseHeaders();
|
||||
headers.add("Accept-Charset","UTF-8");
|
||||
headers.add("Accept-Encoding","identity");
|
||||
private void setResponseHeaders(HttpExchange exchange) {
|
||||
Headers headers = exchange.getResponseHeaders();
|
||||
headers.add("Accept-Charset", "UTF-8");
|
||||
headers.add("Accept-Encoding", "identity");
|
||||
}
|
||||
|
||||
/** Send a successful response */
|
||||
private void sendResponse(HttpExchange exchange, byte[] response) throws IOException {
|
||||
setResponseHeaders(exchange);
|
||||
|
||||
@@ -22,6 +22,10 @@ public String execute(R2Command command, R2Context context) throws R2CommandExce
|
||||
if (!command.hasPrefix("e")) {
|
||||
throw new R2CommandException("Not an eval command");
|
||||
}
|
||||
// echo is an alias for ?e within the 'e' command family.
|
||||
if (command.getSubcommand().equals("cho")) {
|
||||
return String.join(" ", command.getArguments()) + "\n";
|
||||
}
|
||||
// Access the configuration manager
|
||||
R2EvalConfig config = context.getEvalConfig();
|
||||
|
||||
@@ -215,6 +219,7 @@ public String getHelp() {
|
||||
sb.append(" e* list all eval vars as r2 commands\n");
|
||||
sb.append(" ej list all eval vars in JSON format\n");
|
||||
sb.append(" eq list only variable names, one per line\n\n");
|
||||
sb.append(" echo [text] echo text to console (alias for ?e)\n");
|
||||
/*
|
||||
sb.append("Available variables:\n");
|
||||
sb.append(" asm.arch set architecture (x86, arm, etc.)\n");
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package r4ghidra;
|
||||
|
||||
import ghidra.GhidraApplicationLayout;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.framework.HeadlessGhidraApplicationConfiguration;
|
||||
import ghidra.program.database.ProgramDB;
|
||||
import ghidra.program.flatapi.FlatProgramAPI;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.lang.Language;
|
||||
import ghidra.program.model.lang.LanguageID;
|
||||
import ghidra.program.util.DefaultLanguageService;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
|
||||
/** Exercises GET, POST and URI/body command composition through the real HTTP server and REPL. */
|
||||
public class R4GhidraHttpHandlerTest {
|
||||
public static void main(String[] args) throws Exception {
|
||||
Application.initializeApplication(
|
||||
new GhidraApplicationLayout(new File(args[0])),
|
||||
new HeadlessGhidraApplicationConfiguration());
|
||||
Object consumer = new Object();
|
||||
Language language =
|
||||
DefaultLanguageService.getLanguageService()
|
||||
.getLanguage(new LanguageID("x86:LE:64:default"));
|
||||
ProgramDB program =
|
||||
new ProgramDB("http-test", language, language.getDefaultCompilerSpec(), consumer);
|
||||
try {
|
||||
Address base = program.getAddressFactory().getDefaultAddressSpace().getAddress(0x1000);
|
||||
int tx = program.startTransaction("Test memory");
|
||||
try {
|
||||
program
|
||||
.getMemory()
|
||||
.createInitializedBlock(".text", base, 16, (byte) 0, TaskMonitor.DUMMY, false);
|
||||
} finally {
|
||||
program.endTransaction(tx, true);
|
||||
}
|
||||
R4GhidraState.api = new FlatProgramAPI(program);
|
||||
R4GhidraServer.start(0);
|
||||
String url = "http://localhost:" + R4GhidraServer.server.getAddress().getPort();
|
||||
try (HttpClient client = HttpClient.newHttpClient()) {
|
||||
check(client, url, "GET", "/cmd/echo%20hello", "", 200, "hello\n");
|
||||
check(client, url, "GET", "/cmd/?echo%20hello", "", 200, "hello\n");
|
||||
check(client, url, "GET", "/cmd/%3Fe%20hello", "", 200, "hello\n");
|
||||
check(client, url, "POST", "/cmd", "echo hello", 200, "hello\n");
|
||||
check(client, url, "POST", "/cmd/", "echo hello", 200, "hello\n");
|
||||
check(client, url, "POST", "/cmd/echo", "hello", 200, "hello\n");
|
||||
check(client, url, "POST", "/cmd/echo%20hello", "world", 200, "hello world\n");
|
||||
check(client, url, "POST", "/cmd/ignored?echo", "hello", 200, "hello\n");
|
||||
check(client, url, "POST", "/cmd/echo%20hello", "", 200, "hello\n");
|
||||
check(client, url, "POST", "/cmd/?echo%20hello", "", 200, "hello\n");
|
||||
check(client, url, "POST", "/cmd/echo", "", 200, "\n");
|
||||
check(client, url, "POST", "/cmd/%3Fe", "hello", 200, "hello\n");
|
||||
check(client, url, "POST", "/cmd/echo%20%CE%BB", "привет", 200, "λ привет\n");
|
||||
check(client, url, "POST", "/cmd/echo", "a+b%20c&x=y", 200, "a+b%20c&x=y\n");
|
||||
check(client, url, "POST", "/cmd/echo%20a+b%2520c", "end", 200, "a+b%20c end\n");
|
||||
check(client, url, "GET", "/cmd/", "", 400, "Empty request");
|
||||
check(client, url, "POST", "/cmd", "", 400, "Empty request");
|
||||
check(client, url, "POST", "/cmd/", " \t\r\n", 400, "Empty request");
|
||||
check(client, url, "PUT", "/cmd/echo", "hello", 400, "Invalid request");
|
||||
check(
|
||||
client,
|
||||
url,
|
||||
"POST",
|
||||
"/cmd/echo",
|
||||
new byte[] {(byte) 0xc3, 0x28},
|
||||
400,
|
||||
"Invalid UTF-8 encoding!");
|
||||
// Confirm that rejecting a malformed request does not break subsequent commands.
|
||||
check(client, url, "POST", "/cmd/echo", "hello", 200, "hello\n");
|
||||
System.out.println("HTTP command regression tests passed");
|
||||
}
|
||||
} finally {
|
||||
R4GhidraServer.stop();
|
||||
R4GhidraState.api = null;
|
||||
program.release(consumer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void check(
|
||||
HttpClient client,
|
||||
String url,
|
||||
String method,
|
||||
String path,
|
||||
String body,
|
||||
int status,
|
||||
String expected)
|
||||
throws Exception {
|
||||
check(client, url, method, path, body.getBytes(StandardCharsets.UTF_8), status, expected);
|
||||
}
|
||||
|
||||
private static void check(
|
||||
HttpClient client,
|
||||
String url,
|
||||
String method,
|
||||
String path,
|
||||
byte[] body,
|
||||
int status,
|
||||
String expected)
|
||||
throws Exception {
|
||||
HttpRequest request =
|
||||
HttpRequest.newBuilder(URI.create(url + path))
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.method(method, HttpRequest.BodyPublishers.ofByteArray(body))
|
||||
.build();
|
||||
HttpResponse<String> response =
|
||||
client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
if (response.statusCode() != status || !response.body().equals(expected)) {
|
||||
throw new AssertionError(
|
||||
method + " " + path + ": HTTP " + response.statusCode() + " " + response.body());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,23 @@ The plugin registers a new menu item under the Tools menu of Ghidra's Code Brows
|
||||
1. Use the built-in r2 REPL directly within Ghidra
|
||||
2. Connect from an external radare2 instance using r2's web protocols
|
||||
|
||||
### HTTP commands
|
||||
|
||||
The `/cmd/` endpoint accepts GET and POST requests. POST bodies contain raw UTF-8 command text.
|
||||
When the URI also contains a command, the server joins it with the body using a space:
|
||||
|
||||
```sh
|
||||
curl http://localhost:9090/cmd/echo%20hello
|
||||
curl -d 'echo hello' http://localhost:9090/cmd/
|
||||
curl -d hello http://localhost:9090/cmd/echo
|
||||
```
|
||||
|
||||
All three return `hello` followed by a newline. URI commands are percent-decoded; POST bodies
|
||||
are used as-is. An empty POST body executes the URI command. Query-string commands are also
|
||||
supported (for example, `/cmd/?echo`), taking precedence over the path command for both methods.
|
||||
|
||||
Run the HTTP regression tests with `GHIDRA_INSTALL_DIR=/path/to/ghidra make test-http`.
|
||||
|
||||
### Headless Mode
|
||||
|
||||
The Python script provided in the `ghidra_scripts` directory initializes the R4Ghidra server on port 9191 by default. You can change the port by setting the `R4GHIDRA_PORT` environment variable (or `R2WEB_PORT` for backward compatibility). You should provide this script as `-postScript` when launching headless Ghidra:
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test HTTP commands against a real Ghidra program without opening the GUI."""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--ghidra", default=os.environ.get("GHIDRA_INSTALL_DIR"))
|
||||
args = parser.parse_args()
|
||||
if not args.ghidra:
|
||||
parser.error("set GHIDRA_INSTALL_DIR or pass --ghidra with a Ghidra installation")
|
||||
|
||||
|
||||
def java_tool(name):
|
||||
home = os.environ.get("JAVA_HOME")
|
||||
return str(Path(home) / "bin" / name) if home else shutil.which(name)
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="r4ghidra-http-") as directory:
|
||||
work = Path(directory)
|
||||
jars = list((Path(args.ghidra) / "Ghidra").rglob("*.jar"))
|
||||
jars += list((ROOT / "R4Ghidra/lib").glob("*.jar"))
|
||||
classpath = os.pathsep.join(map(str, [work, *jars]))
|
||||
sources = list((ROOT / "R4Ghidra/src/main/java").rglob("*.java"))
|
||||
sources.append(ROOT / "R4Ghidra/src/test/java/r4ghidra/R4GhidraHttpHandlerTest.java")
|
||||
subprocess.run([java_tool("javac"), "-cp", classpath, "-d", work, *sources],
|
||||
check=True, cwd=ROOT)
|
||||
subprocess.run([java_tool("java"), "-Djava.awt.headless=true",
|
||||
f"-Duser.home={work / 'home'}", "-cp", classpath,
|
||||
"r4ghidra.R4GhidraHttpHandlerTest", args.ghidra], check=True, cwd=ROOT)
|
||||
Reference in new issue
Block a user