001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014-2015 ForgeRock AS. 015 */ 016package org.forgerock.openig.script; 017 018import static org.forgerock.http.protocol.Response.newResponsePromise; 019import static org.forgerock.openig.el.Bindings.bindings; 020import static org.forgerock.openig.el.Expressions.evaluate; 021import static org.forgerock.openig.heap.Keys.CLIENT_HANDLER_HEAP_KEY; 022import static org.forgerock.openig.heap.Keys.ENVIRONMENT_HEAP_KEY; 023import static org.forgerock.openig.http.Responses.newInternalServerError; 024import static org.forgerock.openig.util.JsonValues.evaluate; 025 026import java.util.HashMap; 027import java.util.Map; 028import java.util.Map.Entry; 029import java.util.concurrent.ConcurrentHashMap; 030 031import javax.script.ScriptException; 032 033import org.forgerock.http.Client; 034import org.forgerock.http.Handler; 035import org.forgerock.http.protocol.Request; 036import org.forgerock.http.protocol.Response; 037import org.forgerock.json.JsonValueException; 038import org.forgerock.openig.config.Environment; 039import org.forgerock.openig.el.Bindings; 040import org.forgerock.openig.el.ExpressionException; 041import org.forgerock.openig.heap.GenericHeapObject; 042import org.forgerock.openig.heap.GenericHeaplet; 043import org.forgerock.openig.heap.Heap; 044import org.forgerock.openig.heap.HeapException; 045import org.forgerock.openig.ldap.LdapClient; 046import org.forgerock.services.context.Context; 047import org.forgerock.util.promise.NeverThrowsException; 048import org.forgerock.util.promise.Promise; 049 050/** 051 * An abstract scriptable heap object which should be used as the base class for 052 * implementing {@link org.forgerock.http.Filter filters} and {@link Handler handlers}. This heap 053 * object acts as a simple wrapper around the scripting engine. Scripts are 054 * provided with the following variable bindings: 055 * <ul> 056 * <li>{@link Map globals} - the Map of global variables which persist across 057 * successive invocations of the script 058 * <li>{@link org.forgerock.services.context.Context context} - the associated request context 059 * <li>{@link Map contexts} - the visible contexts, keyed by context's name 060 * <li>{@link Request request} - the HTTP request 061 * <li>{@link Client http} - an HTTP client which may be used for performing outbound HTTP requests 062 * <li>{@link LdapClient ldap} - an OpenIG LDAP client which may be used for 063 * performing LDAP requests such as LDAP authentication 064 * <li>{@link org.forgerock.openig.log.Logger logger} - the OpenIG logger 065 * <li>{@link Handler next} - if the heap object is a filter then this variable 066 * will contain the next handler in the filter chain. 067 * <li>{@link Heap heap} - the heap. 068 * </ul> 069 * <p> 070 * <b>NOTE:</b> at the moment only Groovy is supported. 071 * <p><b>NOTE:</b> As of OpenIG 4.0, {@code exchange.request} and {@code exchange.response} are not set anymore. 072 */ 073public abstract class AbstractScriptableHeapObject extends GenericHeapObject { 074 075 /** Creates and initializes a capture filter in a heap environment. */ 076 protected abstract static class AbstractScriptableHeaplet extends GenericHeaplet { 077 private static final String CONFIG_OPTION_FILE = "file"; 078 private static final String CONFIG_OPTION_SOURCE = "source"; 079 private static final String CONFIG_OPTION_TYPE = "type"; 080 private static final String CONFIG_OPTION_ARGS = "args"; 081 082 @Override 083 public Object create() throws HeapException { 084 final Script script = compileScript(); 085 final AbstractScriptableHeapObject component = newInstance(script, heap); 086 Handler clientHandler = heap.resolve(config.get("clientHandler").defaultTo(CLIENT_HANDLER_HEAP_KEY), 087 Handler.class); 088 component.setClientHandler(clientHandler); 089 if (config.isDefined(CONFIG_OPTION_ARGS)) { 090 component.setArgs(config.get(CONFIG_OPTION_ARGS).asMap()); 091 } 092 return component; 093 } 094 095 /** 096 * Creates the new heap object instance using the provided script. 097 * 098 * @param script The compiled script. 099 * @param heap The heap to look for bindings 100 * @return The new heap object instance using the provided script. 101 * @throws HeapException if an exception occurred during creation of the heap 102 * object or any of its dependencies. 103 * @throws JsonValueException if the heaplet (or one of its dependencies) has a 104 * malformed configuration. 105 */ 106 protected abstract AbstractScriptableHeapObject newInstance(final Script script, final Heap heap) 107 throws HeapException; 108 109 private Script compileScript() throws HeapException { 110 final Environment environment = heap.get(ENVIRONMENT_HEAP_KEY, Environment.class); 111 112 if (!config.isDefined(CONFIG_OPTION_TYPE)) { 113 throw new JsonValueException(config, "The configuration option '" 114 + CONFIG_OPTION_TYPE 115 + "' is required and must specify the script mime-type"); 116 } 117 final String mimeType = config.get(CONFIG_OPTION_TYPE).asString(); 118 if (config.isDefined(CONFIG_OPTION_SOURCE)) { 119 if (config.isDefined(CONFIG_OPTION_FILE)) { 120 throw new JsonValueException(config, "Both configuration options '" 121 + CONFIG_OPTION_SOURCE + "' and '" + CONFIG_OPTION_FILE 122 + "' were specified, when at most one is allowed"); 123 } 124 final String source = config.get(CONFIG_OPTION_SOURCE).asString(); 125 try { 126 return Script.fromSource(environment, mimeType, source); 127 } catch (final ScriptException e) { 128 throw new JsonValueException(config, 129 "Unable to compile the script defined in '" + CONFIG_OPTION_SOURCE 130 + "'", e 131 ); 132 } 133 } else if (config.isDefined(CONFIG_OPTION_FILE)) { 134 final String script = evaluate(config.get(CONFIG_OPTION_FILE)); 135 try { 136 return Script.fromFile(environment, mimeType, script); 137 } catch (final ScriptException e) { 138 throw new JsonValueException(config, "Unable to compile the script in file '" 139 + script + "'", e); 140 } 141 } else { 142 throw new JsonValueException(config, "Neither of the configuration options '" 143 + CONFIG_OPTION_SOURCE + "' and '" + CONFIG_OPTION_FILE 144 + "' were specified"); 145 } 146 } 147 148 } 149 150 // TODO: add support for periodically refreshing the Groovy script file. 151 // TODO: json/xml/sql/crest bindings. 152 153 private final Script compiledScript; 154 private final Heap heap; 155 private Handler clientHandler; 156 private final LdapClient ldapClient = LdapClient.getInstance(); 157 private final Map<String, Object> scriptGlobals = new ConcurrentHashMap<>(); 158 private Map<String, Object> args; 159 160 /** 161 * Creates a new scriptable heap object using the provided compiled script. 162 * 163 * @param compiledScript The compiled script. 164 * @param heap The heap to look for bindings 165 */ 166 protected AbstractScriptableHeapObject(final Script compiledScript, Heap heap) { 167 this.compiledScript = compiledScript; 168 this.heap = heap; 169 } 170 171 /** 172 * Sets the HTTP client handler which should be made available to scripts. 173 * 174 * @param clientHandler The HTTP client handler which should be made available to scripts. 175 */ 176 public void setClientHandler(final Handler clientHandler) { 177 this.clientHandler = clientHandler; 178 } 179 180 /** 181 * Sets the parameters which should be made available to scripts. 182 * 183 * @param args The parameters which should be made available to scripts. 184 */ 185 public void setArgs(final Map<String, Object> args) { 186 this.args = args; 187 } 188 189 /** 190 * Runs the compiled script using the provided bindings and optional 191 * forwarding handler. 192 * 193 * @param bindings Base bindings available to the script (will be enriched). 194 * @param next The next handler in the chain if applicable, may be 195 * {@code null}. 196 * @param context request processing context 197 * @return the Promise of a Response produced by the script 198 */ 199 @SuppressWarnings("unchecked") 200 protected final Promise<Response, NeverThrowsException> runScript(final Bindings bindings, 201 final Handler next, 202 final Context context) { 203 try { 204 Object o = compiledScript.run(enrichBindings(bindings, next, context)); 205 if (o instanceof Promise) { 206 return ((Promise<Response, NeverThrowsException>) o); 207 } else if (o instanceof Response) { 208 // Allow to return a response directly from script 209 return newResponsePromise((Response) o); 210 } else { 211 logger.error("Script did not return a Response or a Promise<Response, NeverThrowsException>"); 212 return newResponsePromise(newInternalServerError()); 213 } 214 } catch (final ScriptException e) { 215 logger.warning("Cannot execute script"); 216 logger.warning(e); 217 return newResponsePromise(newInternalServerError().setCause(e)); 218 } 219 } 220 221 private Map<String, Object> enrichBindings(final Bindings source, final Handler next, final Context context) 222 throws ScriptException { 223 // Set engine bindings. 224 final Map<String, Object> bindings = new HashMap<>(); 225 bindings.putAll(source.asMap()); 226 bindings.put("logger", logger); 227 bindings.put("globals", scriptGlobals); 228 if (clientHandler != null) { 229 bindings.put("http", new Client(clientHandler, context)); 230 } 231 bindings.put("ldap", ldapClient); 232 if (next != null) { 233 bindings.put("next", next); 234 } 235 if (args != null) { 236 try { 237 final Bindings exprEvalBindings = bindings().bind(source).bind("heap", heap); 238 for (final Entry<String, Object> entry : args.entrySet()) { 239 checkBindingNotAlreadyUsed(bindings, entry.getKey()); 240 bindings.put(entry.getKey(), evaluate(entry.getValue(), exprEvalBindings)); 241 } 242 } catch (ExpressionException ex) { 243 throw new ScriptException(ex); 244 } 245 } 246 247 // Redirect streams? E.g. in = request entity, out = response entity? 248 return bindings; 249 } 250 251 private void checkBindingNotAlreadyUsed(final Map<String, Object> bindings, 252 final String key) throws ScriptException { 253 if (bindings.containsKey(key)) { 254 final String errorMsg = "Can't override the binding named " + key; 255 logger.error(errorMsg); 256 throw new ScriptException(errorMsg); 257 } 258 } 259}