Skip to content

Instantly share code, notes, and snippets.

@JamesXNelson
Last active December 15, 2015 10:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JamesXNelson/5246694 to your computer and use it in GitHub Desktop.
Save JamesXNelson/5246694 to your computer and use it in GitHub Desktop.
A one-file java compiler servlet, capable of compiling a string of java and running its main method using the servlet's own classpath (preferably using a strict SecurityManager; default "no permission" setup is included.).
package xapi.dist.server;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashSet;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
// We use a GWT client to call into the servlet's runProgram method.
// You can parse http params yourself, if you prefer.
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
// This interface should actually be public somewhere
interface Compiler {
String runProgram(String code, boolean debug);
}
@SuppressWarnings("serial")
public class CompilerServlet extends RemoteServiceServlet implements Compiler {
private static final OpenOption[] tmpOptions = {StandardOpenOption.TRUNCATE_EXISTING};
private static final Charset utf8 = Charset.forName("UTF-8");
@Override
public String runProgram(String code, boolean debug) {
return compileAndRun(code, debug).toString();
}
public StringBuilder compileAndRun(String code, final boolean debug) {
// Fix non-breaking space. You'll want to sanitize html...
code = code.trim().replace((char)0xa0, ' ');
final StringBuilder result = new StringBuilder();
ByteArrayOutputStream std = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
final Path root;
try {
root = Files.createTempDirectory("XApi");
} catch (IOException e1) {
error(result, "Cannot create /tmp files!", e1);
return result;
}
try {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
return result.append("No compiler found on classpath.");
}
try {
// Find package declaration, if any
String pkg = getPackage(code);
String cls = getClassName(code);
String cp = getClasspath();
if (debug) {
result.append("Using package: \"" + pkg + "\" and class \"" + cls + "\".\n");
result.append("Using classpath " + cp + "\n");
}
Path into = saveJavaSource(root, pkg, cls, code, debug);
if (debug) System.out.println("File Saved As " + into.toString());
int exitCode = 0;
try {
exitCode = compiler.run(null, std, err, "-cp", cp, into.toAbsolutePath().toString());
if (exitCode != 0) {
result.append("Compiler returned exit code " + exitCode);
}
} catch (Throwable e) {
exitCode = -1;
error(result, "Compiler error", e);
maybeDie(e);
} finally {
String out = std.toString();
if (out.length() > 0) {
result.append("Std Out: ").append(out).append("\n");
}
out = err.toString();
if (out.length() > 0) {
result.append("Std Err: <pre style='color:red'>").append(out).append("</pre>\n");
}
}
if (exitCode != 0) return result;
// Compile success, now let's run the code!
ArrayList<String> params = new ArrayList<>();
params.add("java");// program to run
params.add("-cp"); // add classpath
params.add(root.toString() + // use the directory we're compiling into
// maybe add our own classpath
(cp.length() > 0 ? File.pathSeparatorChar + cp : "")
);
// maybe add security policy (you MUST do this on any public server)
URL security = TerminalServiceImpl.class.getResource("Public.policy");
if (security != null) {
params.add("-Djava.security.manager");// force a security manager
params.add("-Djava.security.policy=" + security.toExternalForm().replace("file:", ""));
}
// finally, add the main class to run
params.add((pkg.length() == 0 ? "" : pkg + ".") + cls);
// Prepare the process
ProcessBuilder process = new ProcessBuilder(params.toArray(new String[params.size()]));
if (debug) result.append("running " + process.command().toString().replaceAll(", ", " ") + "\n");
// Pipe System.out and System.err in process to files we can read later
Path stdErr = Paths.get(root.toString(), "err.log");
Path stdOut = Paths.get(root.toString(), "std.log");
try {
if (!Files.exists(stdOut)) Files.createFile(stdOut);
if (!Files.exists(stdErr)) Files.createFile(stdErr);
process.redirectError(Redirect.to(stdErr.toFile()));
process.redirectOutput(Redirect.to(stdOut.toFile()));
// Actually run the code
Process running = process.start();
// Wait 'til finish
exitCode = running.waitFor();
if (exitCode == 0) {
if (debug) result.append("Process completed successfully!\n");
} else {// report errors
result.append("Process failed with exit code " + exitCode + "\n");
}
} catch (Throwable e) {
error(result, "Error running code:", e);
maybeDie(e);
} finally {
byte[] errors = Files.readAllBytes(stdErr);
byte[] out = Files.readAllBytes(stdOut);
if (out.length > 0) {
if (debug) result.append("Standard Output:\n");
result.append(new String(out, utf8));
}
if (errors.length > 0) {
result.append("Standard Error:\n");
error(result, new String(errors, utf8), null);
}
}
} catch (Throwable e) {
error(result, "", e);
maybeDie(e);
}
// We're done.
if (debug) {
System.out.println("Ran Code:");
System.out.println(code);
System.out.println("Result:");
System.out.println(result);
}
// Send client result
return result;
} finally {
// clean up our mess, but don't make user wait for our delete.
new Thread() {
@Override
public void run() {
deleteAll(root, debug);
};
}.start();
}
}
private void deleteAll(final Path root, final boolean debug) {
try {
Files.walkFileTree(root, new FileVisitor<Path>() {
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (debug && dir.equals(root)) return FileVisitResult.CONTINUE;
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (debug && file.endsWith(".log")) return FileVisitResult.CONTINUE;
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
});
} catch (Throwable e) {
System.err.println("Error cleaning up directory " + root);
while (e != null) {
e.printStackTrace();
e = e.getCause();
}
maybeDie(e);
}
}
private void error(StringBuilder result, String message, Throwable e) {
result.append("<pre style='color:red'>[ERROR] ");
result.append(message);
while (e != null) {
result.append(e);
result.append(Arrays.asList(e.getStackTrace()).toString().replace(',', '\n'));
e = e.getCause();
}
result.append("</pre>");
}
private String getClassName(String code) {
int start = code.indexOf("class ");
if (start == -1) throw new RuntimeException("Can not find class name in " + code);
// we need to find the min value of any possible class name ending char.
// to make the math easier, we cast to char to get unsigned semantics,
// so -1 becomes 0xFFFF and will be ignored by Math.min
char endSpace = (char)(code.indexOf(' ', start + 6));
char endBrace = (char)(code.indexOf('{', start + 6));
char endBracket = (char)(code.indexOf('<', start + 6));
int end = Math.min(Math.min(endSpace, endBrace), endBracket);
return code.substring(start + 6, end).trim();
}
private String getClasspath() throws URISyntaxException {
ClassLoader loader = TerminalServiceImpl.class.getClassLoader();
LinkedHashSet<String> cp = new LinkedHashSet<>();
while (loader != null) {
if (loader instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader)loader).getURLs();
for (URL url : urls) {
if ("file".equals(url.getProtocol())) {
cp.add(url.toURI().toString().replace("file:", ""));
}
}
}
loader = loader.getParent();
}
Iterator<String> iter = cp.iterator();
StringBuilder builder = new StringBuilder();
if (iter.hasNext()) builder.append(iter.next());
while (iter.hasNext())
builder.append(File.pathSeparatorChar).append(iter.next());
return builder.toString();
}
private String getPackage(String code) {
int start = code.indexOf("package ");
if (start == -1) return "";
int end = code.indexOf(';', start);
return code.substring(start + 8, end);
}
private void maybeDie(Throwable e) {
// This is a security exception in our server, and not the user's code.
// It could signal a user trying to write to files we don't allow,
// or causing some unknown compiler exploit (though we never run bytecode in
// the server's process). Either way, we are already exposing cli,
// so we kill the jvm if our own security doesn't like what we're up to.
if (e instanceof SecurityException) {
System.err.println("Received security exception; "
+ "ending process in case we are being exploited.");
System.exit(1);
}
}
private Path saveJavaSource(Path into, String pkg, String cls, String code, boolean debug) {
try {
// Put file in correct package
if (pkg.length() > 0) {
System.out.println("Creating package directory " + pkg);
into = Paths.get(into.toString(), pkg.split("[.]"));
Files.createDirectories(into);
}
into = Paths.get(into.toString(), cls + ".java");
if (!Files.exists(into)) into = Files.createFile(into);
if (debug) System.out.println("Using source file " + into);
Files.write(into, code.getBytes(utf8), tmpOptions);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("Unable to write file to classpath.");
}
return into;
}
}
/** Sample Public.policy file to put beside servlet class (grants zero useful permissions):
grant {
permission java.util.PropertyPermission "java.version", "read";
permission java.util.PropertyPermission "java.vendor", "read";
permission java.util.PropertyPermission "java.vendor.url", "read";
permission java.util.PropertyPermission "java.class.version", "read";
permission java.util.PropertyPermission "os.name", "read";
permission java.util.PropertyPermission "os.version", "read";
permission java.util.PropertyPermission "os.arch", "read";
permission java.util.PropertyPermission "file.separator", "read";
permission java.util.PropertyPermission "path.separator", "read";
permission java.util.PropertyPermission "line.separator", "read";
permission java.util.PropertyPermission "java.specification.version", "read";
permission java.util.PropertyPermission "java.specification.vendor", "read";
permission java.util.PropertyPermission "java.specification.name", "read";
permission java.util.PropertyPermission "java.vm.specification.version", "read";
permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
permission java.util.PropertyPermission "java.vm.specification.name", "read";
permission java.util.PropertyPermission "java.vm.version", "read";
permission java.util.PropertyPermission "java.vm.vendor", "read";
permission java.util.PropertyPermission "java.vm.name", "read";
};
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment