// --- BEGIN COPYRIGHT BLOCK ---
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; version 2 of the License.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// (C) 2012 Red Hat, Inc.
// All rights reserved.
// --- END COPYRIGHT BLOCK ---
package com.netscape.cms.servlet.cert;

import java.math.BigInteger;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;

import com.netscape.certsrv.apps.CMS;
import com.netscape.certsrv.authentication.IAuthToken;
import com.netscape.certsrv.base.BadRequestDataException;
import com.netscape.certsrv.base.BadRequestException;
import com.netscape.certsrv.base.EBaseException;
import com.netscape.certsrv.base.EPropertyNotFound;
import com.netscape.certsrv.base.SessionContext;
import com.netscape.certsrv.cert.CertEnrollmentRequest;
import com.netscape.certsrv.dbs.certdb.CertId;
import com.netscape.certsrv.dbs.certdb.ICertRecord;
import com.netscape.certsrv.profile.IEnrollProfile;
import com.netscape.certsrv.profile.IProfile;
import com.netscape.certsrv.profile.IProfileAuthenticator;
import com.netscape.certsrv.profile.IProfileContext;
import com.netscape.certsrv.profile.IProfileInput;
import com.netscape.certsrv.profile.ProfileAttribute;
import com.netscape.certsrv.profile.ProfileInput;
import com.netscape.certsrv.registry.IPluginInfo;
import com.netscape.certsrv.registry.IPluginRegistry;
import com.netscape.certsrv.request.IRequest;
import com.netscape.cms.profile.input.SerialNumRenewInput;
import com.netscape.cms.realm.PKIPrincipal;
import com.netscape.cms.servlet.common.AuthCredentials;
import com.netscape.cms.servlet.common.CMSTemplate;
import com.netscape.cms.servlet.profile.SSLClientCertProvider;

import netscape.security.x509.BasicConstraintsExtension;
import netscape.security.x509.X509CertImpl;

public class RenewalProcessor extends CertProcessor {

    public RenewalProcessor(String id, Locale locale) throws EPropertyNotFound, EBaseException {
        super(id, locale);
    }

