/*
 * Copyright (c) 2012 ICM Uniwersytet Warszawski All rights reserved.
 * See LICENCE.txt file for licensing information.
 */
package eu.emi.security.authn.x509.helpers.ocsp;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.x509.AccessDescription;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.cert.ocsp.OCSPException;

import eu.emi.security.authn.x509.OCSPParametes;
import eu.emi.security.authn.x509.OCSPResponder;
import eu.emi.security.authn.x509.StoreUpdateListener;
import eu.emi.security.authn.x509.StoreUpdateListener.Severity;
import eu.emi.security.authn.x509.ValidationErrorCode;
import eu.emi.security.authn.x509.helpers.ObserversHandler;
import eu.emi.security.authn.x509.helpers.pkipath.SimpleValidationErrorException;


/**
 * OCSP checker - uses provided {@link OCSPParametes} to perform OCSP calls using 
 * {@link OCSPCachingClient} and returns the final response. Failures (exceptions) are reported via provided callback.
 * @author K. Benedyczak
 */
public class OCSPVerifier 
{
	private OCSPParametes params;
	private ObserversHandler observers;
	public static String OCSP_CACHE_PFX = "ocspresp_";
	
	public OCSPVerifier(OCSPParametes params, ObserversHandler observers)
	{
		this.params = params;
		this.observers = observers;
	}

	public OCSPResult verify(X509Certificate toCheck, X509Certificate issuerCert) throws SimpleValidationErrorException
	{
		List<OCSPResponder> certResponders = getOCSPUrls(toCheck, issuerCert);
		OCSPResponder[] localResponders = params.getLocalResponders();
		List<OCSPResponder> allResponders = new ArrayList<OCSPResponder>();
		if (params.isPreferLocalResponders())
		{
			Collections.addAll(allResponders, localResponders);
			allResponders.addAll(certResponders);
		} else
		{
			allResponders.addAll(certResponders);
			Collections.addAll(allResponders, localResponders);
		}

		if (allResponders.size() == 0)
			throw new SimpleValidationErrorException(ValidationErrorCode.ocspNoResponder);
		
		File diskCachePath = params.getDiskCachePath() == null ? null : new File(params.getDiskCachePath());
		OCSPCachingClient client = new OCSPCachingClient(params.getCacheTtl(), diskCachePath, 
				OCSP_CACHE_PFX);
		for (int i=0; i<allResponders.size(); i++)
		{
			OCSPResponder responder = allResponders.get(i);
			OCSPResult status;
			
			if (i<allResponders.size()-1)
			{
				try
				{
					status = client.queryForCertificate(responder.getAddress(), toCheck, 
							responder.getCertificate(), null, params.isUseNonce(), 
							params.getConntectTimeout());
				} catch (Exception e)
				{
					observers.notifyObservers(responder.getAddress().toExternalForm(), 
								StoreUpdateListener.OCSP, Severity.WARNING, e);
					continue;
				}				
			} else
			{
				try
				{
					status = client.queryForCertificate(responder.getAddress(), toCheck, 
							responder.getCertificate(), null, params.isUseNonce(), 
							params.getConntectTimeout());
				} catch (IOException e)
				{
					observers.notifyObservers(responder.getAddress().toExternalForm(), 
							StoreUpdateListener.OCSP, Severity.WARNING, e);
					throw new SimpleValidationErrorException(ValidationErrorCode.ocspResponderQueryError,
							responder.getAddress(), e.getMessage());
				} catch (OCSPException e)
				{
					observers.notifyObservers(responder.getAddress().toExternalForm(), 
							StoreUpdateListener.OCSP, Severity.WARNING, e);
					throw new SimpleValidationErrorException(ValidationErrorCode.ocspResponseInvalid,
							responder.getAddress(), e.getMessage());
				} catch (Exception e)
				{
					observers.notifyObservers(responder.getAddress().toExternalForm(), 
							StoreUpdateListener.OCSP, Severity.WARNING, e);
					throw new SimpleValidationErrorException(ValidationErrorCode.ocspOtherError,
							responder.getAddress(), e.toString());
				}
			}
			if (status.getStatus() != OCSPResult.Status.unknown)
				return status;
		}
		return new OCSPResult(OCSPResult.Status.unknown);
	}
	
	protected List<OCSPResponder> getOCSPUrls(X509Certificate certificate, X509Certificate issuerCert) 
			throws SimpleValidationErrorException
	{
		AuthorityInformationAccess authInfoAcc = null;
		byte[] authInfoExt = certificate.getExtensionValue(Extension.authorityInfoAccess.getId());
		if (authInfoExt != null)
		{
			ASN1InputStream aIn = new ASN1InputStream(authInfoExt);
			ASN1Sequence seq;
			try
			{
				ASN1OctetString octs = (ASN1OctetString)aIn.readObject();
				aIn.close();
				aIn = new ASN1InputStream(octs.getOctets());
				seq = ASN1Sequence.getInstance(aIn.readObject());
				aIn.close();
			} catch (IOException e)
			{
				throw new SimpleValidationErrorException(ValidationErrorCode.ocspOtherError,
						"unknown", "Can't extract Authority Info Access extension: " 
								+ e.toString());
			}
			authInfoAcc = AuthorityInformationAccess.getInstance(seq);
		} else
			return new ArrayList<OCSPResponder>(); 

		List<OCSPResponder> ret = new ArrayList<OCSPResponder>();

		AccessDescription[] ads = authInfoAcc.getAccessDescriptions();
		for (int i = 0; i < ads.length; i++)
		{
			if (ads[i].getAccessMethod().equals(AccessDescription.id_ad_ocsp))
			{
				GeneralName name = ads[i].getAccessLocation();
				if (name.getTagNo() == GeneralName.uniformResourceIdentifier)
				{
					String url = ((DERIA5String) name.getName()).getString();
					try
					{
						ret.add(new OCSPResponder(new URL(url), issuerCert));
					} catch (MalformedURLException e)
					{
						observers.notifyObservers(url, StoreUpdateListener.OCSP, Severity.ERROR, 
							new Exception("OCSP responder address in certificate being " +
							"checked is not a valid URL: " + e.getMessage(), e));
					}
				}
			}
		}

		return ret;
	}

}
