package de.geeksfactory.opacclient.networking;
import android.annotation.TargetApi;
import android.os.Build;
import android.util.Log;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.protocol.HttpContext;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.security.auth.x500.X500Principal;
public class TlsSniSocketFactory extends SSLConnectionSocketFactory {
final static String TAG = "opacclient.tls";
private javax.net.ssl.SSLSocketFactory socketfactory;
private final X509HostnameVerifier hostnameVerifier;
protected boolean tls_only = true;
public TlsSniSocketFactory(final SSLContext sslContext) {
super(sslContext, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
socketfactory = sslContext.getSocketFactory();
hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
}
@TargetApi(17)
public Socket createLayeredSocket(
final Socket socket,
final String target,
final int port,
final HttpContext context) throws IOException {
final SSLSocket sslsock = (SSLSocket) this.socketfactory.createSocket(
socket,
target,
port,
true);
// If supported protocols are not explicitly set, remove all SSL protocol versions
final String[] allProtocols = sslsock.getEnabledProtocols();
final List<String> enabledProtocols = new ArrayList<String>(allProtocols.length);
for (String protocol : allProtocols) {
if (!protocol.startsWith("SSL") || !tls_only) {
enabledProtocols.add(protocol);
}
}
if (!enabledProtocols.isEmpty()) {
sslsock.setEnabledProtocols(
enabledProtocols.toArray(new String[enabledProtocols.size()]));
}
Log.d(TAG, "Enabled protocols: " + Arrays.asList(sslsock.getEnabledProtocols()));
Log.d(TAG, "Enabled cipher suites:" + Arrays.asList(sslsock.getEnabledCipherSuites()));
prepareSocket(sslsock);
// Android specific code to enable SNI
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Log.d(TAG, "Enabling SNI for " + target);
try {
Method method = sslsock.getClass().getMethod("setHostname", String.class);
method.invoke(sslsock, target);
} catch (Exception ex) {
Log.d(TAG, "SNI configuration failed", ex);
}
} else {
Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection");
try {
java.lang.reflect.Method setHostnameMethod =
sslsock.getClass().getMethod("setHostname", String.class);
setHostnameMethod.invoke(sslsock, target);
} catch (Exception e) {
Log.w(TAG, "SNI not useable", e);
}
}
Log.d(TAG, "Starting handshake");
sslsock.startHandshake();
verifyHostname(sslsock, target);
return sslsock;
}
private void verifyHostname(final SSLSocket sslsock, final String hostname)
throws IOException {
try {
if (hostname.matches("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$")) {
// In very rare cases, we have endpoints with SSL enabled behind an IP address.
// Getting SSL certificates for an IP address is hard, so they always have
// invalid certificates. There is no way we could do a proper host name check
// here, so we skip it and still have more security than with SSL disabled.
// Note that this only applies to endpoints where an IP address is stored in
// *our* configuration, so this does not allow any new attacks on all other
// endpoints.
return;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
try {
final SSLSession session = sslsock.getSession();
Log.d(TAG, "Secure session established");
Log.d(TAG, " negotiated protocol: " + session.getProtocol());
Log.d(TAG, " negotiated cipher suite: " + session.getCipherSuite());
final Certificate[] certs = session.getPeerCertificates();
final X509Certificate x509 = (X509Certificate) certs[0];
final X500Principal peer = x509.getSubjectX500Principal();
Log.d(TAG, " peer principal: " + peer.toString());
final Collection<List<?>> altNames1 = x509.getSubjectAlternativeNames();
if (altNames1 != null) {
final List<String> altNames = new ArrayList<String>();
for (final List<?> aC : altNames1) {
if (!aC.isEmpty()) {
altNames.add((String) aC.get(1));
}
}
Log.d(TAG, " peer alternative names: " + altNames);
}
final X500Principal issuer = x509.getIssuerX500Principal();
Log.d(TAG, " issuer principal: " + issuer.toString());
final Collection<List<?>> altNames2 = x509.getIssuerAlternativeNames();
if (altNames2 != null) {
final List<String> altNames = new ArrayList<String>();
for (final List<?> aC : altNames2) {
if (!aC.isEmpty()) {
altNames.add((String) aC.get(1));
}
}
Log.d(TAG, " issuer alternative names: " + altNames);
}
} catch (Exception ignore) {
}
}
this.hostnameVerifier.verify(hostname, sslsock);
// verifyHostName() didn't blowup - good!
} catch (final IOException iox) {
// close the socket before re-throwing the exception
try {
sslsock.close();
} catch (final Exception x) { /*ignore*/ }
throw iox;
}
}
}