17
17
package org .springframework .web .context .request ;
18
18
19
19
import java .security .Principal ;
20
- import java .util .Date ;
20
+ import java .text .ParseException ;
21
+ import java .text .SimpleDateFormat ;
22
+ import java .util .Arrays ;
23
+ import java .util .Enumeration ;
21
24
import java .util .Iterator ;
25
+ import java .util .List ;
22
26
import java .util .Locale ;
23
27
import java .util .Map ;
28
+ import java .util .TimeZone ;
24
29
import java .util .regex .Matcher ;
25
30
import java .util .regex .Pattern ;
31
+
26
32
import javax .servlet .http .HttpServletRequest ;
27
33
import javax .servlet .http .HttpServletResponse ;
28
34
import javax .servlet .http .HttpSession ;
45
51
*/
46
52
public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest {
47
53
48
- private static final String HEADER_ETAG = "ETag" ;
49
-
50
- private static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since" ;
51
-
52
- private static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since" ;
53
-
54
- private static final String HEADER_IF_NONE_MATCH = "If-None-Match" ;
55
-
56
- private static final String HEADER_LAST_MODIFIED = "Last-Modified" ;
54
+ private static final String ETAG = "ETag" ;
57
55
58
- private static final String METHOD_GET = "GET " ;
56
+ private static final String IF_MODIFIED_SINCE = "If-Modified-Since " ;
59
57
60
- private static final String METHOD_HEAD = "HEAD " ;
58
+ private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since " ;
61
59
62
- private static final String METHOD_POST = "POST " ;
60
+ private static final String IF_NONE_MATCH = "If-None-Match " ;
63
61
64
- private static final String METHOD_PUT = "PUT " ;
62
+ private static final String LAST_MODIFIED = "Last-Modified " ;
65
63
66
- private static final String METHOD_DELETE = "DELETE" ;
64
+ private static final List < String > SAFE_METHODS = Arrays . asList ( "GET" , "HEAD" ) ;
67
65
68
66
/**
69
67
* Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match"
70
68
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
71
69
*/
72
70
private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern .compile ("\\ *|\\ s*((W\\ /)?(\" [^\" ]*\" ))\\ s*,?" );
73
71
72
+ /**
73
+ * Date formats as specified in the HTTP RFC
74
+ * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
75
+ */
76
+ private static final String [] DATE_FORMATS = new String [] {
77
+ "EEE, dd MMM yyyy HH:mm:ss zzz" ,
78
+ "EEE, dd-MMM-yy HH:mm:ss zzz" ,
79
+ "EEE MMM dd HH:mm:ss yyyy"
80
+ };
81
+
82
+ private static TimeZone GMT = TimeZone .getTimeZone ("GMT" );
74
83
75
84
/** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */
76
85
private static final boolean servlet3Present =
@@ -194,103 +203,62 @@ public boolean isSecure() {
194
203
195
204
@ Override
196
205
public boolean checkNotModified (long lastModifiedTimestamp ) {
197
- HttpServletResponse response = getResponse ();
198
- if (lastModifiedTimestamp >= 0 && !this .notModified ) {
199
- if (isCompatibleWithConditionalRequests (response )) {
200
- this .notModified = isTimestampNotModified (lastModifiedTimestamp );
201
- if (response != null ) {
202
- if (supportsNotModifiedStatus ()) {
203
- if (this .notModified ) {
204
- response .setStatus (HttpServletResponse .SC_NOT_MODIFIED );
205
- }
206
- if (isHeaderAbsent (response , HEADER_LAST_MODIFIED )) {
207
- response .setDateHeader (HEADER_LAST_MODIFIED , lastModifiedTimestamp );
208
- }
209
- }
210
- else if (supportsConditionalUpdate ()) {
211
- if (this .notModified ) {
212
- response .setStatus (HttpServletResponse .SC_PRECONDITION_FAILED );
213
- }
214
- }
215
- }
216
- }
217
- }
218
- return this .notModified ;
206
+ return checkNotModified (null , lastModifiedTimestamp );
219
207
}
220
208
221
209
@ Override
222
210
public boolean checkNotModified (String etag ) {
223
- HttpServletResponse response = getResponse ();
224
- if (StringUtils .hasLength (etag ) && !this .notModified ) {
225
- if (isCompatibleWithConditionalRequests (response )) {
226
- etag = addEtagPadding (etag );
227
- if (hasRequestHeader (HEADER_IF_NONE_MATCH )) {
228
- this .notModified = isEtagNotModified (etag );
229
- }
230
- if (response != null ) {
231
- if (this .notModified && supportsNotModifiedStatus ()) {
232
- response .setStatus (HttpServletResponse .SC_NOT_MODIFIED );
233
- }
234
- if (isHeaderAbsent (response , HEADER_ETAG )) {
235
- response .setHeader (HEADER_ETAG , etag );
236
- }
237
- }
238
- }
239
- }
240
- return this .notModified ;
211
+ return checkNotModified (etag , -1 );
241
212
}
242
213
243
214
@ Override
244
215
public boolean checkNotModified (String etag , long lastModifiedTimestamp ) {
245
216
HttpServletResponse response = getResponse ();
246
- if (StringUtils .hasLength (etag ) && !this .notModified ) {
247
- if (isCompatibleWithConditionalRequests (response )) {
248
- etag = addEtagPadding (etag );
249
- if (hasRequestHeader (HEADER_IF_NONE_MATCH )) {
250
- this .notModified = isEtagNotModified (etag );
251
- }
252
- else if (hasRequestHeader (HEADER_IF_MODIFIED_SINCE )) {
253
- this .notModified = isTimestampNotModified (lastModifiedTimestamp );
254
- }
255
- if (response != null ) {
256
- if (supportsNotModifiedStatus ()) {
257
- if (this .notModified ) {
258
- response .setStatus (HttpServletResponse .SC_NOT_MODIFIED );
259
- }
260
- if (isHeaderAbsent (response , HEADER_ETAG )) {
261
- response .setHeader (HEADER_ETAG , etag );
262
- }
263
- if (isHeaderAbsent (response , HEADER_LAST_MODIFIED )) {
264
- response .setDateHeader (HEADER_LAST_MODIFIED , lastModifiedTimestamp );
265
- }
266
- }
267
- else if (supportsConditionalUpdate ()) {
268
- if (this .notModified ) {
269
- response .setStatus (HttpServletResponse .SC_PRECONDITION_FAILED );
270
- }
271
- }
272
- }
217
+ if (this .notModified || !isStatusOK (response )) {
218
+ return this .notModified ;
219
+ }
220
+
221
+ // Evaluate conditions in order of precedence.
222
+ // See https://tools.ietf.org/html/rfc7232#section-6
223
+
224
+ if (validateIfUnmodifiedSince (lastModifiedTimestamp )) {
225
+ if (this .notModified ) {
226
+ response .setStatus (HttpStatus .PRECONDITION_FAILED .value ());
273
227
}
228
+ return this .notModified ;
274
229
}
275
- return this .notModified ;
276
- }
277
230
278
- public boolean isNotModified () {
279
- return this .notModified ;
280
- }
231
+ boolean validated = validateIfNoneMatch (etag );
281
232
233
+ if (!validated ) {
234
+ validateIfModifiedSince (lastModifiedTimestamp );
235
+ }
282
236
283
- private boolean isCompatibleWithConditionalRequests (HttpServletResponse response ) {
284
- try {
285
- if (response == null || !servlet3Present ) {
286
- // Can't check response.getStatus() - let's assume we're good
287
- return true ;
237
+ // Update response
238
+
239
+ boolean isHttpGetOrHead = SAFE_METHODS .contains (getRequest ().getMethod ());
240
+ if (this .notModified ) {
241
+ response .setStatus (isHttpGetOrHead ?
242
+ HttpStatus .NOT_MODIFIED .value () : HttpStatus .PRECONDITION_FAILED .value ());
243
+ }
244
+ if (isHttpGetOrHead ) {
245
+ if (lastModifiedTimestamp > 0 && isHeaderAbsent (response , LAST_MODIFIED )) {
246
+ response .setDateHeader (LAST_MODIFIED , lastModifiedTimestamp );
247
+ }
248
+ if (StringUtils .hasLength (etag ) && isHeaderAbsent (response , ETAG )) {
249
+ response .setHeader (ETAG , padEtagIfNecessary (etag ));
288
250
}
289
- return HttpStatus .valueOf (response .getStatus ()).is2xxSuccessful ();
290
251
}
291
- catch (IllegalArgumentException ex ) {
252
+
253
+ return this .notModified ;
254
+ }
255
+
256
+ private boolean isStatusOK (HttpServletResponse response ) {
257
+ if (response == null || !servlet3Present ) {
258
+ // Can't check response.getStatus() - let's assume we're good
292
259
return true ;
293
260
}
261
+ return HttpStatus .OK .value () == 200 ;
294
262
}
295
263
296
264
private boolean isHeaderAbsent (HttpServletResponse response , String header ) {
@@ -301,34 +269,78 @@ private boolean isHeaderAbsent(HttpServletResponse response, String header) {
301
269
return (response .getHeader (header ) == null );
302
270
}
303
271
304
- private boolean hasRequestHeader (String headerName ) {
305
- return StringUtils .hasLength (getHeader (headerName ));
272
+ private boolean validateIfUnmodifiedSince (long lastModifiedTimestamp ) {
273
+ if (lastModifiedTimestamp < 0 ) {
274
+ return false ;
275
+ }
276
+ long ifUnmodifiedSince = parseDateHeader (IF_UNMODIFIED_SINCE );
277
+ if (ifUnmodifiedSince == -1 ) {
278
+ return false ;
279
+ }
280
+ // We will perform this validation...
281
+ this .notModified = (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000 ));
282
+ return true ;
306
283
}
307
284
308
- private boolean supportsNotModifiedStatus () {
309
- String method = getRequest ().getMethod ();
310
- return (METHOD_GET .equals (method ) || METHOD_HEAD .equals (method ));
285
+ private boolean validateIfNoneMatch (String etag ) {
286
+ if (!StringUtils .hasLength (etag )) {
287
+ return false ;
288
+ }
289
+ Enumeration <String > ifNoneMatch ;
290
+ try {
291
+ ifNoneMatch = getRequest ().getHeaders (IF_NONE_MATCH );
292
+ }
293
+ catch (IllegalArgumentException ex ) {
294
+ return false ;
295
+ }
296
+ if (!ifNoneMatch .hasMoreElements ()) {
297
+ return false ;
298
+ }
299
+ // We will perform this validation...
300
+ etag = padEtagIfNecessary (etag );
301
+ while (ifNoneMatch .hasMoreElements ()) {
302
+ String clientETags = ifNoneMatch .nextElement ();
303
+
304
+ Matcher eTagMatcher = ETAG_HEADER_VALUE_PATTERN .matcher (clientETags );
305
+ // Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
306
+ while (eTagMatcher .find ()) {
307
+ if (StringUtils .hasLength (eTagMatcher .group ())
308
+ && etag .replaceFirst ("^W/" , "" ).equals (eTagMatcher .group (3 ))) {
309
+ this .notModified = true ;
310
+ break ;
311
+ }
312
+ }
313
+ }
314
+ return true ;
311
315
}
312
316
313
- private boolean supportsConditionalUpdate () {
314
- String method = getRequest ().getMethod ();
315
- return (METHOD_POST .equals (method ) || METHOD_PUT .equals (method ) || METHOD_DELETE .equals (method ))
316
- && hasRequestHeader (HEADER_IF_UNMODIFIED_SINCE );
317
+ private String padEtagIfNecessary (String etag ) {
318
+ if (!StringUtils .hasLength (etag )) {
319
+ return etag ;
320
+ }
321
+ if ((etag .startsWith ("\" " ) || etag .startsWith ("W/\" " )) && etag .endsWith ("\" " )) {
322
+ return etag ;
323
+ }
324
+ return "\" " + etag + "\" " ;
317
325
}
318
326
319
- private boolean isTimestampNotModified (long lastModifiedTimestamp ) {
320
- long ifModifiedSince = parseDateHeader (HEADER_IF_MODIFIED_SINCE );
321
- if (ifModifiedSince != -1 ) {
322
- return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000 ));
327
+ private boolean validateIfModifiedSince (long lastModifiedTimestamp ) {
328
+ if (lastModifiedTimestamp < 0 ) {
329
+ return false ;
323
330
}
324
- long ifUnmodifiedSince = parseDateHeader (HEADER_IF_UNMODIFIED_SINCE );
325
- if (ifUnmodifiedSince ! = -1 ) {
326
- return ( ifUnmodifiedSince < ( lastModifiedTimestamp / 1000 * 1000 )) ;
331
+ long ifModifiedSince = parseDateHeader (IF_MODIFIED_SINCE );
332
+ if (ifModifiedSince = = -1 ) {
333
+ return false ;
327
334
}
328
- return false ;
335
+ // We will perform this validation...
336
+ this .notModified = ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000 );
337
+ return true ;
338
+ }
339
+
340
+ public boolean isNotModified () {
341
+ return this .notModified ;
329
342
}
330
343
331
- @ SuppressWarnings ("deprecation" )
332
344
private long parseDateHeader (String headerName ) {
333
345
long dateValue = -1 ;
334
346
try {
@@ -340,36 +352,32 @@ private long parseDateHeader(String headerName) {
340
352
int separatorIndex = headerValue .indexOf (';' );
341
353
if (separatorIndex != -1 ) {
342
354
String datePart = headerValue .substring (0 , separatorIndex );
343
- try {
344
- dateValue = Date .parse (datePart );
345
- }
346
- catch (IllegalArgumentException ex2 ) {
347
- // Giving up
348
- }
355
+ dateValue = parseDateValue (datePart );
349
356
}
350
357
}
351
358
return dateValue ;
352
359
}
353
360
354
- private boolean isEtagNotModified (String etag ) {
355
- String ifNoneMatch = getHeader (HEADER_IF_NONE_MATCH );
356
- // compare weak/strong ETag as per https://tools.ietf.org/html/rfc7232#section-2.3
357
- String serverETag = etag .replaceFirst ("^W/" , "" );
358
- Matcher eTagMatcher = ETAG_HEADER_VALUE_PATTERN .matcher (ifNoneMatch );
359
- while (eTagMatcher .find ()) {
360
- if ("*" .equals (eTagMatcher .group ())
361
- || serverETag .equals (eTagMatcher .group (3 ))) {
362
- return true ;
363
- }
361
+ private long parseDateValue (String headerValue ) {
362
+ if (headerValue == null ) {
363
+ // No header value sent at all
364
+ return -1 ;
364
365
}
365
- return false ;
366
- }
367
-
368
- private String addEtagPadding (String etag ) {
369
- if (!(etag .startsWith ("\" " ) || etag .startsWith ("W/\" " )) || !etag .endsWith ("\" " )) {
370
- etag = "\" " + etag + "\" " ;
366
+ if (headerValue .length () >= 3 ) {
367
+ // Short "0" or "-1" like values are never valid HTTP date headers...
368
+ // Let's only bother with SimpleDateFormat parsing for long enough values.
369
+ for (String dateFormat : DATE_FORMATS ) {
370
+ SimpleDateFormat simpleDateFormat = new SimpleDateFormat (dateFormat , Locale .US );
371
+ simpleDateFormat .setTimeZone (GMT );
372
+ try {
373
+ return simpleDateFormat .parse (headerValue ).getTime ();
374
+ }
375
+ catch (ParseException ex ) {
376
+ // ignore
377
+ }
378
+ }
371
379
}
372
- return etag ;
380
+ return - 1 ;
373
381
}
374
382
375
383
@ Override
0 commit comments