/**************************************************************************** * Copyright (C) 2012 ecsec GmbH. * All rights reserved. * Contact: ecsec GmbH (info@ecsec.de) * * This file is part of the Open eCard App. * * GNU General Public License Usage * This file may be used under the terms of the GNU General Public * License version 3.0 as published by the Free Software Foundation * and appearing in the file LICENSE.GPL included in the packaging of * this file. Please review the following information to ensure the * GNU General Public License version 3.0 requirements will be met: * http://www.gnu.org/copyleft/gpl.html. * * Other Usage * Alternatively, this file may be used in accordance with the terms * and conditions contained in a signed written agreement between * you and ecsec GmbH. * ***************************************************************************/ package org.openecard.clients.applet; import iso.std.iso_iec._24727.tech.schema.ConnectionHandleType; import java.applet.Applet; import java.io.UnsupportedEncodingException; import java.security.AccessController; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivilegedAction; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import netscape.javascript.JSException; import netscape.javascript.JSObject; import org.json.JSONException; import org.json.JSONObject; import org.openecard.addon.AddonManager; import org.openecard.common.util.ByteUtils; import org.openecard.common.util.ValueGenerators; import org.openecard.control.binding.javascript.JavaScriptBinding; import org.openecard.ws.marshal.MarshallingTypeException; import org.openecard.ws.marshal.WSMarshaller; import org.openecard.ws.marshal.WSMarshallerException; import org.openecard.ws.marshal.WSMarshallerFactory; import org.openecard.ws.schema.StatusChange; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; /** * JavaScript communication handler. * * This class is used to handle all types of communication (eg. events and messages) between JavaScript * and the applet. * * @author Johannes Schmölz <johannes.schmoelz@ecsec.de> * @author Benedikt Biallowons <benedikt.biallowons@ecsec.de> */ public class JSCommunicationHandler { private static final Logger logger = LoggerFactory.getLogger(JSCommunicationHandler.class); private final ExecutorService workerPool; private final JSObjectWrapper jsObjectWrapper; private final String jsStartedCallback; private final String jsEventCallback; private final String jsMessageCallback; private Future<?> eventThread; private JavaScriptBinding binding; private HashMap<String, String> sessionMap; // this is just one session for waitForChange /** * Create a new JSCommunicationHandler. * * @param applet current applet * @param manager Manager instance containing available add-ons. */ public JSCommunicationHandler(ECardApplet applet, AddonManager manager) { workerPool = Executors.newCachedThreadPool(); jsObjectWrapper = new JSObjectWrapper(applet); jsStartedCallback = applet.getParameter("jsStartedCallback"); jsEventCallback = applet.getParameter("jsEventCallback"); jsMessageCallback = applet.getParameter("jsMessageCallback"); binding = new JavaScriptBinding(manager); sessionMap = new HashMap<String, String>(); sessionMap.put("session", ValueGenerators.generateSessionID()); setupJSBinding(); } /** * Prepare the JavaScript internal binding. * * @param cardStates CardStateMap of the client * @param dispatcher dispatcher for sending messages * @param eventHandler to wait for status changes * @param gui to show card insertion dialog * @param reg to get card information shown in insertion dialog */ private void setupJSBinding() { try { // send initial Status and thereby register session binding.handle("getStatus", sessionMap); } catch (Exception ex) { logger.error(ex.getMessage(), ex); } } /** * Stop all running worker threads. */ public void stop() { try { eventThread.cancel(true); workerPool.shutdownNow(); } catch (Exception ex) { logger.error(ex.getMessage(), ex); } } /** * Start event polling and push available events to the JavaScript frontend. */ public void startEventPush() { if (jsEventCallback == null) { return; } eventThread = workerPool.submit(new Runnable() { @Override public void run() { JSObject jsObject = jsObjectWrapper.getNamespacedJSObject(jsEventCallback); String function = JSObjectWrapper.getFunction(jsEventCallback); WSMarshaller m = null; try { m = WSMarshallerFactory.createInstance(); m.removeAllTypeClasses(); m.addXmlTypeClass(StatusChange.class); } catch (WSMarshallerException e) { logger.error(e.getMessage(), e); throw new RuntimeException(e); } while (!Thread.currentThread().isInterrupted()) { Object[] waitForChangeResponse = binding.handle("waitForChange", sessionMap); if (waitForChangeResponse != null) { StatusChange statusChange; try { statusChange = (StatusChange) m.unmarshal(m.str2doc(waitForChangeResponse[0].toString())); Object[] response = buildEvent(statusChange); jsObject.call(function, response); } catch (MarshallingTypeException e) { logger.error("Marshalling of WaitForChange-Response failed", e); } catch (WSMarshallerException e) { logger.error("Marshalling of WaitForChange-Response failed", e); } catch (SAXException e) { logger.error("Marshalling of WaitForChange-Response failed", e); } catch (JSException ignore) { } } } } }); } /** * Send a started event to the JavaScript frontend. */ public void sendStarted() { if (jsStartedCallback == null) { return; } try { jsObjectWrapper.call(jsStartedCallback, this); } catch (JSException ignore) { } } /** * Send a message to the JavaScript frontend. * * @param message containing desired information */ public void sendMessage(String message) { if (jsMessageCallback == null) { return; } try { jsObjectWrapper.call(jsMessageCallback, message); } catch (JSException ignore) { } } /** * This is the entry point for JavaScript to Java communication. * * @param callback to call after finished processing * @param id to identify the request * @param data as input parameters */ public void handle(final String callback, final String id, final String data) { try { final Map<String, String> map = parseParameterJSONMap(data); workerPool.submit(new Runnable() { @Override public void run() { JSObject jsObject = jsObjectWrapper.getNamespacedJSObject(callback); String function = JSObjectWrapper.getFunction(callback); // Some methods triggered by JavaScript calls need privileged access to various // resources like network connections to external hosts (eg. to fetch the TcToken). Object[] response = AccessController.doPrivileged(new PrivilegedAction<Object[]>() { @Override public Object[] run() { return binding.handle(id, map); } }); try { jsObject.call(function, response); } catch (JSException ignore) { } } }); } catch (JSONException ex) { logger.error(ex.getMessage(), ex); } } /** * Parse parameter map from JSON data * * @param jsonData from JavaScript * @return parameter hash map * @throws JSONException */ private static Map<String, String> parseParameterJSONMap(String jsonData) throws JSONException { JSONObject json = new JSONObject(jsonData); Map<String, String> map = new HashMap<String, String>(); for (String key : JSONObject.getNames(json)) { map.put(key, json.getString(key)); } return map; } /** * Build the JavaScript event callback parameter array. * * @param statusChange that occurred * @return JavaScript parameters array */ private static Object[] buildEvent(StatusChange statusChange) { ConnectionHandleType cHandle = statusChange.getConnectionHandle(); String action = statusChange.getAction(); String ifdId = makeId(cHandle.getIFDName()); String ifdName = cHandle.getIFDName(); String cardType = cHandle.getRecognitionInfo() != null ? cHandle.getRecognitionInfo().getCardType() : null; String contextHandle = ByteUtils.toHexString(cHandle.getContextHandle()); String slotIndex = cHandle.getSlotIndex() != null ? cHandle.getSlotIndex().toString() : null; return new Object[] { action, ifdId, ifdName, cardType, contextHandle, slotIndex }; } /** * Helper method to generate a JavaScript compatible id from a string input. * Used by the JavaScript frontend. * * @param input to generate id * @return unique id */ private static String makeId(String input) { try { MessageDigest md = MessageDigest.getInstance("SHA"); byte[] bytes = md.digest(input.getBytes("UTF-8")); return ByteUtils.toHexString(bytes); } catch (NoSuchAlgorithmException ex) { return input.replaceAll(" ", "_"); } catch (UnsupportedEncodingException ex) { return input.replaceAll(" ", "_"); } } /** * JSObject wrapper class to handle namespaced function calls. * * @author Benedikt Biallowons <benedikt.biallowons@ecsec.de> */ private static final class JSObjectWrapper { private final JSObject jsObject; /** * Create wrapper instance from applet. * * @param applet to get JavaScript window object * @throws JSException throw by underlying JSObject.getWindow() */ public JSObjectWrapper(Applet applet) { this.jsObject = JSObject.getWindow(applet); } /** * Call namespaced JavaScript function with single parameter. * * @param function to call * @param parameter to use with the JavaScript function call * @throws JSException thrown by underlying JSObject.call() or namespace resolution */ public void call(String function, Object parameter) { call(function, new Object[] { parameter }); } /** * Call namespaced JavaScript function with parameters array. * * @param function to call * @param parameters to use with the JavaScript function call * @throws JSException thrown by underlying JSObject.call() or namespace resolution */ public void call(String function, Object[] parameters) { getNamespacedJSObject(function).call(getFunction(function), parameters); } /** * Used to get the correct JSObject from possible namespaced function. JavaScript * namespaces are typically implemented via global objects. * * @param function to call * @return the matching JSObject * @throws JSException if JavaScript object can't be found */ public JSObject getNamespacedJSObject(String function) { JSObject jsObj = this.jsObject; String[] namespacesAndFunction = function.split("\\."); for (int i = 0; i < (namespacesAndFunction.length - 1); i++) { jsObj = (JSObject) jsObj.getMember(namespacesAndFunction[i]); } return jsObj; } /** * Get simple function from namespaced function. * * @param namespacedFunction to get function of * @return simple function */ public static String getFunction(String namespacedFunction) { String[] namespacesAndFunction = namespacedFunction.split("\\."); return namespacesAndFunction[namespacesAndFunction.length - 1]; } } }