HttpWriteCacheEnable.java

/*
 * This file is part of Waarp Project (named also Waarp or GG).
 *
 *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
 *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
 * individual contributors.
 *
 *  All Waarp Project is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with
 * Waarp . If not, see <http://www.gnu.org/licenses/>.
 */
package org.waarp.gateway.kernel.http;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelFuture;
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.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import org.waarp.common.logging.SysErrLogger;
import org.waarp.common.utility.WaarpNettyUtil;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;

import static io.netty.handler.codec.http.HttpVersion.*;

/**
 * Utility class to write external file with cache enable properties
 */
public final class HttpWriteCacheEnable {
  /**
   * US locale - all HTTP dates are in english
   */
  public static final Locale LOCALE_US = Locale.US;

  /**
   * GMT timezone - all HTTP dates are on GMT
   */
  public static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT");

  /**
   * format for RFC 1123 date string -- "Sun, 06 Nov 1994 08:49:37 GMT"
   */
  public static final String RFC1123_PATTERN = "EEE, dd MMM yyyyy HH:mm:ss z";

  private static final ArrayList<String> cache_control;

  private static final int MAX_AGE_SECOND = 604800;

  static {
    cache_control = new ArrayList<String>(2);
    cache_control.add(HttpHeaderValues.PUBLIC.toString());
    cache_control.add(HttpHeaderValues.MAX_AGE + "=" + MAX_AGE_SECOND);// 1 week
    cache_control.add(HttpHeaderValues.MUST_REVALIDATE.toString());
  }

  /**
   * set MIME TYPE if possible
   */
  public static final MimetypesFileTypeMap mimetypesFileTypeMap =
      new MimetypesFileTypeMap();

  static {
    mimetypesFileTypeMap.addMimeTypes("text/css css CSS");
    mimetypesFileTypeMap.addMimeTypes("text/javascript js JS");
    // Official but not supported mimetypesFileTypeMap.addMimeTypes("application/javascript js JS")
    mimetypesFileTypeMap.addMimeTypes("application/json json JSON map MAP");
    mimetypesFileTypeMap.addMimeTypes("text/plain txt text TXT");
    mimetypesFileTypeMap.addMimeTypes("text/html htm html HTM HTML htmls htx");
    mimetypesFileTypeMap.addMimeTypes("image/jpeg jpe jpeg jpg JPG");
    mimetypesFileTypeMap.addMimeTypes("image/png png PNG");
    mimetypesFileTypeMap.addMimeTypes("image/gif gif GIF");
    mimetypesFileTypeMap.addMimeTypes("image/x-icon ico ICO");
  }

  private HttpWriteCacheEnable() {
  }

  /**
   * Write a file, taking into account cache enabled and removing session
   * cookie
   *
   * @param request
   * @param ctx
   * @param filename
   * @param cookieNameToRemove
   */
  public static void writeFile(final HttpRequest request,
                               final ChannelHandlerContext ctx,
                               final String filename,
                               final String cookieNameToRemove) {
    // Convert the response content to a ByteBuf.
    HttpResponse response;
    final boolean keepAlive = HttpUtil.isKeepAlive(request);
    final File file = new File(filename);
    if (!file.isFile() || !file.canRead()) {
      SysErrLogger.FAKE_LOGGER.syserr("Cannot read " + file.getAbsolutePath());
      sendError(request, ctx, cookieNameToRemove, keepAlive);
      return;
    }
    final DateFormat rfc1123Format =
        new SimpleDateFormat(RFC1123_PATTERN, LOCALE_US);
    rfc1123Format.setTimeZone(GMT_ZONE);
    final Date lastModifDate = new Date(file.lastModified());
    if (request.headers().contains(HttpHeaderNames.IF_MODIFIED_SINCE)) {
      final String sdate =
          request.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE);
      try {
        final Date ifmodif = rfc1123Format.parse(sdate);
        if (ifmodif.after(lastModifDate)) {
          response = new DefaultHttpResponse(HTTP_1_1,
                                             HttpResponseStatus.NOT_MODIFIED);
          handleCookies(request, response, cookieNameToRemove);
          ctx.writeAndFlush(response);
          return;
        }
      } catch (final ParseException ignored) {
        // nothing
      }
    }
    final int fileLength = (int) file.length();
    final ByteBuf byteBuf =
        ByteBufAllocator.DEFAULT.buffer(fileLength, fileLength);
    FileInputStream inputStream = null;
    try {
      inputStream = new FileInputStream(file);
      byteBuf.writeBytes(inputStream, fileLength);
    } catch (final FileNotFoundException e) {
      WaarpNettyUtil.release(byteBuf);
      sendError(request, ctx, cookieNameToRemove, keepAlive);
      return;
    } catch (final IOException e) {
      WaarpNettyUtil.release(byteBuf);
      sendError(request, ctx, cookieNameToRemove, keepAlive);
      return;
    }

