/*
* 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();
}
}
}
}