Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions spring-web/src/main/java/org/springframework/http/HttpHeaders.java
Original file line number Diff line number Diff line change
Expand Up @@ -1549,13 +1549,9 @@ private ZonedDateTime getFirstZonedDateTime(String headerName, boolean rejectInv
headerValue = headerValue.substring(0, parametersIndex);
}

for (DateTimeFormatter dateFormatter : DATE_PARSERS) {
try {
return ZonedDateTime.parse(headerValue, dateFormatter);
}
catch (DateTimeParseException ex) {
// ignore
}
ZonedDateTime zonedDateTime = getZonedDateTime(headerValue);
if (zonedDateTime != null) {
return zonedDateTime;
}

}
Expand All @@ -1566,6 +1562,26 @@ private ZonedDateTime getFirstZonedDateTime(String headerName, boolean rejectInv
return null;
}

/**
* Parses the date in headers with Date formats specified in the HTTP RFC to use for parsing.
* {@link HttpHeaders#DATE_PARSERS}
* @param date the date header value as string
* @return the parsed date header value
*/
// used in ClientHttpResponse
@Nullable
public static ZonedDateTime getZonedDateTime(String date) {
for (DateTimeFormatter dateFormatter : DATE_PARSERS) {
try {
return ZonedDateTime.parse(date, dateFormatter);
}
catch (DateTimeParseException ex) {
// ignore
}
}
return null;
}

/**
* Return all values of a given header name, even if this header is set
* multiple times.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@

package org.springframework.http.client.reactive;

import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.ResponseCookie;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;

Expand Down Expand Up @@ -51,4 +57,24 @@ default String getId() {
*/
MultiValueMap<String, ResponseCookie> getCookies();

static long mergeMaxAgeAndExpires(@Nullable String maxAgeAttribute, @Nullable String expiresAttribute) {
if (maxAgeAttribute != null) {
return Long.parseLong(maxAgeAttribute);
}
else if (expiresAttribute != null) {
ZonedDateTime expiresDate = HttpHeaders.getZonedDateTime(expiresAttribute);

// Verify that the input date is in the future
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
if (expiresDate == null || expiresDate.isBefore(now)) {
return -1;
}
else {
// Calculate the difference in seconds
return ChronoUnit.SECONDS.between(now, expiresDate);
}
}
return -1;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ private static MultiValueMap<String, ResponseCookie> adaptCookies(HttpClientCont
}

private static long getMaxAgeSeconds(Cookie cookie) {
String maxAgeAttribute = cookie.getAttribute(Cookie.MAX_AGE_ATTR);
return (maxAgeAttribute != null ? Long.parseLong(maxAgeAttribute) : -1);
return ClientHttpResponse.mergeMaxAgeAndExpires(cookie.getAttribute(Cookie.MAX_AGE_ATTR), cookie.getAttribute(Cookie.EXPIRES_ATTR));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,23 +128,26 @@ public MultiValueMap<String, ResponseCookie> getCookies() {
ResponseCookie.fromClientResponse(cookie.name().toString(), cookie.value().toString())
.domain(toString(cookie.domain()))
.path(toString(cookie.path()))
.maxAge(toLong(cookie.maxAge()))
.maxAge(getMaxAgeSeconds(cookie))
.secure(cookie.isSecure())
.httpOnly(cookie.isHttpOnly())
.sameSite(getSameSite(cookie))
.build()));
return CollectionUtils.unmodifiableMultiValueMap(result);
}

private static long getMaxAgeSeconds(HttpSetCookie cookie) {
String maxAge = (cookie.maxAge() == null) ? null : cookie.maxAge().toString();
String expires = toString(cookie.expires());

return ClientHttpResponse.mergeMaxAgeAndExpires(maxAge, expires);
}

@Nullable
private static String toString(@Nullable CharSequence value) {
return (value != null ? value.toString() : null);
}

private static long toLong(@Nullable Long value) {
return (value != null ? value : -1);
}

