1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

18
19 package io.undertow.util;
20
21 import io.undertow.UndertowOptions;
22 import io.undertow.server.HttpServerExchange;
23
24 import java.text.ParsePosition;
25 import java.text.SimpleDateFormat;
26 import java.util.Date;
27 import java.util.Locale;
28 import java.util.TimeZone;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.atomic.AtomicReference;
31
32 /**
33  * Utility for parsing and generating dates
34  *
35  * @author Stuart Douglas
36  */

37 public class DateUtils {
38
39     private static final Locale LOCALE_US = Locale.US;
40
41     private static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT");
42
43     private static final String RFC1123_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z";
44
45     private static final AtomicReference<String> cachedDateString = new AtomicReference<>();
46
47     /**
48      * Thread local cache of this date format. This is technically a small memory leak, however
49      * in practice it is fine, as it will only be used by server threads.
50      * <p>
51      * This is the most common date format, which is why we cache it.
52      */

53     private static final ThreadLocal<SimpleDateFormat> RFC1123_PATTERN_FORMAT = new ThreadLocal<SimpleDateFormat>() {
54         @Override
55         protected SimpleDateFormat initialValue() {
56             SimpleDateFormat df = new SimpleDateFormat(RFC1123_PATTERN, LOCALE_US);
57             return df;
58         }
59     };
60
61     /**
62      * Invalidates the current date
63      */

64     private static final Runnable INVALIDATE_TASK = new Runnable() {
65         @Override
66         public void run() {
67             cachedDateString.set(null);
68         }
69     };
70
71     private static final String RFC1036_PATTERN = "EEEEEEEEE, dd-MMM-yy HH:mm:ss z";
72
73     private static final String ASCITIME_PATTERN = "EEE MMM d HH:mm:ss yyyyy";
74
75     private static final String OLD_COOKIE_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss z";
76
77     private static final String COMMON_LOG_PATTERN = "[dd/MMM/yyyy:HH:mm:ss Z]";
78
79     private static final ThreadLocal<SimpleDateFormat> COMMON_LOG_PATTERN_FORMAT = new ThreadLocal<SimpleDateFormat>() {
80         @Override
81         protected SimpleDateFormat initialValue() {
82             SimpleDateFormat df = new SimpleDateFormat(COMMON_LOG_PATTERN, LOCALE_US);
83             return df;
84         }
85     };
86
87     private static final ThreadLocal<SimpleDateFormat> OLD_COOKIE_FORMAT = new ThreadLocal<SimpleDateFormat>() {
88         @Override
89         protected SimpleDateFormat initialValue() {
90             SimpleDateFormat df = new SimpleDateFormat(OLD_COOKIE_PATTERN, LOCALE_US);
91             df.setTimeZone(GMT_ZONE);
92             return df;
93         }
94     };
95
96     /**
97      * Converts a date to a format suitable for use in a HTTP request
98      *
99      * @param date The date
100      * @return The RFC-1123 formatted date
101      */

102     public static String toDateString(final Date date) {
103         SimpleDateFormat df = RFC1123_PATTERN_FORMAT.get();
104         //we always need to set the time zone
105         //because date format is stupid, and calling parse() can mutate the timezone
106         //see UNDERTOW-458
107         df.setTimeZone(GMT_ZONE);
108         return df.format(date);
109     }
110
111
112     public static String toOldCookieDateString(final Date date) {
113         return OLD_COOKIE_FORMAT.get().format(date);
114     }
115
116     public static String toCommonLogFormat(final Date date) {
117         return COMMON_LOG_PATTERN_FORMAT.get().format(date);
118     }
119
120     /**
121      * Attempts to pass a HTTP date.
122      *
123      * @param date The date to parse
124      * @return The parsed date, or null if parsing failed
125      */

126     public static Date parseDate(final String date) {
127
128         /*
129             IE9 sends a superflous lenght parameter after date in the
130             If-Modified-Since header, which needs to be stripped before
131             parsing.
132
133          */

134
135         final int semicolonIndex = date.indexOf(';');
136         final String trimmedDate = semicolonIndex >= 0 ? date.substring(0, semicolonIndex) : date;
137
138         ParsePosition pp = new ParsePosition(0);
139         SimpleDateFormat dateFormat = RFC1123_PATTERN_FORMAT.get();
140         dateFormat.setTimeZone(GMT_ZONE);
141         Date val = dateFormat.parse(trimmedDate, pp);
142         if (val != null && pp.getIndex() == trimmedDate.length()) {
143             return val;
144         }
145
146         pp = new ParsePosition(0);
147         dateFormat = new SimpleDateFormat(RFC1036_PATTERN, LOCALE_US);
148         dateFormat.setTimeZone(GMT_ZONE);
149         val = dateFormat.parse(trimmedDate, pp);
150         if (val != null && pp.getIndex() == trimmedDate.length()) {
151             return val;
152         }
153
154         pp = new ParsePosition(0);
155         dateFormat = new SimpleDateFormat(ASCITIME_PATTERN, LOCALE_US);
156         dateFormat.setTimeZone(GMT_ZONE);
157         val = dateFormat.parse(trimmedDate, pp);
158         if (val != null && pp.getIndex() == trimmedDate.length()) {
159             return val;
160         }
161
162         pp = new ParsePosition(0);
163         dateFormat = new SimpleDateFormat(OLD_COOKIE_PATTERN, LOCALE_US);
164         dateFormat.setTimeZone(GMT_ZONE);
165         val = dateFormat.parse(trimmedDate, pp);
166         if (val != null && pp.getIndex() == trimmedDate.length()) {
167             return val;
168         }
169
170         return null;
171     }
172
173     /**
174      * Handles the if-modified-since header. returns true if the request should proceed, false otherwise
175      *
176      * @param exchange     the exchange
177      * @param lastModified The last modified date
178      * @return
179      */

180     public static boolean handleIfModifiedSince(final HttpServerExchange exchange, final Date lastModified) {
181         return handleIfModifiedSince(exchange.getRequestHeaders().getFirst(Headers.IF_MODIFIED_SINCE), lastModified);
182     }
183
184     /**
185      * Handles the if-modified-since header. returns true if the request should proceed, false otherwise
186      *
187      * @param modifiedSince the modified since date
188      * @param lastModified  The last modified date
189      * @return
190      */

191     public static boolean handleIfModifiedSince(final String modifiedSince, final Date lastModified) {
192         if (lastModified == null) {
193             return true;
194         }
195         if (modifiedSince == null) {
196             return true;
197         }
198         Date modDate = parseDate(modifiedSince);
199         if (modDate == null) {
200             return true;
201         }
202         return lastModified.getTime() > (modDate.getTime() + 999); //UNDERTOW-341 +999 as there is no millisecond part in the if-modified-since
203     }
204
205     /**
206      * Handles the if-unmodified-since header. returns true if the request should proceed, false otherwise
207      *
208      * @param exchange     the exchange
209      * @param lastModified The last modified date
210      * @return
211      */

212     public static boolean handleIfUnmodifiedSince(final HttpServerExchange exchange, final Date lastModified) {
213         return handleIfUnmodifiedSince(exchange.getRequestHeaders().getFirst(Headers.IF_UNMODIFIED_SINCE), lastModified);
214     }
215
216     /**
217      * Handles the if-unmodified-since header. returns true if the request should proceed, false otherwise
218      *
219      * @param modifiedSince the if unmodified since date
220      * @param lastModified  The last modified date
221      * @return
222      */

223     public static boolean handleIfUnmodifiedSince(final String modifiedSince, final Date lastModified) {
224         if (lastModified == null) {
225             return true;
226         }
227         if (modifiedSince == null) {
228             return true;
229         }
230         Date modDate = parseDate(modifiedSince);
231         if (modDate == null) {
232             return true;
233         }
234         return lastModified.getTime() < (modDate.getTime() + 999); //UNDERTOW-341 +999 as there is no millisecond part in the if-unmodified-since
235     }
236
237     public static void addDateHeaderIfRequired(HttpServerExchange exchange) {
238         HeaderMap responseHeaders = exchange.getResponseHeaders();
239         if (exchange.getConnection().getUndertowOptions().get(UndertowOptions.ALWAYS_SET_DATE, true) && !responseHeaders.contains(Headers.DATE)) {
240             String dateString = getCurrentDateTime(exchange);
241             responseHeaders.put(Headers.DATE, dateString);
242         }
243     }
244
245     public static String getCurrentDateTime(HttpServerExchange exchange) {
246         String dateString = cachedDateString.get();
247         if (dateString == null) {
248             //set the time and register a timer to invalidate it
249             //note that this is racey, it does not matter if multiple threads do this
250             //the perf cost of synchronizing would be more than the perf cost of multiple threads running it
251             long realTime = System.currentTimeMillis();
252             long mod = realTime % 1000;
253             long toGo = 1000 - mod;
254             dateString = DateUtils.toDateString(new Date(realTime));
255             if (cachedDateString.compareAndSet(null, dateString)) {
256                 WorkerUtils.executeAfter(exchange.getIoThread(), INVALIDATE_TASK, toGo, TimeUnit.MILLISECONDS);
257             }
258         }
259         return dateString;
260     }
261
262     private DateUtils() {
263
264     }
265
266 }
267