From 2bdf29dd6d097f4511e387001806e6e2902dbab2 Mon Sep 17 00:00:00 2001 From: m4tt30c91 Date: Thu, 12 Dec 2024 15:03:44 +0100 Subject: [PATCH] Add a way to set custom cookie parsers Add a way to set custom cookie parsers to be compliant with rfc6265 section 4.1.1 --- .../reactive/JdkClientHttpConnector.java | 16 +++++- .../reactive/JdkClientHttpResponse.java | 56 ++++++------------- .../reactive/JettyClientHttpConnector.java | 12 +++- .../reactive/JettyClientHttpResponse.java | 45 ++++----------- .../http/support/DefaultHttpCookieParser.java | 32 +++++++++++ .../http/support/HttpCookieParser.java | 10 ++++ 6 files changed, 97 insertions(+), 74 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/support/DefaultHttpCookieParser.java create mode 100644 spring-web/src/main/java/org/springframework/http/support/HttpCookieParser.java diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java index 5b6f08de1051..d5394676bebf 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java @@ -28,6 +28,8 @@ import java.util.concurrent.Flow; import java.util.function.Function; +import org.springframework.http.support.DefaultHttpCookieParser; +import org.springframework.http.support.HttpCookieParser; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBufferFactory; @@ -50,6 +52,8 @@ public class JdkClientHttpConnector implements ClientHttpConnector { private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; + private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser(); + @Nullable private Duration readTimeout; @@ -106,6 +110,16 @@ public void setReadTimeout(Duration readTimeout) { this.readTimeout = readTimeout; } + /** + * Set the {@code HttpCookieParser} to be used in response parsing. + *

Default is {@code DefaultHttpCookieParser} based on {@code java.net.HttpCookie} capabilities

+ * @param httpCookieParser + */ + public void setHttpCookieParser(HttpCookieParser httpCookieParser) { + Assert.notNull(readTimeout, "httpCookieParser is required"); + this.httpCookieParser = httpCookieParser; + } + @Override public Mono connect( @@ -121,7 +135,7 @@ public Mono connect( this.httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofPublisher()); return Mono.fromCompletionStage(future) - .map(response -> new JdkClientHttpResponse(response, this.bufferFactory)); + .map(response -> new JdkClientHttpResponse(response, this.bufferFactory, this.httpCookieParser)); })); } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java index e1a742b33d86..4bded6855ede 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java @@ -16,32 +16,28 @@ package org.springframework.http.client.reactive; -import java.net.HttpCookie; -import java.net.http.HttpClient; -import java.net.http.HttpResponse; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.Flow; -import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import reactor.adapter.JdkFlowAdapter; -import reactor.core.publisher.Flux; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; -import org.springframework.lang.Nullable; +import org.springframework.http.support.HttpCookieParser; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import reactor.adapter.JdkFlowAdapter; +import reactor.core.publisher.Flux; + +import java.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Flow; +import java.util.function.Function; /** * {@link ClientHttpResponse} for the Java {@link HttpClient}. @@ -52,16 +48,12 @@ */ class JdkClientHttpResponse extends AbstractClientHttpResponse { - private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*"); - - - public JdkClientHttpResponse(HttpResponse>> response, - DataBufferFactory bufferFactory) { + DataBufferFactory bufferFactory, HttpCookieParser httpCookieParser) { super(HttpStatusCode.valueOf(response.statusCode()), adaptHeaders(response), - adaptCookies(response), + adaptCookies(response, httpCookieParser), adaptBody(response, bufferFactory) ); } @@ -74,29 +66,15 @@ private static HttpHeaders adaptHeaders(HttpResponse adaptCookies(HttpResponse>> response) { + private static MultiValueMap adaptCookies(HttpResponse>> response, + HttpCookieParser httpCookieParser) { return response.headers().allValues(HttpHeaders.SET_COOKIE).stream() - .flatMap(header -> { - Matcher matcher = SAME_SITE_PATTERN.matcher(header); - String sameSite = (matcher.matches() ? matcher.group(1) : null); - return HttpCookie.parse(header).stream().map(cookie -> toResponseCookie(cookie, sameSite)); - }) + .flatMap(httpCookieParser::parse) .collect(LinkedMultiValueMap::new, (cookies, cookie) -> cookies.add(cookie.getName(), cookie), LinkedMultiValueMap::addAll); } - private static ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String sameSite) { - return ResponseCookie.from(cookie.getName(), cookie.getValue()) - .domain(cookie.getDomain()) - .httpOnly(cookie.isHttpOnly()) - .maxAge(cookie.getMaxAge()) - .path(cookie.getPath()) - .secure(cookie.getSecure()) - .sameSite(sameSite) - .build(); - } - private static Flux adaptBody(HttpResponse>> response, DataBufferFactory bufferFactory) { return JdkFlowAdapter.flowPublisherToFlux(response.body()) .flatMapIterable(Function.identity()) diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java index ca255fe97375..36207fd82e3a 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java @@ -22,6 +22,8 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.Request; +import org.springframework.http.support.DefaultHttpCookieParser; +import org.springframework.http.support.HttpCookieParser; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -44,6 +46,8 @@ public class JettyClientHttpConnector implements ClientHttpConnector { private JettyDataBufferFactory bufferFactory = new JettyDataBufferFactory(); + private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser(); + /** * Default constructor that creates a new instance of {@link HttpClient}. @@ -99,6 +103,12 @@ public void setBufferFactory(JettyDataBufferFactory bufferFactory) { this.bufferFactory = bufferFactory; } + /** + * Set the cookie parser to use. + */ + public void setHttpCookieParser(HttpCookieParser httpCookieParser) { + this.httpCookieParser = httpCookieParser; + } @Override public Mono connect(HttpMethod method, URI uri, @@ -127,7 +137,7 @@ private Mono execute(JettyClientHttpRequest request) { return Mono.fromDirect(request.toReactiveRequest() .response((reactiveResponse, chunkPublisher) -> { Flux content = Flux.from(chunkPublisher).map(this.bufferFactory::wrap); - return Mono.just(new JettyClientHttpResponse(reactiveResponse, content)); + return Mono.just(new JettyClientHttpResponse(reactiveResponse, content, this.httpCookieParser)); })); } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java index cef7da388e60..a7a9ac3bebf2 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java @@ -16,24 +16,20 @@ package org.springframework.http.client.reactive; -import java.net.HttpCookie; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.reactive.client.ReactiveResponse; -import reactor.core.publisher.Flux; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; +import org.springframework.http.support.HttpCookieParser; import org.springframework.http.support.JettyHeadersAdapter; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; + +import java.util.List; /** * {@link ClientHttpResponse} implementation for the Jetty ReactiveStreams HTTP client. @@ -45,14 +41,11 @@ */ class JettyClientHttpResponse extends AbstractClientHttpResponse { - private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*"); - - - public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux content) { + public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux content, HttpCookieParser httpCookieParser) { super(HttpStatusCode.valueOf(reactiveResponse.getStatus()), adaptHeaders(reactiveResponse), - adaptCookies(reactiveResponse), + adaptCookies(reactiveResponse, httpCookieParser), content); } @@ -60,27 +53,13 @@ private static HttpHeaders adaptHeaders(ReactiveResponse response) { MultiValueMap headers = new JettyHeadersAdapter(response.getHeaders()); return HttpHeaders.readOnlyHttpHeaders(headers); } - private static MultiValueMap adaptCookies(ReactiveResponse response) { - MultiValueMap result = new LinkedMultiValueMap<>(); + private static MultiValueMap adaptCookies(ReactiveResponse response, HttpCookieParser httpCookieParser) { List cookieHeaders = response.getHeaders().getFields(HttpHeaders.SET_COOKIE); - cookieHeaders.forEach(header -> - HttpCookie.parse(header.getValue()).forEach(cookie -> result.add(cookie.getName(), - ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue()) - .domain(cookie.getDomain()) - .path(cookie.getPath()) - .maxAge(cookie.getMaxAge()) - .secure(cookie.getSecure()) - .httpOnly(cookie.isHttpOnly()) - .sameSite(parseSameSite(header.getValue())) - .build())) - ); + MultiValueMap result = cookieHeaders.stream() + .flatMap(header -> httpCookieParser.parse(header.getValue())) + .collect(LinkedMultiValueMap::new, + (cookies, cookie) -> cookies.add(cookie.getName(), cookie), + LinkedMultiValueMap::addAll); return CollectionUtils.unmodifiableMultiValueMap(result); } - - @Nullable - private static String parseSameSite(String headerValue) { - Matcher matcher = SAME_SITE_PATTERN.matcher(headerValue); - return (matcher.matches() ? matcher.group(1) : null); - } - } diff --git a/spring-web/src/main/java/org/springframework/http/support/DefaultHttpCookieParser.java b/spring-web/src/main/java/org/springframework/http/support/DefaultHttpCookieParser.java new file mode 100644 index 000000000000..0eb402eeec03 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/support/DefaultHttpCookieParser.java @@ -0,0 +1,32 @@ +package org.springframework.http.support; + +import org.springframework.http.ResponseCookie; +import org.springframework.lang.Nullable; + +import java.net.HttpCookie; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public final class DefaultHttpCookieParser implements HttpCookieParser { + + private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*"); + + @Override + public Stream parse(String header) { + Matcher matcher = SAME_SITE_PATTERN.matcher(header); + String sameSite = (matcher.matches() ? matcher.group(1) : null); + return HttpCookie.parse(header).stream().map(cookie -> toResponseCookie(cookie, sameSite)); + } + + private static ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String sameSite) { + return ResponseCookie.from(cookie.getName(), cookie.getValue()) + .domain(cookie.getDomain()) + .httpOnly(cookie.isHttpOnly()) + .maxAge(cookie.getMaxAge()) + .path(cookie.getPath()) + .secure(cookie.getSecure()) + .sameSite(sameSite) + .build(); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/support/HttpCookieParser.java b/spring-web/src/main/java/org/springframework/http/support/HttpCookieParser.java new file mode 100644 index 000000000000..e9e0703eab9e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/support/HttpCookieParser.java @@ -0,0 +1,10 @@ +package org.springframework.http.support; + +import org.springframework.http.ResponseCookie; + +import java.util.stream.Stream; + +public interface HttpCookieParser { + + Stream parse(String header); +}