/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.painless; import org.apache.lucene.index.LeafReaderContext; import org.elasticsearch.SpecialPermission; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.painless.Compiler.Loader; import org.elasticsearch.script.CompiledScript; import org.elasticsearch.script.ExecutableScript; import org.elasticsearch.script.LeafSearchScript; import org.elasticsearch.script.ScriptEngine; import org.elasticsearch.script.ScriptException; import org.elasticsearch.script.SearchScript; import org.elasticsearch.search.lookup.SearchLookup; import java.io.IOException; import java.security.AccessControlContext; import java.security.AccessController; import java.security.Permissions; import java.security.PrivilegedAction; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Implementation of a ScriptEngine for the Painless language. */ public final class PainlessScriptEngine extends AbstractComponent implements ScriptEngine { /** * Standard name of the Painless language. */ public static final String NAME = "painless"; /** * Permissions context used during compilation. */ private static final AccessControlContext COMPILATION_CONTEXT; /** * Setup the allowed permissions. */ static { final Permissions none = new Permissions(); none.setReadOnly(); COMPILATION_CONTEXT = new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, none) }); } /** * Default compiler settings to be used. Note that {@link CompilerSettings} is mutable but this instance shouldn't be mutated outside * of {@link PainlessScriptEngine#PainlessScriptEngine(Settings)}. */ private final CompilerSettings defaultCompilerSettings = new CompilerSettings(); /** * Constructor. * @param settings The settings to initialize the engine with. */ public PainlessScriptEngine(final Settings settings) { super(settings); defaultCompilerSettings.setRegexesEnabled(CompilerSettings.REGEX_ENABLED.get(settings)); } /** * Get the type name(s) for the language. * @return Always contains only the single name of the language. */ @Override public String getType() { return NAME; } /** * Get the extension(s) for the language. * @return Always contains only the single extension of the language. */ @Override public String getExtension() { return NAME; } /** * When a script is anonymous (inline), we give it this name. */ static final String INLINE_NAME = "<inline>"; @Override public Object compile(String scriptName, final String scriptSource, final Map<String, String> params) { return compile(GenericElasticsearchScript.class, scriptName, scriptSource, params); } <T> T compile(Class<T> iface, String scriptName, final String scriptSource, final Map<String, String> params) { final CompilerSettings compilerSettings; if (params.isEmpty()) { // Use the default settings. compilerSettings = defaultCompilerSettings; } else { // Use custom settings specified by params. compilerSettings = new CompilerSettings(); // Except regexes enabled - this is a node level setting and can't be changed in the request. compilerSettings.setRegexesEnabled(defaultCompilerSettings.areRegexesEnabled()); Map<String, String> copy = new HashMap<>(params); String value = copy.remove(CompilerSettings.MAX_LOOP_COUNTER); if (value != null) { compilerSettings.setMaxLoopCounter(Integer.parseInt(value)); } value = copy.remove(CompilerSettings.PICKY); if (value != null) { compilerSettings.setPicky(Boolean.parseBoolean(value)); } value = copy.remove(CompilerSettings.INITIAL_CALL_SITE_DEPTH); if (value != null) { compilerSettings.setInitialCallSiteDepth(Integer.parseInt(value)); } value = copy.remove(CompilerSettings.REGEX_ENABLED.getKey()); if (value != null) { throw new IllegalArgumentException("[painless.regex.enabled] can only be set on node startup."); } if (!copy.isEmpty()) { throw new IllegalArgumentException("Unrecognized compile-time parameter(s): " + copy); } } // Check we ourselves are not being called by unprivileged code. SpecialPermission.check(); // Create our loader (which loads compiled code with no permissions). final Loader loader = AccessController.doPrivileged(new PrivilegedAction<Loader>() { @Override public Loader run() { return new Loader(getClass().getClassLoader()); } }); try { // Drop all permissions to actually compile the code itself. return AccessController.doPrivileged(new PrivilegedAction<T>() { @Override public T run() { String name = scriptName == null ? INLINE_NAME : scriptName; return Compiler.compile(loader, iface, name, scriptSource, compilerSettings); } }, COMPILATION_CONTEXT); // Note that it is safe to catch any of the following errors since Painless is stateless. } catch (OutOfMemoryError | StackOverflowError | VerifyError | Exception e) { throw convertToScriptException(scriptName == null ? scriptSource : scriptName, scriptSource, e); } } /** * Retrieve an {@link ExecutableScript} for later use. * @param compiledScript A previously compiled script. * @param vars The variables to be used in the script. * @return An {@link ExecutableScript} with the currently specified variables. */ @Override public ExecutableScript executable(final CompiledScript compiledScript, final Map<String, Object> vars) { return new ScriptImpl((GenericElasticsearchScript) compiledScript.compiled(), vars, null); } /** * Retrieve a {@link SearchScript} for later use. * @param compiledScript A previously compiled script. * @param lookup The object that ultimately allows access to search fields. * @param vars The variables to be used in the script. * @return An {@link SearchScript} with the currently specified variables. */ @Override public SearchScript search(final CompiledScript compiledScript, final SearchLookup lookup, final Map<String, Object> vars) { return new SearchScript() { /** * Get the search script that will have access to search field values. * @param context The LeafReaderContext to be used. * @return A script that will have the search fields from the current context available for use. */ @Override public LeafSearchScript getLeafSearchScript(final LeafReaderContext context) throws IOException { return new ScriptImpl((GenericElasticsearchScript) compiledScript.compiled(), vars, lookup.getLeafSearchLookup(context)); } /** * Whether or not the score is needed. */ @Override public boolean needsScores() { return ((GenericElasticsearchScript) compiledScript.compiled()).uses$_score(); } }; } /** * Action taken when the engine is closed. */ @Override public void close() { // Nothing to do. } private ScriptException convertToScriptException(String scriptName, String scriptSource, Throwable t) { // create a script stack: this is just the script portion List<String> scriptStack = new ArrayList<>(); for (StackTraceElement element : t.getStackTrace()) { if (WriterConstants.CLASS_NAME.equals(element.getClassName())) { // found the script portion int offset = element.getLineNumber(); if (offset == -1) { scriptStack.add("<<< unknown portion of script >>>"); } else { offset--; // offset is 1 based, line numbers must be! int startOffset = getPreviousStatement(scriptSource, offset); int endOffset = getNextStatement(scriptSource, offset); StringBuilder snippet = new StringBuilder(); if (startOffset > 0) { snippet.append("... "); } snippet.append(scriptSource.substring(startOffset, endOffset)); if (endOffset < scriptSource.length()) { snippet.append(" ..."); } scriptStack.add(snippet.toString()); StringBuilder pointer = new StringBuilder(); if (startOffset > 0) { pointer.append(" "); } for (int i = startOffset; i < offset; i++) { pointer.append(' '); } pointer.append("^---- HERE"); scriptStack.add(pointer.toString()); } break; } } throw new ScriptException("compile error", t, scriptStack, scriptSource, PainlessScriptEngine.NAME); } // very simple heuristic: +/- 25 chars. can be improved later. private int getPreviousStatement(String scriptSource, int offset) { return Math.max(0, offset - 25); } private int getNextStatement(String scriptSource, int offset) { return Math.min(scriptSource.length(), offset + 25); } @Override public boolean isInlineScriptEnabled() { return true; } }