How can I securely execute some user supplied JS code using Java8 Nashorn?
The script extends some computations for some servlet based reports. The app has many different (untrusted) users. The scripts should only be able to access a Java Object and those returned by the defined members. By default the scripts could instantiate any class using Class.forName() (using .getClass() of my supplied object). Is there any way to prohibit access to any java class not explicitly specified by me?
I asked this question on the Nashorn mailing list a while back:
Are there any recommendations for the best way to
restrict the classes that Nashorn scripts can create to a whitelist?
Or is the approach the same as any JSR223 engine (custom classloader
on the ScriptEngineManager constructor)?
And got this answer from one of the Nashorn devs:
Hi,
Nashorn already filters classes - only public classes of non-sensitive packages (packages listed in package.access security
property aka 'sensitive'). Package access check is done from a
no-permissions context. i.e., whatever package that can be accessed
from a no-permissions class are only allowed.
Nashorn filters Java reflective and jsr292 access - unless script has RuntimePermission("nashorn.JavaReflection"), the script wont be
able to do reflection.
The above two require running with SecurityManager enabled. Under no security manager, the above filtering won't apply.
You could remove global Java.type function and Packages object (+ com,edu,java,javafx,javax,org,JavaImporter) in global scope and/or
replace those with whatever filtering functions that you implement.
Because, these are the only entry points to Java access from script,
customizing these functions => filtering Java access from scripts.
There is an undocumented option (right now used only to run test262 tests) "--no-java" of nashorn shell that does the above for you. i.e.,
Nashorn won't initialize Java hooks in global scope.
JSR223 does not provide any standards based hook to pass a custom class loader. This may have to be addressed in a (possible) future
update of jsr223.
Hope this helps,
-Sundar
Added in 1.8u40, you can use the ClassFilter to restrict what classes the engine can use.
Here is an example from the Oracle documentation:
import javax.script.ScriptEngine;
import jdk.nashorn.api.scripting.ClassFilter;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
public class MyClassFilterTest {
class MyCF implements ClassFilter {
#Override
public boolean exposeToScripts(String s) {
if (s.compareTo("java.io.File") == 0) return false;
return true;
}
}
public void testClassFilter() {
final String script =
"print(java.lang.System.getProperty(\"java.home\"));" +
"print(\"Create file variable\");" +
"var File = Java.type(\"java.io.File\");";
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ScriptEngine engine = factory.getScriptEngine(
new MyClassFilterTest.MyCF());
try {
engine.eval(script);
} catch (Exception e) {
System.out.println("Exception caught: " + e.toString());
}
}
public static void main(String[] args) {
MyClassFilterTest myApp = new MyClassFilterTest();
myApp.testClassFilter();
}
}
This example prints the following:
C:\Java\jre8
Create file variable
Exception caught: java.lang.RuntimeException: java.lang.ClassNotFoundException:
java.io.File
I've researched ways of allowing users to write a simple script in a sandbox that is allowed access to some basic objects provided by my application (in the same way Google Apps Script works). My conclusion was that this is easier/better documented with Rhino than with Nashorn. You can:
Define a class-shutter to avoid access to other classes: http://codeutopia.net/blog/2009/01/02/sandboxing-rhino-in-java/
Limit the number of instructions to avoid endess-loops with observeInstructionCount: http://www-archive.mozilla.org/rhino/apidocs/org/mozilla/javascript/ContextFactory.html
However be warned that with untrusted users this is not enough, because they can still (by accident or on purpose) allocate a hugh amount of memory, causing your JVM to throw an OutOfMemoryError. I have not found a safe solution to this last point yet.
You can quite easily create a ClassFilter which allows fine-grained control of which Java classes are available in JavaScript.
Following the example from the Oracle Nashorn Docs:
class MyCF implements ClassFilter {
#Override
public boolean exposeToScripts(String s) {
if (s.compareTo("java.io.File") == 0) return false;
return true;
}
}
I have wrapped this an a few other measures in a small library today: Nashorn Sandbox (on GitHub). Enjoy!
So far as I can tell, you can't sandbox Nashorn. An untrusted user can execute the "Additional Nashorn Built-In Functions" listed here:
https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/shell.html
which include "quit()". I tested it; it exits the JVM entirely.
(As an aside, in my setup the global objects, $ENV, $ARG, did not work, which is good.)
If I'm wrong about this, someone please leave a comment.
The best way to secure a JS execution in Nashorn is to enable the SecurityManager and let Nashorn deny the critical operations.
In addition you can create a monitoring class that check the script execution time and memory in order to avoid infinite loops and outOfMemory.
In case you run it in a restricted environment without possibility to setup the SecurityManager, you can think to use the Nashorn ClassFilter to deny all/partial access to the Java classes. In addition to that you must overwrite all the critical JS functions (like quit() etc.).
Have a look at this function that manage all this aspects (except memory management):
public static Object javascriptSafeEval(HashMap<String, Object> parameters, String algorithm, boolean enableSecurityManager, boolean disableCriticalJSFunctions, boolean disableLoadJSFunctions, boolean defaultDenyJavaClasses, List<String> javaClassesExceptionList, int maxAllowedExecTimeInSeconds) throws Exception {
System.setProperty("java.net.useSystemProxies", "true");
Policy originalPolicy = null;
if(enableSecurityManager) {
ProtectionDomain currentProtectionDomain = this.getClass().getProtectionDomain();
originalPolicy = Policy.getPolicy();
final Policy orinalPolicyFinal = originalPolicy;
Policy.setPolicy(new Policy() {
#Override
public boolean implies(ProtectionDomain domain, Permission permission) {
if(domain.equals(currentProtectionDomain))
return true;
return orinalPolicyFinal.implies(domain, permission);
}
});
}
try {
SecurityManager originalSecurityManager = null;
if(enableSecurityManager) {
originalSecurityManager = System.getSecurityManager();
System.setSecurityManager(new SecurityManager() {
//allow only the opening of a socket connection (required by the JS function load())
#Override
public void checkConnect(String host, int port, Object context) {}
#Override
public void checkConnect(String host, int port) {}
});
}
try {
ScriptEngine engineReflex = null;
try{
Class<?> nashornScriptEngineFactoryClass = Class.forName("jdk.nashorn.api.scripting.NashornScriptEngineFactory");
Class<?> classFilterClass = Class.forName("jdk.nashorn.api.scripting.ClassFilter");
engineReflex = (ScriptEngine)nashornScriptEngineFactoryClass.getDeclaredMethod("getScriptEngine", new Class[]{Class.forName("jdk.nashorn.api.scripting.ClassFilter")}).invoke(nashornScriptEngineFactoryClass.newInstance(), Proxy.newProxyInstance(classFilterClass.getClassLoader(), new Class[]{classFilterClass}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("exposeToScripts")) {
if(javaClassesExceptionList != null && javaClassesExceptionList.contains(args[0]))
return defaultDenyJavaClasses;
return !defaultDenyJavaClasses;
}
throw new RuntimeException("no method found");
}
}));
/*
engine = new jdk.nashorn.api.scripting.NashornScriptEngineFactory().getScriptEngine(new jdk.nashorn.api.scripting.ClassFilter() {
#Override
public boolean exposeToScripts(String arg0) {
...
}
});
*/
}catch(Exception ex) {
throw new Exception("Impossible to initialize the Nashorn Engine: " + ex.getMessage());
}
final ScriptEngine engine = engineReflex;
if(parameters != null)
for(Entry<String, Object> entry : parameters.entrySet())
engine.put(entry.getKey(), entry.getValue());
if(disableCriticalJSFunctions)
engine.eval("quit=function(){throw 'quit() not allowed';};exit=function(){throw 'exit() not allowed';};print=function(){throw 'print() not allowed';};echo=function(){throw 'echo() not allowed';};readFully=function(){throw 'readFully() not allowed';};readLine=function(){throw 'readLine() not allowed';};$ARG=null;$ENV=null;$EXEC=null;$OPTIONS=null;$OUT=null;$ERR=null;$EXIT=null;");
if(disableLoadJSFunctions)
engine.eval("load=function(){throw 'load() not allowed';};loadWithNewGlobal=function(){throw 'loadWithNewGlobal() not allowed';};");
//nashorn-polyfill.js
engine.eval("var global=this;var window=this;var process={env:{}};var console={};console.debug=print;console.log=print;console.warn=print;console.error=print;");
class ScriptMonitor{
public Object scriptResult = null;
private boolean stop = false;
Object lock = new Object();
#SuppressWarnings("deprecation")
public void startAndWait(Thread threadToMonitor, int secondsToWait) {
threadToMonitor.start();
synchronized (lock) {
if(!stop) {
try {
if(secondsToWait<1)
lock.wait();
else
lock.wait(1000*secondsToWait);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
if(!stop) {
threadToMonitor.interrupt();
threadToMonitor.stop();
throw new RuntimeException("Javascript forced to termination: Execution time bigger then " + secondsToWait + " seconds");
}
}
public void stop() {
synchronized (lock) {
stop = true;
lock.notifyAll();
}
}
}
final ScriptMonitor scriptMonitor = new ScriptMonitor();
scriptMonitor.startAndWait(new Thread(new Runnable() {
#Override
public void run() {
try {
scriptMonitor.scriptResult = engine.eval(algorithm);
} catch (ScriptException e) {
throw new RuntimeException(e);
} finally {
scriptMonitor.stop();
}
}
}), maxAllowedExecTimeInSeconds);
Object ret = scriptMonitor.scriptResult;
return ret;
} finally {
if(enableSecurityManager)
System.setSecurityManager(originalSecurityManager);
}
} finally {
if(enableSecurityManager)
Policy.setPolicy(originalPolicy);
}
}
The function currently use the deprecated Thread stop(). An improvement can be execute the JS not in a Thread but in a separate Process.
PS: here Nashorn is loaded through reflexion but the equivalent Java code is also provided in the comments
I'd say overriding the supplied class's classloader is easiest way to control access to classes.
(Disclaimer: I'm not really familiar with newer Java, so this answer may be old-school/obsolete)
An external sandbox library can be used if you don't want to implement your own ClassLoader & SecurityManager (that's the only way of sandboxing for now).
I've tried "The Java Sandbox" (http://blog.datenwerke.net/p/the-java-sandbox.html) although it's a bit rough around the edges, but it works.
Without the use of Security Manager it is not possible to securely execute JavaScript on Nashorn.
In all releases of Oracle Hotspot that included Nashorn one can write JavaScript that will execute any Java/JavaScript code on this JVM.
As of January 2019, Oracle Security Team insist that use of Security Manager is mandatory.
One of the problems is already discussed in https://github.com/javadelight/delight-nashorn-sandbox/issues/73
Related
I've read CDI 2.0 specification (JSR 365) and found out the existence of the #Observes(during=AFTER_SUCCESS) annotation, but it actually requires a custom event to be defined in order to work.
This is what i've got:
//simple """transactional""" file system manager using command pattern
#Transactional(value = Transactional.TxType.REQUIRED)
#TransactionScoped
#Stateful
public class TransactionalFileSystemManager implements SessionSynchronization {
private final Deque<Command> commands = new ArrayDeque<>();
public void createFile(InputStream content, Path path, String name) throws IOException {
CreateFile command = CreateFile.execute(content, path, name);
commands.addLast(command);
}
public void deleteFile(Path path) throws IOException {
DeleteFile command = DeleteFile.execute(path);
commands.addLast(command);
}
private void commit() throws IOException{
for(Command c : commands){
c.confirm();
}
}
private void rollback() throws IOException{
Iterator<Command> it = commands.descendingIterator();
while (it.hasNext()) {
Command c = it.next();
c.undo();
}
}
#Override
public void afterBegin() throws EJBException{
}
#Override
public void beforeCompletion() throws EJBException{
}
#Override
public void afterCompletion(boolean commitSucceeded) throws EJBException{
if(commitSucceeded){
try {
commit();
} catch (IOException e) {
throw new EJBException(e);
}
}
else {
try {
rollback();
} catch (IOException e) {
throw new EJBException(e);
}
}
}
}
However, I want to adopt a CDI-only solution so I need to remove anything EJB related (including the SessionSynchronization interface). How can i achieve the same result using CDI?
First the facts: the authoritative source for this topic is the Java Transaction API (JTA) specification. Search for it online, I got this.
Then the bad news: In order to truly participate in a JTA transaction, you either have to implement a connector according to the Java Connector Architecture (JCA) specification or a XAResource according to JTA. Never done any of them, I am afraid both are going to be hard. Nevertheless, if you search, you may find an existing implementation of a File System Connector.
Your code above will never accomplish true 2-phase commit because, if your code fails, the transaction is already committed, so the application state is inconsistent. Or, there is a small time window when the real transaction is committed but the file system change have not beed executed, again the state is inconsistent.
Some workarounds I can think of, none of which solves the consistency problem:
Persist the File System commands in a database. This ensures that they are enqueued transactionally. A scheduled job wakes up and actually tries to execute the queued FS commands.
Register a Synchronization with the current Transaction, fire an appropriate event from there. Your TransactionalFileSystemManager observes this event, no during attribute needed I guess.
in my application the follwoing code is used. Can some one give a detailed explanation for the code that is highlighted?
I understood that in first highlighted block java reflection is used in invoking the method handle_validation..but need the detailed explanation.
Then in second highlighted block RemoteException is thrown..
My exact question is why they used reflection to call EngineHandlerIF and then why they are using RMI in this EngineHandlerIF to invoke the definition of method in EngineHandler?
private static EngineHandlerIF init() {
ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[] { "validation.xml" });
String[] beans = ctx.getBeanDefinitionNames();
for (String string : beans) {
logger.info(string);
}
EngineHandlerIF engine = (EngineHandlerIF) ctx.getBean("engine");
return engine;
}
private Object callEngineMethod(MiddlewareMessage mmsg) {
Object resultObj;
try {
**String methodName = "handle_validation";
Method method = EngineHandlerIF.class.getDeclaredMethod(methodName, MiddlewareMessage.class);
method.setAccessible(true);
resultObj = method.invoke(engine, new Object[] { mmsg });**
} catch (Exception e) {
logger.error("sendMessage Exception: ", e);
return new Boolean(false);
}
return resultObj;
}
EngineHandlerIF:
----------------
**public abstract String handle_validation(MiddlewareMessage mmsg) throws RemoteException;**
EngineHandler:
--------------
public String handle_validation(MiddlewareMessage mmsg) throws Exception {
//some code
}
I understood that in first highlighted block java reflection is used
in invoking the method handle_validation..but need the detailed
explanation.
That's pretty much it. The only other bit is the
method.setAccessible(true);
which makes the method accessible to the caller (e.g. from private to public), thus allowing you to call it. However the above method does appear to be public already. Perhaps this is some legacy following a refactor ?
Note that this isn't RMI (remote method invocation), but rather reflection. The only RMI I can see here is the handle_validation() method possibly throwing a RemoteException.
Maybe someone had just discovered the hammer of a reflection so everything, including method that were already public, started looking like a nut.
It is garbage: throw it away. Just call the method directly.
How can I call a function when a specific event occurs in my java Applet ?
In my Javascript I has the following code, that always return a empty value.
$(function () {
alert(document.applets[0].returnClientId());
});
I need to call this alert, just when a specific method is executed. To be more specific, the method called identify, that exists in my java Applet.
The identify method, exists in my Util.class, that don't extends from JApplet.
My FormMain.class extends from JApplet and I call some methods (including the identify method) from this Util.class.
UPDATE: My Java Code
public String getClientid() {
return clientid;
}
public void setClientid(String clientid) {
this.clientid = clientid;
}
public String returnClientId() {
return getClientid();
}
public void identify() {
try {
fingerprintSDK.prepareForIdentification(this.template);
ResultSet rs = identifyStmt.executeQuery();
while (rs.next()) {
byte[] templateBuffer = rs.getBytes("template");
Template referenceTemplate = new Template(templateBuffer);
boolean matched = fingerprintSDK.identify(referenceTemplate);
if (matched) {
// ui is my FormMain instance
ui.showImage(GrFingerJava.getBiometricImage(template,
fingerprint, fingerprintSDK));
ui.writeLog("Found. Client = "
+ rs.getString("Name"));
ui.setClienteid(rs.getString("Cliente_Id"));
ui.disableTemplate();
return;
}
}
ui.writeLog("Not Found.");
ui.enableTemplate();
} catch (SQLException e) {
ui.writeLog(e.getMessage());
} catch (GrFingerJavaException e) {
ui.writeLog(e.getMessage());
}
}
The Identify method is executed just when my User put the finger in the biometric device.
Someone has some idea ?
Suppose you have the following JS function
function foo(client) {
alert(client);
}
you'll modify your Utils.java as follows
public void identify() {
// the rest of your code
String id = rs.getString("Cliente_Id");
ui.setClienteid(id);
// call the Javascript function
JSObject.getWindow(ui).eval(String.format("foo(%s)", id));
}
Invoking Javascript code from an Applet
Java to Javascript communication (Official Oracle docs)
To compile the code using netscape.javascript.* package you need the $JAVA_HOME/jre/lib/plugin.jar. See here
In order to call JavaScript, Java code uses the
netscape.javascript.JSObject and netscape.javascript.JSException
classes. Since the release of Java 2, Standard Edition version 1.4,
these classes are provided in the jar file jre/lib/plugin.jar within
either the Java Development Kit or Java Runtime Environment. If you
reference these JavaScript classes, you will need to add plugin.jar to
your compilation classpath. This can be done through your Java IDE, if
you use one, or by passing the -classpath command-line argument to the
Java compiler javac.
At run-time, the Java Plug-In automatically makes these classes
available to applets, so no changes to the applet or how it is set up
are necessary.
I'm creating a sandbox for JEXL scripts to execute in so that a malicious user can't access data outside the variables we give them access to and also can't perform a DOS attack on the server. I'd like to document this for anybody else also doing this and also get other people's input into the approach.
The following is a list of the things I'm aware of that needs to be addressed:
Only allow instantiating classes using 'new' that are on a whitelist.
Do not allow accessing the getClass method on any class because then forName can be called and any class can be accessed.
Restrict access to resources such as files.
Allow an expression only a certain amount of time to execute so that we can limit the amount of resources it consumes.
This does not apply to JEXL but may apply to the scripting language you are using:
Do not allow an object to have a custom finalize method because the finalize method is called from the finalizer thread and will execute with the original AccessControlContext instead of the one being used to create the object and execute the code in it.
UPDATE: This was all done using JEXL 2.0.1. You may have to adapt this to make it work with newer versions.
Here is my approach for dealing with each of these cases. I've created unit tests to test each of these cases and I have verified that they work.
JEXL makes this pretty easy. Just create a custom ClassLoader. Override the two loadClass() methods. On JexlEngine call setClassLoader().
Again, JEXL makes this pretty easy. You must block both '.class' and '.getClass()'. Create your own Uberspect class which extends UberspectImpl. Override getPropertyGet, if identifier equals "class" return null. Override getMethod, if method equals "getClass" return null. When constructing JexlEngine pass a reference to your Uberspect implementation.
class MyUberspectImpl extends UberspectImpl {
public MyUberspectImpl(Log jexlLog) {
super(jexlLog);
}
#Override
public JexlPropertyGet getPropertyGet(Object obj, Object identifier, JexlInfo info) {
// for security we do not allow access to .class property
if ("class".equals(identifier)) throw new RuntimeException("Access to getClass() method is not allowed");
JexlPropertyGet propertyGet = super.getPropertyGet(obj, identifier, info);
return propertyGet;
}
#Override
public JexlMethod getMethod(Object obj, String method, Object[] args, JexlInfo info) {
// for security we do not allow access to .getClass() method
if ("getClass".equals(method)) throw new RuntimeException("Access to getClass() method is not allowed");
return super.getMethod(obj, method, args, info);
}
}
You do this using Java's AccessController mechanism. I'll give a quick run-down of doing this. Start java with -Djava.security.policy=policyfile. Make a file named policyfile containing this line:
grant { permission java.security.AllPermission; };
Set the default SecurityManager with this call: System.setSecurityManager(new SecurityManager()); Now you can control permissions and your app by default has all permissions. It would be better if you limit the permissions of your app to only what it requires of course. Next, create an AccessControlContext that limits the permissions to the bare minimum and call AccessController.doPrivileged() and pass the AccessControlContext, then execute the JEXL script inside doPrivileged(). Here is a small program that demonstrates this. The JEXL script calls System.exit(1) and if it isn't wrapped in doPrivileged() it would successfully terminate the JVM.
System.out.println("java.security.policy=" + System.getProperty("java.security.policy"));
System.setSecurityManager(new SecurityManager());
try {
Permissions perms = new Permissions();
perms.add(new RuntimePermission("accessDeclaredMembers"));
ProtectionDomain domain = new ProtectionDomain(new CodeSource( null, (Certificate[]) null ), perms );
AccessControlContext restrictedAccessControlContext = new AccessControlContext(new ProtectionDomain[] { domain } );
JexlEngine jexlEngine = new JexlEngine();
final Script finalExpression = jexlEngine.createScript(
"i = 0; intClazz = i.class; "
+ "clazz = intClazz.forName(\"java.lang.System\"); "
+ "m = clazz.methods; m[0].invoke(null, 1); c");
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
#Override
public Object run() throws Exception {
return finalExpression.execute(new MapContext());
}
}, restrictedAccessControlContext);
}
catch (Throwable ex) {
ex.printStackTrace();
}
The trick with this is interrupting the script before it finishes. One way I found to do this is to create a custom JexlArithmetic class. Then override each method in that class and before calling the real method in the super class check if the script should stop executing. I'm using an ExecutorService to create threads. When Future.get() is called pass the amount of time to wait. If a TimeoutException is thrown call Future.cancel() which interrupts the Thread running the script. Inside each overridden method in the new JexlArithmetic class check Thread.interrupted() and if true throw java.util.concurrent.CancellationException.
Is there a better location to put code which will get executed regularly as a script is being executed so that it can be interrupted?
Here is an excerpt of the MyJexlArithmetic class. You have to add all the other methods:
public class MyJexlArithmetic extends JexlArithmetic {
public MyJexlArithmetic(boolean lenient) {
super(lenient);
}
private void checkInterrupted() {
if (Thread.interrupted()) throw new CancellationException();
}
#Override
public boolean equals(Object left, Object right) {
checkInterrupted();
return super.equals(left, right); //To change body of generated methods, choose Tools | Templates.
}
#Override
public Object add(Object left, Object right) {
checkInterrupted();
return super.add(left, right);
}
}
Here is how I am instantiating JexlEngine:
Log jexlLog = LogFactory.getLog("JEXL");
Map <String, Object> functions = new HashMap();
jexlEngine = new JexlEngine(new MyUberspectImpl(jexlLog), new MyJexlArithmetic(false), functions, jexlLog);
Using the JDK 6 ScriptEngine mechanism, anything that goes wrong during an "eval" or "invokeMethod" or whatever results in a ScriptException being thrown back to the invoking Java environment. As far as I've been able to tell by experimentation and source code reading, the best I can do to get information back from Javascript when my Javascript code wants to throw an exception is to throw a string. That string shows up in the "getMessage" return value from the ScriptException object. Not pretty.
It seems like it would be nice to be able to:
if (somethingWrong) {
throw { error: Errors.INVALID_SOMETHING, context: whatever() };
}
from Javascript, and then the Java code could somehow get at that object. I fear however that given the current implementation of the Rhino ScriptEngine wrapper, that's just not possible. If anybody knows a trick that does work, however, I'd love to see it.
Rhino does have poor support for Exceptions, they arent as usable as or helpful as they are in other Scripting engines like Groovy or JRuby.
To help you debug problems, I think you can use this trick. Rhino adds a non standard property to the Error object which you can access to print out some information about the Error.
try {
someCode()
}
catch(e) {
if(e.rhinoException != null)
{
e.rhinoException.printStackTrace();
}
}
You could also use Bindings to indicate that something went wrong?
I understand what you are looking for now. Its not pretty, in fact its terrible, but you can get access to the underlying JavaScriptException and then get hold of the value where you threw the Exception.
package test;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import sun.org.mozilla.javascript.*;
public class Main {
private static ScriptEngineManager mgr = new ScriptEngineManager();
private static ScriptEngine engine = mgr.getEngineByName("JavaScript");
public static void main(String... args) {
System.out.println("START");
try {
engine.eval("throw { error: \"cheese\", context: \"payload\" };");
} catch (ScriptException e) {
JavaScriptException jse = (JavaScriptException)e.getCause();
Object obj = jse.getValue();
ScriptableObject so = (ScriptableObject)obj;
System.out.println(so.get("error", so));
System.out.println(so.get("context", so));
}
System.out.println("END");
}
}
You could serialize your error object as a JSON string and then deserialize in Java.
FWIW, I think it would also be hugely useful if you could throw a Java exception object and have that propagate up the stack. For instance, to adapt the code from above:
try {
engine.eval("throw new java.lang.IllegalArgumentException( 'no cookie!' )" );
}
catch (ScriptException e) {
Throwable cause = e.getCause();
if ( cause != null && cause instanceof IllegalArgumentException ) {
// ...react explicitly
}
else {
// ...react generically
}