Allow to set custom cookie parsers
Provides a way to be compliant with RFC 6265 section 4.1.1. See gh-34081
This commit is contained in:
@@ -34,6 +34,8 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.support.DefaultHttpCookieParser;
|
||||
import org.springframework.http.support.HttpCookieParser;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -50,6 +52,8 @@ public class JdkClientHttpConnector implements ClientHttpConnector {
|
||||
|
||||
private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
|
||||
|
||||
private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser();
|
||||
|
||||
private @Nullable Duration readTimeout;
|
||||
|
||||
|
||||
@@ -105,6 +109,16 @@ public class JdkClientHttpConnector implements ClientHttpConnector {
|
||||
this.readTimeout = readTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@code HttpCookieParser} to be used in response parsing.
|
||||
* <p>Default is {@code DefaultHttpCookieParser} based on {@code java.net.HttpCookie} capabilities</p>
|
||||
* @param httpCookieParser
|
||||
*/
|
||||
public void setHttpCookieParser(HttpCookieParser httpCookieParser) {
|
||||
Assert.notNull(readTimeout, "httpCookieParser is required");
|
||||
this.httpCookieParser = httpCookieParser;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<ClientHttpResponse> connect(
|
||||
@@ -120,7 +134,7 @@ public class JdkClientHttpConnector implements ClientHttpConnector {
|
||||
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));
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.http.client.reactive;
|
||||
|
||||
import java.net.HttpCookie;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -25,10 +24,7 @@ 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 org.jspecify.annotations.Nullable;
|
||||
import reactor.adapter.JdkFlowAdapter;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@@ -38,6 +34,7 @@ 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.http.support.HttpCookieParser;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
@@ -52,16 +49,12 @@ import org.springframework.util.MultiValueMap;
|
||||
*/
|
||||
class JdkClientHttpResponse extends AbstractClientHttpResponse {
|
||||
|
||||
private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
|
||||
|
||||
|
||||
|
||||
public JdkClientHttpResponse(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response,
|
||||
DataBufferFactory bufferFactory) {
|
||||
DataBufferFactory bufferFactory, HttpCookieParser httpCookieParser) {
|
||||
|
||||
super(HttpStatusCode.valueOf(response.statusCode()),
|
||||
adaptHeaders(response),
|
||||
adaptCookies(response),
|
||||
adaptCookies(response, httpCookieParser),
|
||||
adaptBody(response, bufferFactory)
|
||||
);
|
||||
}
|
||||
@@ -74,29 +67,15 @@ class JdkClientHttpResponse extends AbstractClientHttpResponse {
|
||||
return HttpHeaders.readOnlyHttpHeaders(multiValueMap);
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, ResponseCookie> adaptCookies(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response) {
|
||||
private static MultiValueMap<String, ResponseCookie> adaptCookies(HttpResponse<Flow.Publisher<List<ByteBuffer>>> 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<DataBuffer> adaptBody(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response, DataBufferFactory bufferFactory) {
|
||||
return JdkFlowAdapter.flowPublisherToFlux(response.body())
|
||||
.flatMapIterable(Function.identity())
|
||||
|
||||
@@ -28,6 +28,8 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.JettyDataBufferFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.support.DefaultHttpCookieParser;
|
||||
import org.springframework.http.support.HttpCookieParser;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -43,6 +45,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}.
|
||||
@@ -83,6 +87,12 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
|
||||
this.bufferFactory = bufferFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cookie parser to use.
|
||||
*/
|
||||
public void setHttpCookieParser(HttpCookieParser httpCookieParser) {
|
||||
this.httpCookieParser = httpCookieParser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
|
||||
@@ -111,7 +121,7 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
|
||||
return Mono.fromDirect(request.toReactiveRequest()
|
||||
.response((reactiveResponse, chunkPublisher) -> {
|
||||
Flux<DataBuffer> content = Flux.from(chunkPublisher).map(this.bufferFactory::wrap);
|
||||
return Mono.just(new JettyClientHttpResponse(reactiveResponse, content));
|
||||
return Mono.just(new JettyClientHttpResponse(reactiveResponse, content, this.httpCookieParser));
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -16,20 +16,17 @@
|
||||
|
||||
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 org.jspecify.annotations.Nullable;
|
||||
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.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
@@ -45,14 +42,11 @@ import org.springframework.util.MultiValueMap;
|
||||
*/
|
||||
class JettyClientHttpResponse extends AbstractClientHttpResponse {
|
||||
|
||||
private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
|
||||
|
||||
|
||||
public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux<DataBuffer> content) {
|
||||
public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux<DataBuffer> content, HttpCookieParser httpCookieParser) {
|
||||
|
||||
super(HttpStatusCode.valueOf(reactiveResponse.getStatus()),
|
||||
adaptHeaders(reactiveResponse),
|
||||
adaptCookies(reactiveResponse),
|
||||
adaptCookies(reactiveResponse, httpCookieParser),
|
||||
content);
|
||||
}
|
||||
|
||||
@@ -60,26 +54,14 @@ class JettyClientHttpResponse extends AbstractClientHttpResponse {
|
||||
MultiValueMap<String, String> headers = new JettyHeadersAdapter(response.getHeaders());
|
||||
return HttpHeaders.readOnlyHttpHeaders(headers);
|
||||
}
|
||||
private static MultiValueMap<String, ResponseCookie> adaptCookies(ReactiveResponse response) {
|
||||
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
|
||||
private static MultiValueMap<String, ResponseCookie> adaptCookies(ReactiveResponse response, HttpCookieParser httpCookieParser) {
|
||||
List<HttpField> 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<String, ResponseCookie> 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);
|
||||
}
|
||||
|
||||
private static @Nullable String parseSameSite(String headerValue) {
|
||||
Matcher matcher = SAME_SITE_PATTERN.matcher(headerValue);
|
||||
return (matcher.matches() ? matcher.group(1) : null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.springframework.http.support;
|
||||
|
||||
import org.springframework.http.ResponseCookie;
|
||||
|
||||
import java.net.HttpCookie;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
public final class DefaultHttpCookieParser implements HttpCookieParser {
|
||||
|
||||
private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
|
||||
|
||||
@Override
|
||||
public Stream<ResponseCookie> 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.springframework.http.support;
|
||||
|
||||
import org.springframework.http.ResponseCookie;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public interface HttpCookieParser {
|
||||
|
||||
Stream<ResponseCookie> parse(String header);
|
||||
}
|
||||
Reference in New Issue
Block a user