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}