@Nullable
private static String getSameSite(HttpSetCookie cookie) {
if (cookie instanceof DefaultHttpSetCookie defaultCookie && defaultCookie.sameSite() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
import java.lang.annotation.Target;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand All @@ -48,9 +51,11 @@
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.ResponseCookie;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
Expand All @@ -65,6 +70,9 @@ class ClientHttpConnectorTests {

private static final Set<HttpMethod> METHODS_WITH_BODY =
Set.of(HttpMethod.PUT, HttpMethod.POST, HttpMethod.PATCH);
public static final String MAX_AGE_AND_EXPIRES = "Max-Age-And-Expires";
public static final String MAX_AGE_ONLY = "Max-Age-Only";
public static final String EXPIRES_ONLY = "Expires-Only";

private final MockWebServer server = new MockWebServer();

Expand Down Expand Up @@ -172,6 +180,70 @@ void cancelResponseBody(ClientHttpConnector connector) {
.verify();
}

@ParameterizedConnectorTest
void testExpiresMaxAgeAttributes(ClientHttpConnector connector) {
// maxAge is set to 12 days from system time.
long maxAge = 1036800L;

// expires date is set to 10 days from system time.
long expires = 864000L;
ZonedDateTime currentDateTime = ZonedDateTime.now(java.time.ZoneOffset.UTC);
ZonedDateTime futureDateTime = currentDateTime.plusSeconds(expires);

// processing time may affect the calculation of ZonedDateTime.now during merge of expires and max age
// therefore we check range with buffer of 2 seconds
long maxAgeLowerLimit = maxAge - 2;
long maxAgeUpperLimit = maxAge + 2;
long expiresLowerLimit = expires - 2;
long expiresUpperLimit = expires + 2;

List<HttpCookie> httpCookies = getHttpCookies(maxAge, futureDateTime);
prepareResponse(response -> {
response.setResponseCode(200);
httpCookies.forEach(httpCookie -> response.addHeader("Set-Cookie", httpCookie.toString()));
});

ClientHttpResponse response = connector.connect(HttpMethod.POST, this.server.url("/").uri(),
ReactiveHttpOutputMessage::setComplete).block();
assertThat(response).isNotNull();
assertThat(response.getCookies()).isNotEmpty();

List<ResponseCookie> maxAgeAndExpiresCookies = response.getCookies().get(MAX_AGE_AND_EXPIRES);
assertThat(maxAgeAndExpiresCookies).size().isEqualTo(1);
Duration maxAgeAndExpires = maxAgeAndExpiresCookies.get(0).getMaxAge();
assertThat(maxAgeAndExpires.getSeconds()).isGreaterThanOrEqualTo(maxAgeLowerLimit);
assertThat(maxAgeAndExpires.getSeconds()).isLessThanOrEqualTo(maxAgeUpperLimit);

List<ResponseCookie> maxAgeOnlyCookies = response.getCookies().get(MAX_AGE_ONLY);
assertThat(maxAgeOnlyCookies).size().isEqualTo(1);
Duration maxAgeOnly = maxAgeOnlyCookies.get(0).getMaxAge();
assertThat(maxAgeOnly.getSeconds()).isGreaterThanOrEqualTo(maxAgeLowerLimit);
assertThat(maxAgeOnly.getSeconds()).isLessThanOrEqualTo(maxAgeUpperLimit);

List<ResponseCookie> expiresOnlyCookies = response.getCookies().get(EXPIRES_ONLY);
assertThat(expiresOnlyCookies).size().isEqualTo(1);
Duration expiresOnly = expiresOnlyCookies.get(0).getMaxAge();
assertThat(expiresOnly.getSeconds()).isGreaterThanOrEqualTo(expiresLowerLimit);
assertThat(expiresOnly.getSeconds()).isLessThanOrEqualTo(expiresUpperLimit);
}

private List<HttpCookie> getHttpCookies(long maxAge, ZonedDateTime futureDateTime) {
String expires = futureDateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME);

List<HttpCookie> httpCookies = new ArrayList<>();

String maxAgeAndExpiresValue = String.format("; Max-Age=%d; Expires=%s", maxAge, expires);
httpCookies.add(new HttpCookie(MAX_AGE_AND_EXPIRES, maxAgeAndExpiresValue));

String maxAgeOnlyValue = String.format("; Max-Age=%d", maxAge);
httpCookies.add(new HttpCookie(MAX_AGE_ONLY, maxAgeOnlyValue));

String expiresValue = String.format("; Expires=%s", expires);
httpCookies.add(new HttpCookie(EXPIRES_ONLY, expiresValue));

return httpCookies;
}

private Buffer randomBody(int size) {
Buffer responseBody = new Buffer();
Random rnd = new Random();
Expand Down Expand Up @@ -211,7 +283,9 @@ static List<Named<ClientHttpConnector>> connectors() {
return Arrays.asList(
named("Reactor Netty", new ReactorClientHttpConnector()),
named("Jetty", new JettyClientHttpConnector()),
named("HttpComponents", new HttpComponentsClientHttpConnector())
named("HttpComponents", new HttpComponentsClientHttpConnector()),
named("Jdk", new JdkClientHttpConnector()),
named("Reactor Netty 2", new ReactorNetty2ClientHttpConnector())
);
}

Expand Down