Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PAYARA-3729 EJB HTTP Endpoint improvments #3926

Merged
merged 8 commits into from May 6, 2019
51 changes: 48 additions & 3 deletions appserver/ejb/ejb-http-remoting/client/pom.xml
Expand Up @@ -46,16 +46,16 @@
<artifactId>ejb-http-remoting</artifactId>
<version>5.192-SNAPSHOT</version>
</parent>

<artifactId>ejb-http-client</artifactId>

<name>EJB - HTTP Client</name>
<description>
Module providing support for the EJB HTTP Client. This contains an InitialContext based lookup mechanism that uses HTTP calls back
to Payara to lookup EJB beans, as well as a proxy mechanism to invoke methods on an EJB, which will be sent via HTTP all well
to Payara.
</description>

<build>
<plugins>
<plugin>
Expand All @@ -70,6 +70,31 @@
<unpackBundle>true</unpackBundle>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.1.0</version>
<configuration>
</configuration>
<executions>
<!-- enable flattening -->
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<!-- ensure proper cleanup -->
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Expand All @@ -86,5 +111,25 @@
<groupId>javax.json</groupId>
<artifactId>javax.json-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-binding</artifactId>
<scope>runtime</scope>
<exclusions>
<exclusion>
<groupId>jakarta.json.bind</groupId>
<artifactId>jakarta.json.bind-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.json.bind</groupId>
<artifactId>javax.json.bind-api</artifactId>
</dependency>
</dependencies>
</project>
Expand Up @@ -47,11 +47,16 @@
import java.io.IOException;
import java.io.Reader;
import java.util.Base64;
import java.util.List;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.json.*;
import javax.json.bind.Jsonb;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonString;
import javax.json.JsonValue;
import javax.json.bind.JsonbBuilder;
import javax.naming.InitialContext;
import javax.naming.NamingException;
Expand All @@ -72,111 +77,114 @@
public class InvokeEJBServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

private static final Logger logger = Logger.getLogger(InvokeEJBServlet.class.getName());

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().append("Served at: ").append(request.getContextPath());
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
JsonObject requestPayload = readJsonObject(request.getReader());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another error case would be malformed Json

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not address this since the caller usually is our client which we should make sure does not use malformed JSON. While being a possible problem it does not seemed so important to handle to me.


final JsonObject requestPayload = readJsonObject(request.getReader());

