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