package aQute.bnd.url;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import aQute.lib.io.IO;
import aQute.service.reporter.Reporter;

/**
 * TODO Needs testing Can be used to override default verification of HTTPS. The
 * 'trusted' property on this plugin must contain a list of issuer certificates
 * that are trusted. If none are specified the verification is NOT done.
 */
@aQute.bnd.annotation.plugin.BndPlugin(name = "url.https.verification", parameters = HttpsVerification.Config.class)
public class HttpsVerification extends DefaultURLConnectionHandler {
	private SSLSocketFactory			factory;
	private HostnameVerifier			verifier;
	private boolean						verify			= true;
	private String						certificatesPath;
	private X509Certificate[]	certificateChain;

	interface Config {
		String trusted();
	}

	public HttpsVerification() {

	}

	// http://stackoverflow.com/questions/24555890/using-a-custom-truststore-in-java-as-well-as-the-default-one
	public HttpsVerification(String certificates, boolean hostnameVerify,
			Reporter reporter) {
		certificatesPath = certificates;
		this.verify = hostnameVerify;
		this.setReporter(reporter);
	}

	public HttpsVerification(X509Certificate[] certificateChain, boolean b, Reporter hc) {
		this.certificateChain = certificateChain;
		this.verify = b;
		this.setReporter(hc);
	}

	/**
	 * Initialize the SSL Context, factory and verifier.
	 * 
	 * @throws NoSuchAlgorithmException
	 * @throws KeyManagementException
	 * @throws IOException
	 * @throws CertificateException
	 * @throws FileNotFoundException
	 */
	private synchronized void init() throws NoSuchAlgorithmException, KeyManagementException, FileNotFoundException,
			CertificateException, IOException {
		if (factory == null) {
			List<X509Certificate> certificates = createCertificates(certificatesPath);
			final X509Certificate trusted[] = certificates.toArray(new X509Certificate[0]);

			TrustManager[] trustAllCerts = new TrustManager[] {
					getTrustManager(trusted)
			};
			SSLContext context = SSLContext.getInstance("TLS");
			context.init(null, trustAllCerts, new SecureRandom());
			factory = context.getSocketFactory();

			//
			// We can already verify the name through the matchers
			// of the URL so we can ignore that for our connections
			//
			verifier = new HostnameVerifier() {
				public boolean verify(String string, SSLSession session) {
					return verify;
				}
			};
		}
	}

	X509TrustManager getTrustManager(final X509Certificate[] trusted) {
		X509TrustManager tm = new X509TrustManager() {
			public X509Certificate[] getAcceptedIssuers() {
				return trusted;
			}

			public void checkServerTrusted(X509Certificate[] certs, String authType)
					throws CertificateException {
			}

			public void checkClientTrusted(X509Certificate[] certs, String authType)
					throws CertificateException {
			}
		};
		return tm;
	}

	/**
	 * Ensure Https verification is disabled or matches given certificates
	 */
	public void handle(URLConnection connection) throws Exception {

		if (connection instanceof HttpsURLConnection && matches(connection)) {
			HttpsURLConnection https = (HttpsURLConnection) connection;
			init();
			https.setSSLSocketFactory(factory);
			https.setHostnameVerifier(verifier);
		}
	}

	/**
	 * Set the properties
	 */
	@Override
	public void setProperties(Map<String,String> map) throws Exception {
		super.setProperties(map);

		certificatesPath = map.get("trusted");
	}

	List<X509Certificate> createCertificates(String paths)
			throws FileNotFoundException, CertificateException, IOException {
		List<X509Certificate> certificates = new ArrayList<X509Certificate>();
		if (paths != null) {
			for (String path : paths.split("\\s*,\\s*")) {
				File file = new File(path); // This is a system specific path!
				if (file.isFile()) {
					try (InputStream inStream = IO.stream(file)) {
						CertificateFactory cf = CertificateFactory.getInstance("X.509");
						X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream);
						certificates.add(cert);
					}
				}
			}
		} else if (certificateChain != null) {
			for (X509Certificate cert : certificateChain) {
				certificates.add(cert);
			}
		}
		return certificates;
	}

	@Override
	public String toString() {
		return "HttpsVerification [verify=" + verify + ", certificatesPath=" + certificatesPath + "]";
	}
}
