001/* 002 * Copyright 2002-2019 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.mock.web; 018 019import java.time.DateTimeException; 020import java.time.ZonedDateTime; 021import java.time.format.DateTimeFormatter; 022 023import javax.servlet.http.Cookie; 024 025import org.springframework.lang.Nullable; 026import org.springframework.util.Assert; 027import org.springframework.util.StringUtils; 028 029/** 030 * Extension of {@code Cookie} with extra attributes, as defined in 031 * <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>. 032 * 033 * @author Vedran Pavic 034 * @author Juergen Hoeller 035 * @author Sam Brannen 036 * @since 5.1 037 */ 038public class MockCookie extends Cookie { 039 040 private static final long serialVersionUID = 4312531139502726325L; 041 042 043 @Nullable 044 private ZonedDateTime expires; 045 046 @Nullable 047 private String sameSite; 048 049 050 /** 051 * Construct a new {@link MockCookie} with the supplied name and value. 052 * @param name the name 053 * @param value the value 054 * @see Cookie#Cookie(String, String) 055 */ 056 public MockCookie(String name, String value) { 057 super(name, value); 058 } 059 060 /** 061 * Set the "Expires" attribute for this cookie. 062 * @since 5.1.11 063 */ 064 public void setExpires(@Nullable ZonedDateTime expires) { 065 this.expires = expires; 066 } 067 068 /** 069 * Get the "Expires" attribute for this cookie. 070 * @since 5.1.11 071 * @return the "Expires" attribute for this cookie, or {@code null} if not set 072 */ 073 @Nullable 074 public ZonedDateTime getExpires() { 075 return this.expires; 076 } 077 078 /** 079 * Set the "SameSite" attribute for this cookie. 080 * <p>This limits the scope of the cookie such that it will only be attached 081 * to same-site requests if the supplied value is {@code "Strict"} or cross-site 082 * requests if the supplied value is {@code "Lax"}. 083 * @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">RFC6265 bis</a> 084 */ 085 public void setSameSite(@Nullable String sameSite) { 086 this.sameSite = sameSite; 087 } 088 089 /** 090 * Get the "SameSite" attribute for this cookie. 091 * @return the "SameSite" attribute for this cookie, or {@code null} if not set 092 */ 093 @Nullable 094 public String getSameSite() { 095 return this.sameSite; 096 } 097 098 099 /** 100 * Factory method that parses the value of the supplied "Set-Cookie" header. 101 * @param setCookieHeader the "Set-Cookie" value; never {@code null} or empty 102 * @return the created cookie 103 */ 104 public static MockCookie parse(String setCookieHeader) { 105 Assert.notNull(setCookieHeader, "Set-Cookie header must not be null"); 106 String[] cookieParts = setCookieHeader.split("\\s*=\\s*", 2); 107 Assert.isTrue(cookieParts.length == 2, () -> "Invalid Set-Cookie header '" + setCookieHeader + "'"); 108 109 String name = cookieParts[0]; 110 String[] valueAndAttributes = cookieParts[1].split("\\s*;\\s*", 2); 111 String value = valueAndAttributes[0]; 112 String[] attributes = 113 (valueAndAttributes.length > 1 ? valueAndAttributes[1].split("\\s*;\\s*") : new String[0]); 114 115 MockCookie cookie = new MockCookie(name, value); 116 for (String attribute : attributes) { 117 if (StringUtils.startsWithIgnoreCase(attribute, "Domain")) { 118 cookie.setDomain(extractAttributeValue(attribute, setCookieHeader)); 119 } 120 else if (StringUtils.startsWithIgnoreCase(attribute, "Max-Age")) { 121 cookie.setMaxAge(Integer.parseInt(extractAttributeValue(attribute, setCookieHeader))); 122 } 123 else if (StringUtils.startsWithIgnoreCase(attribute, "Expires")) { 124 try { 125 cookie.setExpires(ZonedDateTime.parse(extractAttributeValue(attribute, setCookieHeader), 126 DateTimeFormatter.RFC_1123_DATE_TIME)); 127 } 128 catch (DateTimeException ex) { 129 // ignore invalid date formats 130 } 131 } 132 else if (StringUtils.startsWithIgnoreCase(attribute, "Path")) { 133 cookie.setPath(extractAttributeValue(attribute, setCookieHeader)); 134 } 135 else if (StringUtils.startsWithIgnoreCase(attribute, "Secure")) { 136 cookie.setSecure(true); 137 } 138 else if (StringUtils.startsWithIgnoreCase(attribute, "HttpOnly")) { 139 cookie.setHttpOnly(true); 140 } 141 else if (StringUtils.startsWithIgnoreCase(attribute, "SameSite")) { 142 cookie.setSameSite(extractAttributeValue(attribute, setCookieHeader)); 143 } 144 } 145 return cookie; 146 } 147 148 private static String extractAttributeValue(String attribute, String header) { 149 String[] nameAndValue = attribute.split("="); 150 Assert.isTrue(nameAndValue.length == 2, 151 () -> "No value in attribute '" + nameAndValue[0] + "' for Set-Cookie header '" + header + "'"); 152 return nameAndValue[1]; 153 } 154 155}