mitm.common.security.ca.CAImpl.java Source code

Java tutorial

Introduction

Here is the source code for mitm.common.security.ca.CAImpl.java

Source

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