package com.intrbiz.bergamot.check.http;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;
import org.apache.log4j.Logger;
import com.intrbiz.bergamot.crypto.util.TLSInfo;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
public class HTTPClientHandler extends ChannelInboundHandlerAdapter
{
private final Logger logger = Logger.getLogger(HTTPClientHandler.class);
private final String url;
private volatile long start;
private final FullHttpRequest request;
private final Consumer<HTTPCheckResponse> responseHandler;
private final Consumer<Throwable> errorHandler;
private final SSLEngine sslEngine;
private final Timer timer;
private volatile TimerTask timeoutTask;
private volatile boolean timedOut = false;
public HTTPClientHandler(String url, Timer timer, SSLEngine sslEngine, FullHttpRequest request, Consumer<HTTPCheckResponse> responseHandler, Consumer<Throwable> errorHandler)
{
super();
this.url = url;
this.sslEngine = sslEngine;
this.request = request;
this.responseHandler = responseHandler;
this.errorHandler = errorHandler;
this.timer = timer;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
{
if (! this.timedOut)
{
FullHttpResponse response = (FullHttpResponse) msg;
long runtime = System.currentTimeMillis() - this.start;
logger.debug("Got HTTP response: " + response.getStatus() + " in: " + runtime + "ms");
if (logger.isTraceEnabled()) logger.trace("Response:\n" + response);
// cancel the timeout
if (this.timeoutTask != null) this.timeoutTask.cancel();
// SSL shit
TLSInfo tlsInfo = null;
if (this.sslEngine != null)
{
try
{
tlsInfo = TLSInfo.fromSSLEngine(this.sslEngine);
}
catch (Exception e)
{
logger.error("Failed to get TLS info", e);
}
}
// invoke the callback
if (this.responseHandler != null)
this.responseHandler.accept(new HTTPCheckResponse(this.url, runtime, response, tlsInfo));
}
// close
if (ctx.channel().isActive()) ctx.close();
}
@Override
public void channelActive(ChannelHandlerContext ctx)
{
logger.debug("Connection opened: " + ctx.channel().remoteAddress());
if (this.sslEngine == null)
{
// plain request
this.sendRequest(ctx);
}
}
public void userEventTriggered(ChannelHandlerContext ctx, Object event)
{
if (this.sslEngine != null && event instanceof SslHandshakeCompletionEvent)
{
SslHandshakeCompletionEvent ev = (SslHandshakeCompletionEvent) event;
logger.debug("SSL handshake complete: " + ev.isSuccess());
if (ev.isSuccess())
{
this.sendRequest(ctx);
}
}
}
protected void sendRequest(ChannelHandlerContext ctx)
{
logger.debug("Sending HTTP request");
this.start = System.currentTimeMillis();
// schedule timeout for 60 seconds
this.timeoutTask = new TimeoutTask(this);
this.timer.schedule(this.timeoutTask, this.start + 60_000L);
// send the request
if (logger.isTraceEnabled()) logger.trace("Request:\n" + this.request);
ctx.writeAndFlush(this.request);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception
{
logger.debug("Connection closed: " + ctx.channel().remoteAddress());
if (this.timeoutTask != null) this.timeoutTask.cancel();
// forcefully invalidate our session
// we don't want to reuse any negotiated ciphers etc, this might break tests
if (this.sslEngine != null)
{
SSLSession session = this.sslEngine.getSession();
if (session != null) session.invalidate();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
{
logger.error("Error processing HTTP request: " + cause);
// invoke the callback
if (this.errorHandler != null)
this.errorHandler.accept(cause);
}
protected void onTimeout()
{
// invoke the error timeout
this.timedOut = true;
// invoke the error handler
if (this.errorHandler != null)
this.errorHandler.accept(new TimeoutException("Timeout getting response from server"));
}
/**
* Timeout timer task which will nullify the HTTPClientHandler immediately
*/
protected static class TimeoutTask extends TimerTask
{
private volatile HTTPClientHandler handler;
public TimeoutTask(HTTPClientHandler handler)
{
this.handler = handler;
}
@Override
public void run()
{
if (this.handler != null) this.handler.onTimeout();
this.handler = null;
}
@Override
public boolean cancel()
{
this.handler = null;
return super.cancel();
}
};
}