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