/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* See LICENSE.txt included in this distribution for the specific
* language governing permissions and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at LICENSE.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
*/
package org.opensolaris.opengrok.authorization;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.opensolaris.opengrok.logger.LoggerFactory;
/**
* Class loader for authorization plugins.
*
* @author Krystof Tulinger
*/
public class AuthorizationPluginClassLoader extends ClassLoader {
private final Map<String, Class> cache = new HashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizationPluginClassLoader.class);
private final static String[] CLASS_WHITELIST = new String[]{
"org.opensolaris.opengrok.configuration.Group",
"org.opensolaris.opengrok.configuration.Project",
"org.opensolaris.opengrok.configuration.RuntimeEnvironment",
"org.opensolaris.opengrok.authorization.IAuthorizationPlugin",
"org.opensolaris.opengrok.authorization.plugins.*",
"org.opensolaris.opengrok.util.*",
"org.opensolaris.opengrok.logger.*"
};
private final static String[] PACKAGE_BLACKLIST = new String[]{
"java",
"javax",
"org.w3c",
"org.xml",
"org.omg",
"sun"
};
private final File directory;
public AuthorizationPluginClassLoader(File directory) {
super(AuthorizationPluginClassLoader.class.getClassLoader());
this.directory = directory;
}
private Class loadClassFromJar(String classname) throws ClassNotFoundException {
File[] jars = directory.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jar");
}
});
if (jars == null) {
throw new ClassNotFoundException(
"Cannot load class " + classname,
new IOException("Directory " + directory + " is not accessible"));
}
for (File f : jars) {
try (JarFile jar = new JarFile(f)) {
String filename = classname.replace('.', File.separatorChar) + ".class";
JarEntry entry = (JarEntry) jar.getEntry(filename);
if (entry != null && entry.getName().endsWith(".class")) {
try (InputStream is = jar.getInputStream(entry)) {
byte[] bytes = loadBytes(is);
Class c = defineClass(classname, bytes, 0, bytes.length);
LOGGER.log(Level.FINE, "Class \"{0}\" found in file \"{1}\"",
new Object[]{
classname,
f.getAbsolutePath()
});
return c;
}
}
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, "Loading class threw an exception:", ex);
} catch (Throwable ex) {
LOGGER.log(Level.SEVERE, "Loading class threw an uknown exception:", ex);
}
}
throw new ClassNotFoundException("Class \"" + classname + "\" could not be found");
}
private Class loadClassFromFile(String classname) throws ClassNotFoundException {
try {
String filename = classname.replace('.', File.separatorChar) + ".class";
File f = new File(directory, filename);
try (FileInputStream in = new FileInputStream(f)) {
byte[] bytes = loadBytes(in);
Class c = defineClass(classname, bytes, 0, bytes.length);
LOGGER.log(Level.FINEST, "Class \"{0}\" found in file \"{1}\"",
new Object[]{
classname,
f.getAbsolutePath()
});
return c;
}
} catch (IOException e) {
throw new ClassNotFoundException(e.toString(), e);
} catch (Throwable e) {
throw new ClassNotFoundException(e.toString(), e);
}
}
private byte[] loadBytes(InputStream in) throws IOException {
byte[] bytes = new byte[in.available()];
in.read(bytes);
return bytes;
}
private boolean checkWhiteList(String name) {
for (int i = 0; i < CLASS_WHITELIST.length; i++) {
String pattern = CLASS_WHITELIST[i];
pattern = pattern.replaceAll("\\.", "\\\\.");
pattern = pattern.replaceAll("\\*", ".*");
if (name.matches(pattern)) {
return true;
}
}
return false;
}
private void checkClassname(String name) throws SecurityException {
if (name.startsWith("org.opensolaris.opengrok.")
&& !checkWhiteList(name)) {
throw new SecurityException("Tried to load a blacklisted class \"" + name + "\"\n"
+ "Allowed classes from opengrok package are only: "
+ Arrays.toString(CLASS_WHITELIST));
}
}
private void checkPackage(String name) throws SecurityException {
for (int i = 0; i < PACKAGE_BLACKLIST.length; i++) {
if (name.startsWith(PACKAGE_BLACKLIST[i] + ".")) {
throw new SecurityException("Tried to load a class \"" + name
+ "\" to a blacklisted package "
+ "\"" + PACKAGE_BLACKLIST[i] + "\"\n"
+ "Disabled packages are: "
+ Arrays.toString(PACKAGE_BLACKLIST));
}
}
}
/**
* Loads the class with given name.
*
* Order of lookup:
* <ol>
* <li>already loaded classes </li>
* <li>parent class loader</li>
* <li>loading from .class files</li>
* <li>loading from .jar files</li>
* </ol>
*
* Package blacklist: {@link #PACKAGE_BLACKLIST}.<br>
* Classes whitelist: {@link #CLASS_WHITELIST}.
*
* @param name class name
* @return loaded class or null
* @throws ClassNotFoundException if class is not found
* @throws SecurityException if the loader cannot access the class
*/
@Override
public Class loadClass(String name) throws ClassNotFoundException, SecurityException {
return loadClass(name, true);
}
/**
* Loads the class with given name.
*
* Order of lookup:
* <ol>
* <li>already loaded classes </li>
* <li>parent class loader</li>
* <li>loading from .class files</li>
* <li>loading from .jar files</li>
* </ol>
*
* Package blacklist: {@link #PACKAGE_BLACKLIST}.<br>
* Classes whitelist: {@link #CLASS_WHITELIST}.
*
* @param name class name
* @param resolveIt if the class should be resolved
* @return loaded class or null
* @throws ClassNotFoundException if class is not found
* @throws SecurityException if the loader cannot access the class
*/
@Override
public Class loadClass(String name, boolean resolveIt) throws ClassNotFoundException, SecurityException {
Class c;
if ((c = cache.get(name)) != null) {
if (resolveIt) {
resolveClass(c);
}
return c;
}
checkClassname(name);
// find already loaded class
if ((c = findLoadedClass(name)) != null) {
cache.put(name, c);
if (resolveIt) {
resolveClass(c);
}
return c;
}
// try if parent classloader can load this class
if (this.getParent() != null) {
try {
if ((c = this.getParent().loadClass(name)) != null) {
cache.put(name, c);
if (resolveIt) {
resolveClass(c);
}
return c;
}
} catch (ClassNotFoundException ex) {
}
}
try {
checkPackage(name);
// load it from file
if ((c = loadClassFromFile(name)) != null) {
cache.put(name, c);
if (resolveIt) {
resolveClass(c);
}
return c;
}
} catch (ClassNotFoundException ex) {
}
try {
checkPackage(name);
// load it from jar
if ((c = loadClassFromJar(name)) != null) {
cache.put(name, c);
if (resolveIt) {
resolveClass(c);
}
return c;
}
} catch (ClassNotFoundException ex) {
}
throw new ClassNotFoundException("Class \"" + name + "\" was not found");
}
}