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