Support mixed URI and POST body HTTP commands

This commit is contained in:
pancake committed 2026-10-15 23:37:42 +02:00
1 parent 9a422cc037
commit a7dfaa7712
7 files changed
+223 -44

No files matched your search

+3
View File
@@ -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:
+3
View File
@@ -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());
}
}
}
+17
View File
@@ -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:
+35
View File
@@ -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)