Add limited support for Makefile parsing. (#103)

* Add limited support for Makefile parsing.
As the AWS Linux images do not contain make.

* javaExec: Extract makefile functionality in its own class

* Implement review comments
This commit is contained in:
Maximilian Paß
2022-04-07 22:40:19 +02:00
committed by GitHub
parent 830517f464
commit d7b1c2d691
4 changed files with 230 additions and 11 deletions

View File

@ -1,15 +1,6 @@
package poseidon;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;
import java.util.Map;
import java.util.Scanner;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.apigatewaymanagementapi.AmazonApiGatewayManagementApi;
import com.amazonaws.services.apigatewaymanagementapi.AmazonApiGatewayManagementApiClientBuilder;
import com.amazonaws.services.apigatewaymanagementapi.model.PostToConnectionRequest;
@ -20,6 +11,16 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayV2WebSocketEvent;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;
import java.util.Map;
import java.util.Scanner;
// AwsFunctionRequest contains the java files that needs to be executed.
class AwsFunctionRequest {
String[] cmd;
@ -64,6 +65,16 @@ public class App implements RequestHandler<APIGatewayV2WebSocketEvent, APIGatewa
// disableOutput: If set to true, no output will be sent over the WebSocket connection.
private boolean disableOutput = false;
// Unwrapps the passed command. We expect a "sh -c" wrapped command.
public static String unwrapCommand(String[] cmd) {
return cmd[cmd.length - 1];
}
// Wrapps the passed command with "sh -c".
public static String[] wrapCommand(String cmd) {
return new String[]{"sh", "-c", cmd};
}
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayV2WebSocketEvent input, final Context context) {
APIGatewayV2WebSocketEvent.RequestContext ctx = input.getRequestContext();
String[] domains = ctx.getDomainName().split("\\.");
@ -78,7 +89,13 @@ public class App implements RequestHandler<APIGatewayV2WebSocketEvent, APIGatewa
try {
File workingDirectory = this.writeFS(execution.files);
ProcessBuilder pb = new ProcessBuilder(execution.cmd);
String[] cmd = execution.cmd;
try {
SimpleMakefile make = new SimpleMakefile(execution.files);
cmd = wrapCommand(make.parseCommand(unwrapCommand(execution.cmd)));
} catch (NoMakefileFoundException | NoMakeCommandException | InvalidMakefileException ignored) {}
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.directory(workingDirectory);
Process p = pb.start();
InputStream stdout = p.getInputStream(), stderr = p.getErrorStream();

View File

@ -0,0 +1,101 @@
package poseidon;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// NoMakefileFoundException is thrown if no makefile could be found.
class NoMakefileFoundException extends Exception {}
// NoMakeCommandException is thrown if no make command is called.
class NoMakeCommandException extends Exception {}
// InvalidMakefileException is thrown if there is no valid rule to be executed.
class InvalidMakefileException extends Exception {}
// SimpleMakefile adds limited support for the execution of a makefile as passed command. The default Java image does not contain make.
class SimpleMakefile {
// This pattern validates if a command is a make command.
private static final Pattern isMakeCommand = Pattern.compile("^make(?:\\s+(?<startRule>\\w*))?$");
// This pattern identifies the rules in a makefile.
private static final Pattern makeRules = Pattern.compile("(?<name>.*):\\n(?<commands>(?:\\t.+\\n?)*)");
// The first rule of the makefile.
private String firstRule = null;
// The rules included in the makefile.
private final Map<String, String[]> rules = new HashMap<>();
private static String concatCommands(String[] commands) {
return String.join(" && ", commands);
}
// getMakefile returns the makefile out of the passed files map.
private static String getMakefile(Map<String, String> files) throws NoMakefileFoundException {
String makefileB64;
if (files.containsKey("Makefile")) {
makefileB64 = files.get("Makefile");
} else if (files.containsKey("makefile")) {
makefileB64 = files.get("makefile");
} else {
throw new NoMakefileFoundException();
}
return new String(Base64.getDecoder().decode(makefileB64), StandardCharsets.UTF_8);
}
public SimpleMakefile(Map<String, String> files) throws NoMakefileFoundException {
this.parseRules(getMakefile(files));
}
// parseRules uses the passed makefile to parse rules into the objet's map "rules".
private void parseRules(String makefile) {
Matcher makeRuleMatcher = makeRules.matcher(makefile);
while (makeRuleMatcher.find()) {
String ruleName = makeRuleMatcher.group("name");
if (firstRule == null) {
firstRule = ruleName;
}
String[] ruleCommands = makeRuleMatcher.group("commands").split("\n");
String[] trimmedCommands = Arrays.stream(ruleCommands).map(String::trim).toArray(String[]::new);
rules.put(ruleName, trimmedCommands);
}
}
// getCommand returns a bash line of commands that would be executed by the passed rule.
public String getCommand(String rule) {
if (rule == null || rule.isEmpty()) {
rule = this.firstRule;
}
return concatCommands(rules.get(rule));
}
// parseCommand returns a bash line of commands that would be executed by the passed command.
public String parseCommand(String shellCommand) throws InvalidMakefileException, NoMakeCommandException {
Matcher makeCommandMatcher = isMakeCommand.matcher(shellCommand);
if (!makeCommandMatcher.find()) {
throw new NoMakeCommandException();
}
String ruleArgument = makeCommandMatcher.group("startRule");
if (ruleArgument.isEmpty()) {
ruleArgument = this.firstRule;
}
if ((this.firstRule == null) || !rules.containsKey(ruleArgument)) {
throw new InvalidMakefileException();
}
return getCommand(ruleArgument);
}
}

View File

@ -4,12 +4,30 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2WebSocketEvent;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
public class AppTest {
static final String RecursiveMathContent = Base64.getEncoder().encodeToString(
("package org.example;\n" +
"\n" +
"public class RecursiveMath {\n" +
"\n" +
" public static void main(String[] args) {\n" +
" System.out.println(\"Mein Text\");\n" +
" }\n" +
"\n" +
" public static double power(int base, int exponent) {\n" +
" return 42;\n" +
" }\n" +
"}").getBytes(StandardCharsets.UTF_8));
@Test
public void successfulResponse() {
App app = new App();
@ -21,7 +39,8 @@ public class AppTest {
Map<String, String> headers = new HashMap<>();
headers.put(App.disableOutputHeaderKey, "True");
input.setHeaders(headers);
input.setBody("{\n \"action\": \"java11Exec\",\n \"cmd\": [\n \"sh\",\n \"-c\",\n \"javac org/example/RecursiveMath.java && java org/example/RecursiveMath\"\n ],\n \"files\": {\n \"org/example/RecursiveMath.java\": \"cGFja2FnZSBvcmcuZXhhbXBsZTsKCnB1YmxpYyBjbGFzcyBSZWN1cnNpdmVNYXRoIHsKCiAgICBwdWJsaWMgc3RhdGljIHZvaWQgbWFpbihTdHJpbmdbXSBhcmdzKSB7CiAgICAgICAgU3lzdGVtLm91dC5wcmludGxuKCJNZWluIFRleHQiKTsKICAgIH0KCiAgICBwdWJsaWMgc3RhdGljIGRvdWJsZSBwb3dlcihpbnQgYmFzZSwgaW50IGV4cG9uZW50KSB7CiAgICAgICAgcmV0dXJuIDQyOwogICAgfQp9Cgo=\"\n }\n}");
input.setBody("{\"action\":\"java11Exec\",\"cmd\":[\"sh\",\"-c\",\"javac org/example/RecursiveMath.java && java org/example/RecursiveMath\"]," +
"\"files\":{\"org/example/RecursiveMath.java\":\"" + RecursiveMathContent + "\"}}");
APIGatewayProxyResponseEvent result = app.handleRequest(input, null);
assertEquals(200, result.getStatusCode().intValue());
}

View File

@ -0,0 +1,82 @@
package poseidon;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static poseidon.AppTest.RecursiveMathContent;
public class SimpleMakefileTest {
static final String SuccessfulMakefile = Base64.getEncoder().encodeToString(
("run:\n" +
"\tjavac org/example/RecursiveMath.java\n" +
"\tjava org/example/RecursiveMath\n" +
"\n" +
"test:\n" +
"\techo Hi\n"
).getBytes(StandardCharsets.UTF_8));
static final String NotSupportedMakefile = Base64.getEncoder().encodeToString(
("run: test\n" +
"\tjavac org/example/RecursiveMath.java\n" +
"\tjava org/example/RecursiveMath\n" +
"\n" +
"test:\n" +
"\techo Hi\n"
).getBytes(StandardCharsets.UTF_8));
@Test
public void sucessfullMake() {
Map<String, String> files = new HashMap<>();
files.put("Makefile", SuccessfulMakefile);
files.put("org/example/RecursiveMath.java", RecursiveMathContent);
try {
String command = "make run";
SimpleMakefile makefile = new SimpleMakefile(files);
String cmd = makefile.parseCommand(command);
assertEquals("javac org/example/RecursiveMath.java && java org/example/RecursiveMath", cmd);
} catch (NoMakefileFoundException | InvalidMakefileException | NoMakeCommandException ignored) {
fail();
}
}
@Test
public void withoutMake() {
Map<String, String> files = new HashMap<>();
files.put("Makefile", SuccessfulMakefile);
files.put("org/example/RecursiveMath.java", RecursiveMathContent);
try {
String command = "javac org/example/RecursiveMath.java";
SimpleMakefile make = new SimpleMakefile(files);
make.parseCommand(command);
fail();
} catch (NoMakefileFoundException | InvalidMakefileException ignored) {
fail();
} catch (NoMakeCommandException ignored) {}
}
@Test
public void withNotSupportedMakefile() {
Map<String, String> files = new HashMap<>();
files.put("Makefile", NotSupportedMakefile);
files.put("org/example/RecursiveMath.java", RecursiveMathContent);
try {
String command = "make run";
SimpleMakefile makefile = new SimpleMakefile(files);
makefile.parseCommand(command);
fail();
} catch (NoMakefileFoundException | NoMakeCommandException ignored) {
fail();
} catch (InvalidMakefileException ignored) {}
}
}