/* * Copyright (c) 2011-2012 ICM Uniwersytet Warszawski All rights reserved. * See LICENCE.txt file for licensing information. */ package eu.emi.security.authn.x509.helpers.crl; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReference; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.security.InvalidAlgorithmParameterException; import java.security.cert.CRLException; import java.security.cert.CRLSelector; import java.security.cert.X509CRL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Timer; import javax.security.auth.x500.X500Principal; import eu.emi.security.authn.x509.StoreUpdateListener.Severity; import eu.emi.security.authn.x509.helpers.ObserversHandler; import eu.emi.security.authn.x509.helpers.WeakTimerTask; import eu.emi.security.authn.x509.helpers.pkipath.PlainStoreUtils; import eu.emi.security.authn.x509.impl.CRLParameters; /** * Handles an in-memory CRL store. * <p> * CRLs may be provided as URLs or local files. If the CRL is provided as a local file * (i.e. is not an absolute URL) then it can contain wildcard characters ('*', '?'). * In case of wildcard locations, the actual file list is regenerated on each update. * <p> * All CRLs are loaded and parsed to establish CA->CRL mapping. This mapping is updated * after the updateInterval time is passed. * <p> * Faulty CRL locations together with the respective errors can be obtained * by using a listener. * <p> * It is possible to pass more then one location of CRLs of the same CA. * <p> * The class is implemented in an asynchronous mode: CRLs are resolved on regular intervals * (or only once on startup). The CRL searching is independent of the updates. It can block to * download, read and subsequently parse a CRL if it is not present in the in-memory cache. * <p> * CRLs downloaded from a remote URL (http or ftp) can be cached on a local disk. If the update * task can not download the CRL which was previously cached on disk, * then the version from disk is returned. * <p> * This class is thread safe. * </p> * * @author K. Benedyczak */ public class PlainCRLStoreSpi extends AbstractCRLStoreSPI { //constant state private final PlainStoreUtils utils; private Timer timer; //variable state private Object intervalLock = new Object(); private Map<X500Principal, Set<URL>> ca2location; private Map<URL, SoftReference<X509CRL>> loadedCRLs; /** * Creates a new CRL store. The store will be empty until the {@link #start()} method is called. * @param params CRL parameters * @param t timer * @param observers observers handler * @throws InvalidAlgorithmParameterException invalid algorithm parameter exception */ public PlainCRLStoreSpi(CRLParameters params, Timer t, ObserversHandler observers) throws InvalidAlgorithmParameterException { super(params, observers); loadedCRLs = new HashMap<URL, SoftReference<X509CRL>>(); ca2location = new HashMap<X500Principal, Set<URL>>(); utils = new PlainStoreUtils(this.params.getDiskCachePath(), "-crl", this.params.getCrls()); timer = t; } /** * Initiates the store operation (the initial update and subsequent refreshes) */ public void start() { update(); scheduleUpdate(); } protected X509CRL loadCRL(URL url) throws IOException, CRLException, URISyntaxException { String protocol = url.getProtocol(); boolean local = false; if (protocol.equalsIgnoreCase("file")) local = true; X509CRL ret; try { URLConnection conn = url.openConnection(); if (!local) { conn.setConnectTimeout(params.getRemoteConnectionTimeout()); conn.setReadTimeout(params.getRemoteConnectionTimeout()); } InputStream is = new BufferedInputStream(conn.getInputStream()); ret = loadCrlWrapper(is); } catch (IOException e) { if (!local && params.getDiskCachePath() != null) { File input = utils.getCacheFile(url); if (input.exists()) { InputStream is = new BufferedInputStream( new FileInputStream(input)); ret = loadCrlWrapper(is); notifyObservers(url.toExternalForm(), Severity.WARNING, new IOException("Warning: CRL was not loaded from its URL, " + "but its previously cached copy was loaded from disk file " + input.getPath(), e)); return ret; } else throw e; } throw e; } if (!local) utils.saveCacheFile(ret.getEncoded(), url); return ret; } /** * Wrapper as BC provider in some cases returns null instead of exception when there are problems. * @param is input stream * @return generated CRL * @throws IOException IO exception * @throws CRLException CRL exception */ private X509CRL loadCrlWrapper(InputStream is) throws IOException, CRLException { X509CRL ret = (X509CRL)factory.generateCRL(is); if (ret == null) throw new CRLException("Unknown problem when parsing/loading the CRL"); is.close(); return ret; } public List<String> getLocations() { return utils.getLocations(); } @Override public void setUpdateInterval(long newInterval) { synchronized (intervalLock) { long old = updateInterval; this.updateInterval = newInterval; if (old <= 0) scheduleUpdate(); } } public long getUpdateInterval() { long ret; synchronized (intervalLock) { ret = updateInterval; } return ret; } /** * Removes those mappings which are for the not known locations. * Happens when a file was removed from a wildcard listing. */ private synchronized void removeStaleIssuerMapping() { Iterator<Entry<X500Principal, Set<URL>>> itMain = ca2location.entrySet().iterator(); while (itMain.hasNext()) { Entry<X500Principal, Set<URL>> entry = itMain.next(); Iterator<URL> it = entry.getValue().iterator(); while (it.hasNext()) { URL u = it.next(); if (!utils.isPresent(u)) { it.remove(); loadedCRLs.remove(u); } } } } /** * For all URLs tries to load a CRL */ private void reloadCRLs(Collection<URL> locations) { for (URL location: locations) { reloadCRL(location); } } protected X509CRL reloadCRL(URL location) { X509CRL crl; try { crl = loadCRL(location); notifyObservers(location.toExternalForm(), Severity.NOTIFICATION, null); } catch (Exception e) { notifyObservers(location.toExternalForm(), Severity.ERROR, e); return null; } addCRL(crl, location); return crl; } protected synchronized void addCRL(X509CRL crl, URL location) { Set<URL> set = ca2location.get(crl.getIssuerX500Principal()); if (set == null) { set = new HashSet<URL>(); ca2location.put(crl.getIssuerX500Principal(), set); } set.add(location); loadedCRLs.put(location, new SoftReference<X509CRL>(crl)); } /** * 1. work only if updateNeeded() * 2. for all wildcards refresh file lists * 3. remove the locations not valid anymore * 4. for all location URLs try to get the CRL * 5. update timestamp * 6. schedule the next update if enabled */ private void update() { utils.establishWildcardsLocations(); removeStaleIssuerMapping(); reloadCRLs(utils.getURLLocations()); reloadCRLs(utils.getResolvedWildcards()); } private void scheduleUpdate() { long updateInterval = getUpdateInterval(); if (updateInterval > 0) timer.schedule(new CRLAsyncUpdateTask(this), updateInterval); } private X509CRL getOrLoadCRL(URL location) { X509CRL ret = loadedCRLs.get(location).get(); if (ret != null) return ret; return reloadCRL(location); } protected synchronized Collection<X509CRL> getCRLForIssuer(X500Principal issuer) { Set<URL> locations = ca2location.get(issuer); if (locations == null) return Collections.emptyList(); List<X509CRL> ret = new ArrayList<X509CRL>(locations.size()); for (URL location: locations) ret.add(getOrLoadCRL(location)); return ret; } @Override protected Collection<X509CRL> getCRLWithMatcher(CRLSelector selectorRaw) { List<X509CRL> ret = new ArrayList<X509CRL>(); for (Set<URL> caLocations: ca2location.values()) { for (URL location: caLocations) { X509CRL crl = getOrLoadCRL(location); if (selectorRaw.match(crl)) ret.add(crl); } } return ret; } /** * After calling this method no notification will be produced and subsequent * updates won't be scheduled. However one next update may be run. */ @Override public void dispose() { setUpdateInterval(-1); } /** * This class follows a quite advanced but important pattern: * - it is static so there is no hidden reference from it to the wrapping class * - instead it has a weak reference to the wrapping object * - when the weak reference is nullified, it means that the wrapping object was discarded * by the GC and is no more usable: in this case the update task is automatically stopped. * <p> * This mechanism guarantees that even in case that the validator is not disposed manually * the memory is freed as needed. * * @author K. Benedyczak */ private static class CRLAsyncUpdateTask extends WeakTimerTask<PlainCRLStoreSpi> { public CRLAsyncUpdateTask(PlainCRLStoreSpi partner) { super(partner); } public void run() { PlainCRLStoreSpi partner = partnerRef.get(); if (partner == null) return; //the work is over, no more reschedules try { if (partner.getUpdateInterval() > 0) partner.update(); partner.scheduleUpdate(); } catch (RuntimeException e) { //here we are really screwed up - there is a bug and no way to report it e.printStackTrace(); } } } }