    /*
     * Renewal - Renewal is retrofitted into the Profile Enrollment
     * Framework.  The authentication and authorization are taken from
     * the renewal profile, while the input (with requests)  and grace
     * period constraint are taken from the original cert's request record.
     *
     * Things to note:
     * * the renew request will contain the original profile instead of the new
     */
    public HashMap<String, Object> processRenewal(
            CertEnrollmentRequest data,
            HttpServletRequest request,
            AuthCredentials credentials)
            throws EBaseException {
        try {
            if (CMS.debugOn()) {
                HashMap<String, String> params = data.toParams();
                printParameterValues(params);
            }

            CMS.debug("RenewalProcessor: processRenewal()");

            startTiming("enrollment");
            request.setAttribute("reqType", "renewal");

            // in case of renew, "profile" is the orig profile
            // while "renewProfile" is the current profile used for renewal
            String renewProfileId = (this.profileID == null) ? data.getProfileId() : this.profileID;
            CMS.debug("RenewalProcessor: profile: " + renewProfileId);

            IProfile renewProfile = ps.getProfile(renewProfileId);
            if (renewProfile == null) {
                CMS.debug(CMS.getUserMessage(locale, "CMS_PROFILE_NOT_FOUND",
                        CMSTemplate.escapeJavaScriptStringHTML(renewProfileId)));
                throw new BadRequestDataException(CMS.getUserMessage(locale, "CMS_PROFILE_NOT_FOUND",CMSTemplate.escapeJavaScriptStringHTML(renewProfileId)));
            }
            if (!ps.isProfileEnable(renewProfileId)) {
                CMS.debug("RenewalProcessor: Profile " + renewProfileId + " not enabled");
                throw new BadRequestDataException("Profile " + renewProfileId + " not enabled");
            }

            BigInteger certSerial = null;

            // get serial number from <SerialNumber> element (no auth required)
            CertId serial = data.getSerialNum();
            if (serial != null) {
                CMS.debug("RenewalProcessor: serial number: " + serial);
                certSerial = serial.toBigInteger();
            }

            // if not found, get serial number from profile input (no auth required)
            if (certSerial == null) {

                IPluginRegistry registry = (IPluginRegistry) CMS.getSubsystem(CMS.SUBSYSTEM_REGISTRY);

                // find SerialNumRenewInput
                for (ProfileInput input : data.getInputs()) {

                    String inputId = input.getId();
                    if (inputId == null) {
                        throw new BadRequestException("Missing input ID");
                    }

                    String classId = input.getClassId();
                    if (classId == null) {
                        throw new BadRequestException("Missing class ID in input " + inputId);
                    }

                    IPluginInfo pluginInfo = registry.getPluginInfo("profileInput", classId);
                    if (pluginInfo == null) {
                        throw new BadRequestException("Unregistered class ID " + classId + " in input " + inputId);
                    }

                    String className = pluginInfo.getClassName();
                    if (!SerialNumRenewInput.class.getName().equals(className)) {
                        // check the next input
                        continue;
                    }

                    CMS.debug("RenewalProcessor: found SerialNumRenewInput");
                    ProfileAttribute attribute = input.getAttribute(SerialNumRenewInput.SERIAL_NUM);

                    if (attribute == null) {
                        throw new BadRequestException("Missing attribute " + SerialNumRenewInput.SERIAL_NUM + " in input " + inputId);
                    }

                    String value = attribute.getValue();
                    CMS.debug("RenewalProcessor: profile input " + SerialNumRenewInput.SERIAL_NUM + " value: " + value);

                    if (!StringUtils.isEmpty(value)) {
                        serial = new CertId(value);
                        certSerial = serial.toBigInteger();
                        break;
                    }
                }
            }

            // if still not found, get serial number from client certificate (if provided)
            if (certSerial == null) {

                if (!request.isSecure()) {
                    throw new BadRequestException("Missing serial number");
                }

                // ssl client auth is to be used
                // this is not authentication. Just use the cert to search
                // for orig request and find the right profile
                CMS.debug("RenewalProcessor: get serial number from client certificate");
                certSerial = getSerialNumberFromCert(request);
            }

            CMS.debug("processRenewal: serial number of cert to renew:" + certSerial.toString());
            ICertRecord rec = certdb.readCertificateRecord(certSerial);
            if (rec == null) {
                CMS.debug("processRenewal: cert record not found for serial number " + certSerial.toString());
                throw new EBaseException(CMS.getUserMessage(locale, "CMS_INTERNAL_ERROR"));
            }

            // check to see if the cert is revoked or revoked_expired
            if ((rec.getStatus().equals(ICertRecord.STATUS_REVOKED))
                    || (rec.getStatus().equals(ICertRecord.STATUS_REVOKED_EXPIRED))) {
                CMS.debug("processRenewal: cert found to be revoked. Serial number = "
                        + certSerial.toString());
                throw new BadRequestDataException(CMS.getUserMessage(locale, "CMS_CA_CANNOT_RENEW_REVOKED_CERT"));
            }

            X509CertImpl origCert = rec.getCertificate();
            if (origCert == null) {
                CMS.debug("processRenewal: original cert not found in cert record for serial number "
                        + certSerial.toString());
                throw new EBaseException(CMS.getUserMessage(locale, "CMS_INTERNAL_ERROR"));
            }

            Date origNotAfter = origCert.getNotAfter();
            CMS.debug("processRenewal: origNotAfter =" + origNotAfter.toString());

            String origSubjectDN = origCert.getSubjectDN().getName();
            CMS.debug("processRenewal: orig subj dn =" + origSubjectDN);

            IRequest origReq = getOriginalRequest(certSerial, rec);
            if (origReq == null) {
                CMS.debug("processRenewal: original request not found");
                throw new EBaseException(CMS.getUserMessage(locale, "CMS_INTERNAL_ERROR"));
            }

            String profileId = origReq.getExtDataInString(IRequest.PROFILE_ID);
            CMS.debug("RenewalSubmitter: renewal original profileId=" + profileId);

            String aidString = origReq.getExtDataInString(
                    IRequest.AUTHORITY_ID);

            Integer origSeqNum = origReq.getExtDataInInteger(IEnrollProfile.REQUEST_SEQ_NUM);
            IProfile profile = ps.getProfile(profileId);
            if (profile == null) {
                CMS.debug(CMS.getUserMessage(locale, "CMS_PROFILE_NOT_FOUND",CMSTemplate.escapeJavaScriptStringHTML(profileId)));
                throw new EBaseException(CMS.getUserMessage(locale, "CMS_PROFILE_NOT_FOUND", CMSTemplate.escapeJavaScriptStringHTML(profileId)));
            }
            if (!ps.isProfileEnable(profileId)) {
                CMS.debug("RenewalSubmitter: Profile " + profileId + " not enabled");
                throw new BadRequestDataException("Profile " + profileId + " not enabled");
            }

            IProfileContext ctx = profile.createContext();

            if (aidString != null)
                ctx.set(IEnrollProfile.REQUEST_AUTHORITY_ID, aidString);

            IProfileAuthenticator authenticator = renewProfile.getAuthenticator();
            IProfileAuthenticator origAuthenticator = profile.getAuthenticator();

            if (authenticator != null) {
                CMS.debug("RenewalSubmitter: authenticator " + authenticator.getName() + " found");
                setCredentialsIntoContext(request, credentials, authenticator, ctx);
            }

            // for renewal, this will override or add auth info to the profile context
            if (origAuthenticator != null) {
                CMS.debug("RenewalSubmitter: for renewal, original authenticator " +
                        origAuthenticator.getName() + " found");
                setCredentialsIntoContext(request, credentials, origAuthenticator, ctx);
            }

            // for renewal, input needs to be retrieved from the orig req record
            CMS.debug("processRenewal: set original Inputs into profile Context");
            setInputsIntoContext(origReq, profile, ctx, locale);
            ctx.set(IEnrollProfile.CTX_RENEWAL, "true");
            ctx.set("renewProfileId", renewProfileId);
            ctx.set(IEnrollProfile.CTX_RENEWAL_SEQ_NUM, origSeqNum.toString());

            // for ssl authentication; pass in servlet for retrieving
            // ssl client certificates
            SessionContext context = SessionContext.getContext();
            context.put("profileContext", ctx);
            context.put("sslClientCertProvider", new SSLClientCertProvider(request));
            CMS.debug("RenewalSubmitter: set sslClientCertProvider");
            if (origSubjectDN != null)
                context.put("origSubjectDN", origSubjectDN);

            // before creating the request, authenticate the request
            IAuthToken authToken = null;
            Principal principal = request.getUserPrincipal();
            if (principal instanceof PKIPrincipal)
                authToken = ((PKIPrincipal) principal).getAuthToken();
            if (authToken == null)
                authToken = authenticate(request, origReq, authenticator, context, true, credentials);

            // authentication success, now authorize
            authorize(profileId, renewProfile, authToken);

            ///////////////////////////////////////////////
            // create and populate requests
            ///////////////////////////////////////////////
            startTiming("request_population");
            IRequest[] reqs = profile.createRequests(ctx, locale);
            populateRequests(data, true, locale, origNotAfter, origSubjectDN, origReq, profileId,
                    profile, ctx, authenticator, authToken, reqs);
            endTiming("request_population");

            ///////////////////////////////////////////////
            // submit request
            ///////////////////////////////////////////////
            String errorCode = submitRequests(locale, profile, authToken, reqs);
            String errorReason = null;

            List<String> errors = new ArrayList<String>();
            if (errorCode != null) {
                for (IRequest req: reqs) {
                    String error = req.getError(locale);
                    if (error != null) {
                        String code = req.getErrorCode(locale);
                        errors.add(codeToReason(locale, code, error, req.getRequestId()));
                    }
                }
                errorReason = StringUtils.join(errors, '\n');
            }

            HashMap<String, Object> ret = new HashMap<String, Object>();
            ret.put(ARG_REQUESTS, reqs);
            ret.put(ARG_ERROR_CODE, errorCode);
            ret.put(ARG_ERROR_REASON, errorReason);
            ret.put(ARG_PROFILE, profile);

            CMS.debug("RenewalSubmitter: done serving");
            endTiming("enrollment");

            return ret;
        } finally {
            SessionContext.releaseContext();
            endAllEvents();
        }
    }

