001package org.intellimate.izou.addon; 002 003import org.apache.logging.log4j.Level; 004import org.intellimate.izou.main.Main; 005import org.intellimate.izou.security.SecurityFunctions; 006import org.intellimate.izou.system.Context; 007import org.intellimate.izou.system.context.ContextImplementation; 008import org.intellimate.izou.util.AddonThreadPoolUser; 009import org.intellimate.izou.util.IdentifiableSet; 010import org.intellimate.izou.util.IzouModule; 011import ro.fortsoft.pf4j.*; 012 013import javax.crypto.SecretKey; 014import java.io.File; 015import java.io.FileInputStream; 016import java.io.FileOutputStream; 017import java.io.IOException; 018import java.security.KeyStore; 019import java.security.KeyStoreException; 020import java.security.NoSuchAlgorithmException; 021import java.security.UnrecoverableEntryException; 022import java.security.cert.CertificateException; 023import java.util.*; 024import java.util.concurrent.CompletableFuture; 025import java.util.stream.Collectors; 026 027/** 028 * Manages all the AddOns. 029 */ 030public class AddOnManager extends IzouModule implements AddonThreadPoolUser { 031 private IdentifiableSet<AddOnModel> addOns = new IdentifiableSet<>(); 032 private HashMap<AddOnModel, PluginWrapper> pluginWrappers = new HashMap<>(); 033 private Set<AspectOrAffected> aspectOrAffectedSet = new HashSet<>(); 034 private List<Runnable> initializedCallback = new ArrayList<>(); 035 036 public AddOnManager(Main main) { 037 super(main); 038 } 039 040 /** 041 * retrieves and registers all AddOns. 042 */ 043 public void retrieveAndRegisterAddOns() { 044 addOns.addAll(loadAddOns()); 045 registerAllAddOns(addOns); 046 initialized(); 047 } 048 049 /** 050 * Adds AddOns without registering them. 051 * @param addOns a List containing all the AddOns 052 */ 053 public void addAddOnsWithoutRegistering(List<AddOnModel> addOns) { 054 this.addOns.addAll(addOns); 055 } 056 057 /** 058 * registers all AddOns. 059 * 060 * @param addOns a List containing all the AddOns 061 */ 062 public void addAndRegisterAddOns(List<AddOnModel> addOns) { 063 this.addOns.addAll(addOns); 064 registerAllAddOns(this.addOns); 065 initialized(); 066 } 067 068 public void registerAllAddOns(IdentifiableSet<AddOnModel> addOns) { 069 initAddOns(addOns); 070 List<CompletableFuture<Void>> futures = addOns.stream() 071 .map(addOn -> submit((Runnable) addOn::register)) 072 .collect(Collectors.toList()); 073 try { 074 timeOut(futures, 30000); 075 } catch (InterruptedException e) { 076 debug("interrupted while trying to time out the addOns", e); 077 } 078 } 079 080 private void initAddOns(IdentifiableSet<AddOnModel> addOns) { 081 List<CompletableFuture<Void>> futures = addOns.stream() 082 .map(addOn -> { 083 Context context = new ContextImplementation(addOn, main, Level.DEBUG.name()); 084 return submit(() -> addOn.initAddOn(context)); 085 }) 086 .collect(Collectors.toList()); 087 try { 088 timeOut(futures, 30000); 089 } catch (InterruptedException e) { 090 debug("interrupted while trying to time out the addOns", e); 091 } 092 } 093 094 /** 095 * This method searches all the "/lib"-directory for AddOns and adds them to the addOnList 096 * @return the retrieved addOns 097 */ 098 private List<AddOnModel> loadAddOns() { 099 debug("searching for addons in: " + getMain().getFileSystemManager().getLibLocation()); 100 PluginManager pluginManager = new DefaultPluginManager(getMain().getFileSystemManager().getLibLocation(), 101 new ArrayList<>(aspectOrAffectedSet)); 102 // load the plugins 103 debug("loading plugins"); 104 pluginManager.loadPlugins(); 105 debug("loaded: " + pluginManager.getPlugins().toString()); 106 107 // start (active/resolved) the plugins 108 try { 109 debug("starting plugins"); 110 pluginManager.startPlugins(); 111 } catch (Exception | NoClassDefFoundError e) { 112 error("Error while trying to start the PF4J-Plugins", e); 113 } 114 try { 115 debug("retrieving addons from the plugins"); 116 List<AddOnModel> addOns = pluginManager.getExtensions(AddOnModel.class); 117 debug("retrieved: " + addOns.toString()); 118 KeyManager keyManager = new KeyManager(); 119 addOns.stream() 120 .filter(addOn -> addOn.getClass().getClassLoader() instanceof IzouPluginClassLoader) 121 .forEach(addOn -> { 122 IzouPluginClassLoader izouPluginClassLoader = (IzouPluginClassLoader) addOn.getClass() 123 .getClassLoader(); 124 PluginWrapper plugin = pluginManager.getPlugin(izouPluginClassLoader.getPluginDescriptor() 125 .getPluginId()); 126 keyManager.manageAddOnKey(plugin.getDescriptor()); 127 pluginWrappers.put(addOn, plugin); 128 addOn.setPlugin(plugin); 129 }); 130 keyManager.saveAddOnKeys(); 131 return addOns; 132 } catch (Exception e) { 133 log.fatal("Error while trying to start the AddOns", e); 134 return new ArrayList<>(); 135 } 136 } 137 138 /** 139 * returns the addOn loaded from the ClassLoader 140 * @param classLoader the classLoader 141 * @return the (optional) AddOnModel 142 */ 143 public Optional<AddOnModel> getAddOnForClassLoader(ClassLoader classLoader) { 144 return addOns.stream() 145 .filter(addOnModel -> addOnModel.getClass().getClassLoader().equals(classLoader)) 146 .findFirst(); 147 } 148 149 /** 150 * returns the (optional) PluginWrapper for the AddonModel. 151 * If the return is empty, it means that the AddOn was not loaded through pf4j 152 * @param addOnModel the AddOnModel 153 * @return the PluginWrapper if loaded through pf4j or empty if added as an argument 154 */ 155 public Optional<PluginWrapper> getPluginWrapper(AddOnModel addOnModel) { 156 return Optional.of(pluginWrappers.get(addOnModel)); 157 } 158 159 /** 160 * checks whether the AddOn was loaded through pf4j 161 * @param addOnModel the AddOnModel to check 162 * @return true if loaded, false if not 163 */ 164 public boolean loadedThroughPF4J (AddOnModel addOnModel) { 165 return pluginWrappers.get(addOnModel) != null; 166 } 167 168 /** 169 * adds an aspect-class url to the list. Must be done before loading of the addons! 170 * @param aspectOrAffected the aspect or affected to add 171 */ 172 public void addAspectOrAffected(AspectOrAffected aspectOrAffected) { 173 if (!aspectOrAffectedSet.add(aspectOrAffected)) { 174 error("set is already containing an instance of " + aspectOrAffected); 175 } 176 } 177 178 /** 179 * adds an listener to the initialized state (all addons registered). 180 * @param runnable the runnable to add 181 */ 182 public void addInitializedListener(Runnable runnable) { 183 initializedCallback.add(runnable); 184 } 185 186 /** 187 * called after the addons were initialized 188 */ 189 private void initialized() { 190 initializedCallback.forEach(this::submit); 191 initializedCallback = new LinkedList<>(); 192 } 193 194 /** 195 * The KeyManager in the AddOnManager loads or creates a {@link SecretKey} for each AddOn at addOn load time, 196 * depending if one already exists. It then distributes them to each addOn being loaded, and then finaly saves them 197 * again. 198 * <p> 199 * This is necessary for the {@link org.intellimate.izou.security.storage.SecureStorage} in order to save 200 * data matching to each addOn. This secret key serves as key to the data of an addOn being saved. In other 201 * words, each addOn data is matched with the secret key instead of the plugin descriptor itself in order to 202 * avoid serialization of the addon descriptor, which would entail a huge mess. So the secret key of each addOn 203 * is pretty much a "signature" of each addOn, easily identifying it. 204 * </p> 205 */ 206 private class KeyManager { 207 private HashMap<String, SecretKey> addOnKeys; 208 boolean changed; 209 210 /** 211 * Creates a new KeyManager object 212 */ 213 private KeyManager() { 214 addOnKeys = new HashMap<>(); 215 retrieveAddonKeys(); 216 } 217 218 /** 219 * Check if a SecretKey already exists for the plugin descriptor, if not creates a new one, and then gives it to 220 * the plugin descriptor. 221 * @param descriptor The plugin descriptor to give a SecretKey 222 */ 223 private void manageAddOnKey(PluginDescriptor descriptor) { 224 SecretKey secretKey = addOnKeys.get(descriptor.getPluginId()); 225 226 if (secretKey == null) { 227 SecurityFunctions module = new SecurityFunctions(); 228 secretKey = module.generateKey(); 229 addOnKeys.put(descriptor.getPluginId(), secretKey); 230 changed = true; 231 } 232 233 descriptor.setSecureID(secretKey); 234 } 235 236 /** 237 * Retrieves all saved addOnKeys (they cannot change, since there are dependencies on them, so they are saved 238 * and retrieved) 239 */ 240 private void retrieveAddonKeys() { 241 changed = false; 242 243 try { 244 final String keyStoreFile = getMain().getFileSystemManager().getSystemDataLocation() + File.separator 245 + "addon_keys.keystore"; 246 KeyStore keyStore = createKeyStore(keyStoreFile, "4b[X:+H4CS&avY<)"); 247 248 KeyStore.PasswordProtection keyPassword = new KeyStore.PasswordProtection("Ev45j>eP}QTR?K9_" 249 .toCharArray()); 250 Enumeration<String> aliases = keyStore.aliases(); 251 while (aliases.hasMoreElements()) { 252 String alias = aliases.nextElement(); 253 KeyStore.Entry entry = keyStore.getEntry(alias, keyPassword); 254 SecretKey key = ((KeyStore.SecretKeyEntry) entry).getSecretKey(); 255 addOnKeys.put(alias, key); 256 } 257 } catch(NullPointerException e) { 258 return; 259 } catch (UnrecoverableEntryException | NoSuchAlgorithmException | KeyStoreException e) { 260 error("Unable to retrieve key", e); 261 } 262 } 263 264 /** 265 * Save all addOnKeys in the instance variable {@code addOnKeys} in a keystore 266 */ 267 private void saveAddOnKeys() { 268 if (!changed) { 269 return; 270 } 271 final String keyStoreFile = getMain().getFileSystemManager().getSystemDataLocation() 272 + File.separator + "addon_keys.keystore"; 273 KeyStore keyStore = createKeyStore(keyStoreFile, "4b[X:+H4CS&avY<)"); 274 275 for (String mapKey : addOnKeys.keySet()) { 276 try { 277 KeyStore.SecretKeyEntry keyStoreEntry = new KeyStore.SecretKeyEntry(addOnKeys.get(mapKey)); 278 KeyStore.PasswordProtection keyPassword = new KeyStore.PasswordProtection("Ev45j>eP}QTR?K9_" 279 .toCharArray()); 280 keyStore.setEntry(mapKey, keyStoreEntry, keyPassword); 281 } catch (KeyStoreException e) { 282 error("Unable to store key", e); 283 } 284 } 285 286 try { 287 keyStore.store(new FileOutputStream(keyStoreFile), "4b[X:+H4CS&avY<)".toCharArray()); 288 } catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException e) { 289 error("Unable to store key", e); 290 } 291 } 292 293 /** 294 * Creates a new keystore for addOn secret keys 295 * 296 * @param fileName the path to the keystore 297 * @param password the password to use with the keystore 298 * @return the newly created keystore 299 */ 300 private KeyStore createKeyStore(String fileName, String password) { 301 File file = new File(fileName); 302 KeyStore keyStore = null; 303 try { 304 keyStore = KeyStore.getInstance("JCEKS"); 305 if (file.exists()) { 306 keyStore.load(new FileInputStream(file), password.toCharArray()); 307 } else { 308 keyStore.load(null, null); 309 keyStore.store(new FileOutputStream(fileName), password.toCharArray()); 310 } 311 } catch (CertificateException | IOException | KeyStoreException | NoSuchAlgorithmException e) { 312 error("Unable to create key store", e); 313 } 314 315 return keyStore; 316 } 317 } 318}