package openeye.net;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.List;
import java.util.zip.GZIPOutputStream;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import openeye.Log;
import org.apache.commons.lang3.tuple.Pair;
public abstract class GenericSender<I, O> {
private final List<String> bundledRoots = ImmutableList.of("isrg_root_x1.pem", "identrust_root_x3.pem");
private SSLSocketFactory createSocketFactoryWithRoots(List<String> roots) throws GeneralSecurityException, IOException {
final String defaultKsAlgorithm = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(defaultKsAlgorithm);
// for 'full' keystore
// Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
// keyStore.load(Files.newInputStream(ksPath), "changeit".toCharArray());
// for single cert keystore
keyStore.load(null);
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
for (String root : roots) {
InputStream data = getClass().getClassLoader().getResourceAsStream(root);
Preconditions.checkNotNull(data, "Failed to found resource %s", root);
Certificate cert = certificateFactory.generateCertificate(data);
keyStore.setCertificateEntry(root, cert);
}
final String defaultTmAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(defaultTmAlgorithm);
tmf.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
return sslContext.getSocketFactory();
}
@SuppressWarnings("serial")
public static class HttpTransactionException extends RuntimeException {
private HttpTransactionException(String format, Object... args) {
super(String.format(format, args));
}
private HttpTransactionException(Throwable cause) {
super(cause);
}
}
public enum EncryptionState {
NOT_SUPPORTED,
NO_ROOT_CERTIFICATE,
OK,
UNKNOWN;
}
private enum HttpStatus {
OK,
REDIRECT;
}
private String host;
private String path;
private int maxRetries = 2;
private int maxRedirects = 5;
private int timeout = 20000;
private EncryptionState encryptionState = EncryptionState.UNKNOWN;
public GenericSender(String host, String path) {
this.host = host;
this.path = path;
}
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public void setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public EncryptionState getEncryptionState() {
return encryptionState;
}
public O sendAndReceive(I request) {
int retry = 0;
int redirect = 0;
while (retry < maxRetries) {
Log.debug("Trying to connect to %s%s, retry %s, redirect %s", host, path, retry, redirect);
try {
final HttpURLConnection connection;
try {
final Pair<HttpURLConnection, EncryptionState> result = createConnection();
encryptionState = Ordering.natural().min(result.getRight(), encryptionState);
connection = result.getLeft();
} catch (GeneralSecurityException t) {
// giving up, something broken in encryption
throw new HttpTransactionException(t);
}
trySendRequest(request, connection);
final HttpStatus statusCode = checkStatusCode(connection);
if (statusCode == HttpStatus.REDIRECT) {
if (redirect++ >= maxRedirects) throw new HttpTransactionException("Too many redirects");
final String redirectPath = connection.getHeaderField("Location");
if (redirectPath == null) throw new HttpTransactionException("Invalid redirect");
try {
final URL url = new URL(redirectPath);
// ignoring protocol and port - there is no valid scenario when this is needed
this.host = url.getHost();
this.path = url.getPath();
} catch (MalformedURLException e) {
throw new HttpTransactionException("Invalid redirect: '%s'", redirectPath);
}
connection.disconnect();
retry = 0;
continue;
} else {
return tryReceiveResponse(connection);
}
} catch (HttpTransactionException e) {
throw e;
} catch (SocketTimeoutException e) {
Log.warn("Connection timed out (retry %d)", retry);
} catch (Throwable t) {
Log.warn(t, "Failed to send/receive report (retry %d)", retry);
}
retry++;
}
throw new HttpTransactionException("Too much retries");
}
private Pair<HttpURLConnection, EncryptionState> createConnection() throws IOException, GeneralSecurityException {
// non-business versions of Java 6 can't handle our awesome certificates
if (System.getProperty("java.specification.version").equals("1.6")) {
final URL url = new URL("http", host, path);
return createHttpConnection(url);
} else {
final URL url = new URL("https", host, path);
return createHttpsConnection(url);
}
}
private Pair<HttpURLConnection, EncryptionState> createHttpConnection(final URL url) throws IOException, ProtocolException {
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
configureAndConnect(url, connection);
return Pair.of(connection, EncryptionState.NOT_SUPPORTED);
}
private Pair<HttpURLConnection, EncryptionState> createHttpsConnection(final URL url) throws IOException, ProtocolException, GeneralSecurityException {
try {
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
configureAndConnect(url, connection);
return Pair.of(connection, EncryptionState.OK);
} catch (SSLHandshakeException e) {
HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
final SSLSocketFactory sslSocketFactory = createSocketFactoryWithRoots(bundledRoots);
connection.setSSLSocketFactory(sslSocketFactory);
configureAndConnect(url, connection);
return Pair.of((HttpURLConnection)connection, EncryptionState.NO_ROOT_CERTIFICATE);
}
}
private void configureAndConnect(URL url, HttpURLConnection connection) throws ProtocolException, IOException {
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setConnectTimeout(timeout);
connection.setReadTimeout(timeout);
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("Content-Encoding", "gzip");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", "Die Fledermaus/11");
connection.setRequestProperty("Host", url.getAuthority());
// Doing manual redirects
// Partially for logging and partially since parts of behaviour are controlled by global flags
connection.setInstanceFollowRedirects(false);
connection.connect();
}
protected void trySendRequest(I request, URLConnection connection) throws IOException {
OutputStream requestStream = connection.getOutputStream();
requestStream = new GZIPOutputStream(requestStream);
try {
encodeRequest(requestStream, request);
requestStream.flush();
} finally {
requestStream.close();
}
}
protected HttpStatus checkStatusCode(HttpURLConnection connection) throws IOException {
int statusCode = connection.getResponseCode();
switch (statusCode) {
case HttpURLConnection.HTTP_OK:
case HttpURLConnection.HTTP_NO_CONTENT:
return HttpStatus.OK;
case 307: // Temporary Redirect
case 308: // Permanent Redirect
// 301 and 302 are invalid, since method cannot change
return HttpStatus.REDIRECT;
case HttpURLConnection.HTTP_NOT_FOUND:
throw new HttpTransactionException("Endpoint not found");
case HttpURLConnection.HTTP_INTERNAL_ERROR:
throw new HttpTransactionException("Internal server error");
default:
throw new HttpTransactionException("HttpStatus %d != 200", statusCode);
}
}
protected O tryReceiveResponse(HttpURLConnection connection) throws IOException {
InputStream stream = connection.getInputStream();
try {
return decodeResponse(stream);
} finally {
stream.close();
}
}
protected abstract void encodeRequest(OutputStream output, I request) throws IOException;
protected abstract O decodeResponse(InputStream input) throws IOException;
}