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}