001/* 002 * Copyright 2002-2020 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.util; 018 019import java.io.File; 020import java.io.FileNotFoundException; 021import java.net.URI; 022import java.util.Collection; 023import java.util.Enumeration; 024import java.util.Map; 025import java.util.StringTokenizer; 026import java.util.TreeMap; 027 028import javax.servlet.ServletContext; 029import javax.servlet.ServletRequest; 030import javax.servlet.ServletRequestWrapper; 031import javax.servlet.ServletResponse; 032import javax.servlet.ServletResponseWrapper; 033import javax.servlet.http.Cookie; 034import javax.servlet.http.HttpServletRequest; 035import javax.servlet.http.HttpServletResponse; 036import javax.servlet.http.HttpSession; 037 038import org.springframework.http.HttpHeaders; 039import org.springframework.http.HttpRequest; 040import org.springframework.http.server.ServletServerHttpRequest; 041import org.springframework.lang.Nullable; 042import org.springframework.util.Assert; 043import org.springframework.util.CollectionUtils; 044import org.springframework.util.LinkedMultiValueMap; 045import org.springframework.util.MultiValueMap; 046import org.springframework.util.ObjectUtils; 047import org.springframework.util.StringUtils; 048 049/** 050 * Miscellaneous utilities for web applications. 051 * Used by various framework classes. 052 * 053 * @author Rod Johnson 054 * @author Juergen Hoeller 055 * @author Sebastien Deleuze 056 */ 057public abstract class WebUtils { 058 059 /** 060 * Standard Servlet 2.3+ spec request attribute for include request URI. 061 * <p>If included via a {@code RequestDispatcher}, the current resource will see the 062 * originating request. Its own request URI is exposed as a request attribute. 063 */ 064 public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri"; 065 066 /** 067 * Standard Servlet 2.3+ spec request attribute for include context path. 068 * <p>If included via a {@code RequestDispatcher}, the current resource will see the 069 * originating context path. Its own context path is exposed as a request attribute. 070 */ 071 public static final String INCLUDE_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.include.context_path"; 072 073 /** 074 * Standard Servlet 2.3+ spec request attribute for include servlet path. 075 * <p>If included via a {@code RequestDispatcher}, the current resource will see the 076 * originating servlet path. Its own servlet path is exposed as a request attribute. 077 */ 078 public static final String INCLUDE_SERVLET_PATH_ATTRIBUTE = "javax.servlet.include.servlet_path"; 079 080 /** 081 * Standard Servlet 2.3+ spec request attribute for include path info. 082 * <p>If included via a {@code RequestDispatcher}, the current resource will see the 083 * originating path info. Its own path info is exposed as a request attribute. 084 */ 085 public static final String INCLUDE_PATH_INFO_ATTRIBUTE = "javax.servlet.include.path_info"; 086 087 /** 088 * Standard Servlet 2.3+ spec request attribute for include query string. 089 * <p>If included via a {@code RequestDispatcher}, the current resource will see the 090 * originating query string. Its own query string is exposed as a request attribute. 091 */ 092 public static final String INCLUDE_QUERY_STRING_ATTRIBUTE = "javax.servlet.include.query_string"; 093 094 /** 095 * Standard Servlet 2.4+ spec request attribute for forward request URI. 096 * <p>If forwarded to via a RequestDispatcher, the current resource will see its 097 * own request URI. The originating request URI is exposed as a request attribute. 098 */ 099 public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri"; 100 101 /** 102 * Standard Servlet 2.4+ spec request attribute for forward context path. 103 * <p>If forwarded to via a RequestDispatcher, the current resource will see its 104 * own context path. The originating context path is exposed as a request attribute. 105 */ 106 public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path"; 107 108 /** 109 * Standard Servlet 2.4+ spec request attribute for forward servlet path. 110 * <p>If forwarded to via a RequestDispatcher, the current resource will see its 111 * own servlet path. The originating servlet path is exposed as a request attribute. 112 */ 113 public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path"; 114 115 /** 116 * Standard Servlet 2.4+ spec request attribute for forward path info. 117 * <p>If forwarded to via a RequestDispatcher, the current resource will see its 118 * own path ingo. The originating path info is exposed as a request attribute. 119 */ 120 public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info"; 121 122 /** 123 * Standard Servlet 2.4+ spec request attribute for forward query string. 124 * <p>If forwarded to via a RequestDispatcher, the current resource will see its 125 * own query string. The originating query string is exposed as a request attribute. 126 */ 127 public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string"; 128 129 /** 130 * Standard Servlet 2.3+ spec request attribute for error page status code. 131 * <p>To be exposed to JSPs that are marked as error pages, when forwarding 132 * to them directly rather than through the servlet container's error page 133 * resolution mechanism. 134 */ 135 public static final String ERROR_STATUS_CODE_ATTRIBUTE = "javax.servlet.error.status_code"; 136 137 /** 138 * Standard Servlet 2.3+ spec request attribute for error page exception type. 139 * <p>To be exposed to JSPs that are marked as error pages, when forwarding 140 * to them directly rather than through the servlet container's error page 141 * resolution mechanism. 142 */ 143 public static final String ERROR_EXCEPTION_TYPE_ATTRIBUTE = "javax.servlet.error.exception_type"; 144 145 /** 146 * Standard Servlet 2.3+ spec request attribute for error page message. 147 * <p>To be exposed to JSPs that are marked as error pages, when forwarding 148 * to them directly rather than through the servlet container's error page 149 * resolution mechanism. 150 */ 151 public static final String ERROR_MESSAGE_ATTRIBUTE = "javax.servlet.error.message"; 152 153 /** 154 * Standard Servlet 2.3+ spec request attribute for error page exception. 155 * <p>To be exposed to JSPs that are marked as error pages, when forwarding 156 * to them directly rather than through the servlet container's error page 157 * resolution mechanism. 158 */ 159 public static final String ERROR_EXCEPTION_ATTRIBUTE = "javax.servlet.error.exception"; 160 161 /** 162 * Standard Servlet 2.3+ spec request attribute for error page request URI. 163 * <p>To be exposed to JSPs that are marked as error pages, when forwarding 164 * to them directly rather than through the servlet container's error page 165 * resolution mechanism. 166 */ 167 public static final String ERROR_REQUEST_URI_ATTRIBUTE = "javax.servlet.error.request_uri"; 168 169 /** 170 * Standard Servlet 2.3+ spec request attribute for error page servlet name. 171 * <p>To be exposed to JSPs that are marked as error pages, when forwarding 172 * to them directly rather than through the servlet container's error page 173 * resolution mechanism. 174 */ 175 public static final String ERROR_SERVLET_NAME_ATTRIBUTE = "javax.servlet.error.servlet_name"; 176 177 /** 178 * Prefix of the charset clause in a content type String: ";charset=". 179 */ 180 public static final String CONTENT_TYPE_CHARSET_PREFIX = ";charset="; 181 182 /** 183 * Default character encoding to use when {@code request.getCharacterEncoding} 184 * returns {@code null}, according to the Servlet spec. 185 * @see ServletRequest#getCharacterEncoding 186 */ 187 public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1"; 188 189 /** 190 * Standard Servlet spec context attribute that specifies a temporary 191 * directory for the current web application, of type {@code java.io.File}. 192 */ 193 public static final String TEMP_DIR_CONTEXT_ATTRIBUTE = "javax.servlet.context.tempdir"; 194 195 /** 196 * HTML escape parameter at the servlet context level 197 * (i.e. a context-param in {@code web.xml}): "defaultHtmlEscape". 198 */ 199 public static final String HTML_ESCAPE_CONTEXT_PARAM = "defaultHtmlEscape"; 200 201 /** 202 * Use of response encoding for HTML escaping parameter at the servlet context level 203 * (i.e. a context-param in {@code web.xml}): "responseEncodedHtmlEscape". 204 * @since 4.1.2 205 */ 206 public static final String RESPONSE_ENCODED_HTML_ESCAPE_CONTEXT_PARAM = "responseEncodedHtmlEscape"; 207 208 /** 209 * Web app root key parameter at the servlet context level 210 * (i.e. a context-param in {@code web.xml}): "webAppRootKey". 211 */ 212 public static final String WEB_APP_ROOT_KEY_PARAM = "webAppRootKey"; 213 214 /** Default web app root key: "webapp.root". */ 215 public static final String DEFAULT_WEB_APP_ROOT_KEY = "webapp.root"; 216 217 /** Name suffixes in case of image buttons. */ 218 public static final String[] SUBMIT_IMAGE_SUFFIXES = {".x", ".y"}; 219 220 /** Key for the mutex session attribute. */ 221 public static final String SESSION_MUTEX_ATTRIBUTE = WebUtils.class.getName() + ".MUTEX"; 222 223 224 /** 225 * Set a system property to the web application root directory. 226 * The key of the system property can be defined with the "webAppRootKey" 227 * context-param in {@code web.xml}. Default is "webapp.root". 228 * <p>Can be used for tools that support substitution with {@code System.getProperty} 229 * values, like log4j's "${key}" syntax within log file locations. 230 * @param servletContext the servlet context of the web application 231 * @throws IllegalStateException if the system property is already set, 232 * or if the WAR file is not expanded 233 * @see #WEB_APP_ROOT_KEY_PARAM 234 * @see #DEFAULT_WEB_APP_ROOT_KEY 235 * @see WebAppRootListener 236 */ 237 public static void setWebAppRootSystemProperty(ServletContext servletContext) throws IllegalStateException { 238 Assert.notNull(servletContext, "ServletContext must not be null"); 239 String root = servletContext.getRealPath("/"); 240 if (root == null) { 241 throw new IllegalStateException( 242 "Cannot set web app root system property when WAR file is not expanded"); 243 } 244 String param = servletContext.getInitParameter(WEB_APP_ROOT_KEY_PARAM); 245 String key = (param != null ? param : DEFAULT_WEB_APP_ROOT_KEY); 246 String oldValue = System.getProperty(key); 247 if (oldValue != null && !StringUtils.pathEquals(oldValue, root)) { 248 throw new IllegalStateException("Web app root system property already set to different value: '" + 249 key + "' = [" + oldValue + "] instead of [" + root + "] - " + 250 "Choose unique values for the 'webAppRootKey' context-param in your web.xml files!"); 251 } 252 System.setProperty(key, root); 253 servletContext.log("Set web app root system property: '" + key + "' = [" + root + "]"); 254 } 255 256 /** 257 * Remove the system property that points to the web app root directory. 258 * To be called on shutdown of the web application. 259 * @param servletContext the servlet context of the web application 260 * @see #setWebAppRootSystemProperty 261 */ 262 public static void removeWebAppRootSystemProperty(ServletContext servletContext) { 263 Assert.notNull(servletContext, "ServletContext must not be null"); 264 String param = servletContext.getInitParameter(WEB_APP_ROOT_KEY_PARAM); 265 String key = (param != null ? param : DEFAULT_WEB_APP_ROOT_KEY); 266 System.getProperties().remove(key); 267 } 268 269 /** 270 * Return whether default HTML escaping is enabled for the web application, 271 * i.e. the value of the "defaultHtmlEscape" context-param in {@code web.xml} 272 * (if any). 273 * <p>This method differentiates between no param specified at all and 274 * an actual boolean value specified, allowing to have a context-specific 275 * default in case of no setting at the global level. 276 * @param servletContext the servlet context of the web application 277 * @return whether default HTML escaping is enabled for the given application 278 * ({@code null} = no explicit default) 279 */ 280 @Nullable 281 public static Boolean getDefaultHtmlEscape(@Nullable ServletContext servletContext) { 282 if (servletContext == null) { 283 return null; 284 } 285 String param = servletContext.getInitParameter(HTML_ESCAPE_CONTEXT_PARAM); 286 return (StringUtils.hasText(param) ? Boolean.valueOf(param) : null); 287 } 288 289 /** 290 * Return whether response encoding should be used when HTML escaping characters, 291 * thus only escaping XML markup significant characters with UTF-* encodings. 292 * This option is enabled for the web application with a ServletContext param, 293 * i.e. the value of the "responseEncodedHtmlEscape" context-param in {@code web.xml} 294 * (if any). 295 * <p>This method differentiates between no param specified at all and 296 * an actual boolean value specified, allowing to have a context-specific 297 * default in case of no setting at the global level. 298 * @param servletContext the servlet context of the web application 299 * @return whether response encoding is to be used for HTML escaping 300 * ({@code null} = no explicit default) 301 * @since 4.1.2 302 */ 303 @Nullable 304 public static Boolean getResponseEncodedHtmlEscape(@Nullable ServletContext servletContext) { 305 if (servletContext == null) { 306 return null; 307 } 308 String param = servletContext.getInitParameter(RESPONSE_ENCODED_HTML_ESCAPE_CONTEXT_PARAM); 309 return (StringUtils.hasText(param) ? Boolean.valueOf(param) : null); 310 } 311 312 /** 313 * Return the temporary directory for the current web application, 314 * as provided by the servlet container. 315 * @param servletContext the servlet context of the web application 316 * @return the File representing the temporary directory 317 */ 318 public static File getTempDir(ServletContext servletContext) { 319 Assert.notNull(servletContext, "ServletContext must not be null"); 320 return (File) servletContext.getAttribute(TEMP_DIR_CONTEXT_ATTRIBUTE); 321 } 322 323 /** 324 * Return the real path of the given path within the web application, 325 * as provided by the servlet container. 326 * <p>Prepends a slash if the path does not already start with a slash, 327 * and throws a FileNotFoundException if the path cannot be resolved to 328 * a resource (in contrast to ServletContext's {@code getRealPath}, 329 * which returns null). 330 * @param servletContext the servlet context of the web application 331 * @param path the path within the web application 332 * @return the corresponding real path 333 * @throws FileNotFoundException if the path cannot be resolved to a resource 334 * @see javax.servlet.ServletContext#getRealPath 335 */ 336 public static String getRealPath(ServletContext servletContext, String path) throws FileNotFoundException { 337 Assert.notNull(servletContext, "ServletContext must not be null"); 338 // Interpret location as relative to the web application root directory. 339 if (!path.startsWith("/")) { 340 path = "/" + path; 341 } 342 String realPath = servletContext.getRealPath(path); 343 if (realPath == null) { 344 throw new FileNotFoundException( 345 "ServletContext resource [" + path + "] cannot be resolved to absolute file path - " + 346 "web application archive not expanded?"); 347 } 348 return realPath; 349 } 350 351 /** 352 * Determine the session id of the given request, if any. 353 * @param request current HTTP request 354 * @return the session id, or {@code null} if none 355 */ 356 @Nullable 357 public static String getSessionId(HttpServletRequest request) { 358 Assert.notNull(request, "Request must not be null"); 359 HttpSession session = request.getSession(false); 360 return (session != null ? session.getId() : null); 361 } 362 363 /** 364 * Check the given request for a session attribute of the given name. 365 * Returns null if there is no session or if the session has no such attribute. 366 * Does not create a new session if none has existed before! 367 * @param request current HTTP request 368 * @param name the name of the session attribute 369 * @return the value of the session attribute, or {@code null} if not found 370 */ 371 @Nullable 372 public static Object getSessionAttribute(HttpServletRequest request, String name) { 373 Assert.notNull(request, "Request must not be null"); 374 HttpSession session = request.getSession(false); 375 return (session != null ? session.getAttribute(name) : null); 376 } 377 378 /** 379 * Check the given request for a session attribute of the given name. 380 * Throws an exception if there is no session or if the session has no such 381 * attribute. Does not create a new session if none has existed before! 382 * @param request current HTTP request 383 * @param name the name of the session attribute 384 * @return the value of the session attribute, or {@code null} if not found 385 * @throws IllegalStateException if the session attribute could not be found 386 */ 387 public static Object getRequiredSessionAttribute(HttpServletRequest request, String name) 388 throws IllegalStateException { 389 390 Object attr = getSessionAttribute(request, name); 391 if (attr == null) { 392 throw new IllegalStateException("No session attribute '" + name + "' found"); 393 } 394 return attr; 395 } 396 397 /** 398 * Set the session attribute with the given name to the given value. 399 * Removes the session attribute if value is null, if a session existed at all. 400 * Does not create a new session if not necessary! 401 * @param request current HTTP request 402 * @param name the name of the session attribute 403 * @param value the value of the session attribute 404 */ 405 public static void setSessionAttribute(HttpServletRequest request, String name, @Nullable Object value) { 406 Assert.notNull(request, "Request must not be null"); 407 if (value != null) { 408 request.getSession().setAttribute(name, value); 409 } 410 else { 411 HttpSession session = request.getSession(false); 412 if (session != null) { 413 session.removeAttribute(name); 414 } 415 } 416 } 417 418 /** 419 * Return the best available mutex for the given session: 420 * that is, an object to synchronize on for the given session. 421 * <p>Returns the session mutex attribute if available; usually, 422 * this means that the HttpSessionMutexListener needs to be defined 423 * in {@code web.xml}. Falls back to the HttpSession itself 424 * if no mutex attribute found. 425 * <p>The session mutex is guaranteed to be the same object during 426 * the entire lifetime of the session, available under the key defined 427 * by the {@code SESSION_MUTEX_ATTRIBUTE} constant. It serves as a 428 * safe reference to synchronize on for locking on the current session. 429 * <p>In many cases, the HttpSession reference itself is a safe mutex 430 * as well, since it will always be the same object reference for the 431 * same active logical session. However, this is not guaranteed across 432 * different servlet containers; the only 100% safe way is a session mutex. 433 * @param session the HttpSession to find a mutex for 434 * @return the mutex object (never {@code null}) 435 * @see #SESSION_MUTEX_ATTRIBUTE 436 * @see HttpSessionMutexListener 437 */ 438 public static Object getSessionMutex(HttpSession session) { 439 Assert.notNull(session, "Session must not be null"); 440 Object mutex = session.getAttribute(SESSION_MUTEX_ATTRIBUTE); 441 if (mutex == null) { 442 mutex = session; 443 } 444 return mutex; 445 } 446 447 448 /** 449 * Return an appropriate request object of the specified type, if available, 450 * unwrapping the given request as far as necessary. 451 * @param request the servlet request to introspect 452 * @param requiredType the desired type of request object 453 * @return the matching request object, or {@code null} if none 454 * of that type is available 455 */ 456 @SuppressWarnings("unchecked") 457 @Nullable 458 public static <T> T getNativeRequest(ServletRequest request, @Nullable Class<T> requiredType) { 459 if (requiredType != null) { 460 if (requiredType.isInstance(request)) { 461 return (T) request; 462 } 463 else if (request instanceof ServletRequestWrapper) { 464 return getNativeRequest(((ServletRequestWrapper) request).getRequest(), requiredType); 465 } 466 } 467 return null; 468 } 469 470 /** 471 * Return an appropriate response object of the specified type, if available, 472 * unwrapping the given response as far as necessary. 473 * @param response the servlet response to introspect 474 * @param requiredType the desired type of response object 475 * @return the matching response object, or {@code null} if none 476 * of that type is available 477 */ 478 @SuppressWarnings("unchecked") 479 @Nullable 480 public static <T> T getNativeResponse(ServletResponse response, @Nullable Class<T> requiredType) { 481 if (requiredType != null) { 482 if (requiredType.isInstance(response)) { 483 return (T) response; 484 } 485 else if (response instanceof ServletResponseWrapper) { 486 return getNativeResponse(((ServletResponseWrapper) response).getResponse(), requiredType); 487 } 488 } 489 return null; 490 } 491 492 /** 493 * Determine whether the given request is an include request, 494 * that is, not a top-level HTTP request coming in from the outside. 495 * <p>Checks the presence of the "javax.servlet.include.request_uri" 496 * request attribute. Could check any request attribute that is only 497 * present in an include request. 498 * @param request current servlet request 499 * @return whether the given request is an include request 500 */ 501 public static boolean isIncludeRequest(ServletRequest request) { 502 return (request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE) != null); 503 } 504 505 /** 506 * Expose the Servlet spec's error attributes as {@link javax.servlet.http.HttpServletRequest} 507 * attributes under the keys defined in the Servlet 2.3 specification, for error pages that 508 * are rendered directly rather than through the Servlet container's error page resolution: 509 * {@code javax.servlet.error.status_code}, 510 * {@code javax.servlet.error.exception_type}, 511 * {@code javax.servlet.error.message}, 512 * {@code javax.servlet.error.exception}, 513 * {@code javax.servlet.error.request_uri}, 514 * {@code javax.servlet.error.servlet_name}. 515 * <p>Does not override values if already present, to respect attribute values 516 * that have been exposed explicitly before. 517 * <p>Exposes status code 200 by default. Set the "javax.servlet.error.status_code" 518 * attribute explicitly (before or after) in order to expose a different status code. 519 * @param request current servlet request 520 * @param ex the exception encountered 521 * @param servletName the name of the offending servlet 522 */ 523 public static void exposeErrorRequestAttributes(HttpServletRequest request, Throwable ex, 524 @Nullable String servletName) { 525 526 exposeRequestAttributeIfNotPresent(request, ERROR_STATUS_CODE_ATTRIBUTE, HttpServletResponse.SC_OK); 527 exposeRequestAttributeIfNotPresent(request, ERROR_EXCEPTION_TYPE_ATTRIBUTE, ex.getClass()); 528 exposeRequestAttributeIfNotPresent(request, ERROR_MESSAGE_ATTRIBUTE, ex.getMessage()); 529 exposeRequestAttributeIfNotPresent(request, ERROR_EXCEPTION_ATTRIBUTE, ex); 530 exposeRequestAttributeIfNotPresent(request, ERROR_REQUEST_URI_ATTRIBUTE, request.getRequestURI()); 531 if (servletName != null) { 532 exposeRequestAttributeIfNotPresent(request, ERROR_SERVLET_NAME_ATTRIBUTE, servletName); 533 } 534 } 535 536 /** 537 * Expose the specified request attribute if not already present. 538 * @param request current servlet request 539 * @param name the name of the attribute 540 * @param value the suggested value of the attribute 541 */ 542 private static void exposeRequestAttributeIfNotPresent(ServletRequest request, String name, Object value) { 543 if (request.getAttribute(name) == null) { 544 request.setAttribute(name, value); 545 } 546 } 547 548 /** 549 * Clear the Servlet spec's error attributes as {@link javax.servlet.http.HttpServletRequest} 550 * attributes under the keys defined in the Servlet 2.3 specification: 551 * {@code javax.servlet.error.status_code}, 552 * {@code javax.servlet.error.exception_type}, 553 * {@code javax.servlet.error.message}, 554 * {@code javax.servlet.error.exception}, 555 * {@code javax.servlet.error.request_uri}, 556 * {@code javax.servlet.error.servlet_name}. 557 * @param request current servlet request 558 */ 559 public static void clearErrorRequestAttributes(HttpServletRequest request) { 560 request.removeAttribute(ERROR_STATUS_CODE_ATTRIBUTE); 561 request.removeAttribute(ERROR_EXCEPTION_TYPE_ATTRIBUTE); 562 request.removeAttribute(ERROR_MESSAGE_ATTRIBUTE); 563 request.removeAttribute(ERROR_EXCEPTION_ATTRIBUTE); 564 request.removeAttribute(ERROR_REQUEST_URI_ATTRIBUTE); 565 request.removeAttribute(ERROR_SERVLET_NAME_ATTRIBUTE); 566 } 567 568 /** 569 * Retrieve the first cookie with the given name. Note that multiple 570 * cookies can have the same name but different paths or domains. 571 * @param request current servlet request 572 * @param name cookie name 573 * @return the first cookie with the given name, or {@code null} if none is found 574 */ 575 @Nullable 576 public static Cookie getCookie(HttpServletRequest request, String name) { 577 Assert.notNull(request, "Request must not be null"); 578 Cookie[] cookies = request.getCookies(); 579 if (cookies != null) { 580 for (Cookie cookie : cookies) { 581 if (name.equals(cookie.getName())) { 582 return cookie; 583 } 584 } 585 } 586 return null; 587 } 588 589 /** 590 * Check if a specific input type="submit" parameter was sent in the request, 591 * either via a button (directly with name) or via an image (name + ".x" or 592 * name + ".y"). 593 * @param request current HTTP request 594 * @param name the name of the parameter 595 * @return if the parameter was sent 596 * @see #SUBMIT_IMAGE_SUFFIXES 597 */ 598 public static boolean hasSubmitParameter(ServletRequest request, String name) { 599 Assert.notNull(request, "Request must not be null"); 600 if (request.getParameter(name) != null) { 601 return true; 602 } 603 for (String suffix : SUBMIT_IMAGE_SUFFIXES) { 604 if (request.getParameter(name + suffix) != null) { 605 return true; 606 } 607 } 608 return false; 609 } 610 611 /** 612 * Obtain a named parameter from the given request parameters. 613 * <p>See {@link #findParameterValue(java.util.Map, String)} 614 * for a description of the lookup algorithm. 615 * @param request current HTTP request 616 * @param name the <i>logical</i> name of the request parameter 617 * @return the value of the parameter, or {@code null} 618 * if the parameter does not exist in given request 619 */ 620 @Nullable 621 public static String findParameterValue(ServletRequest request, String name) { 622 return findParameterValue(request.getParameterMap(), name); 623 } 624 625 /** 626 * Obtain a named parameter from the given request parameters. 627 * <p>This method will try to obtain a parameter value using the 628 * following algorithm: 629 * <ol> 630 * <li>Try to get the parameter value using just the given <i>logical</i> name. 631 * This handles parameters of the form <tt>logicalName = value</tt>. For normal 632 * parameters, e.g. submitted using a hidden HTML form field, this will return 633 * the requested value.</li> 634 * <li>Try to obtain the parameter value from the parameter name, where the 635 * parameter name in the request is of the form <tt>logicalName_value = xyz</tt> 636 * with "_" being the configured delimiter. This deals with parameter values 637 * submitted using an HTML form submit button.</li> 638 * <li>If the value obtained in the previous step has a ".x" or ".y" suffix, 639 * remove that. This handles cases where the value was submitted using an 640 * HTML form image button. In this case the parameter in the request would 641 * actually be of the form <tt>logicalName_value.x = 123</tt>. </li> 642 * </ol> 643 * @param parameters the available parameter map 644 * @param name the <i>logical</i> name of the request parameter 645 * @return the value of the parameter, or {@code null} 646 * if the parameter does not exist in given request 647 */ 648 @Nullable 649 public static String findParameterValue(Map<String, ?> parameters, String name) { 650 // First try to get it as a normal name=value parameter 651 Object value = parameters.get(name); 652 if (value instanceof String[]) { 653 String[] values = (String[]) value; 654 return (values.length > 0 ? values[0] : null); 655 } 656 else if (value != null) { 657 return value.toString(); 658 } 659 // If no value yet, try to get it as a name_value=xyz parameter 660 String prefix = name + "_"; 661 for (String paramName : parameters.keySet()) { 662 if (paramName.startsWith(prefix)) { 663 // Support images buttons, which would submit parameters as name_value.x=123 664 for (String suffix : SUBMIT_IMAGE_SUFFIXES) { 665 if (paramName.endsWith(suffix)) { 666 return paramName.substring(prefix.length(), paramName.length() - suffix.length()); 667 } 668 } 669 return paramName.substring(prefix.length()); 670 } 671 } 672 // We couldn't find the parameter value... 673 return null; 674 } 675 676 /** 677 * Return a map containing all parameters with the given prefix. 678 * Maps single values to String and multiple values to String array. 679 * <p>For example, with a prefix of "spring_", "spring_param1" and 680 * "spring_param2" result in a Map with "param1" and "param2" as keys. 681 * @param request the HTTP request in which to look for parameters 682 * @param prefix the beginning of parameter names 683 * (if this is null or the empty string, all parameters will match) 684 * @return map containing request parameters <b>without the prefix</b>, 685 * containing either a String or a String array as values 686 * @see javax.servlet.ServletRequest#getParameterNames 687 * @see javax.servlet.ServletRequest#getParameterValues 688 * @see javax.servlet.ServletRequest#getParameterMap 689 */ 690 public static Map<String, Object> getParametersStartingWith(ServletRequest request, @Nullable String prefix) { 691 Assert.notNull(request, "Request must not be null"); 692 Enumeration<String> paramNames = request.getParameterNames(); 693 Map<String, Object> params = new TreeMap<>(); 694 if (prefix == null) { 695 prefix = ""; 696 } 697 while (paramNames != null && paramNames.hasMoreElements()) { 698 String paramName = paramNames.nextElement(); 699 if (prefix.isEmpty() || paramName.startsWith(prefix)) { 700 String unprefixed = paramName.substring(prefix.length()); 701 String[] values = request.getParameterValues(paramName); 702 if (values == null || values.length == 0) { 703 // Do nothing, no values found at all. 704 } 705 else if (values.length > 1) { 706 params.put(unprefixed, values); 707 } 708 else { 709 params.put(unprefixed, values[0]); 710 } 711 } 712 } 713 return params; 714 } 715 716 /** 717 * Parse the given string with matrix variables. An example string would look 718 * like this {@code "q1=a;q1=b;q2=a,b,c"}. The resulting map would contain 719 * keys {@code "q1"} and {@code "q2"} with values {@code ["a","b"]} and 720 * {@code ["a","b","c"]} respectively. 721 * @param matrixVariables the unparsed matrix variables string 722 * @return a map with matrix variable names and values (never {@code null}) 723 * @since 3.2 724 */ 725 public static MultiValueMap<String, String> parseMatrixVariables(String matrixVariables) { 726 MultiValueMap<String, String> result = new LinkedMultiValueMap<>(); 727 if (!StringUtils.hasText(matrixVariables)) { 728 return result; 729 } 730 StringTokenizer pairs = new StringTokenizer(matrixVariables, ";"); 731 while (pairs.hasMoreTokens()) { 732 String pair = pairs.nextToken(); 733 int index = pair.indexOf('='); 734 if (index != -1) { 735 String name = pair.substring(0, index); 736 if (name.equalsIgnoreCase("jsessionid")) { 737 continue; 738 } 739 String rawValue = pair.substring(index + 1); 740 for (String value : StringUtils.commaDelimitedListToStringArray(rawValue)) { 741 result.add(name, value); 742 } 743 } 744 else { 745 result.add(pair, ""); 746 } 747 } 748 return result; 749 } 750 751 /** 752 * Check the given request origin against a list of allowed origins. 753 * A list containing "*" means that all origins are allowed. 754 * An empty list means only same origin is allowed. 755 * 756 * <p><strong>Note:</strong> as of 5.1 this method ignores 757 * {@code "Forwarded"} and {@code "X-Forwarded-*"} headers that specify the 758 * client-originated address. Consider using the {@code ForwardedHeaderFilter} 759 * to extract and use, or to discard such headers. 760 * 761 * @return {@code true} if the request origin is valid, {@code false} otherwise 762 * @since 4.1.5 763 * @see <a href="https://tools.ietf.org/html/rfc6454">RFC 6454: The Web Origin Concept</a> 764 */ 765 public static boolean isValidOrigin(HttpRequest request, Collection<String> allowedOrigins) { 766 Assert.notNull(request, "Request must not be null"); 767 Assert.notNull(allowedOrigins, "Allowed origins must not be null"); 768 769 String origin = request.getHeaders().getOrigin(); 770 if (origin == null || allowedOrigins.contains("*")) { 771 return true; 772 } 773 else if (CollectionUtils.isEmpty(allowedOrigins)) { 774 return isSameOrigin(request); 775 } 776 else { 777 return allowedOrigins.contains(origin); 778 } 779 } 780 781 /** 782 * Check if the request is a same-origin one, based on {@code Origin}, {@code Host}, 783 * {@code Forwarded}, {@code X-Forwarded-Proto}, {@code X-Forwarded-Host} and 784 * {@code X-Forwarded-Port} headers. 785 * 786 * <p><strong>Note:</strong> as of 5.1 this method ignores 787 * {@code "Forwarded"} and {@code "X-Forwarded-*"} headers that specify the 788 * client-originated address. Consider using the {@code ForwardedHeaderFilter} 789 * to extract and use, or to discard such headers. 790 791 * @return {@code true} if the request is a same-origin one, {@code false} in case 792 * of cross-origin request 793 * @since 4.2 794 */ 795 public static boolean isSameOrigin(HttpRequest request) { 796 HttpHeaders headers = request.getHeaders(); 797 String origin = headers.getOrigin(); 798 if (origin == null) { 799 return true; 800 } 801 802 String scheme; 803 String host; 804 int port; 805 if (request instanceof ServletServerHttpRequest) { 806 // Build more efficiently if we can: we only need scheme, host, port for origin comparison 807 HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); 808 scheme = servletRequest.getScheme(); 809 host = servletRequest.getServerName(); 810 port = servletRequest.getServerPort(); 811 } 812 else { 813 URI uri = request.getURI(); 814 scheme = uri.getScheme(); 815 host = uri.getHost(); 816 port = uri.getPort(); 817 } 818 819 UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build(); 820 return (ObjectUtils.nullSafeEquals(scheme, originUrl.getScheme()) && 821 ObjectUtils.nullSafeEquals(host, originUrl.getHost()) && 822 getPort(scheme, port) == getPort(originUrl.getScheme(), originUrl.getPort())); 823 } 824 825 private static int getPort(@Nullable String scheme, int port) { 826 if (port == -1) { 827 if ("http".equals(scheme) || "ws".equals(scheme)) { 828 port = 80; 829 } 830 else if ("https".equals(scheme) || "wss".equals(scheme)) { 831 port = 443; 832 } 833 } 834 return port; 835 } 836 837}