Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A faviocon handler for Netty HTTP apps. Handles the cache headers and caches the icon bytes.
package com.outbrain.ob1k.server.netty;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaders.Names.DATE;
import static io.netty.handler.codec.http.HttpHeaders.Names.IF_MODIFIED_SINCE;
import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.io.ByteStreams;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.CharsetUtil;
/**
* A handler that serves a cached favicon file from a specified (resource) path.
*
* @author Eran Harel
*/
@ChannelHandler.Sharable
public class FaviconHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private static final Logger log = LoggerFactory.getLogger(FaviconHandler.class);
public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
private final byte[] iconBytes;
private long startupTime = System.currentTimeMillis();
public FaviconHandler(final String iconFilePath) {
iconBytes = loadIconBytes(iconFilePath);
}
private byte[] loadIconBytes(final String iconFilePath) {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(iconFilePath);
if (null == inputStream) {
log.error("Failed to find icon file {}", iconFilePath);
return null;
}
try {
return ByteStreams.toByteArray(inputStream);
} catch (IOException e) {
log.error("Failed to load icon file {}", iconFilePath);
return null;
}
}
// handle only /favicon.ico http requests
public boolean acceptInboundMessage(Object msg) throws Exception {
if (null == iconBytes || !(msg instanceof FullHttpRequest)) {
return false;
}
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.getUri();
return HttpMethod.GET.equals(request.getMethod()) && "/favicon.ico".endsWith(uri);
}
@Override
protected void channelRead0(final ChannelHandlerContext ctx, final FullHttpRequest request) throws Exception {
if (!request.getDecoderResult().isSuccess()) {
sendError(ctx, BAD_REQUEST);
return;
}
// Cache Validation
String ifModifiedSince = request.headers().get(IF_MODIFIED_SINCE);
if (ifModifiedSince != null && !ifModifiedSince.isEmpty()) {
SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
// Only compare up to the second because the datetime format we send to the client
// does not have milliseconds
long lastDownloadTime = dateFormatter.parse(ifModifiedSince).getTime();
if (startupTime < lastDownloadTime) {
sendNotModified(ctx);
return;
}
}
final DefaultHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(iconBytes));
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, iconBytes.length);
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "image/x-icon");
if (isKeepAlive(request)) {
response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
ChannelFuture lastContentFuture = ctx.writeAndFlush(response);
// Decide whether to close the connection or not.
if (!isKeepAlive(request)) {
// Close the connection when the whole content is written out.
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
final ByteBuf content = Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, content);
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
// Close the connection as soon as the error message is sent.
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
/**
* When file timestamp is the same as what the browser is sending up, send a "304 Not Modified"
*
* @param ctx Context
*/
private static void sendNotModified(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED);
setDateHeader(response);
// Close the connection as soon as the error message is sent.
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
/**
* Sets the Date header for the HTTP response
*
* @param response HTTP response
*/
private static void setDateHeader(FullHttpResponse response) {
SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
Calendar time = new GregorianCalendar();
response.headers().set(DATE, dateFormatter.format(time.getTime()));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment