Created
October 14, 2014 07:43
-
-
Save eranharel/4205fbcccded3ad86fb2 to your computer and use it in GitHub Desktop.
A faviocon handler for Netty HTTP apps. Handles the cache headers and caches the icon bytes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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