/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Copyright 2012 The Netty Project * * Contents have been modified */ package com.addthis.hydra.query.web; import javax.activation.MimetypesFileTypeMap; import java.io.IOException; import java.io.Writer; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; import java.util.TimeZone; import java.nio.file.Files; import java.nio.file.Path; import java.text.SimpleDateFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import static io.netty.handler.codec.http.HttpHeaders.Names.CACHE_CONTROL; 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.EXPIRES; import static io.netty.handler.codec.http.HttpHeaders.Names.LAST_MODIFIED; import static io.netty.handler.codec.http.HttpHeaders.Names.LOCATION; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import static io.netty.handler.codec.http.HttpResponseStatus.FOUND; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import io.netty.util.CharsetUtil; /** * A simple handler that serves incoming HTTP requests to send their respective * HTTP responses. It also implements {@code 'If-Modified-Since'} header to * take advantage of browser cache, as described in * <a href="http://tools.ietf.org/html/rfc2616#section-14.25">RFC 2616</a>. * <p/> * <h3>How Browser Caching Works</h3> * <p/> * Web browser caching works with HTTP headers as illustrated by the following * sample: * <ol> * <li>Request #1 returns the content of {@code /file1.txt}.</li> * <li>Contents of {@code /file1.txt} is cached by the browser.</li> * <li>Request #2 for {@code /file1.txt} does return the contents of the * file again. Rather, a 304 Not Modified is returned. This tells the * browser to use the contents stored in its cache.</li> * <li>The server knows the file has not been modified because the * {@code If-Modified-Since} date is the same as the file's last * modified date.</li> * </ol> * <p/> * <pre> * Request #1 Headers * =================== * GET /file1.txt HTTP/1.1 * * Response #1 Headers * =================== * HTTP/1.1 200 OK * Date: Tue, 01 Mar 2011 22:44:26 GMT * Last-Modified: Wed, 30 Jun 2010 21:36:48 GMT * Expires: Tue, 01 Mar 2012 22:44:26 GMT * Cache-Control: private, max-age=31536000 * * Request #2 Headers * =================== * GET /file1.txt HTTP/1.1 * If-Modified-Since: Wed, 30 Jun 2010 21:36:48 GMT * * Response #2 Headers * =================== * HTTP/1.1 304 Not Modified * Date: Tue, 01 Mar 2011 22:44:28 GMT * * </pre> */ public class HttpUtils { private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; static final String HTTP_DATE_GMT_TIMEZONE = "GMT"; static final int HTTP_CACHE_SECONDS = 60; private HttpUtils() { } static void sendRedirect(ChannelHandlerContext ctx, String newUri) { log.trace("issuing redirect to {}", newUri); FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND); response.headers().set(LOCATION, newUri); // Close the connection as soon as the redirect message is sent. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } public static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { FullHttpResponse response = new DefaultFullHttpResponse( HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8)); response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); log.trace("issuing error of {}", status); // Close the connection as soon as the error message is sent. ctx.channel().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 */ 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 */ 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())); } /** * Sets the Date and Cache headers for the HTTP Response * * @param response HTTP response * @param fileToCache file to extract content type */ static void setDateAndCacheHeaders(HttpResponse response, Path fileToCache) throws IOException { SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); // Date header Calendar time = new GregorianCalendar(); response.headers().set(DATE, dateFormatter.format(time.getTime())); // Add cache headers time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); response.headers().set(EXPIRES, dateFormatter.format(time.getTime())); response.headers().set(CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); response.headers().set( LAST_MODIFIED, dateFormatter.format(new Date(Files.getLastModifiedTime(fileToCache).toMillis()))); } /** * Sets the content type header for the HTTP Response * * @param response HTTP response * @param file file to extract content type */ static void setContentTypeHeader(HttpResponse response, Path file) { MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); response.headers().set(CONTENT_TYPE, mimeTypesMap.getContentType(file.toString())); } static void setContentTypeHeader(HttpResponse response, String type) { response.headers().set(CONTENT_TYPE, type); } static HttpResponse startResponse(Writer writer) throws IOException { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK); setContentTypeHeader(response, "application/json; charset=utf-8"); return response; } }