package org.intellimate.izou.addon;
import org.apache.logging.log4j.Level;
import org.intellimate.izou.identification.AddOnInformationManager;
import org.intellimate.izou.main.Main;
import org.intellimate.izou.security.SecurityFunctions;
import org.intellimate.izou.security.storage.SecureStorageImpl;
import org.intellimate.izou.system.Context;
import org.intellimate.izou.system.context.ContextImplementation;
import org.intellimate.izou.util.AddonThreadPoolUser;
import org.intellimate.izou.util.IdentifiableSet;
import org.intellimate.izou.util.IzouModule;
import ro.fortsoft.pf4j.*;
import javax.crypto.SecretKey;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableEntryException;
import java.security.cert.CertificateException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
* Manages all the AddOns.
*/
//TODO isolate pf4j calls & catch errors accordingly
public class AddOnManager extends IzouModule implements AddonThreadPoolUser {
private IdentifiableSet<AddOnModel> addOns = new IdentifiableSet<>();
private HashMap<AddOnModel, PluginWrapper> pluginWrappers = new HashMap<>();
private Set<AspectOrAffected> aspectOrAffectedSet = new HashSet<>();
private List<Runnable> initializedCallback = new ArrayList<>();
private AddOnInformationManager addOnInformationManager;
/**
* Creates a new instance of the AddOnManager
*
* @param main The main instance
*/
public AddOnManager(Main main) {
super(main);
addOnInformationManager = main.getAddOnInformationManager();
}
/**
* Retrieves and registers all AddOns.
*/
public void retrieveAndRegisterAddOns() {
addOns.addAll(loadAddOns());
registerAllAddOns(addOns);
initialized();
}
/**
* Adds AddOns without registering them.
*
* @param addOns a List containing all the AddOns
*/
public void addAddOnsWithoutRegistering(List<AddOnModel> addOns) {
this.addOns.addAll(addOns);
}
/**
* Registers all AddOns.
*
* @param addOns a List containing all the AddOns
*/
public void addAndRegisterAddOns(List<AddOnModel> addOns) {
this.addOns.addAll(addOns);
registerAllAddOns(this.addOns);
initialized();
}
/**
* TODO: Missing doc
*
* @param addOns
*/
public void registerAllAddOns(IdentifiableSet<AddOnModel> addOns) {
initAddOns(addOns);
createAddOnInfos(addOns);
List<CompletableFuture<Void>> futures = addOns.stream()
.map(addOn -> submit((Runnable) addOn::register))
.collect(Collectors.toList());
try {
timeOut(futures, 30000);
} catch (InterruptedException e) {
debug("interrupted while trying to time out the addOns", e);
}
}
/**
* Checks that addOns have all required properties and creating the addOn information list if they do
*/
private void createAddOnInfos(IdentifiableSet<AddOnModel> addOns) {
addOns.stream().forEach(addOn -> addOnInformationManager.registerAddOn(addOn));
}
/**
* TODO: Missing doc
*
* @param addOns
*/
private void initAddOns(IdentifiableSet<AddOnModel> addOns) {
List<CompletableFuture<Void>> futures = addOns.stream()
.map(addOn -> {
Context context = new ContextImplementation(addOn, main, Level.DEBUG.name());
return submit(() -> addOn.initAddOn(context));
})
.collect(Collectors.toList());
try {
timeOut(futures, 30000);
} catch (InterruptedException e) {
debug("interrupted while trying to time out the addOns", e);
}
}
/**
* This method searches all the "/lib"-directory for AddOns and adds them to the addOnList
*
* @return the retrieved addOns
*/
private List<AddOnModel> loadAddOns() {
debug("searching for addons in: " + getMain().getFileSystemManager().getLibLocation());
PluginManager pluginManager = new DefaultPluginManager(getMain().getFileSystemManager().getLibLocation(),
new ArrayList<>(aspectOrAffectedSet));
// load the plugins
debug("loading plugins");
pluginManager.loadPlugins();
debug("loaded: " + pluginManager.getPlugins().toString());
// start (active/resolved) the plugins
try {
debug("starting plugins");
pluginManager.startPlugins();
} catch (Exception | NoClassDefFoundError e) {
error("Error while trying to start the PF4J-Plugins", e);
}
try {
debug("retrieving addons from the plugins");
List<AddOnModel> addOns = pluginManager.getExtensions(AddOnModel.class);
debug("retrieved: " + addOns.toString());
KeyManager keyManager = new KeyManager();
addOns.stream()
.filter(addOn -> addOn.getClass().getClassLoader() instanceof IzouPluginClassLoader)
.forEach(addOn -> {
IzouPluginClassLoader izouPluginClassLoader = (IzouPluginClassLoader) addOn.getClass()
.getClassLoader();
PluginWrapper plugin = pluginManager.getPlugin(izouPluginClassLoader.getPluginDescriptor()
.getPluginId());
keyManager.manageAddOnKey(plugin.getDescriptor());
pluginWrappers.put(addOn, plugin);
addOn.setPlugin(plugin);
});
keyManager.saveAddOnKeys();
return addOns;
} catch (Exception e) {
log.fatal("Error while trying to start the AddOns", e);
return new ArrayList<>();
}
}
/**
* Returns the (optional) PluginWrapper for the AddonModel.
* If the return is empty, it means that the AddOn was not loaded through pf4j
*
* @param addOnModel the AddOnModel
* @return the PluginWrapper if loaded through pf4j or empty if added as an argument
*/
public Optional<PluginWrapper> getPluginWrapper(AddOnModel addOnModel) {
return Optional.of(pluginWrappers.get(addOnModel));
}
/**
* Checks whether the AddOn was loaded through pf4j
*
* @param addOnModel the AddOnModel to check
* @return true if loaded, false if not
*/
public boolean loadedThroughPF4J (AddOnModel addOnModel) {
return pluginWrappers.get(addOnModel) != null;
}
/**
* Adds an aspect-class url to the list. Must be done before loading of the addons!
*
* @param aspectOrAffected the aspect or affected to add
*/
public void addAspectOrAffected(AspectOrAffected aspectOrAffected) {
if (!aspectOrAffectedSet.add(aspectOrAffected)) {
error("set is already containing an instance of " + aspectOrAffected);
}
}
/**
* adds an listener to the initialized state (all addons registered).
* @param runnable the runnable to add
*/
public void addInitializedListener(Runnable runnable) {
initializedCallback.add(runnable);
}
/**
* Called after the addons were initialized
*/
private void initialized() {
initializedCallback.forEach(this::submit);
initializedCallback = new LinkedList<>();
}
/**
* The KeyManager in the AddOnManager loads or creates a {@link SecretKey} for each AddOn at addOn load time,
* depending if one already exists. It then distributes them to each addOn being loaded, and then finaly saves them
* again.
* <p>
* This is necessary for the {@link SecureStorageImpl} in order to save
* data matching to each addOn. This secret key serves as key to the data of an addOn being saved. In other
* words, each addOn data is matched with the secret key instead of the plugin descriptor itself in order to
* avoid serialization of the addon descriptor, which would entail a huge mess. So the secret key of each addOn
* is pretty much a "signature" of each addOn, easily identifying it.
* </p>
*/
private class KeyManager {
private HashMap<String, SecretKey> addOnKeys;
boolean changed;
/**
* Creates a new KeyManager object
*/
private KeyManager() {
addOnKeys = new HashMap<>();
retrieveAddonKeys();
}
/**
* Check if a SecretKey already exists for the plugin descriptor, if not creates a new one, and then gives it to
* the plugin descriptor.
* @param descriptor The plugin descriptor to give a SecretKey
*/
private void manageAddOnKey(PluginDescriptor descriptor) {
SecretKey secretKey = addOnKeys.get(descriptor.getPluginId());
if (secretKey == null) {
SecurityFunctions module = new SecurityFunctions();
secretKey = module.generateKey();
addOnKeys.put(descriptor.getPluginId(), secretKey);
changed = true;
}
descriptor.setSecureID(secretKey);
}
/**
* Retrieves all saved addOnKeys (they cannot change, since there are dependencies on them, so they are saved
* and retrieved)
*/
private void retrieveAddonKeys() {
changed = false;
try {
final String keyStoreFile = getMain().getFileSystemManager().getSystemDataLocation() + File.separator
+ "addon_keys.keystore";
KeyStore keyStore = createKeyStore(keyStoreFile, "4b[X:+H4CS&avY<)");
KeyStore.PasswordProtection keyPassword = new KeyStore.PasswordProtection("Ev45j>eP}QTR?K9_"
.toCharArray());
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
KeyStore.Entry entry = keyStore.getEntry(alias, keyPassword);
SecretKey key = ((KeyStore.SecretKeyEntry) entry).getSecretKey();
addOnKeys.put(alias, key);
}
} catch(NullPointerException e) {
return;
} catch (UnrecoverableEntryException | NoSuchAlgorithmException | KeyStoreException e) {
error("Unable to retrieve key", e);
}
}
/**
* Save all addOnKeys in the instance variable {@code addOnKeys} in a keystore
*/
private void saveAddOnKeys() {
if (!changed) {
return;
}
final String keyStoreFile = getMain().getFileSystemManager().getSystemDataLocation()
+ File.separator + "addon_keys.keystore";
KeyStore keyStore = createKeyStore(keyStoreFile, "4b[X:+H4CS&avY<)");
for (String mapKey : addOnKeys.keySet()) {
try {
KeyStore.SecretKeyEntry keyStoreEntry = new KeyStore.SecretKeyEntry(addOnKeys.get(mapKey));
KeyStore.PasswordProtection keyPassword = new KeyStore.PasswordProtection("Ev45j>eP}QTR?K9_"
.toCharArray());
keyStore.setEntry(mapKey, keyStoreEntry, keyPassword);
} catch (KeyStoreException e) {
error("Unable to store key", e);
}
}
try {
keyStore.store(new FileOutputStream(keyStoreFile), "4b[X:+H4CS&avY<)".toCharArray());
} catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException e) {
error("Unable to store key", e);
}
}
/**
* Creates a new keystore for addOn secret keys
*
* @param fileName the path to the keystore
* @param password the password to use with the keystore
* @return the newly created keystore
*/
private KeyStore createKeyStore(String fileName, String password) {
File file = new File(fileName);
KeyStore keyStore = null;
try {
keyStore = KeyStore.getInstance("JCEKS");
if (file.exists()) {
keyStore.load(new FileInputStream(file), password.toCharArray());
} else {
keyStore.load(null, null);
keyStore.store(new FileOutputStream(fileName), password.toCharArray());
}
} catch (CertificateException | IOException | KeyStoreException | NoSuchAlgorithmException e) {
error("Unable to create key store", e);
}
return keyStore;
}
}
}