    response =
        new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.OK, byteBuf);
    response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);
    setContentTypeHeader(response, file);
    setDateAndCacheHeaders(response, rfc1123Format, lastModifDate);

    if (!keepAlive) {
      response.headers()
              .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
    } else if (request.protocolVersion().equals(HTTP_1_0)) {
      response.headers()
              .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
    }

    // Write the initial line and the header.
    final ChannelFuture sendFileFuture = ctx.writeAndFlush(response);

    // Decide whether to close the connection or not.
    if (!keepAlive) {
      // Close the connection when the whole content is written out.
      sendFileFuture.addListener(ChannelFutureListener.CLOSE);
    }
  }

  /**
   * Sets the content type header for the HTTP Response
   *
   * @param response HTTP response
   * @param file file to extract content type
   */
  private static void setContentTypeHeader(final HttpResponse response,
                                           final File file) {
    response.headers().set(HttpHeaderNames.CONTENT_TYPE,
                           mimetypesFileTypeMap.getContentType(file.getPath()));
  }

  /**
   * Sets the Date and Cache headers for the HTTP Response
   *
   * @param response HTTP response
   * @param rfc1123Format
   * @param lastModifDate
   */
  private static void setDateAndCacheHeaders(final HttpResponse response,
                                             final DateFormat rfc1123Format,
                                             final Date lastModifDate) {
    // Date header
    final Calendar time = new GregorianCalendar();
    response.headers()
            .set(HttpHeaderNames.DATE, rfc1123Format.format(time.getTime()));

    // Add cache headers
    time.add(Calendar.SECOND, MAX_AGE_SECOND);
    response.headers()
            .set(HttpHeaderNames.EXPIRES, rfc1123Format.format(time.getTime()));
    response.headers().set(HttpHeaderNames.CACHE_CONTROL, cache_control);
    response.headers().set(HttpHeaderNames.LAST_MODIFIED,
                           rfc1123Format.format(lastModifDate));
  }

  private static void sendError(final HttpRequest request,
                                final ChannelHandlerContext ctx,
                                final String cookieNameToRemove,
                                final boolean keepAlive) {
    final HttpResponse response;
    response =
        new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.NOT_FOUND);
    response.headers().add(HttpHeaderNames.CONTENT_LENGTH, 0);
    if (!keepAlive) {
      response.headers()
              .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
    } else if (request.protocolVersion().equals(HTTP_1_0)) {
      response.headers()
              .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
    }
    handleCookies(request, response, cookieNameToRemove);
    final ChannelFuture sendFileFuture = ctx.writeAndFlush(response);
    // Decide whether to close the connection or not.
    if (!keepAlive) {
      // Close the connection when the whole content is written out.
      sendFileFuture.addListener(ChannelFutureListener.CLOSE);
    }
  }

  /**
   * Remove the given named cookie
   *
   * @param request
   * @param response
   * @param cookieNameToRemove
   */
  public static void handleCookies(final HttpRequest request,
                                   final HttpResponse response,
                                   final String cookieNameToRemove) {
    final String cookieString = request.headers().get(HttpHeaderNames.COOKIE);
    if (cookieString != null) {
      final Set<Cookie> cookies = ServerCookieDecoder.LAX.decode(cookieString);
      if (!cookies.isEmpty()) {
        // Reset the sessions if necessary.
        // Remove all Session for images
        for (final Cookie cookie : cookies) {
          if (cookie.name().equalsIgnoreCase(cookieNameToRemove)) {
            // nothing
          } else {
            response.headers().add(HttpHeaderNames.SET_COOKIE,
                                   ServerCookieEncoder.LAX.encode(cookie));
          }
        }
      }
    }
  }

}