    private BigInteger getSerialNumberFromCert(HttpServletRequest request) throws EBaseException {

        SSLClientCertProvider sslCCP = new SSLClientCertProvider(request);
        X509Certificate[] certs = sslCCP.getClientCertificateChain();

        if (certs == null || certs.length == 0) {
            CMS.debug("RenewalProcessor: missing SSL client certificate chain");
            throw new BadRequestException("Missing SSL client certificate chain");
        }

        CMS.debug("RenewalProcessor: has SSL client cert chain");
        // shouldn't expect leaf cert to be always at the
        // same location

        X509Certificate clientCert = null;
        for (X509Certificate cert : certs) {

            CMS.debug("RenewalProcessor: cert " + cert.getSubjectDN());
            clientCert = cert;

            byte[] extBytes = clientCert.getExtensionValue("2.5.29.19");

            // try to see if this is a leaf cert
            // look for BasicConstraint extension
            if (extBytes == null) {
                // found leaf cert
                CMS.debug("RenewalProcessor: found leaf cert");
                break;
            }

            CMS.debug("RenewalProcessor: found cert having BasicConstraints ext");
            // it's got BasicConstraints extension
            // so it's not likely to be a leaf cert,
            // however, check the isCA field regardless

            try {
                BasicConstraintsExtension bce = new BasicConstraintsExtension(true, extBytes);
                if (!(Boolean) bce.get("is_ca")) {
                    CMS.debug("RenewalProcessor: found CA cert in chain");
                    break;
                } // else found a ca cert, continue

            } catch (Exception e) {
                CMS.debug("RenewalProcessor: Invalid certificate extension:" + e);
                throw new BadRequestException("Invalid certificate extension: " + e.getMessage(), e);
            }
        }

        // clientCert cannot be null here

        return clientCert.getSerialNumber();
    }