String beanName = requestPayload.getString("lookup");
if (request.getRequestURI().endsWith("lookup")) {
boolean success = excuteInAppContext(() -> {

try {
response.getWriter().print(
new InitialContext().lookup(requestPayload.getString("lookup"))
.getClass()
.getInterfaces()[0]
.getName());
return true;
} catch (IOException | NamingException e) {
// Ignore for now
}

return false;
});

if (!success) {
response.sendError(SC_INTERNAL_SERVER_ERROR, "Name " + requestPayload.getString("lookup") + " not found when doing initial lookup");
try {
response.getWriter().print(lookupBeanInterface(beanName));
} catch (NamingException ex) {
response.sendError(SC_INTERNAL_SERVER_ERROR,
"Name " + beanName + " not found when doing initial lookup.");
} catch (Exception ex) {
logger.log(Level.WARNING, "EJB bean lookup failed.", ex);
response.sendError(SC_INTERNAL_SERVER_ERROR,
"Error while looking up EJB with name " + beanName + ": " + ex.getMessage());
}

return;
}

// Convert JSON encoded method parameter type names to actually Class instances
Class<?>[] argTypes =
requestPayload.getJsonArray("argTypes").stream()
.map(e -> toClass(e))
.toArray(Class[]::new);

// Convert JSON encoded method parameter values to their object instances
List<JsonValue> jsonArgValues = requestPayload.getJsonArray("argValues");
Object[] argValues = new Object[argTypes.length];
for (int i = 0; i < jsonArgValues.size(); i++) {
argValues[i] = toObject(jsonArgValues.get(i), argTypes[i]);
}

boolean success = excuteInAppContext(() -> {
} else {
String methodName = requestPayload.getString("method");
JsonArray argTypeNames = requestPayload.getJsonArray("argTypes");
JsonArray argValuesJson = requestPayload.getJsonArray("argValues");
String principal = requestPayload.getString(SECURITY_PRINCIPAL, "");
String credentials = requestPayload.getString(SECURITY_CREDENTIALS, "");
try {
// Obtain the target EJB that we're going to invoke
Object bean = new InitialContext().lookup(requestPayload.getString("lookup"));

// Authenticates the caller and if successful sets the security context
// *for the outgoing EJB call*. In other words, the security context for this
// Servlet will not be changed.
if (requestPayload.containsKey(SECURITY_PRINCIPAL)) {
ProgrammaticLogin login = new ProgrammaticLogin();
login.login(
base64Decode(requestPayload.getString(SECURITY_PRINCIPAL)),
base64Decode(requestPayload.getString(SECURITY_CREDENTIALS)),
null, true);
}

// Actually invoke the target EJB
Object result =
bean.getClass()
.getMethod(requestPayload.getString("method"), argTypes)
.invoke(bean, argValues);

Object result = invokeBeanMethod(beanName, methodName, argTypeNames, argValuesJson, principal, credentials);
response.setContentType(APPLICATION_JSON);
response.getWriter().print(result instanceof String? result : JsonbBuilder.create().toJson(result));

return true;

} catch (Exception e) {
e.printStackTrace();
response.getWriter().print(JsonbBuilder.create().toJson(result));
} catch (NamingException ex) {
response.sendError(SC_INTERNAL_SERVER_ERROR,
"Name " + beanName + " not found when invoking method " + methodName);
} catch (Exception ex) {
logger.log(Level.WARNING, "EJB bean method invocation failed.", ex);
response.sendError(SC_INTERNAL_SERVER_ERROR,
"Error while invoking invoking method " + methodName + " on EJB with name " + beanName + ": "
+ ex.getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And error handling in client is likely next topic to look at.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, one I know and find irritating is that the client does not detect that the endpoint isn't enabled. That said @arjantijms reminded me that for security reasons we have to be careful how much information we do expose to the user.

}

return false;
});

if (!success) {
response.sendError(SC_INTERNAL_SERVER_ERROR, "Name " + requestPayload.getString("lookup") + " not found when invoking");
}
}

private JsonObject readJsonObject(Reader reader) {
private static JsonObject readJsonObject(Reader reader) {
try (JsonReader jsonReader = Json.createReader(reader)) {
return jsonReader.readObject();
}
}

private Class<?> toClass(JsonValue classNameValue) {
private static String lookupBeanInterface(String beanName) throws Exception {
return excuteInAppContext(beanName, bean -> {
int bangIndex = beanName.indexOf('!');
if (bangIndex > 0) {
return beanName.substring(bangIndex + 1);
}
// there should only be one interface otherwise plain name would not be allowed (portable names at least)
return bean.getClass().getInterfaces()[0].getName();
});
}

private static Object invokeBeanMethod(String beanName, String methodName, JsonArray argTypeNames,
JsonArray argValuesJson, String principal, String credentials) throws Exception {
return excuteInAppContext(beanName, bean -> {
// Authenticates the caller and if successful sets the security context
// *for the outgoing EJB call*. In other words, the security context for this
// Servlet will not be changed.
if (!principal.isEmpty()) {
new ProgrammaticLogin().login(base64Decode(principal), base64Decode(credentials), null, true);
}
// Actually invoke the target EJB
Class<?>[] argTypes = toClasses(argTypeNames);
Object[] argValues = toObjects(argTypes, argValuesJson);
return bean.getClass().getMethod(methodName, argTypes).invoke(bean, argValues);
});
}

/**
* Convert JSON encoded method parameter type names to actually Class instances
*/
private static Class<?>[] toClasses(JsonArray classNames) {
return classNames.stream().map(e -> toClass(e)).toArray(Class[]::new);
}

private static Class<?> toClass(JsonValue classNameValue) {
try {
String className = null;
if (classNameValue instanceof JsonString) {
className = ((JsonString) classNameValue).getString();
} else {
className = classNameValue.toString().replace("\"", "");
return Class.forName(((JsonString) classNameValue).getString());
}
return Class.forName(className);
return Class.forName(classNameValue.toString().replace("\"", ""));
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
}
}

private Object toObject(JsonValue objectValue, Class<?> type) {
/**
* Convert JSON encoded method parameter values to their object instances
*/
private static Object[] toObjects(Class<?>[] argTypes, JsonArray jsonArgValues) {
Object[] argValues = new Object[argTypes.length];
for (int i = 0; i < jsonArgValues.size(); i++) {
argValues[i] = toObject(jsonArgValues.get(i), argTypes[i]);
}
return argValues;
}

private static Object toObject(JsonValue objectValue, Class<?> type) {
try (Jsonb jsonb = JsonbBuilder.create()) {
return jsonb.fromJson(objectValue.toString(), type);
} catch (Exception e) {
Expand All @@ -185,36 +193,56 @@ private Object toObject(JsonValue objectValue, Class<?> type) {
}
}

private boolean excuteInAppContext(Supplier<Boolean> body) {
private static <T> T excuteInAppContext(String beanName, EjbOperation<T> operation) throws Exception {
ApplicationRegistry registry = Globals.get(ApplicationRegistry.class);

Thread currentThread = Thread.currentThread();
if (beanName.startsWith("java:global/")) {
String applicationName = beanName.substring(12, beanName.indexOf('/', 12));
ClassLoader existingContextClassLoader = currentThread.getContextClassLoader();
try {
currentThread.setContextClassLoader(registry.get(applicationName).getAppClassLoader());
Object bean = new InitialContext().lookup(beanName);
return operation.execute(bean);
} finally {
if (existingContextClassLoader != null) {
currentThread.setContextClassLoader(existingContextClassLoader);
}
}
}
NamingException lastLookupError = null;
for (String applicationName : registry.getAllApplicationNames()) {
ClassLoader existingContextClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoader existingContextClassLoader = currentThread.getContextClassLoader();
try {

Thread.currentThread().setContextClassLoader(registry.get(applicationName).getAppClassLoader());

currentThread.setContextClassLoader(registry.get(applicationName).getAppClassLoader());
try {
if (body.get()) {
return true;
}
} catch (Exception e) {
// ignore
Object bean = new InitialContext().lookup(beanName);
return operation.execute(bean);
} catch (NamingException ex) {
lastLookupError = ex;
// try next app
}

} finally {
if (existingContextClassLoader != null) {
Thread.currentThread().setContextClassLoader(existingContextClassLoader);
currentThread.setContextClassLoader(existingContextClassLoader);
}
}

}

return false;
if (lastLookupError != null) {
throw lastLookupError;
}
return null;
}

private static String base64Decode(String input) {
return new String(Base64.getDecoder().decode(input));
}

/**
* Needed because of the {@link Exception} thrown.
*/
interface EjbOperation<T> {

T execute(Object bean) throws Exception;
}
}