Last active
December 15, 2015 10:38
-
-
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.).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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