From 1faeb0ec8721c568b2eb78cbb77ae7bf92260a7c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 10 Jan 2016 22:36:45 -0500 Subject: [PATCH] Add HttpCookie + server support through HttpHeaders --- .../org/springframework/http/HttpCookie.java | 122 ++ .../org/springframework/http/HttpHeaders.java | 1159 +++++++++++++++++ .../reactive/AbstractServerHttpRequest.java | 104 +- .../reactive/AbstractServerHttpResponse.java | 12 +- .../reactive/ReactorServerHttpRequest.java | 12 +- .../reactive/ReactorServerHttpResponse.java | 5 + .../reactive/RxNettyServerHttpRequest.java | 38 +- .../reactive/RxNettyServerHttpResponse.java | 24 +- .../reactive/ServletServerHttpRequest.java | 26 +- .../reactive/ServletServerHttpResponse.java | 21 + .../reactive/UndertowServerHttpRequest.java | 27 +- .../reactive/UndertowServerHttpResponse.java | 18 + .../reactive/CookieIntegrationTests.java | 160 +++ 13 files changed, 1698 insertions(+), 30 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java new file mode 100644 index 0000000000..18a140d236 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.http; + +/** + * Representation for an HTTP Cookie. + * + * @author Rossen Stoyanchev + * @see RFC 6265 + */ +public class HttpCookie { + + private final String name; + + private final String value; + + private String domain; + + private String path; + + private long maxAge = Long.MIN_VALUE; + + private boolean secure; + + private boolean httpOnly; + + + public HttpCookie(String name, String value) { + this.name = name; + this.value = value; + } + + /** + * Return the cookie name. + */ + public String getName() { + return this.name; + } + + /** + * Return the cookie value. + */ + public String getValue() { + return this.value; + } + + public HttpCookie setPath(String path) { + this.path = path; + return this; + } + + /** + * Return the domain attribute of the cookie. + */ + public String getDomain() { + return this.domain; + } + + public HttpCookie setDomain(String domain) { + this.domain = domain; + return this; + } + + /** + * Return the path attribute of the cookie. + */ + public String getPath() { + return this.path; + } + + public HttpCookie setMaxAge(long maxAge) { + this.maxAge = maxAge; + return this; + } + + /** + * Return the maximum age attribute of the cookie in seconds or + * {@link Long#MIN_VALUE} if not set. + */ + public long getMaxAge() { + return this.maxAge; + } + + public HttpCookie setSecure(boolean secure) { + this.secure = secure; + return this; + } + + /** + * Return true if the "Secure" attribute of the cookie is present. + */ + public boolean isSecure() { + return this.secure; + } + + public HttpCookie setHttpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + + /** + * Return true if the "HttpOnly" attribute of the cookie is present. + * @see http://www.owasp.org/index.php/HTTPOnly + */ + public boolean isHttpOnly() { + return this.httpOnly; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java new file mode 100644 index 0000000000..26380c84f8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java @@ -0,0 +1,1159 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http; + +import java.io.Serializable; +import java.net.URI; +import java.nio.charset.Charset; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; + +import org.springframework.util.Assert; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +// A copy of HttpHeaders with additional support for: +// - HTTP cookies + +// To be merged into HttpHeaders from spring-web + +public class HttpHeaders implements MultiValueMap, Serializable { + + private static final long serialVersionUID = -8578554704772377436L; + + /** + * The HTTP {@code Accept} header field name. + * @see Section 5.3.2 of RFC 7231 + */ + public static final String ACCEPT = "Accept"; + /** + * The HTTP {@code Accept-Charset} header field name. + * @see Section 5.3.3 of RFC 7231 + */ + public static final String ACCEPT_CHARSET = "Accept-Charset"; + /** + * The HTTP {@code Accept-Encoding} header field name. + * @see Section 5.3.4 of RFC 7231 + */ + public static final String ACCEPT_ENCODING = "Accept-Encoding"; + /** + * The HTTP {@code Accept-Language} header field name. + * @see Section 5.3.5 of RFC 7231 + */ + public static final String ACCEPT_LANGUAGE = "Accept-Language"; + /** + * The HTTP {@code Accept-Ranges} header field name. + * @see Section 5.3.5 of RFC 7233 + */ + public static final String ACCEPT_RANGES = "Accept-Ranges"; + /** + * The CORS {@code Access-Control-Allow-Credentials} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + /** + * The CORS {@code Access-Control-Allow-Headers} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + /** + * The CORS {@code Access-Control-Allow-Methods} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + /** + * The CORS {@code Access-Control-Allow-Origin} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + /** + * The CORS {@code Access-Control-Expose-Headers} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + /** + * The CORS {@code Access-Control-Max-Age} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; + /** + * The CORS {@code Access-Control-Request-Headers} request header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + /** + * The CORS {@code Access-Control-Request-Method} request header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + /** + * The HTTP {@code Age} header field name. + * @see Section 5.1 of RFC 7234 + */ + public static final String AGE = "Age"; + /** + * The HTTP {@code Allow} header field name. + * @see Section 7.4.1 of RFC 7231 + */ + public static final String ALLOW = "Allow"; + /** + * The HTTP {@code Authorization} header field name. + * @see Section 4.2 of RFC 7235 + */ + public static final String AUTHORIZATION = "Authorization"; + /** + * The HTTP {@code Cache-Control} header field name. + * @see Section 5.2 of RFC 7234 + */ + public static final String CACHE_CONTROL = "Cache-Control"; + /** + * The HTTP {@code Connection} header field name. + * @see Section 6.1 of RFC 7230 + */ + public static final String CONNECTION = "Connection"; + /** + * The HTTP {@code Content-Encoding} header field name. + * @see Section 3.1.2.2 of RFC 7231 + */ + public static final String CONTENT_ENCODING = "Content-Encoding"; + /** + * The HTTP {@code Content-Disposition} header field name + * @see RFC 6266 + */ + public static final String CONTENT_DISPOSITION = "Content-Disposition"; + /** + * The HTTP {@code Content-Language} header field name. + * @see Section 3.1.3.2 of RFC 7231 + */ + public static final String CONTENT_LANGUAGE = "Content-Language"; + /** + * The HTTP {@code Content-Length} header field name. + * @see Section 3.3.2 of RFC 7230 + */ + public static final String CONTENT_LENGTH = "Content-Length"; + /** + * The HTTP {@code Content-Location} header field name. + * @see Section 3.1.4.2 of RFC 7231 + */ + public static final String CONTENT_LOCATION = "Content-Location"; + /** + * The HTTP {@code Content-Range} header field name. + * @see Section 4.2 of RFC 7233 + */ + public static final String CONTENT_RANGE = "Content-Range"; + /** + * The HTTP {@code Content-Type} header field name. + * @see Section 3.1.1.5 of RFC 7231 + */ + public static final String CONTENT_TYPE = "Content-Type"; + /** + * The HTTP {@code Cookie} header field name. + * @see Section 4.3.4 of RFC 2109 + */ + public static final String COOKIE = "Cookie"; + /** + * The HTTP {@code Date} header field name. + * @see Section 7.1.1.2 of RFC 7231 + */ + public static final String DATE = "Date"; + /** + * The HTTP {@code ETag} header field name. + * @see Section 2.3 of RFC 7232 + */ + public static final String ETAG = "ETag"; + /** + * The HTTP {@code Expect} header field name. + * @see Section 5.1.1 of RFC 7231 + */ + public static final String EXPECT = "Expect"; + /** + * The HTTP {@code Expires} header field name. + * @see Section 5.3 of RFC 7234 + */ + public static final String EXPIRES = "Expires"; + /** + * The HTTP {@code From} header field name. + * @see Section 5.5.1 of RFC 7231 + */ + public static final String FROM = "From"; + /** + * The HTTP {@code Host} header field name. + * @see Section 5.4 of RFC 7230 + */ + public static final String HOST = "Host"; + /** + * The HTTP {@code If-Match} header field name. + * @see Section 3.1 of RFC 7232 + */ + public static final String IF_MATCH = "If-Match"; + /** + * The HTTP {@code If-Modified-Since} header field name. + * @see Section 3.3 of RFC 7232 + */ + public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; + /** + * The HTTP {@code If-None-Match} header field name. + * @see Section 3.2 of RFC 7232 + */ + public static final String IF_NONE_MATCH = "If-None-Match"; + /** + * The HTTP {@code If-Range} header field name. + * @see Section 3.2 of RFC 7233 + */ + public static final String IF_RANGE = "If-Range"; + /** + * The HTTP {@code If-Unmodified-Since} header field name. + * @see Section 3.4 of RFC 7232 + */ + public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + /** + * The HTTP {@code Last-Modified} header field name. + * @see Section 2.2 of RFC 7232 + */ + public static final String LAST_MODIFIED = "Last-Modified"; + /** + * The HTTP {@code Link} header field name. + * @see RFC 5988 + */ + public static final String LINK = "Link"; + /** + * The HTTP {@code Location} header field name. + * @see Section 7.1.2 of RFC 7231 + */ + public static final String LOCATION = "Location"; + /** + * The HTTP {@code Max-Forwards} header field name. + * @see Section 5.1.2 of RFC 7231 + */ + public static final String MAX_FORWARDS = "Max-Forwards"; + /** + * The HTTP {@code Origin} header field name. + * @see RFC 6454 + */ + public static final String ORIGIN = "Origin"; + /** + * The HTTP {@code Pragma} header field name. + * @see Section 5.4 of RFC 7234 + */ + public static final String PRAGMA = "Pragma"; + /** + * The HTTP {@code Proxy-Authenticate} header field name. + * @see Section 4.3 of RFC 7235 + */ + public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; + /** + * The HTTP {@code Proxy-Authorization} header field name. + * @see Section 4.4 of RFC 7235 + */ + public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; + /** + * The HTTP {@code Range} header field name. + * @see Section 3.1 of RFC 7233 + */ + public static final String RANGE = "Range"; + /** + * The HTTP {@code Referer} header field name. + * @see Section 5.5.2 of RFC 7231 + */ + public static final String REFERER = "Referer"; + /** + * The HTTP {@code Retry-After} header field name. + * @see Section 7.1.3 of RFC 7231 + */ + public static final String RETRY_AFTER = "Retry-After"; + /** + * The HTTP {@code Server} header field name. + * @see Section 7.4.2 of RFC 7231 + */ + public static final String SERVER = "Server"; + /** + * The HTTP {@code Set-Cookie} header field name. + * @see Section 4.2.2 of RFC 2109 + */ + public static final String SET_COOKIE = "Set-Cookie"; + /** + * The HTTP {@code Set-Cookie2} header field name. + * @see RFC 2965 + */ + public static final String SET_COOKIE2 = "Set-Cookie2"; + /** + * The HTTP {@code TE} header field name. + * @see Section 4.3 of RFC 7230 + */ + public static final String TE = "TE"; + /** + * The HTTP {@code Trailer} header field name. + * @see Section 4.4 of RFC 7230 + */ + public static final String TRAILER = "Trailer"; + /** + * The HTTP {@code Transfer-Encoding} header field name. + * @see Section 3.3.1 of RFC 7230 + */ + public static final String TRANSFER_ENCODING = "Transfer-Encoding"; + /** + * The HTTP {@code Upgrade} header field name. + * @see Section 6.7 of RFC 7230 + */ + public static final String UPGRADE = "Upgrade"; + /** + * The HTTP {@code User-Agent} header field name. + * @see Section 5.5.3 of RFC 7231 + */ + public static final String USER_AGENT = "User-Agent"; + /** + * The HTTP {@code Vary} header field name. + * @see Section 7.1.4 of RFC 7231 + */ + public static final String VARY = "Vary"; + /** + * The HTTP {@code Via} header field name. + * @see Section 5.7.1 of RFC 7230 + */ + public static final String VIA = "Via"; + /** + * The HTTP {@code Warning} header field name. + * @see Section 5.5 of RFC 7234 + */ + public static final String WARNING = "Warning"; + /** + * The HTTP {@code WWW-Authenticate} header field name. + * @see Section 4.1 of RFC 7235 + */ + public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + + /** + * Date formats as specified in the HTTP RFC + * @see Section 7.1.1.1 of RFC 7231 + */ + private static final String[] DATE_FORMATS = new String[] { + "EEE, dd MMM yyyy HH:mm:ss zzz", + "EEE, dd-MMM-yy HH:mm:ss zzz", + "EEE MMM dd HH:mm:ss yyyy" + }; + + private static TimeZone GMT = TimeZone.getTimeZone("GMT"); + + + private final Map> headers; + + private final Map> cookies; + + + /** + * Constructs a new, empty instance of the {@code HttpHeaders} object. + */ + public HttpHeaders() { + this(new LinkedCaseInsensitiveMap>(8, Locale.ENGLISH), null, false); + } + + /** + * Constructor with a map of HTTP cookies that enables lazy initialization + * of input cookies on first access of the map. + * @param inputCookies a Map with input cookies + */ + public HttpHeaders(Map> inputCookies) { + this(new LinkedCaseInsensitiveMap>(8, Locale.ENGLISH), inputCookies, false); + Assert.notNull(cookies, "'inputCookies' is required."); + } + + /** + * Private constructor that can create read-only {@code HttpHeader} instances. + */ + private HttpHeaders(Map> headers, Map> cookies, + boolean readOnly) { + + Assert.notNull(headers, "'headers' must not be null"); + if (readOnly) { + Map> map = + new LinkedCaseInsensitiveMap>(headers.size(), Locale.ENGLISH); + for (Entry> entry : headers.entrySet()) { + List values = Collections.unmodifiableList(entry.getValue()); + map.put(entry.getKey(), values); + } + this.headers = Collections.unmodifiableMap(map); + this.cookies = (cookies != null ? Collections.unmodifiableMap(cookies) : Collections.emptyMap()); + } + else { + this.headers = headers; + this.cookies = (cookies != null ? cookies : new LinkedHashMap<>()); + } + } + + /** + * Set the list of acceptable {@linkplain MediaType media types}, + * as specified by the {@code Accept} header. + */ + public void setAccept(List acceptableMediaTypes) { + set(ACCEPT, MediaType.toString(acceptableMediaTypes)); + } + + /** + * Return the list of acceptable {@linkplain MediaType media types}, + * as specified by the {@code Accept} header. + *

Returns an empty list when the acceptable media types are unspecified. + */ + public List getAccept() { + String value = getFirst(ACCEPT); + List result = (value != null ? MediaType.parseMediaTypes(value) : Collections.emptyList()); + + // Some containers parse 'Accept' into multiple values + if (result.size() == 1) { + List acceptHeader = get(ACCEPT); + if (acceptHeader.size() > 1) { + value = StringUtils.collectionToCommaDelimitedString(acceptHeader); + result = MediaType.parseMediaTypes(value); + } + } + + return result; + } + + /** + * Set the (new) value of the {@code Access-Control-Allow-Credentials} response header. + */ + public void setAccessControlAllowCredentials(boolean allowCredentials) { + set(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(allowCredentials)); + } + + /** + * Returns the value of the {@code Access-Control-Allow-Credentials} response header. + */ + public boolean getAccessControlAllowCredentials() { + return new Boolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS)); + } + + /** + * Set the (new) value of the {@code Access-Control-Allow-Headers} response header. + */ + public void setAccessControlAllowHeaders(List allowedHeaders) { + set(ACCESS_CONTROL_ALLOW_HEADERS, toCommaDelimitedString(allowedHeaders)); + } + + /** + * Returns the value of the {@code Access-Control-Allow-Headers} response header. + */ + public List getAccessControlAllowHeaders() { + return getFirstValueAsList(ACCESS_CONTROL_ALLOW_HEADERS); + } + + /** + * Set the (new) value of the {@code Access-Control-Allow-Methods} response header. + */ + public void setAccessControlAllowMethods(List allowedMethods) { + set(ACCESS_CONTROL_ALLOW_METHODS, StringUtils.collectionToCommaDelimitedString(allowedMethods)); + } + + /** + * Returns the value of the {@code Access-Control-Allow-Methods} response header. + */ + public List getAccessControlAllowMethods() { + List result = new ArrayList(); + String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS); + if (value != null) { + String[] tokens = value.split(",\\s*"); + for (String token : tokens) { + result.add(HttpMethod.valueOf(token)); + } + } + return result; + } + + /** + * Set the (new) value of the {@code Access-Control-Allow-Origin} response header. + */ + public void setAccessControlAllowOrigin(String allowedOrigin) { + set(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigin); + } + + /** + * Returns the value of the {@code Access-Control-Allow-Origin} response header. + */ + public String getAccessControlAllowOrigin() { + return getFirst(ACCESS_CONTROL_ALLOW_ORIGIN); + } + + /** + * Set the (new) value of the {@code Access-Control-Expose-Headers} response header. + */ + public void setAccessControlExposeHeaders(List exposedHeaders) { + set(ACCESS_CONTROL_EXPOSE_HEADERS, toCommaDelimitedString(exposedHeaders)); + } + + /** + * Returns the value of the {@code Access-Control-Expose-Headers} response header. + */ + public List getAccessControlExposeHeaders() { + return getFirstValueAsList(ACCESS_CONTROL_EXPOSE_HEADERS); + } + + /** + * Set the (new) value of the {@code Access-Control-Max-Age} response header. + */ + public void setAccessControlMaxAge(long maxAge) { + set(ACCESS_CONTROL_MAX_AGE, Long.toString(maxAge)); + } + + /** + * Returns the value of the {@code Access-Control-Max-Age} response header. + *

Returns -1 when the max age is unknown. + */ + public long getAccessControlMaxAge() { + String value = getFirst(ACCESS_CONTROL_MAX_AGE); + return (value != null ? Long.parseLong(value) : -1); + } + + /** + * Set the (new) value of the {@code Access-Control-Request-Headers} request header. + */ + public void setAccessControlRequestHeaders(List requestHeaders) { + set(ACCESS_CONTROL_REQUEST_HEADERS, toCommaDelimitedString(requestHeaders)); + } + + /** + * Returns the value of the {@code Access-Control-Request-Headers} request header. + */ + public List getAccessControlRequestHeaders() { + return getFirstValueAsList(ACCESS_CONTROL_REQUEST_HEADERS); + } + + /** + * Set the (new) value of the {@code Access-Control-Request-Method} request header. + */ + public void setAccessControlRequestMethod(HttpMethod requestedMethod) { + set(ACCESS_CONTROL_REQUEST_METHOD, requestedMethod.name()); + } + + /** + * Returns the value of the {@code Access-Control-Request-Method} request header. + */ + public HttpMethod getAccessControlRequestMethod() { + String value = getFirst(ACCESS_CONTROL_REQUEST_METHOD); + return (value != null ? HttpMethod.valueOf(value) : null); + } + + /** + * Set the list of acceptable {@linkplain Charset charsets}, + * as specified by the {@code Accept-Charset} header. + */ + public void setAcceptCharset(List acceptableCharsets) { + StringBuilder builder = new StringBuilder(); + for (Iterator iterator = acceptableCharsets.iterator(); iterator.hasNext();) { + Charset charset = iterator.next(); + builder.append(charset.name().toLowerCase(Locale.ENGLISH)); + if (iterator.hasNext()) { + builder.append(", "); + } + } + set(ACCEPT_CHARSET, builder.toString()); + } + + /** + * Return the list of acceptable {@linkplain Charset charsets}, + * as specified by the {@code Accept-Charset} header. + */ + public List getAcceptCharset() { + List result = new ArrayList(); + String value = getFirst(ACCEPT_CHARSET); + if (value != null) { + String[] tokens = value.split(",\\s*"); + for (String token : tokens) { + int paramIdx = token.indexOf(';'); + String charsetName; + if (paramIdx == -1) { + charsetName = token; + } + else { + charsetName = token.substring(0, paramIdx); + } + if (!charsetName.equals("*")) { + result.add(Charset.forName(charsetName)); + } + } + } + return result; + } + + /** + * Set the set of allowed {@link HttpMethod HTTP methods}, + * as specified by the {@code Allow} header. + */ + public void setAllow(Set allowedMethods) { + set(ALLOW, StringUtils.collectionToCommaDelimitedString(allowedMethods)); + } + + /** + * Return the set of allowed {@link HttpMethod HTTP methods}, + * as specified by the {@code Allow} header. + *

Returns an empty set when the allowed methods are unspecified. + */ + public Set getAllow() { + String value = getFirst(ALLOW); + if (!StringUtils.isEmpty(value)) { + List allowedMethod = new ArrayList(5); + String[] tokens = value.split(",\\s*"); + for (String token : tokens) { + allowedMethod.add(HttpMethod.valueOf(token)); + } + return EnumSet.copyOf(allowedMethod); + } + else { + return EnumSet.noneOf(HttpMethod.class); + } + } + + /** + * Set the (new) value of the {@code Cache-Control} header. + */ + public void setCacheControl(String cacheControl) { + set(CACHE_CONTROL, cacheControl); + } + + /** + * Returns the value of the {@code Cache-Control} header. + */ + public String getCacheControl() { + return getFirst(CACHE_CONTROL); + } + + /** + * Set the (new) value of the {@code Connection} header. + */ + public void setConnection(String connection) { + set(CONNECTION, connection); + } + + /** + * Set the (new) value of the {@code Connection} header. + */ + public void setConnection(List connection) { + set(CONNECTION, toCommaDelimitedString(connection)); + } + + /** + * Returns the value of the {@code Connection} header. + */ + public List getConnection() { + return getFirstValueAsList(CONNECTION); + } + + /** + * Set the (new) value of the {@code Content-Disposition} header + * for {@code form-data}. + * @param name the control name + * @param filename the filename (may be {@code null}) + */ + public void setContentDispositionFormData(String name, String filename) { + Assert.notNull(name, "'name' must not be null"); + StringBuilder builder = new StringBuilder("form-data; name=\""); + builder.append(name).append('\"'); + if (filename != null) { + builder.append("; filename=\""); + builder.append(filename).append('\"'); + } + set(CONTENT_DISPOSITION, builder.toString()); + } + + /** + * Set the length of the body in bytes, as specified by the + * {@code Content-Length} header. + */ + public void setContentLength(long contentLength) { + set(CONTENT_LENGTH, Long.toString(contentLength)); + } + + /** + * Return the length of the body in bytes, as specified by the + * {@code Content-Length} header. + *

Returns -1 when the content-length is unknown. + */ + public long getContentLength() { + String value = getFirst(CONTENT_LENGTH); + return (value != null ? Long.parseLong(value) : -1); + } + + /** + * Set the {@linkplain MediaType media type} of the body, + * as specified by the {@code Content-Type} header. + */ + public void setContentType(MediaType mediaType) { + Assert.isTrue(!mediaType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'"); + Assert.isTrue(!mediaType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'"); + set(CONTENT_TYPE, mediaType.toString()); + } + + /** + * Return the {@linkplain MediaType media type} of the body, as specified + * by the {@code Content-Type} header. + *

Returns {@code null} when the content-type is unknown. + */ + public MediaType getContentType() { + String value = getFirst(CONTENT_TYPE); + return (StringUtils.hasLength(value) ? MediaType.parseMediaType(value) : null); + } + + /** + * Add an HTTP cookie. Supported only when writing output cookies. + */ + public void addCookie(HttpCookie cookie) { + String name = cookie.getName(); + Set set = this.cookies.get(name); + if (set == null) { + set = new LinkedHashSet<>(); + this.cookies.put(name, set); + } + set.add(cookie); + } + + /** + * Return a map with {@link HttpCookie}s. When reading input cookies this map + * cannot be modified. When writing output cookies, this map is mutable. + */ + public Map> getCookies() { + return this.cookies; + } + + /** + * Set the date and time at which the message was created, as specified + * by the {@code Date} header. + *

The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + */ + public void setDate(long date) { + setDate(DATE, date); + } + + /** + * Return the date and time at which the message was created, as specified + * by the {@code Date} header. + *

The date is returned as the number of milliseconds since + * January 1, 1970 GMT. Returns -1 when the date is unknown. + * @throws IllegalArgumentException if the value can't be converted to a date + */ + public long getDate() { + return getFirstDate(DATE); + } + + /** + * Set the (new) entity tag of the body, as specified by the {@code ETag} header. + */ + public void setETag(String eTag) { + if (eTag != null) { + Assert.isTrue(eTag.startsWith("\"") || eTag.startsWith("W/"), + "Invalid eTag, does not start with W/ or \""); + Assert.isTrue(eTag.endsWith("\""), "Invalid eTag, does not end with \""); + } + set(ETAG, eTag); + } + + /** + * Return the entity tag of the body, as specified by the {@code ETag} header. + */ + public String getETag() { + return getFirst(ETAG); + } + + /** + * Set the date and time at which the message is no longer valid, + * as specified by the {@code Expires} header. + *

The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + */ + public void setExpires(long expires) { + setDate(EXPIRES, expires); + } + + /** + * Return the date and time at which the message is no longer valid, + * as specified by the {@code Expires} header. + *

The date is returned as the number of milliseconds since + * January 1, 1970 GMT. Returns -1 when the date is unknown. + */ + public long getExpires() { + try { + return getFirstDate(EXPIRES); + } + catch (IllegalArgumentException ex) { + return -1; + } + } + + /** + * Set the (new) value of the {@code If-Modified-Since} header. + *

The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + */ + public void setIfModifiedSince(long ifModifiedSince) { + setDate(IF_MODIFIED_SINCE, ifModifiedSince); + } + + /** + * Return the value of the {@code If-Modified-Since} header. + *

The date is returned as the number of milliseconds since + * January 1, 1970 GMT. Returns -1 when the date is unknown. + */ + public long getIfModifiedSince() { + return getFirstDate(IF_MODIFIED_SINCE); + } + + /** + * Set the (new) value of the {@code If-None-Match} header. + */ + public void setIfNoneMatch(String ifNoneMatch) { + set(IF_NONE_MATCH, ifNoneMatch); + } + + /** + * Set the (new) values of the {@code If-None-Match} header. + */ + public void setIfNoneMatch(List ifNoneMatchList) { + set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList)); + } + + protected String toCommaDelimitedString(List list) { + StringBuilder builder = new StringBuilder(); + for (Iterator iterator = list.iterator(); iterator.hasNext();) { + String ifNoneMatch = iterator.next(); + builder.append(ifNoneMatch); + if (iterator.hasNext()) { + builder.append(", "); + } + } + return builder.toString(); + } + + /** + * Return the value of the {@code If-None-Match} header. + */ + public List getIfNoneMatch() { + return getFirstValueAsList(IF_NONE_MATCH); + } + + protected List getFirstValueAsList(String header) { + List result = new ArrayList(); + String value = getFirst(header); + if (value != null) { + String[] tokens = value.split(",\\s*"); + for (String token : tokens) { + result.add(token); + } + } + return result; + } + + /** + * Set the time the resource was last changed, as specified by the + * {@code Last-Modified} header. + *

The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + */ + public void setLastModified(long lastModified) { + setDate(LAST_MODIFIED, lastModified); + } + + /** + * Return the time the resource was last changed, as specified by the + * {@code Last-Modified} header. + *

The date is returned as the number of milliseconds since + * January 1, 1970 GMT. Returns -1 when the date is unknown. + */ + public long getLastModified() { + return getFirstDate(LAST_MODIFIED); + } + + /** + * Set the (new) location of a resource, + * as specified by the {@code Location} header. + */ + public void setLocation(URI location) { + set(LOCATION, location.toASCIIString()); + } + + /** + * Return the (new) location of a resource + * as specified by the {@code Location} header. + *

Returns {@code null} when the location is unknown. + */ + public URI getLocation() { + String value = getFirst(LOCATION); + return (value != null ? URI.create(value) : null); + } + + /** + * Set the (new) value of the {@code Origin} header. + */ + public void setOrigin(String origin) { + set(ORIGIN, origin); + } + + /** + * Return the value of the {@code Origin} header. + */ + public String getOrigin() { + return getFirst(ORIGIN); + } + + /** + * Set the (new) value of the {@code Pragma} header. + */ + public void setPragma(String pragma) { + set(PRAGMA, pragma); + } + + /** + * Return the value of the {@code Pragma} header. + */ + public String getPragma() { + return getFirst(PRAGMA); + } + + /** + * Sets the (new) value of the {@code Range} header. + */ + public void setRange(List ranges) { + String value = HttpRange.toString(ranges); + set(RANGE, value); + } + + /** + * Returns the value of the {@code Range} header. + *

Returns an empty list when the range is unknown. + */ + public List getRange() { + String value = getFirst(RANGE); + return HttpRange.parseRanges(value); + } + + /** + * Set the (new) value of the {@code Upgrade} header. + */ + public void setUpgrade(String upgrade) { + set(UPGRADE, upgrade); + } + + /** + * Returns the value of the {@code Upgrade} header. + */ + public String getUpgrade() { + return getFirst(UPGRADE); + } + + /** + * Parse the first header value for the given header name as a date, + * return -1 if there is no value, or raise {@link IllegalArgumentException} + * if the value cannot be parsed as a date. + */ + public long getFirstDate(String headerName) { + String headerValue = getFirst(headerName); + if (headerValue == null) { + return -1; + } + for (String dateFormat : DATE_FORMATS) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); + simpleDateFormat.setTimeZone(GMT); + try { + return simpleDateFormat.parse(headerValue).getTime(); + } + catch (ParseException ex) { + // ignore + } + } + throw new IllegalArgumentException("Cannot parse date value \"" + headerValue + + "\" for \"" + headerName + "\" header"); + } + + /** + * Set the given date under the given header name after formatting it as a string + * using the pattern {@code "EEE, dd MMM yyyy HH:mm:ss zzz"}. The equivalent of + * {@link #set(String, String)} but for date headers. + */ + public void setDate(String headerName, long date) { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US); + dateFormat.setTimeZone(GMT); + set(headerName, dateFormat.format(new Date(date))); + } + + /** + * Return the first header value for the given header name, if any. + * @param headerName the header name + * @return the first header value, or {@code null} if none + */ + @Override + public String getFirst(String headerName) { + List headerValues = this.headers.get(headerName); + return (headerValues != null ? headerValues.get(0) : null); + } + + /** + * Add the given, single header value under the given name. + * @param headerName the header name + * @param headerValue the header value + * @throws UnsupportedOperationException if adding headers is not supported + * @see #put(String, List) + * @see #set(String, String) + */ + @Override + public void add(String headerName, String headerValue) { + List headerValues = this.headers.get(headerName); + if (headerValues == null) { + headerValues = new LinkedList(); + this.headers.put(headerName, headerValues); + } + headerValues.add(headerValue); + } + + /** + * Set the given, single header value under the given name. + * @param headerName the header name + * @param headerValue the header value + * @throws UnsupportedOperationException if adding headers is not supported + * @see #put(String, List) + * @see #add(String, String) + */ + @Override + public void set(String headerName, String headerValue) { + List headerValues = new LinkedList(); + headerValues.add(headerValue); + this.headers.put(headerName, headerValues); + } + + @Override + public void setAll(Map values) { + for (Entry entry : values.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + } + + @Override + public Map toSingleValueMap() { + LinkedHashMap singleValueMap = new LinkedHashMap(this.headers.size()); + for (Entry> entry : this.headers.entrySet()) { + singleValueMap.put(entry.getKey(), entry.getValue().get(0)); + } + return singleValueMap; + } + + + // Map implementation + + @Override + public int size() { + return this.headers.size(); + } + + @Override + public boolean isEmpty() { + return this.headers.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.headers.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.headers.containsValue(value); + } + + @Override + public List get(Object key) { + return this.headers.get(key); + } + + @Override + public List put(String key, List value) { + return this.headers.put(key, value); + } + + @Override + public List remove(Object key) { + return this.headers.remove(key); + } + + @Override + public void putAll(Map> map) { + this.headers.putAll(map); + } + + @Override + public void clear() { + this.headers.clear(); + } + + @Override + public Set keySet() { + return this.headers.keySet(); + } + + @Override + public Collection> values() { + return this.headers.values(); + } + + @Override + public Set>> entrySet() { + return this.headers.entrySet(); + } + + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof HttpHeaders)) { + return false; + } + HttpHeaders otherHeaders = (HttpHeaders) other; + return this.headers.equals(otherHeaders.headers); + } + + @Override + public int hashCode() { + return this.headers.hashCode(); + } + + @Override + public String toString() { + return this.headers.toString(); + } + + + /** + * Return a {@code HttpHeaders} object that can only be read, not written to. + */ + public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) { + return new HttpHeaders(headers, headers.getCookies(), true); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index e7a35a8e6b..24eeeb61c1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -17,8 +17,13 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpCookie; /** * Common base class for {@link ServerHttpRequest} implementations. @@ -46,8 +51,8 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { } /** - * Initialize a URI that represents the request. - * Invoked lazily on the first call to {@link #getURI()} and then cached. + * Initialize a URI that represents the request. Invoked lazily on the first + * call to {@link #getURI()} and then cached. * @throws URISyntaxException */ protected abstract URI initUri() throws URISyntaxException; @@ -55,15 +60,102 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { @Override public HttpHeaders getHeaders() { if (this.headers == null) { - this.headers = HttpHeaders.readOnlyHttpHeaders(initHeaders()); + this.headers = new HttpHeaders(new HttpCookieInputMap()); + initHeaders(this.headers); } return this.headers; } /** - * Initialize the headers from the underlying request. - * Invoked lazily on the first call to {@link #getHeaders()} and then cached. + * Initialize the headers from the underlying request. Invoked lazily on the + * first call to {@link #getHeaders()} and then cached. + * @param headers the map to add headers to */ - protected abstract HttpHeaders initHeaders(); + protected abstract void initHeaders(HttpHeaders headers); + + /** + * Initialize the cookies from the underlying request. Invoked lazily on the + * first access to cookies via {@link #getHeaders()} and then cached. + * @param cookies the map to add cookies to + */ + protected abstract void initCookies(Map> cookies); + + + /** + * Read-only map of input cookies with lazy initialization. + */ + private class HttpCookieInputMap implements Map> { + + private Map> cookies; + + + private Map> getCookies() { + if (this.cookies == null) { + this.cookies = new LinkedHashMap<>(); + initCookies(this.cookies); + } + return this.cookies; + } + + @Override + public int size() { + return getCookies().size(); + } + + @Override + public boolean isEmpty() { + return getCookies().isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return getCookies().containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return getCookies().containsValue(value); + } + + @Override + public Set get(Object key) { + return getCookies().get(key); + } + + @Override + public Set keySet() { + return getCookies().keySet(); + } + + @Override + public Collection> values() { + return getCookies().values(); + } + + @Override + public Set>> entrySet() { + return getCookies().entrySet(); + } + + @Override + public Set put(String key, Set value) { + throw new UnsupportedOperationException("Read-only map of cookies."); + } + + @Override + public Set remove(Object key) { + throw new UnsupportedOperationException("Read-only map of cookies."); + } + + @Override + public void putAll(Map> m) { + throw new UnsupportedOperationException("Read-only map of cookies."); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Read-only map of cookies."); + } + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 2c87f89c46..f75f4be2ad 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -42,7 +42,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { @Override public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + return (this.headersWritten ? org.springframework.http.HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } @Override @@ -64,6 +64,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { if (!this.headersWritten) { try { writeHeadersInternal(); + writeCookies(); } finally { this.headersWritten = true; @@ -73,9 +74,14 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { /** * Implement this method to apply header changes from {@link #getHeaders()} - * to the underlying response. This method is protected from being called - * more than once. + * to the underlying response. This method is called once only. */ protected abstract void writeHeadersInternal(); + /** + * Implement this method to add cookies from {@link #getHeaders()} to the + * underlying response. This method is called once only. + */ + protected abstract void writeCookies(); + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index e1f62514c7..4ae9fc9f0a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -18,11 +18,14 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Set; import reactor.Flux; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; @@ -58,12 +61,15 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected HttpHeaders initHeaders() { - HttpHeaders headers = new HttpHeaders(); + protected void initHeaders(HttpHeaders headers) { for (String name : this.channel.headers().names()) { headers.put(name, this.channel.headers().getAll(name)); } - return headers; + } + + @Override + protected void initCookies(Map> cookies) { + // https://github.com/reactor/reactor/issues/614 } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 64a44b94f6..b7cbed3b8c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -67,4 +67,9 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { } } + @Override + protected void writeCookies() { + // https://github.com/reactor/reactor/issues/614 + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index afdabae668..3e6c541f66 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -19,13 +19,18 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.cookie.Cookie; import io.reactivex.netty.protocol.http.server.HttpServerRequest; import reactor.Flux; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; @@ -53,26 +58,43 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { @Override public HttpMethod getMethod() { - return HttpMethod.valueOf(this.getRxNettyRequest().getHttpMethod().name()); + return HttpMethod.valueOf(this.request.getHttpMethod().name()); } @Override protected URI initUri() throws URISyntaxException { - return new URI(this.getRxNettyRequest().getUri()); + return new URI(this.request.getUri()); } @Override - protected HttpHeaders initHeaders() { - HttpHeaders headers = new HttpHeaders(); - for (String name : this.getRxNettyRequest().getHeaderNames()) { - headers.put(name, this.getRxNettyRequest().getAllHeaderValues(name)); + protected void initHeaders(HttpHeaders headers) { + for (String name : this.request.getHeaderNames()) { + headers.put(name, this.request.getAllHeaderValues(name)); + } + } + + @Override + protected void initCookies(Map> map) { + for (String name : this.request.getCookies().keySet()) { + Set set = map.get(name); + if (set == null) { + set = new LinkedHashSet<>(); + map.put(name, set); + } + for (Cookie cookie : this.request.getCookies().get(name)) { + set.add(new HttpCookie(name, cookie.value()) + .setDomain(cookie.domain()) + .setPath(cookie.path()) + .setMaxAge(cookie.maxAge()) + .setSecure(cookie.isSecure()) + .setHttpOnly(cookie.isHttpOnly())); + } } - return headers; } @Override public Flux getBody() { - Observable content = this.getRxNettyRequest().getContent().map(ByteBuf::nioBuffer); + Observable content = this.request.getContent().map(ByteBuf::nioBuffer); content = content.concatWith(Observable.empty()); // See GH issue #58 return RxJava1Converter.from(content); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 5f47e78ced..467c1d6dfd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -19,14 +19,15 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.DefaultCookie; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import org.reactivestreams.Publisher; -import reactor.Flux; import reactor.Mono; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; -import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -53,13 +54,13 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { @Override public void setStatusCode(HttpStatus status) { - getRxNettyResponse().setStatus(HttpResponseStatus.valueOf(status.value())); + this.response.setStatus(HttpResponseStatus.valueOf(status.value())); } @Override protected Mono setBodyInternal(Publisher publisher) { Observable content = RxJava1Converter.from(publisher).map(this::toBytes); - Observable completion = getRxNettyResponse().writeBytes(content); + Observable completion = this.response.writeBytes(content); return RxJava1Converter.from(completion).after(); } @@ -77,4 +78,19 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { } } + @Override + protected void writeCookies() { + for (String name : getHeaders().getCookies().keySet()) { + for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + Cookie cookie = new DefaultCookie(name, httpCookie.getValue()); + cookie.setDomain(httpCookie.getDomain()); + cookie.setPath(httpCookie.getPath()); + cookie.setMaxAge(httpCookie.getMaxAge()); + cookie.setSecure(httpCookie.isSecure()); + cookie.setHttpOnly(httpCookie.isHttpOnly()); + this.response.addCookie(cookie); + } + } + } + } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index a076768c7f..867c9356a2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -21,12 +21,16 @@ import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Enumeration; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import org.reactivestreams.Publisher; import reactor.Flux; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -73,8 +77,7 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected HttpHeaders initHeaders() { - HttpHeaders headers = new HttpHeaders(); + protected void initHeaders(HttpHeaders headers) { for (Enumeration names = getServletRequest().getHeaderNames(); names.hasMoreElements(); ) { String name = (String) names.nextElement(); for (Enumeration values = getServletRequest().getHeaders(name); values.hasMoreElements(); ) { @@ -105,7 +108,24 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { headers.setContentLength(contentLength); } } - return headers; + } + + @Override + protected void initCookies(Map> map) { + for (Cookie cookie : this.request.getCookies()) { + String name = cookie.getName(); + Set set = map.get(name); + if (set == null) { + set = new LinkedHashSet<>(); + map.put(name, set); + } + set.add(new HttpCookie(name, cookie.getValue()) + .setDomain(cookie.getDomain()) + .setPath(cookie.getPath()) + .setMaxAge(cookie.getMaxAge() == -1 ? Long.MIN_VALUE : cookie.getMaxAge()) + .setHttpOnly(cookie.isHttpOnly()) + .setSecure(cookie.getSecure())); + } } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index a292b44f9b..a4e7e91446 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -21,11 +21,13 @@ import java.nio.charset.Charset; import java.util.List; import java.util.Map; import java.util.function.Function; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; import reactor.Mono; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.Assert; @@ -84,4 +86,23 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } } + @Override + protected void writeCookies() { + for (String name : getHeaders().getCookies().keySet()) { + for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + Cookie cookie = new Cookie(name, httpCookie.getValue()); + if (httpCookie.getDomain() != null) { + cookie.setDomain(httpCookie.getDomain()); + } + if (httpCookie.getPath() != null) { + cookie.setPath(httpCookie.getPath()); + } + cookie.setMaxAge(httpCookie.getMaxAge() == Long.MIN_VALUE ? -1 : (int) httpCookie.getMaxAge()); + cookie.setSecure(httpCookie.isSecure()); + cookie.setHttpOnly(httpCookie.isHttpOnly()); + this.response.addCookie(cookie); + } + } + } + } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 6b9b3c32f4..53013fbe54 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -19,12 +19,17 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.Cookie; import io.undertow.util.HeaderValues; import org.reactivestreams.Publisher; import reactor.Flux; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; @@ -67,12 +72,28 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected HttpHeaders initHeaders() { - HttpHeaders headers = new HttpHeaders(); + protected void initHeaders(HttpHeaders headers) { for (HeaderValues values : this.getUndertowExchange().getRequestHeaders()) { headers.put(values.getHeaderName().toString(), values); } - return headers; + } + + @Override + protected void initCookies(Map> map) { + for (String name : this.exchange.getRequestCookies().keySet()) { + Set set = map.get(name); + if (set == null) { + set = new LinkedHashSet<>(); + map.put(name, set); + } + Cookie cookie = this.exchange.getRequestCookies().get(name); + set.add(new HttpCookie(name, cookie.getValue()) + .setDomain(cookie.getDomain()) + .setPath(cookie.getPath()) + .setMaxAge(cookie.getMaxAge() != null ? cookie.getMaxAge() : Long.MIN_VALUE) + .setSecure(cookie.isSecure()) + .setHttpOnly(cookie.isHttpOnly())); + } } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index b32255f461..27c38342f8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -22,10 +22,13 @@ import java.util.Map; import java.util.function.Function; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.Cookie; +import io.undertow.server.handlers.CookieImpl; import io.undertow.util.HttpString; import org.reactivestreams.Publisher; import reactor.Mono; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -75,4 +78,19 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { } } + @Override + protected void writeCookies() { + for (String name : getHeaders().getCookies().keySet()) { + for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + Cookie cookie = new CookieImpl(name, httpCookie.getValue()); + cookie.setDomain(httpCookie.getDomain()); + cookie.setPath(httpCookie.getPath()); + cookie.setMaxAge(httpCookie.getMaxAge() == Long.MIN_VALUE ? null : (int) httpCookie.getMaxAge()); + cookie.setSecure(httpCookie.isSecure()); + cookie.setHttpOnly(httpCookie.isHttpOnly()); + this.exchange.getResponseCookies().putIfAbsent(name, cookie); + } + } + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java new file mode 100644 index 0000000000..6a1dccb4ae --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.http.server.reactive; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import reactor.Mono; + +import org.springframework.http.HttpCookie; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.boot.HttpServer; +import org.springframework.http.server.reactive.boot.JettyHttpServer; +import org.springframework.http.server.reactive.boot.RxNettyHttpServer; +import org.springframework.http.server.reactive.boot.TomcatHttpServer; +import org.springframework.http.server.reactive.boot.UndertowHttpServer; +import org.springframework.util.SocketUtils; +import org.springframework.web.client.RestTemplate; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * Temporarily does not extend AbstractHttpHandlerIntegrationTests in order to + * exclude Reactor Net due to https://github.com/reactor/reactor/issues/614. + * + * @author Rossen Stoyanchev + */ +@RunWith(Parameterized.class) +public class CookieIntegrationTests { + + protected int port; + + @Parameterized.Parameter(0) + public HttpServer server; + + private CookieHandler cookieHandler; + + + @Parameterized.Parameters(name = "server [{0}]") + public static Object[][] arguments() { + return new Object[][] { + {new JettyHttpServer()}, + {new RxNettyHttpServer()}, +// {new ReactorHttpServer()}, + {new TomcatHttpServer()}, + {new UndertowHttpServer()} + }; + } + + + @Before + public void setup() throws Exception { + this.port = SocketUtils.findAvailableTcpPort(); + this.server.setPort(this.port); + this.server.setHandler(createHttpHandler()); + this.server.afterPropertiesSet(); + this.server.start(); + } + + protected HttpHandler createHttpHandler() { + this.cookieHandler = new CookieHandler(); + return this.cookieHandler; + } + + @After + public void tearDown() throws Exception { + this.server.stop(); + } + + + @SuppressWarnings("unchecked") + @Test + public void basicTest() throws Exception { + URI url = new URI("http://localhost:" + port); + String header = "SID=31d4d96e407aad42; lang=en-US"; + ResponseEntity response = new RestTemplate().exchange( + RequestEntity.get(url).header("Cookie", header).build(), Void.class); + + Map> requestCookies = this.cookieHandler.requestCookies; + assertEquals(2, requestCookies.size()); + + Set set = requestCookies.get("SID"); + assertEquals(1, set.size()); + assertEquals("31d4d96e407aad42", set.iterator().next().getValue()); + + set = requestCookies.get("lang"); + assertEquals(1, set.size()); + assertEquals("en-US", set.iterator().next().getValue()); + + List headerValues = response.getHeaders().get("Set-Cookie"); + assertEquals(2, headerValues.size()); + + List parts = splitCookieHeader(headerValues.get(0)); + assertThat(parts, containsInAnyOrder(equalTo("SID=31d4d96e407aad42"), + equalToIgnoringCase("Path=/"), equalToIgnoringCase("Secure"), + equalToIgnoringCase("HttpOnly"))); + + parts = splitCookieHeader(headerValues.get(1)); + assertThat(parts, containsInAnyOrder(equalTo("lang=en-US"), + equalToIgnoringCase("Path=/"), equalToIgnoringCase("Domain=example.com"))); + } + + // No client side HttpCookie support yet + private List splitCookieHeader(String value) { + List list = new ArrayList<>(); + for (String s : value.split(";")){ + list.add(s.trim()); + } + return list; + } + + + private class CookieHandler implements HttpHandler { + + private Map> requestCookies; + + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + + this.requestCookies = request.getHeaders().getCookies(); + this.requestCookies.size(); // Cause lazy loading + + response.getHeaders().addCookie(new HttpCookie("SID", "31d4d96e407aad42") + .setPath("/").setHttpOnly(true).setSecure(true)); + response.getHeaders().addCookie(new HttpCookie("lang", "en-US") + .setDomain("example.com").setPath("/")); + response.writeHeaders(); + + return Mono.empty(); + } + } + +}