xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/framework/PluginClassLoader.java (revision 311d83f92b44ee6d7ae3f9dce4250a19e8a6a981)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved.
22  * Portions Copyright (c) 2020, Chris Fraire <cfraire@me.com>.
23  */
24 package org.opengrok.indexer.framework;
25 
26 import java.io.File;
27 import java.io.FileInputStream;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.util.Arrays;
31 import java.util.HashMap;
32 import java.util.Map;
33 import java.util.jar.JarEntry;
34 import java.util.jar.JarFile;
35 import java.util.logging.Level;
36 import java.util.logging.Logger;
37 import org.opengrok.indexer.logger.LoggerFactory;
38 
39 /**
40  * Class loader for plugins from .class and .jar files.
41  *
42  * @author Krystof Tulinger
43  */
44 public class PluginClassLoader extends ClassLoader {
45 
46     private final Map<String, Class<?>> cache = new HashMap<>();
47 
48     private static final Logger LOGGER = LoggerFactory.getLogger(PluginClassLoader.class);
49     private static final String[] CLASS_WHITELIST = new String[]{
50             "org.opengrok.indexer.configuration.Group",
51             "org.opengrok.indexer.configuration.Project",
52             "org.opengrok.indexer.configuration.RuntimeEnvironment",
53             "org.opengrok.indexer.authorization.IAuthorizationPlugin",
54             "org.opengrok.indexer.authorization.plugins.*",
55             "org.opengrok.indexer.authorization.AuthorizationException",
56             "org.opengrok.indexer.util.*",
57             "org.opengrok.indexer.logger.*",
58             "org.opengrok.indexer.Metrics"
59     };
60 
61     private static final String[] PACKAGE_BLACKLIST = new String[]{
62             "java",
63             "javax",
64             "org.w3c",
65             "org.xml",
66             "org.omg",
67             "sun"
68     };
69 
70     private static final String CLASS_SUFFIX = ".class";
71 
72     private final File directory;
73 
PluginClassLoader(File directory)74     public PluginClassLoader(File directory) {
75         super(PluginClassLoader.class.getClassLoader());
76         this.directory = directory;
77     }
78 
loadClassFromJar(String classname)79     private Class<?> loadClassFromJar(String classname) throws ClassNotFoundException {
80         File[] jars = directory.listFiles((dir, name) -> name.endsWith(".jar"));
81 
82         if (jars == null) {
83             throw new ClassNotFoundException(
84                     "Cannot load class " + classname,
85                     new IOException("Directory " + directory + " is not accessible"));
86         }
87 
88         for (File f : jars) {
89             try (JarFile jar = new JarFile(f)) {
90                 // jar files always use / separator
91                 String filename = classname.replace('.', '/') + CLASS_SUFFIX;
92                 JarEntry entry = (JarEntry) jar.getEntry(filename);
93                 if (entry != null && entry.getName().endsWith(CLASS_SUFFIX)) {
94                     try (InputStream is = jar.getInputStream(entry)) {
95                         byte[] bytes = loadBytes(is);
96                         Class<?> c = defineClass(classname, bytes, 0, bytes.length);
97                         LOGGER.log(Level.FINE, "Class \"{0}\" found in file \"{1}\"",
98                                 new Object[]{
99                                         classname,
100                                         f.getAbsolutePath()
101                                 });
102                         return c;
103                     }
104                 }
105             } catch (IOException ex) {
106                 LOGGER.log(Level.SEVERE, "Loading class threw an exception:", ex);
107             } catch (Throwable ex) {
108                 LOGGER.log(Level.SEVERE, "Loading class threw an unknown exception", ex);
109             }
110         }
111         throw new ClassNotFoundException("Class \"" + classname + "\" could not be found");
112     }
113 
loadClassFromFile(String classname)114     private Class<?> loadClassFromFile(String classname) throws ClassNotFoundException {
115         try {
116             String filename = classname.replace('.', File.separatorChar) + CLASS_SUFFIX;
117             File f = new File(directory, filename);
118             try (FileInputStream in = new FileInputStream(f)) {
119                 byte[] bytes = loadBytes(in);
120 
121                 Class<?> c = defineClass(classname, bytes, 0, bytes.length);
122                 LOGGER.log(Level.FINEST, "Class \"{0}\" found in file \"{1}\"",
123                         new Object[]{
124                                 classname,
125                                 f.getAbsolutePath()
126                         });
127                 return c;
128             }
129         } catch (Throwable e) {
130             throw new ClassNotFoundException(e.toString(), e);
131         }
132     }
133 
loadBytes(InputStream in)134     private byte[] loadBytes(InputStream in) throws IOException {
135         byte[] bytes = new byte[in.available()];
136         if (in.read(bytes) != bytes.length) {
137             throw new IOException("unexpected truncated read");
138         }
139         return bytes;
140     }
141 
checkWhiteList(String name)142     private boolean checkWhiteList(String name) {
143         for (String pattern : CLASS_WHITELIST) {
144             pattern = pattern.replaceAll("\\.", "\\\\.");
145             pattern = pattern.replaceAll("\\*", ".*");
146             if (name.matches(pattern)) {
147                 return true;
148             }
149         }
150         return false;
151     }
152 
checkClassname(String name)153     private void checkClassname(String name) throws SecurityException {
154         if (name.startsWith("org.opengrok.")
155                 && !checkWhiteList(name)) {
156             throw new SecurityException("Tried to load a blacklisted class \"" + name + "\"\n"
157                     + "Allowed classes from opengrok package are only: "
158                     + Arrays.toString(CLASS_WHITELIST));
159         }
160     }
161 
checkPackage(String name)162     private void checkPackage(String name) throws SecurityException {
163         for (String s : PACKAGE_BLACKLIST) {
164             if (name.startsWith(s + ".")) {
165                 throw new SecurityException("Tried to load a class \"" + name
166                         + "\" to a blacklisted package "
167                         + "\"" + s + "\"\n"
168                         + "Disabled packages are: "
169                         + Arrays.toString(PACKAGE_BLACKLIST));
170             }
171         }
172     }
173 
174     /**
175      * Loads the class with given name.
176      * <p>
177      * Order of lookup:
178      * <ol>
179      * <li>already loaded classes </li>
180      * <li>parent class loader</li>
181      * <li>loading from .class files</li>
182      * <li>loading from .jar files</li>
183      * </ol>
184      * <p>
185      * Package blacklist: {@link #PACKAGE_BLACKLIST}.<br>
186      * Classes whitelist: {@link #CLASS_WHITELIST}.
187      *
188      * @param name class name
189      * @return loaded class or null
190      * @throws ClassNotFoundException if class is not found
191      * @throws SecurityException      if the loader cannot access the class
192      */
193     @Override
loadClass(String name)194     public Class<?> loadClass(String name) throws ClassNotFoundException, SecurityException {
195         return loadClass(name, true);
196     }
197 
198     /**
199      * Loads the class with given name.
200      * <p>
201      * Order of lookup:
202      * <ol>
203      * <li>already loaded classes </li>
204      * <li>parent class loader</li>
205      * <li>loading from .class files</li>
206      * <li>loading from .jar files</li>
207      * </ol>
208      * <p>
209      * Package blacklist: {@link #PACKAGE_BLACKLIST}.<br>
210      * Classes whitelist: {@link #CLASS_WHITELIST}.
211      *
212      * @param name      class name
213      * @param resolveIt if the class should be resolved
214      * @return loaded class or null
215      * @throws ClassNotFoundException if class is not found
216      * @throws SecurityException      if the loader cannot access the class
217      */
218     @Override
loadClass(String name, boolean resolveIt)219     public Class<?> loadClass(String name, boolean resolveIt) throws ClassNotFoundException, SecurityException {
220         Class<?> c = cache.get(name);
221 
222         if (c != null) {
223             if (resolveIt) {
224                 resolveClass(c);
225             }
226             return c;
227         }
228 
229         checkClassname(name);
230 
231         // find already loaded class
232         if ((c = findLoadedClass(name)) != null) {
233             cache.put(name, c);
234             if (resolveIt) {
235                 resolveClass(c);
236             }
237             return c;
238         }
239 
240         // try if parent classloader can load this class
241         if (this.getParent() != null) {
242             try {
243                 if ((c = this.getParent().loadClass(name)) != null) {
244                     cache.put(name, c);
245                     if (resolveIt) {
246                         resolveClass(c);
247                     }
248                     return c;
249                 }
250             } catch (ClassNotFoundException ignored) {
251             }
252         }
253 
254         try {
255             checkPackage(name);
256             // load it from file
257             if ((c = loadClassFromFile(name)) != null) {
258                 cache.put(name, c);
259                 if (resolveIt) {
260                     resolveClass(c);
261                 }
262                 return c;
263             }
264         } catch (ClassNotFoundException ignored) {
265         }
266 
267         try {
268             checkPackage(name);
269             // load it from jar
270             if ((c = loadClassFromJar(name)) != null) {
271                 cache.put(name, c);
272                 if (resolveIt) {
273                     resolveClass(c);
274                 }
275                 return c;
276             }
277         } catch (ClassNotFoundException ignored) {
278         }
279 
280         throw new ClassNotFoundException("Class \"" + name + "\" was not found");
281     }
282 }
283