View Javadoc

1   /*
2    *  Copyright 2004-2006 Stefan Reuter
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   *
16   */
17  package org.asteriskjava.fastagi;
18  
19  import org.asteriskjava.util.LogFactory;
20  import org.asteriskjava.util.Log;
21  
22  import javax.script.ScriptEngine;
23  import javax.script.ScriptEngineManager;
24  import javax.script.Bindings;
25  import javax.script.ScriptException;
26  import java.io.*;
27  import java.util.Arrays;
28  import java.util.regex.Matcher;
29  import java.util.List;
30  import java.util.ArrayList;
31  import java.net.URLClassLoader;
32  import java.net.URL;
33  import java.net.MalformedURLException;
34  
35  /**
36   * A MappingStrategy that uses {@see javax.script.ScriptEngine} to run AgiScripts. This MappingStrategy
37   * can be used to run JavaScript, Groovy, JRuby, etc. scripts.
38   *
39   * @since 1.0.0
40   */
41  public class ScriptEngineMappingStrategy implements MappingStrategy
42  {
43      protected final Log logger = LogFactory.getLog(getClass());
44  
45      /**
46       * The binding under which the AGI request is made available to scripts.
47       */
48      public static final String REQUEST = "request";
49  
50      /**
51       * The binding under which the AGI channel is made available to scripts.
52       */
53      public static final String CHANNEL = "channel";
54  
55      private static final String[] DEFAULT_SCRIPT_PATH = new String[]{"agi"};
56      private static final String[] DEFAULT_LIB_PATH = new String[]{"lib"};
57  
58      protected String[] scriptPath;
59      protected String[] libPath;
60      protected ScriptEngineManager scriptEngineManager = null;
61  
62      /**
63       * Creates a new ScriptEngineMappingStrategy that searches for scripts in the current directory.
64       */
65      public ScriptEngineMappingStrategy()
66      {
67          this(DEFAULT_SCRIPT_PATH, DEFAULT_LIB_PATH);
68      }
69  
70      /**
71       * Creates a new ScriptEngineMappingStrategy that searches for scripts on the given path.
72       *
73       * @param scriptPath array of directory names to search for script files.
74       * @param libPath    array of directory names to search for additional libraries (jar files).
75       */
76      public ScriptEngineMappingStrategy(String[] scriptPath, String[] libPath)
77      {
78          setScriptPath(scriptPath);
79          setLibPath(libPath);
80      }
81  
82      /**
83       * Sets the path to search for script files.<p>
84       * Default is "agi".
85       *
86       * @param scriptPath array of directory names to search for script files.
87       */
88      public void setScriptPath(String[] scriptPath)
89      {
90          this.scriptPath = Arrays.copyOf(scriptPath, scriptPath.length);;
91      }
92  
93      /**
94       * Sets the path to search for additional libraries (jar files).<p>
95       * Default is "lib".
96       *
97       * @param libPath array of directory names to search for additional libraries (jar files).
98       */
99      public void setLibPath(String[] libPath)
100     {
101         this.libPath = Arrays.copyOf(libPath, libPath.length);
102     }
103 
104     @Override
105     public AgiScript determineScript(AgiRequest request, AgiChannel channel)
106     {
107         // check is a file corresponding to the AGI request is found on the scriptPath
108         final File file = searchFile(request.getScript(), scriptPath);
109         if (file == null)
110         {
111             return null;
112         }
113 
114         // check if there is a ScriptEngine that can handle the file
115         final ScriptEngine scriptEngine = getScriptEngine(file);
116         if (scriptEngine == null)
117         {
118             logger.debug("No ScriptEngine found that can handle '" + file.getPath() + "'");
119             return null;
120         }
121 
122         return new ScriptEngineAgiScript(file, scriptEngine);
123     }
124 
125     /**
126      * Searches for a ScriptEngine that can handle the given file.
127      *
128      * @param file the file to search a ScriptEngine for.
129      * @return the ScriptEngine or <code>null</code> if none is found.
130      */
131     protected ScriptEngine getScriptEngine(File file)
132     {
133         final String extension = getExtension(file.getName());
134         if (extension == null)
135         {
136             return null;
137         }
138 
139         return getScriptEngineManager().getEngineByExtension(extension);
140     }
141 
142     /**
143      * Returns the ScriptEngineManager to use for loading the ScriptEngine. The ScriptEngineManager is only
144      * created once and reused for subsequent requests. Override this method to provide your own implementation.
145      *
146      * @return the ScriptEngineManager to use for loading the ScriptEngine.
147      * @see javax.script.ScriptEngineManager#ScriptEngineManager()
148      */
149     protected synchronized ScriptEngineManager getScriptEngineManager()
150     {
151         if (scriptEngineManager == null)
152         {
153             this.scriptEngineManager = new ScriptEngineManager(getClassLoader());
154         }
155         return scriptEngineManager;
156     }
157 
158     /**
159      * Returns the ClassLoader to use for the ScriptEngineManager. Adds all jar files in the "lib" subdirectory of
160      * the current directory to the class path. Override this method to provide your own ClassLoader.
161      *
162      * @return the ClassLoader to use for the ScriptEngineManager.
163      * @see #getScriptEngineManager()
164      */
165     protected ClassLoader getClassLoader()
166     {
167         final ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();
168         final List<URL> jarFileUrls = new ArrayList<URL>();
169 
170         if (libPath == null || libPath.length == 0)
171         {
172             return parentClassLoader;
173         }
174 
175         for (String libPathEntry : libPath)
176         {
177             final File libDir = new File(libPathEntry);
178             if (!libDir.isDirectory())
179             {
180                 continue;
181             }
182 
183             final File[] jarFiles = libDir.listFiles(new FilenameFilter()
184             {
185                 public boolean accept(File dir, String name)
186                 {
187                     return name.endsWith(".jar");
188                 }
189             });
190 
191             for (File jarFile : jarFiles)
192             {
193                 try
194                 {
195                     jarFileUrls.add(jarFile.toURI().toURL());
196                 }
197                 catch (MalformedURLException e)
198                 {
199                     // should not happen
200                 }
201             }
202         }
203 
204         if (jarFileUrls.size() == 0)
205         {
206             return parentClassLoader;
207         }
208 
209         return new URLClassLoader(jarFileUrls.toArray(new URL[jarFileUrls.size()]), parentClassLoader);
210     }
211 
212     /**
213      * Searches for the file with the given name on the path.
214      *
215      * @param scriptName the name of the file to search for.
216      * @param path       an array of directories to search for the file in order of preference.
217      * @return the canonical file if found on the path or <code>null</code> if not found.
218      */
219     protected File searchFile(String scriptName, String[] path)
220     {
221         if (scriptName == null || path == null)
222         {
223             return null;
224         }
225 
226         for (String pathElement : path)
227         {
228             final File pathElementDir = new File(pathElement);
229             // skip if pathElement is not a directory
230             if (!pathElementDir.isDirectory())
231             {
232                 continue;
233             }
234 
235             final File file = new File(pathElementDir, scriptName.replaceAll("/", Matcher.quoteReplacement(File.separator)));
236             if (!file.exists())
237             {
238                 continue;
239             }
240 
241             try
242             {
243                 // prevent attacks with scripts using ".." in their name.
244                 if (!isInside(file, pathElementDir))
245                 {
246                     return null;
247                 }
248             }
249             catch (IOException e)
250             {
251                 logger.warn("Unable to check whether '" + file.getPath() + "' is below '" + pathElementDir.getPath() + "'");
252                 continue;
253             }
254 
255             try
256             {
257                 return file.getCanonicalFile();
258             }
259             catch (IOException e)
260             {
261                 logger.error("Unable to get canonical file for '" + file.getPath() + "'", e);
262             }
263         }
264         return null;
265     }
266 
267     /**
268      * Checks whether a file is contained within a given directory (or a sub directory) or not.
269      *
270      * @param file the file to check.
271      * @param dir  the directory to check.
272      * @return <code>true</code> if file is below directory, <code>false</code> otherwise.
273      * @throws IOException if the canonical path of file or dir cannot be determined.
274      */
275     protected final boolean isInside(File file, File dir) throws IOException
276     {
277         return file.getCanonicalPath().startsWith(dir.getCanonicalPath());
278     }
279 
280     /**
281      * Returns the extension (the part after the last ".") of the given script.
282      *
283      * @param scriptName the name of the script to return the extension of.
284      * @return the extension of the script or <code>null</code> if there is no extension.
285      */
286     protected static String getExtension(String scriptName)
287     {
288         if (scriptName == null)
289         {
290             return null;
291         }
292 
293         int filePosition = scriptName.lastIndexOf("/");
294         String fileName;
295 
296         if (scriptName.lastIndexOf("\\") > filePosition)
297         {
298             filePosition = scriptName.lastIndexOf("\\");
299         }
300 
301         if (filePosition >= 0)
302         {
303             fileName = scriptName.substring(filePosition + 1);
304         }
305         else
306         {
307             fileName = scriptName;
308         }
309 
310         final int extensionPosition = fileName.lastIndexOf(".");
311         if (extensionPosition >= 0)
312         {
313             return fileName.substring(extensionPosition + 1);
314         }
315 
316         return null;
317     }
318 
319     protected static Reader getReader(File file) throws FileNotFoundException
320     {
321         final InputStream is = new FileInputStream(file);
322         return new InputStreamReader(is);
323     }
324 
325     protected class ScriptEngineAgiScript implements NamedAgiScript
326     {
327         final File file;
328         final ScriptEngine scriptEngine;
329 
330         /**
331          * Creates a new ScriptEngineAgiScript.
332          *
333          * @param file         the file that contains the script to execute.
334          * @param scriptEngine the ScriptEngine to use for executing the script.
335          */
336         public ScriptEngineAgiScript(File file, ScriptEngine scriptEngine)
337         {
338             this.file = file;
339             this.scriptEngine = scriptEngine;
340         }
341 
342         public String getName()
343         {
344             return file == null ? null : file.getName();
345         }
346 
347         public void service(AgiRequest request, AgiChannel channel) throws AgiException
348         {
349             final Bindings bindings = scriptEngine.createBindings();
350 
351             bindings.put(ScriptEngine.FILENAME, file.getPath());
352             bindings.put(REQUEST, request);
353             bindings.put(CHANNEL, channel);
354 
355             // support for custom bindings
356             populateBindings(file, request, channel, bindings);
357 
358             try
359             {
360                 scriptEngine.eval(getReader(file), bindings);
361             }
362             catch (ScriptException e)
363             {
364                 throw new AgiException("Execution of script '" + file.getPath() + "' with ScriptEngine failed", e);
365             }
366             catch (FileNotFoundException e)
367             {
368                 throw new AgiException("Script '" + file.getPath() + "' not found", e);
369             }
370         }
371     }
372 
373     /**
374      * Override this method if you want to add additional bindings before the script is run. By default the
375      * AGI request, AGI channel and the filename are available to scripts under the bindings "request", "channel"
376      * and "javax.script.filename".
377      *
378      * @param file     the script file.
379      * @param request  the AGI request.
380      * @param channel  the AGI channel.
381      * @param bindings the bindings to populate.
382      */
383     protected void populateBindings(File file, AgiRequest request, AgiChannel channel, Bindings bindings)
384     {
385 
386     }
387 }