View Javadoc
1   /*
2    * This file is part of Waarp Project (named also Waarp or GG).
3    *
4    *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
5    *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
6    * individual contributors.
7    *
8    *  All Waarp Project is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or (at your
11   * option) any later version.
12   *
13   * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
14   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15   * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16   *
17   *  You should have received a copy of the GNU General Public License along with
18   * Waarp . If not, see <http://www.gnu.org/licenses/>.
19   */
20  package org.waarp.gateway.kernel.http;
21  
22  import io.netty.buffer.ByteBuf;
23  import io.netty.buffer.ByteBufAllocator;
24  import io.netty.channel.ChannelFuture;
25  import io.netty.channel.ChannelFutureListener;
26  import io.netty.channel.ChannelHandlerContext;
27  import io.netty.handler.codec.http.DefaultFullHttpResponse;
28  import io.netty.handler.codec.http.DefaultHttpResponse;
29  import io.netty.handler.codec.http.HttpHeaderNames;
30  import io.netty.handler.codec.http.HttpHeaderValues;
31  import io.netty.handler.codec.http.HttpRequest;
32  import io.netty.handler.codec.http.HttpResponse;
33  import io.netty.handler.codec.http.HttpResponseStatus;
34  import io.netty.handler.codec.http.HttpUtil;
35  import io.netty.handler.codec.http.cookie.Cookie;
36  import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
37  import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
38  import org.waarp.common.logging.SysErrLogger;
39  import org.waarp.common.utility.WaarpNettyUtil;
40  
41  import javax.activation.MimetypesFileTypeMap;
42  import java.io.File;
43  import java.io.FileInputStream;
44  import java.io.FileNotFoundException;
45  import java.io.IOException;
46  import java.text.DateFormat;
47  import java.text.ParseException;
48  import java.text.SimpleDateFormat;
49  import java.util.ArrayList;
50  import java.util.Calendar;
51  import java.util.Date;
52  import java.util.GregorianCalendar;
53  import java.util.Locale;
54  import java.util.Set;
55  import java.util.TimeZone;
56  
57  import static io.netty.handler.codec.http.HttpVersion.*;
58  
59  /**
60   * Utility class to write external file with cache enable properties
61   */
62  public final class HttpWriteCacheEnable {
63    /**
64     * US locale - all HTTP dates are in english
65     */
66    public static final Locale LOCALE_US = Locale.US;
67  
68    /**
69     * GMT timezone - all HTTP dates are on GMT
70     */
71    public static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT");
72  
73    /**
74     * format for RFC 1123 date string -- "Sun, 06 Nov 1994 08:49:37 GMT"
75     */
76    public static final String RFC1123_PATTERN = "EEE, dd MMM yyyyy HH:mm:ss z";
77  
78    private static final ArrayList<String> cache_control;
79  
80    private static final int MAX_AGE_SECOND = 604800;
81  
82    static {
83      cache_control = new ArrayList<String>(2);
84      cache_control.add(HttpHeaderValues.PUBLIC.toString());
85      cache_control.add(HttpHeaderValues.MAX_AGE + "=" + MAX_AGE_SECOND);// 1 week
86      cache_control.add(HttpHeaderValues.MUST_REVALIDATE.toString());
87    }
88  
89    /**
90     * set MIME TYPE if possible
91     */
92    public static final MimetypesFileTypeMap mimetypesFileTypeMap =
93        new MimetypesFileTypeMap();
94  
95    static {
96      mimetypesFileTypeMap.addMimeTypes("text/css css CSS");
97      mimetypesFileTypeMap.addMimeTypes("text/javascript js JS");
98      // Official but not supported mimetypesFileTypeMap.addMimeTypes("application/javascript js JS")
99      mimetypesFileTypeMap.addMimeTypes("application/json json JSON map MAP");
100     mimetypesFileTypeMap.addMimeTypes("text/plain txt text TXT");
101     mimetypesFileTypeMap.addMimeTypes("text/html htm html HTM HTML htmls htx");
102     mimetypesFileTypeMap.addMimeTypes("image/jpeg jpe jpeg jpg JPG");
103     mimetypesFileTypeMap.addMimeTypes("image/png png PNG");
104     mimetypesFileTypeMap.addMimeTypes("image/gif gif GIF");
105     mimetypesFileTypeMap.addMimeTypes("image/x-icon ico ICO");
106   }
107 
108   private HttpWriteCacheEnable() {
109   }
110 
111   /**
112    * Write a file, taking into account cache enabled and removing session
113    * cookie
114    *
115    * @param request
116    * @param ctx
117    * @param filename
118    * @param cookieNameToRemove
119    */
120   public static void writeFile(final HttpRequest request,
121                                final ChannelHandlerContext ctx,
122                                final String filename,
123                                final String cookieNameToRemove) {
124     // Convert the response content to a ByteBuf.
125     HttpResponse response;
126     final boolean keepAlive = HttpUtil.isKeepAlive(request);
127     final File file = new File(filename);
128     if (!file.isFile() || !file.canRead()) {
129       SysErrLogger.FAKE_LOGGER.syserr("Cannot read " + file.getAbsolutePath());
130       sendError(request, ctx, cookieNameToRemove, keepAlive);
131       return;
132     }
133     final DateFormat rfc1123Format =
134         new SimpleDateFormat(RFC1123_PATTERN, LOCALE_US);
135     rfc1123Format.setTimeZone(GMT_ZONE);
136     final Date lastModifDate = new Date(file.lastModified());
137     if (request.headers().contains(HttpHeaderNames.IF_MODIFIED_SINCE)) {
138       final String sdate =
139           request.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE);
140       try {
141         final Date ifmodif = rfc1123Format.parse(sdate);
142         if (ifmodif.after(lastModifDate)) {
143           response = new DefaultHttpResponse(HTTP_1_1,
144                                              HttpResponseStatus.NOT_MODIFIED);
145           handleCookies(request, response, cookieNameToRemove);
146           ctx.writeAndFlush(response);
147           return;
148         }
149       } catch (final ParseException ignored) {
150         // nothing
151       }
152     }
153     final int fileLength = (int) file.length();
154     final ByteBuf byteBuf =
155         ByteBufAllocator.DEFAULT.buffer(fileLength, fileLength);
156     FileInputStream inputStream = null;
157     try {
158       inputStream = new FileInputStream(file);
159       byteBuf.writeBytes(inputStream, fileLength);
160     } catch (final FileNotFoundException e) {
161       WaarpNettyUtil.release(byteBuf);
162       sendError(request, ctx, cookieNameToRemove, keepAlive);
163       return;
164     } catch (final IOException e) {
165       WaarpNettyUtil.release(byteBuf);
166       sendError(request, ctx, cookieNameToRemove, keepAlive);
167       return;
168     }
169 
170     response =
171         new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.OK, byteBuf);
172     response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);
173     setContentTypeHeader(response, file);
174     setDateAndCacheHeaders(response, rfc1123Format, lastModifDate);
175 
176     if (!keepAlive) {
177       response.headers()
178               .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
179     } else if (request.protocolVersion().equals(HTTP_1_0)) {
180       response.headers()
181               .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
182     }
183 
184     // Write the initial line and the header.
185     final ChannelFuture sendFileFuture = ctx.writeAndFlush(response);
186 
187     // Decide whether to close the connection or not.
188     if (!keepAlive) {
189       // Close the connection when the whole content is written out.
190       sendFileFuture.addListener(ChannelFutureListener.CLOSE);
191     }
192   }
193 
194   /**
195    * Sets the content type header for the HTTP Response
196    *
197    * @param response HTTP response
198    * @param file file to extract content type
199    */
200   private static void setContentTypeHeader(final HttpResponse response,
201                                            final File file) {
202     response.headers().set(HttpHeaderNames.CONTENT_TYPE,
203                            mimetypesFileTypeMap.getContentType(file.getPath()));
204   }
205 
206   /**
207    * Sets the Date and Cache headers for the HTTP Response
208    *
209    * @param response HTTP response
210    * @param rfc1123Format
211    * @param lastModifDate
212    */
213   private static void setDateAndCacheHeaders(final HttpResponse response,
214                                              final DateFormat rfc1123Format,
215                                              final Date lastModifDate) {
216     // Date header
217     final Calendar time = new GregorianCalendar();
218     response.headers()
219             .set(HttpHeaderNames.DATE, rfc1123Format.format(time.getTime()));
220 
221     // Add cache headers
222     time.add(Calendar.SECOND, MAX_AGE_SECOND);
223     response.headers()
224             .set(HttpHeaderNames.EXPIRES, rfc1123Format.format(time.getTime()));
225     response.headers().set(HttpHeaderNames.CACHE_CONTROL, cache_control);
226     response.headers().set(HttpHeaderNames.LAST_MODIFIED,
227                            rfc1123Format.format(lastModifDate));
228   }
229 
230   private static void sendError(final HttpRequest request,
231                                 final ChannelHandlerContext ctx,
232                                 final String cookieNameToRemove,
233                                 final boolean keepAlive) {
234     final HttpResponse response;
235     response =
236         new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.NOT_FOUND);
237     response.headers().add(HttpHeaderNames.CONTENT_LENGTH, 0);
238     if (!keepAlive) {
239       response.headers()
240               .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
241     } else if (request.protocolVersion().equals(HTTP_1_0)) {
242       response.headers()
243               .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
244     }
245     handleCookies(request, response, cookieNameToRemove);
246     final ChannelFuture sendFileFuture = ctx.writeAndFlush(response);
247     // Decide whether to close the connection or not.
248     if (!keepAlive) {
249       // Close the connection when the whole content is written out.
250       sendFileFuture.addListener(ChannelFutureListener.CLOSE);
251     }
252   }
253 
254   /**
255    * Remove the given named cookie
256    *
257    * @param request
258    * @param response
259    * @param cookieNameToRemove
260    */
261   public static void handleCookies(final HttpRequest request,
262                                    final HttpResponse response,
263                                    final String cookieNameToRemove) {
264     final String cookieString = request.headers().get(HttpHeaderNames.COOKIE);
265     if (cookieString != null) {
266       final Set<Cookie> cookies = ServerCookieDecoder.LAX.decode(cookieString);
267       if (!cookies.isEmpty()) {
268         // Reset the sessions if necessary.
269         // Remove all Session for images
270         for (final Cookie cookie : cookies) {
271           if (cookie.name().equalsIgnoreCase(cookieNameToRemove)) {
272             // nothing
273           } else {
274             response.headers().add(HttpHeaderNames.SET_COOKIE,
275                                    ServerCookieEncoder.LAX.encode(cookie));
276           }
277         }
278       }
279     }
280   }
281 
282 }