Java tutorial
/* * Copyright (c) 2009-2011, Martijn Brinkers, Djigzo. * * This file is part of Djigzo email encryption. * * Djigzo is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License * version 3, 19 November 2007 as published by the Free Software * Foundation. * * Djigzo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public * License along with Djigzo. If not, see <http://www.gnu.org/licenses/> * * Additional permission under GNU AGPL version 3 section 7 * * If you modify this Program, or any covered work, by linking or * combining it with aspectjrt.jar, aspectjweaver.jar, tyrex-1.0.3.jar, * freemarker.jar, dom4j.jar, mx4j-jmx.jar, mx4j-tools.jar, * spice-classman-1.0.jar, spice-loggerstore-0.5.jar, spice-salt-0.8.jar, * spice-xmlpolicy-1.0.jar, saaj-api-1.3.jar, saaj-impl-1.3.jar, * wsdl4j-1.6.1.jar (or modified versions of these libraries), * containing parts covered by the terms of Eclipse Public License, * tyrex license, freemarker license, dom4j license, mx4j license, * Spice Software License, Common Development and Distribution License * (CDDL), Common Public License (CPL) the licensors of this Program grant * you additional permission to convey the resulting work. */ package mitm.common.security.ca; import java.security.KeyStoreException; import java.security.cert.CertStoreException; import java.util.Date; import java.util.concurrent.atomic.AtomicBoolean; import mitm.common.hibernate.DatabaseAction; import mitm.common.hibernate.DatabaseActionExecutor; import mitm.common.hibernate.DatabaseActionExecutorBuilder; import mitm.common.hibernate.DatabaseException; import mitm.common.hibernate.SessionManager; import mitm.common.mail.EmailAddressUtils; import mitm.common.security.KeyAndCertStore; import mitm.common.security.KeyAndCertificate; import mitm.common.security.ca.handlers.BuiltInCertificateRequestHandler; import mitm.common.security.ca.hibernate.CertificateRequestEntity; import mitm.common.util.Check; import mitm.common.util.DateTimeUtils; import mitm.common.util.ThreadUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.commons.lang.time.DateUtils; import org.hibernate.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CAImpl implements CA { private final static Logger logger = LoggerFactory.getLogger(CAImpl.class); /* * Time in milliseconds to sleep when an exception occurs in the thread */ private final int EXCEPTION_SLEEP_TIME = 10000; /* * If a certificate handler does not immediately issue a certificate, the request will be stored in the * CertificateRequestStore. */ private final CertificateRequestStore certificateRequestStore; /* * The registry with all available CertificateRequestHandler's */ private final CertificateRequestHandlerRegistry handlerRegistry; /* * Additional (can be null) resolver that can resolve certain parameters (for example the common name) * of a certificate request */ private CertificateRequestResolver certificateRequestResolver; /* * Used for getting the signing certificate and to store the issued certificate */ private final KeyAndCertStore keyAndCertStore; /* * manages database sessions */ private final SessionManager sessionManager; /* * Sleep time (in seconds) when there is nothing to do. Can be pretty long (but you are advised to make it * shorter than expirationTime) because the thread will be 'kicked' when a new entry will be added. */ private int threadSleepTime; /* * The maximum time in seconds a request will be kept */ private int expirationTime; /* * The array with delay times used for updating nextUpdate. */ private int[] delayTimes; /* * The thread that periodically checks the certificateRequestStore */ private final RequestHandlerThread thread; /* * Executes database actions within a transaction */ private final DatabaseActionExecutor databaseActionExecutor; public CAImpl(CertificateRequestStore certificateRequestStore, CertificateRequestHandlerRegistry handlerRegistry, KeyAndCertStore keyAndCertStore, SessionManager sessionManager, int threadSleepTime, int expirationTime, int[] delayTimes) { Check.notNull(certificateRequestStore, "certificateRequestStore"); Check.notNull(handlerRegistry, "handlerRegistry"); Check.notNull(keyAndCertStore, "keyAndCertStore"); Check.notNull(delayTimes, "delayTimes"); this.certificateRequestStore = certificateRequestStore; this.handlerRegistry = handlerRegistry; this.keyAndCertStore = keyAndCertStore; this.sessionManager = sessionManager; this.threadSleepTime = threadSleepTime; this.expirationTime = expirationTime; this.delayTimes = delayTimes; validateDelayTimes(); databaseActionExecutor = DatabaseActionExecutorBuilder.createDatabaseActionExecutor(sessionManager); thread = new RequestHandlerThread(); } private void validateDelayTimes() { if (delayTimes.length == 0) { throw new IllegalArgumentException("Delay times must be specified."); } int prev = 0; for (int time : delayTimes) { if (time < prev) { throw new IllegalArgumentException("Delay times must be in increasing order."); } prev = time; } } public void startThread() { thread.setDaemon(true); thread.start(); } private CertificateRequest toRequest(RequestParameters parameters, String handlerName) throws CAException { CertificateRequestEntity request = new CertificateRequestEntity(handlerName); String email = EmailAddressUtils.canonicalizeAndValidate(parameters.getEmail(), true); if (email == null) { throw new CAException( StringUtils.defaultString(parameters.getEmail()) + " is not a valid email address."); } request.setSubject(parameters.getSubject()); request.setEmail(email); request.setValidity(parameters.getValidity()); request.setKeyLength(parameters.getKeyLength()); request.setSignatureAlgorithm(parameters.getSignatureAlgorithm()); request.setCRLDistributionPoint(parameters.getCRLDistributionPoint()); return request; } private void addKeyAndCertificate(KeyAndCertificate keyAndCertificate) throws CAException { try { keyAndCertStore.addKeyAndCertificate(keyAndCertificate); } catch (CertStoreException e) { throw new CAException(e); } catch (KeyStoreException e) { throw new CAException(e); } } private void setNextUpdate(CertificateRequest certificateRequest) { int index = certificateRequest.getIteration(); /* * Use last delay if we have no more delay's left */ if (index > delayTimes.length - 1) { index = delayTimes.length - 1; } Date nextUpdate = DateUtils.addSeconds(new Date(), delayTimes[index]); logger.debug("Next update for " + StringUtils.defaultString(certificateRequest.getEmail()) + " set to " + nextUpdate); certificateRequest.setNextUpdate(nextUpdate); } @Override public KeyAndCertificate requestCertificate(RequestParameters parameters) throws CAException { Check.notNull(parameters, "parameters"); String handlerName = parameters.getCertificateRequestHandler(); if (handlerName == null) { /* * Use the built-in Djigzo certificate handler if no handler name was specified. */ handlerName = BuiltInCertificateRequestHandler.NAME; } logger.info("Requesting a certificate for user " + parameters.getEmail() + " using handler " + handlerName); CertificateRequestHandler handler = handlerRegistry.getHandler(handlerName); if (handler == null) { throw new CAException("CertificateRequestHandler with name " + handlerName + " not available."); } CertificateRequest request = toRequest(parameters, handlerName); /* * If the certificateRequestResolver is set, resolve request parameters. For example, using an * LDAP certificateRequestResolver, the common name for the request can be resolved */ if (certificateRequestResolver != null) { try { certificateRequestResolver.resolve(parameters.getEmail(), request); } catch (CertificateRequestResolverException e) { throw new CAException(e); } } KeyAndCertificate keyAndCertificate = handler.handleRequest(request); if (keyAndCertificate != null) { /* * The certificate was issued immediately so handle it directly. */ addKeyAndCertificate(keyAndCertificate); } else { request.setLastUpdated(new Date()); setNextUpdate(request); /* * The certificate was not issued immediately so the request will be scheduled. */ certificateRequestStore.addRequest(request); /* * Wakeup the thread */ thread.kick(); } return keyAndCertificate; } private void handlePendingRequest(CertificateRequest request) throws CAException { String handlerName = request.getCertificateHandlerName(); CertificateRequestHandler handler = handlerRegistry.getHandler(handlerName); if (handler == null) { /* * A handler was not found. We won't throw an exception because otherwise it will keep on throwing * exception is great succession. We will set the last message of the request. */ String message = "A Certificate Request Handler with name " + handlerName + " is not available."; logger.warn(message); request.setLastMessage(message); return; } KeyAndCertificate keyAndCertificate = handler.handleRequest(request); if (keyAndCertificate != null) { logger.info( "A certificate for email " + StringUtils.defaultString(request.getEmail()) + " was issued."); /* * The certificate was issued. We can add it and remove it from the certificateRequestStore */ addKeyAndCertificate(keyAndCertificate); deleteRequest(request); } else { logger.debug("A certificate for email " + StringUtils.defaultString(request.getEmail()) + " was not yet ready."); } } private boolean isExpired(CertificateRequest request) { if (request.getCreated() == null) { /* * Should not happen. */ logger.warn("Created date is not set. The request will be expired."); return true; } long diff = DateTimeUtils.diffMilliseconds(new Date(), request.getCreated()); return diff > (expirationTime * DateUtils.MILLIS_PER_SECOND); } private void deleteRequest(CertificateRequest request) { certificateRequestStore.deleteRequest(request.getID()); } private Boolean handleNext() throws DatabaseException { return databaseActionExecutor.executeTransaction(new DatabaseAction<Boolean>() { @Override public Boolean doAction(Session session) throws DatabaseException { Session previousSession = sessionManager.getSession(); sessionManager.setSession(session); try { return handleNextAction(); } finally { sessionManager.setSession(previousSession); } } }); } private boolean handleNextAction() { logger.debug("Check for next request."); CertificateRequest request = certificateRequestStore.getNextRequest(); if (request != null) { if (!isExpired(request)) { try { handlePendingRequest(request); } catch (Throwable t) { /* * We won't throw an exception because otherwise it will keep on throwing exception in * great succession. We will set the last message of the request. */ request.setLastMessage("An error occured handling the request. Message: " + ExceptionUtils.getRootCauseMessage(t)); logger.error("An error occured handling the request", t); } finally { request.setLastUpdated(new Date()); request.setIteration(request.getIteration() + 1); setNextUpdate(request); } } else { logger.warn("Certificate request for email " + StringUtils.defaultString(request.getEmail()) + " is expired. Request will be removed."); deleteRequest(request); } } return request != null; } public CertificateRequestResolver getCertificateRequestResolver() { return certificateRequestResolver; } public void setCertificateRequestResolver(CertificateRequestResolver certificateRequestResolver) { this.certificateRequestResolver = certificateRequestResolver; } public int getThreadSleepTime() { return threadSleepTime; } public void setThreadSleepTime(int threadSleepTime) { this.threadSleepTime = threadSleepTime; } public int getExpirationTime() { return expirationTime; } public void setExpirationTime(int expirationTime) { this.expirationTime = expirationTime; } public int[] getDelayTimes() { return delayTimes; } public void setDelayTimes(int[] delayTimes) { this.delayTimes = delayTimes; } public void kick() { thread.kick(); } public void requestStop() { thread.requestStop(); } /* * Background thread that will handle pending certificate requests. */ private class RequestHandlerThread extends Thread { private final AtomicBoolean stop = new AtomicBoolean(); /* * Object used to notify the thread that it should wakeup. */ private final Object wakeup = new Object(); public RequestHandlerThread() { super("RequestHandlerThread Thread"); } @Override public void run() { logger.info("Starting RequestHandler thread."); do { try { synchronized (wakeup) { boolean nextHandled = handleNext(); if (!nextHandled) { /* * Sleep for some time if there is nothing to do. */ try { wakeup.wait(threadSleepTime * DateUtils.MILLIS_PER_SECOND); } catch (InterruptedException e) { // ignore } } } } catch (Throwable t) { logger.error("Error in RequestHandler thread.", t); /* * Sleep some time to make sure the thread doesn't consume 100% CPU when some kind of unhandled * exception occurs over and over. */ ThreadUtils.sleepQuietly(EXCEPTION_SLEEP_TIME); } } while (!stop.get()); logger.info("RequestHandlerThread stopped."); } public void kick() { synchronized (wakeup) { wakeup.notify(); } } public void requestStop() { stop.set(true); kick(); } } }