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
Changes from 7 commits
84473a7
dad810b
a84fd18
29cf387
5526012
9162783
b00d903
9b20d97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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()); | ||
|
||
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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And error handling in client is likely next topic to look at. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
@@ -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; | ||
} | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.