    /*
     * fill input info from "request" to context.
     * This is expected to be used by renewal where the request
     * is retrieved from request record
     */
    private void setInputsIntoContext(IRequest request, IProfile profile, IProfileContext ctx, Locale locale) {
        // passing inputs into context
        Enumeration<String> inputIds = profile.getProfileInputIds();

        if (inputIds != null) {
            while (inputIds.hasMoreElements()) {
                String inputId = inputIds.nextElement();
                IProfileInput profileInput = profile.getProfileInput(inputId);
                Enumeration<String> inputNames = profileInput.getValueNames();

                while (inputNames.hasMoreElements()) {
                    String inputName = inputNames.nextElement();
                    String inputValue = "";
                    CMS.debug("RenewalSubmitter: setInputsIntoContext() getting input name= " + inputName);
                    try {
                        inputValue = profileInput.getValue(inputName, locale, request);
                    } catch (Exception e) {
                        CMS.debug("RenewalSubmitter: setInputsIntoContext() getvalue() failed: " + e.toString());
                    }

                    if (inputValue != null) {
                        CMS.debug("RenewalSubmitter: setInputsIntoContext() setting value in ctx:" + inputValue);
                        ctx.set(inputName, inputValue);
                    } else {
                        CMS.debug("RenewalSubmitter: setInputsIntoContext() value null");
                    }
                }
            }
        }

    }

}
