package org.icij.net.http;
import java.util.Locale;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.TrustManagerFactory;
import org.apache.commons.io.FilenameUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
/**
* Extends {@link HttpClientBuilder} with the ability to pin a certificate
* and a hostname.
*
* @author Matthew Caruana Galizia <mcaruana@icij.org>
*/
public class PinnedHttpClientBuilder extends HttpClientBuilder {
private HostnameVerifier hostnameVerifier = null;
private SSLContext sslContext = null;
public static PinnedHttpClientBuilder createWithDefaults() {
final PinnedHttpClientBuilder builder = new PinnedHttpClientBuilder();
builder
.setMaxConnPerRoute(32)
.setMaxConnTotal(128)
.disableRedirectHandling()
.setRetryHandler(new CountdownHttpRequestRetryHandler());
return builder;
}
public PinnedHttpClientBuilder() {
super();
}
public PinnedHttpClientBuilder setVerifyHostname(final String verifyHostname) {
if (null == verifyHostname) {
hostnameVerifier = null;
return this;
} else if (verifyHostname.equals("*")) {
hostnameVerifier = NoopHostnameVerifier.INSTANCE;
} else {
hostnameVerifier = new BodgeHostnameVerifier(verifyHostname);
}
return this;
}
public PinnedHttpClientBuilder pinCertificate(final String trustStorePath) throws RuntimeException {
return pinCertificate(trustStorePath, "");
}
public PinnedHttpClientBuilder pinCertificate(final String trustStorePath, final String trustStorePassword)
throws RuntimeException {
if (null != trustStorePath) {
try {
final TrustManagerFactory trustManager = TrustManagerFactory.getInstance("X509");
trustManager.init(createTrustStore(trustStorePath, trustStorePassword));
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManager.getTrustManagers(), null);
} catch (IOException | NoSuchAlgorithmException | CertificateException | KeyStoreException
| KeyManagementException e) {
throw new RuntimeException("Unable to pin certificate: " + trustStorePath + ".", e);
}
} else {
sslContext = null;
}
return this;
}
public CloseableHttpClient build() {
if (null != hostnameVerifier) {
super.setSSLHostnameVerifier(hostnameVerifier);
}
if (null != sslContext) {
super.setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext, hostnameVerifier));
}
return super.build();
}
public static KeyStore createTrustStore(final String trustStorePath, final String trustStorePassword)
throws IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException {
final String trustStoreExtension = FilenameUtils.getExtension(trustStorePath).toUpperCase(Locale.ROOT);
final String trustStoreType;
// Key store types are defined in Oracle's Cryptography Standard Algorithm Name Documentation:
// http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyStore
if (trustStoreExtension.equals("P12")) {
trustStoreType = "PKCS12";
} else {
trustStoreType = KeyStore.getDefaultType();
}
final KeyStore trustStore = KeyStore.getInstance(trustStoreType);
try (
final InputStream input = new BufferedInputStream(new FileInputStream(trustStorePath))
) {
if (trustStoreExtension.equals("PEM") || trustStoreExtension.equals("DER")) {
final X509Certificate certificate = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(input);
// Create an empty key store.
// This operation should never throw an exception.
trustStore.load(null, null);
trustStore.setCertificateEntry(Integer.toString(1), certificate);
} else {
trustStore.load(input, trustStorePassword.toCharArray());
}
}
return trustStore;
}
public static class BodgeHostnameVerifier implements HostnameVerifier {
private static final HostnameVerifier defaultVerifier = new DefaultHostnameVerifier();
private final String verifyHostname;
public BodgeHostnameVerifier(final String verifyHostname) {
super();
this.verifyHostname = verifyHostname;
}
@Override
public final boolean verify(final String host, final SSLSession session) {
return defaultVerifier.verify(verifyHostname, session);
}
}
}