From aa7f6ac1e54d3ac3a69ee21250bb83a4daf94474 Mon Sep 17 00:00:00 2001 From: Gorkem Ercan Date: Thu, 26 Apr 2018 21:40:53 -0400 Subject: [PATCH] Remove com.sun.* dependency Removes com.sun.* dependency. Uses vert.x as the http server. Infers main class name from manifest.mf Signed-off-by: Gorkem Ercan --- java8/proxy/build.gradle | 2 + .../java/openwhisk/java/action/JarLoader.java | 94 ------ .../java/openwhisk/java/action/Proxy.java | 312 ++++++++++-------- 3 files changed, 179 insertions(+), 229 deletions(-) delete mode 100644 java8/proxy/src/main/java/openwhisk/java/action/JarLoader.java diff --git a/java8/proxy/build.gradle b/java8/proxy/build.gradle index 922908b6..942a52fb 100644 --- a/java8/proxy/build.gradle +++ b/java8/proxy/build.gradle @@ -6,6 +6,8 @@ repositories { dependencies { compile 'com.google.code.gson:gson:2.6.2' + compile 'io.vertx:vertx-core:3.5.1' + compile 'io.vertx:vertx-web:3.5.1' } jar { diff --git a/java8/proxy/src/main/java/openwhisk/java/action/JarLoader.java b/java8/proxy/src/main/java/openwhisk/java/action/JarLoader.java deleted file mode 100644 index 76de8f09..00000000 --- a/java8/proxy/src/main/java/openwhisk/java/action/JarLoader.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package openwhisk.java.action; - -import java.io.File; -import java.io.InputStream; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.Base64; -import java.util.Collections; -import java.util.Map; - -import com.google.gson.JsonObject; - -public class JarLoader extends URLClassLoader { - private final Class mainClass; - private final Method mainMethod; - - public static Path saveBase64EncodedFile(InputStream encoded) throws Exception { - Base64.Decoder decoder = Base64.getDecoder(); - - InputStream decoded = decoder.wrap(encoded); - - File destinationFile = File.createTempFile("useraction", ".jar"); - destinationFile.deleteOnExit(); - Path destinationPath = destinationFile.toPath(); - - Files.copy(decoded, destinationPath, StandardCopyOption.REPLACE_EXISTING); - - return destinationPath; - } - - public JarLoader(Path jarPath, String entrypoint) - throws MalformedURLException, ClassNotFoundException, NoSuchMethodException, SecurityException { - super(new URL[] { jarPath.toUri().toURL() }); - - final String[] splittedEntrypoint = entrypoint.split("#"); - final String entrypointClassName = splittedEntrypoint[0]; - final String entrypointMethodName = splittedEntrypoint.length > 1 ? splittedEntrypoint[1] : "main"; - - this.mainClass = loadClass(entrypointClassName); - - Method m = mainClass.getMethod(entrypointMethodName, new Class[] { JsonObject.class }); - m.setAccessible(true); - int modifiers = m.getModifiers(); - if (m.getReturnType() != JsonObject.class || !Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) { - throw new NoSuchMethodException("main"); - } - - this.mainMethod = m; - } - - public JsonObject invokeMain(JsonObject arg, Map env) throws Exception { - augmentEnv(env); - return (JsonObject) mainMethod.invoke(null, arg); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static void augmentEnv(Map newEnv) { - try { - for (Class cl : Collections.class.getDeclaredClasses()) { - if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) { - Field field = cl.getDeclaredField("m"); - field.setAccessible(true); - Object obj = field.get(System.getenv()); - Map map = (Map) obj; - map.putAll(newEnv); - } - } - } catch (Exception e) {} - } -} diff --git a/java8/proxy/src/main/java/openwhisk/java/action/Proxy.java b/java8/proxy/src/main/java/openwhisk/java/action/Proxy.java index 6233e92b..6dcf9d72 100644 --- a/java8/proxy/src/main/java/openwhisk/java/action/Proxy.java +++ b/java8/proxy/src/main/java/openwhisk/java/action/Proxy.java @@ -14,156 +14,198 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package openwhisk.java.action; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLClassLoader; import java.nio.file.Path; +import java.util.Collection; import java.util.HashMap; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import java.util.Map; +import java.util.jar.Attributes; import com.google.gson.JsonParser; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; - -public class Proxy { - private HttpServer server; - - private JarLoader loader = null; - - public Proxy(int port) throws IOException { - this.server = HttpServer.create(new InetSocketAddress(port), -1); - - this.server.createContext("/init", new InitHandler()); - this.server.createContext("/run", new RunHandler()); - this.server.setExecutor(null); // creates a default executor +import io.vertx.core.AbstractVerticle; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServer; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.ext.web.handler.ErrorHandler; + +public class Proxy extends AbstractVerticle { + + private static final int HTTP_INTERNAL_SERVER_ERROR = 500; + // Return 502 if the function is not found + private static final int ERROR_FUNCTION_NOT_FOUND = 502; + private static final int HTTP_OK = 200; + + private static final String[] OW_ENV_KEYS = { "api_key", "namespace", "action_name", "activation_id", "deadline" }; + private static final int PORT = 8080; + + private URLClassLoader urlClassLoader; + private Method mainMethod; + + @Override + public void start() throws InterruptedException { + HttpServer httpServer = vertx.createHttpServer(); + Router router = Router.router(vertx); + + router.route().handler(BodyHandler.create()); + router.route().failureHandler(ErrorHandler.create(true)); + + router.route("/init").handler(this::initHandler); + router.route("/run").handler(this::runHandler); + + httpServer.requestHandler(router::accept).listen(PORT); + } + + private void initHandler(RoutingContext rc) { + if (urlClassLoader != null || mainMethod != null) { + rc.response().setStatusCode(HTTP_INTERNAL_SERVER_ERROR).end("Cannot initialize the action more than once."); + return; } - public void start() { - server.start(); - } + final JsonObject request = rc.getBodyAsJson(); + final JsonObject value = request.getJsonObject("value"); + final byte[] jarBinary = value.getBinary("code"); - private static void writeResponse(HttpExchange t, int code, String content) throws IOException { - byte[] bytes = content.getBytes(StandardCharsets.UTF_8); - t.sendResponseHeaders(code, bytes.length); - OutputStream os = t.getResponseBody(); - os.write(bytes); - os.close(); + Proxy.writeCodeOnFileSystem(vertx, jarBinary, ar -> { + if (ar.failed()) { + rc.fail(ar.cause()); + } else { + if (!ar.result().toFile().isFile()) { + rc.response().setStatusCode(HTTP_INTERNAL_SERVER_ERROR).end("Error invoking function"); + } + try { + final URL url = ar.result().toUri().toURL(); + urlClassLoader = new URLClassLoader(new URL[] { url}); + String clazzName = value.getString("main"); + String methodName = "main"; + if(clazzName == null || clazzName.isEmpty()){ + clazzName = getClassNameFromManifest(url); + } + if(clazzName.indexOf('#')>-1){ + final String[] splitted = clazzName.split("#"); + clazzName = splitted[0]; + methodName= splitted[1]; + } + if(clazzName == null ){ + rc.response().setStatusCode(ERROR_FUNCTION_NOT_FOUND).end("Main class is empty, can not determine the function to invoke"); + return; + } + final Class mainClass = urlClassLoader.loadClass(clazzName); + + final Method m = mainClass.getMethod(methodName, new Class[] { com.google.gson.JsonObject.class }); + m.setAccessible(true); + final int modifiers = m.getModifiers(); + if (m.getReturnType() != com.google.gson.JsonObject.class || !Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) { + throw new NoSuchMethodException(methodName); + } + this.mainMethod = m; + + rc.response().setStatusCode(HTTP_OK).end("OK"); + + }catch(NoSuchMethodException e){ + rc.response().setStatusCode(ERROR_FUNCTION_NOT_FOUND).end(e); + } + catch (ClassNotFoundException | IOException e) { + rc.response().setStatusCode(HTTP_INTERNAL_SERVER_ERROR).end("An error has occurred (see logs for details): " + e); + } + } + }); + } + + private String getClassNameFromManifest(URL url ) throws IOException{ + final URL jarUrl = new URL("jar", "", url + "!/"); + final JarURLConnection connection = (JarURLConnection)jarUrl.openConnection(); + final Attributes attributes = connection.getMainAttributes(); + return attributes != null + ? attributes.getValue(Attributes.Name.MAIN_CLASS) + : null; + } + + private void runHandler(RoutingContext rc) { + final JsonObject request = rc.getBodyAsJson(); + final JsonObject value = request.getJsonObject("value"); + final JsonParser parser = new JsonParser(); + final com.google.gson.JsonObject req = parser.parse(value.toString()).getAsJsonObject(); + + final HashMap env = new HashMap<>(); + for (String envKey : OW_ENV_KEYS) { + env.put(String.format("__OW_%s", envKey.toUpperCase()), value.getString(envKey)); } - - private static void writeError(HttpExchange t, String errorMessage) throws IOException { - JsonObject message = new JsonObject(); - message.addProperty("error", errorMessage); - writeResponse(t, 502, message.toString()); + augmentEnv(env); + + Thread.currentThread().setContextClassLoader(urlClassLoader); + System.setSecurityManager(new WhiskSecurityManager()); + + try { + final com.google.gson.JsonObject out = (com.google.gson.JsonObject) this.mainMethod.invoke(null, req); + if (out == null) { + printAndRespond(rc, new NullPointerException("The action returned null")); + }else{ + rc.response().setStatusCode(HTTP_OK).putHeader(HttpHeaders.CONTENT_TYPE, "application/json").end(out.toString()); + } + } catch (IllegalAccessException e) { + printAndRespond(rc, e); + } catch (InvocationTargetException e) { + printAndRespond(rc, e.getCause()); } - private class InitHandler implements HttpHandler { - public void handle(HttpExchange t) throws IOException { - if (loader != null) { - Proxy.writeError(t, "Cannot initialize the action more than once."); - return; - } - - try { - InputStream is = t.getRequestBody(); - JsonParser parser = new JsonParser(); - JsonElement ie = parser.parse(new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))); - JsonObject inputObject = ie.getAsJsonObject(); - - JsonObject message = inputObject.getAsJsonObject("value"); - String mainClass = message.getAsJsonPrimitive("main").getAsString(); - String base64Jar = message.getAsJsonPrimitive("code").getAsString(); - - // FIXME: this is obviously not very useful. The idea is that we - // will implement/use - // a streaming parser for the incoming JSON object so that we - // can stream the contents - // of the jar straight to a file. - InputStream jarIs = new ByteArrayInputStream(base64Jar.getBytes(StandardCharsets.UTF_8)); - - // Save the bytes to a file. - Path jarPath = JarLoader.saveBase64EncodedFile(jarIs); - - // Start up the custom classloader. This also checks that the - // main method exists. - loader = new JarLoader(jarPath, mainClass); - - Proxy.writeResponse(t, 200, "OK"); - return; - } catch (Exception e) { - e.printStackTrace(System.err); - Proxy.writeError(t, "An error has occurred (see logs for details): " + e); - return; - } - } - } + } + + private void printAndRespond(RoutingContext rc, Throwable e) { + rc.response().setStatusCode(HTTP_INTERNAL_SERVER_ERROR).end("An error has occurred (see logs for details): " + e); + } - private class RunHandler implements HttpHandler { - public void handle(HttpExchange t) throws IOException { - if (loader == null) { - Proxy.writeError(t, "Cannot invoke an uninitialized action."); - return; - } - - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - SecurityManager sm = System.getSecurityManager(); - - try { - InputStream is = t.getRequestBody(); - JsonParser parser = new JsonParser(); - JsonElement ie = parser.parse(new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))); - JsonObject inputObject = ie.getAsJsonObject().getAsJsonObject("value"); - - HashMap env = new HashMap(); - for (String p : new String[] { "api_key", "namespace", "action_name", "activation_id", "deadline" }) { - try { - String val = ie.getAsJsonObject().getAsJsonPrimitive(p).getAsString(); - env.put(String.format("__OW_%s", p.toUpperCase()), val); - } catch (Exception e) {} - } - - Thread.currentThread().setContextClassLoader(loader); - System.setSecurityManager(new WhiskSecurityManager()); - - // User code starts running here. - JsonObject output = loader.invokeMain(inputObject, env); - // User code finished running here. - - if(output == null) { - throw new NullPointerException("The action returned null"); - } - - Proxy.writeResponse(t, 200, output.toString()); - return; - } catch (InvocationTargetException ite) { - // These are exceptions from the action, wrapped in ite because - // of reflection - Throwable underlying = ite.getCause(); - underlying.printStackTrace(System.err); - Proxy.writeError(t, - "An error has occured while invoking the action (see logs for details): " + underlying); - } catch (Exception e) { - e.printStackTrace(System.err); - Proxy.writeError(t, "An error has occurred (see logs for details): " + e); - } finally { - System.setSecurityManager(sm); - Thread.currentThread().setContextClassLoader(cl); - } + static void writeCodeOnFileSystem(Vertx vertx, byte[] binary, Handler> completionHandler) { + try { + final File file = File.createTempFile("useraction", ".jar"); + vertx.fileSystem().writeFile(file.getAbsolutePath(), Buffer.buffer(binary), res -> { + if (res.failed()) { + completionHandler.handle(Future.failedFuture(res.cause())); + } else { + completionHandler.handle(Future.succeededFuture(file.toPath())); } + }); + } catch (IOException e) { + completionHandler.handle(Future.failedFuture(e)); } + } + + public static void main(String[] args) throws InterruptedException { + Vertx vertx = Vertx.vertx(); + vertx.deployVerticle(new Proxy()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void augmentEnv(Map newEnv) { + try { + for (Class cl : Collection.class.getDeclaredClasses()) { + if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) { + Field field = cl.getDeclaredField("m"); + field.setAccessible(true); + Object obj = field.get(System.getenv()); + Map map = (Map) obj; + map.putAll(newEnv); + } + } + } catch (Exception e) { + // Not handled. + } + } - public static void main(String args[]) throws Exception { - Proxy proxy = new Proxy(8080); - proxy.start(); - } }