001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.context.request; 018 019import java.security.Principal; 020import java.text.ParseException; 021import java.text.SimpleDateFormat; 022import java.util.Arrays; 023import java.util.Enumeration; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Locale; 027import java.util.Map; 028import java.util.TimeZone; 029import java.util.regex.Matcher; 030import java.util.regex.Pattern; 031import javax.servlet.http.HttpServletRequest; 032import javax.servlet.http.HttpServletResponse; 033import javax.servlet.http.HttpSession; 034 035import org.springframework.http.HttpMethod; 036import org.springframework.http.HttpStatus; 037import org.springframework.util.ClassUtils; 038import org.springframework.util.CollectionUtils; 039import org.springframework.util.ObjectUtils; 040import org.springframework.util.StringUtils; 041import org.springframework.web.util.WebUtils; 042 043/** 044 * {@link WebRequest} adapter for an {@link javax.servlet.http.HttpServletRequest}. 045 * 046 * @author Juergen Hoeller 047 * @author Brian Clozel 048 * @author Markus Malkusch 049 * @since 2.0 050 */ 051public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest { 052 053 private static final String ETAG = "ETag"; 054 055 private static final String IF_MODIFIED_SINCE = "If-Modified-Since"; 056 057 private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; 058 059 private static final String IF_NONE_MATCH = "If-None-Match"; 060 061 private static final String LAST_MODIFIED = "Last-Modified"; 062 063 private static final List<String> SAFE_METHODS = Arrays.asList("GET", "HEAD"); 064 065 /** 066 * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match". 067 * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a> 068 */ 069 private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); 070 071 /** 072 * Date formats as specified in the HTTP RFC. 073 * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a> 074 */ 075 private static final String[] DATE_FORMATS = new String[] { 076 "EEE, dd MMM yyyy HH:mm:ss zzz", 077 "EEE, dd-MMM-yy HH:mm:ss zzz", 078 "EEE MMM dd HH:mm:ss yyyy" 079 }; 080 081 private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); 082 083 /** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */ 084 private static final boolean servlet3Present = 085 ClassUtils.hasMethod(HttpServletResponse.class, "getHeader", String.class); 086 087 private boolean notModified = false; 088 089 090 /** 091 * Create a new ServletWebRequest instance for the given request. 092 * @param request current HTTP request 093 */ 094 public ServletWebRequest(HttpServletRequest request) { 095 super(request); 096 } 097 098 /** 099 * Create a new ServletWebRequest instance for the given request/response pair. 100 * @param request current HTTP request 101 * @param response current HTTP response (for automatic last-modified handling) 102 */ 103 public ServletWebRequest(HttpServletRequest request, HttpServletResponse response) { 104 super(request, response); 105 } 106 107 108 @Override 109 public Object getNativeRequest() { 110 return getRequest(); 111 } 112 113 @Override 114 public Object getNativeResponse() { 115 return getResponse(); 116 } 117 118 @Override 119 public <T> T getNativeRequest(Class<T> requiredType) { 120 return WebUtils.getNativeRequest(getRequest(), requiredType); 121 } 122 123 @Override 124 public <T> T getNativeResponse(Class<T> requiredType) { 125 return WebUtils.getNativeResponse(getResponse(), requiredType); 126 } 127 128 /** 129 * Return the HTTP method of the request. 130 * @since 4.0.2 131 */ 132 public HttpMethod getHttpMethod() { 133 return HttpMethod.resolve(getRequest().getMethod()); 134 } 135 136 @Override 137 public String getHeader(String headerName) { 138 return getRequest().getHeader(headerName); 139 } 140 141 @Override 142 public String[] getHeaderValues(String headerName) { 143 String[] headerValues = StringUtils.toStringArray(getRequest().getHeaders(headerName)); 144 return (!ObjectUtils.isEmpty(headerValues) ? headerValues : null); 145 } 146 147 @Override 148 public Iterator<String> getHeaderNames() { 149 return CollectionUtils.toIterator(getRequest().getHeaderNames()); 150 } 151 152 @Override 153 public String getParameter(String paramName) { 154 return getRequest().getParameter(paramName); 155 } 156 157 @Override 158 public String[] getParameterValues(String paramName) { 159 return getRequest().getParameterValues(paramName); 160 } 161 162 @Override 163 public Iterator<String> getParameterNames() { 164 return CollectionUtils.toIterator(getRequest().getParameterNames()); 165 } 166 167 @Override 168 public Map<String, String[]> getParameterMap() { 169 return getRequest().getParameterMap(); 170 } 171 172 @Override 173 public Locale getLocale() { 174 return getRequest().getLocale(); 175 } 176 177 @Override 178 public String getContextPath() { 179 return getRequest().getContextPath(); 180 } 181 182 @Override 183 public String getRemoteUser() { 184 return getRequest().getRemoteUser(); 185 } 186 187 @Override 188 public Principal getUserPrincipal() { 189 return getRequest().getUserPrincipal(); 190 } 191 192 @Override 193 public boolean isUserInRole(String role) { 194 return getRequest().isUserInRole(role); 195 } 196 197 @Override 198 public boolean isSecure() { 199 return getRequest().isSecure(); 200 } 201 202 203 @Override 204 public boolean checkNotModified(long lastModifiedTimestamp) { 205 return checkNotModified(null, lastModifiedTimestamp); 206 } 207 208 @Override 209 public boolean checkNotModified(String etag) { 210 return checkNotModified(etag, -1); 211 } 212 213 @Override 214 public boolean checkNotModified(String etag, long lastModifiedTimestamp) { 215 HttpServletResponse response = getResponse(); 216 if (this.notModified || !isStatusOK(response)) { 217 return this.notModified; 218 } 219 220 // Evaluate conditions in order of precedence. 221 // See https://tools.ietf.org/html/rfc7232#section-6 222 223 if (validateIfUnmodifiedSince(lastModifiedTimestamp)) { 224 if (this.notModified) { 225 response.setStatus(HttpStatus.PRECONDITION_FAILED.value()); 226 } 227 return this.notModified; 228 } 229 230 boolean validated = validateIfNoneMatch(etag); 231 if (!validated) { 232 validateIfModifiedSince(lastModifiedTimestamp); 233 } 234 235 // Update response 236 boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod()); 237 if (this.notModified) { 238 response.setStatus(isHttpGetOrHead ? 239 HttpStatus.NOT_MODIFIED.value() : HttpStatus.PRECONDITION_FAILED.value()); 240 } 241 if (isHttpGetOrHead) { 242 if(lastModifiedTimestamp > 0 && isHeaderAbsent(response, LAST_MODIFIED)) { 243 response.setDateHeader(LAST_MODIFIED, lastModifiedTimestamp); 244 } 245 if (StringUtils.hasLength(etag) && isHeaderAbsent(response, ETAG)) { 246 response.setHeader(ETAG, padEtagIfNecessary(etag)); 247 } 248 } 249 250 return this.notModified; 251 } 252 253 private boolean isStatusOK(HttpServletResponse response) { 254 if (response == null || !servlet3Present) { 255 // Can't check response.getStatus() - let's assume we're good 256 return true; 257 } 258 return response.getStatus() == 200; 259 } 260 261 private boolean isHeaderAbsent(HttpServletResponse response, String header) { 262 if (response == null || !servlet3Present) { 263 // Can't check response.getHeader(header) - let's assume it's not set 264 return true; 265 } 266 return (response.getHeader(header) == null); 267 } 268 269 private boolean validateIfUnmodifiedSince(long lastModifiedTimestamp) { 270 if (lastModifiedTimestamp < 0) { 271 return false; 272 } 273 long ifUnmodifiedSince = parseDateHeader(IF_UNMODIFIED_SINCE); 274 if (ifUnmodifiedSince == -1) { 275 return false; 276 } 277 // We will perform this validation... 278 this.notModified = (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000)); 279 return true; 280 } 281 282 private boolean validateIfNoneMatch(String etag) { 283 if (!StringUtils.hasLength(etag)) { 284 return false; 285 } 286 287 Enumeration<String> ifNoneMatch; 288 try { 289 ifNoneMatch = getRequest().getHeaders(IF_NONE_MATCH); 290 } 291 catch (IllegalArgumentException ex) { 292 return false; 293 } 294 if (!ifNoneMatch.hasMoreElements()) { 295 return false; 296 } 297 298 // We will perform this validation... 299 etag = padEtagIfNecessary(etag); 300 while (ifNoneMatch.hasMoreElements()) { 301 String clientETags = ifNoneMatch.nextElement(); 302 Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(clientETags); 303 // Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3 304 while (etagMatcher.find()) { 305 if (StringUtils.hasLength(etagMatcher.group()) && 306 etag.replaceFirst("^W/", "").equals(etagMatcher.group(3))) { 307 this.notModified = true; 308 break; 309 } 310 } 311 } 312 313 return true; 314 } 315 316 private String padEtagIfNecessary(String etag) { 317 if (!StringUtils.hasLength(etag)) { 318 return etag; 319 } 320 if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) { 321 return etag; 322 } 323 return "\"" + etag + "\""; 324 } 325 326 private boolean validateIfModifiedSince(long lastModifiedTimestamp) { 327 if (lastModifiedTimestamp < 0) { 328 return false; 329 } 330 long ifModifiedSince = parseDateHeader(IF_MODIFIED_SINCE); 331 if (ifModifiedSince == -1) { 332 return false; 333 } 334 // We will perform this validation... 335 this.notModified = ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000); 336 return true; 337 } 338 339 public boolean isNotModified() { 340 return this.notModified; 341 } 342 343 private long parseDateHeader(String headerName) { 344 long dateValue = -1; 345 try { 346 dateValue = getRequest().getDateHeader(headerName); 347 } 348 catch (IllegalArgumentException ex) { 349 String headerValue = getHeader(headerName); 350 // Possibly an IE 10 style value: "Wed, 09 Apr 2014 09:57:42 GMT; length=13774" 351 int separatorIndex = headerValue.indexOf(';'); 352 if (separatorIndex != -1) { 353 String datePart = headerValue.substring(0, separatorIndex); 354 dateValue = parseDateValue(datePart); 355 } 356 } 357 return dateValue; 358 } 359 360 private long parseDateValue(String headerValue) { 361 if (headerValue == null) { 362 // No header value sent at all 363 return -1; 364 } 365 if (headerValue.length() >= 3) { 366 // Short "0" or "-1" like values are never valid HTTP date headers... 367 // Let's only bother with SimpleDateFormat parsing for long enough values. 368 for (String dateFormat : DATE_FORMATS) { 369 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); 370 simpleDateFormat.setTimeZone(GMT); 371 try { 372 return simpleDateFormat.parse(headerValue).getTime(); 373 } 374 catch (ParseException ex) { 375 // ignore 376 } 377 } 378 } 379 return -1; 380 } 381 382 @Override 383 public String getDescription(boolean includeClientInfo) { 384 HttpServletRequest request = getRequest(); 385 StringBuilder sb = new StringBuilder(); 386 sb.append("uri=").append(request.getRequestURI()); 387 if (includeClientInfo) { 388 String client = request.getRemoteAddr(); 389 if (StringUtils.hasLength(client)) { 390 sb.append(";client=").append(client); 391 } 392 HttpSession session = request.getSession(false); 393 if (session != null) { 394 sb.append(";session=").append(session.getId()); 395 } 396 String user = request.getRemoteUser(); 397 if (StringUtils.hasLength(user)) { 398 sb.append(";user=").append(user); 399 } 400 } 401 return sb.toString(); 402 } 403 404 405 @Override 406 public String toString() { 407 return "ServletWebRequest: " + getDescription(true); 408 } 409 410}