Skip to content

Commit ba74de9

Browse files
m4tt30c91rstoyanchev
authored andcommitted
Allow to set custom cookie parsers
Provides a way to be compliant with RFC 6265 section 4.1.1. See gh-34081
1 parent 011e398 commit ba74de9

File tree

6 files changed

+84
-56
lines changed

6 files changed

+84
-56
lines changed

spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import org.springframework.core.io.buffer.DataBufferFactory;
3535
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
3636
import org.springframework.http.HttpMethod;
37+
import org.springframework.http.support.DefaultHttpCookieParser;
38+
import org.springframework.http.support.HttpCookieParser;
3739
import org.springframework.util.Assert;
3840

3941
/**
@@ -50,6 +52,8 @@ public class JdkClientHttpConnector implements ClientHttpConnector {
5052

5153
private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
5254

55+
private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser();
56+
5357
private @Nullable Duration readTimeout;
5458

5559

@@ -105,6 +109,16 @@ public void setReadTimeout(Duration readTimeout) {
105109
this.readTimeout = readTimeout;
106110
}
107111

112+
/**
113+
* Set the {@code HttpCookieParser} to be used in response parsing.
114+
* <p>Default is {@code DefaultHttpCookieParser} based on {@code java.net.HttpCookie} capabilities</p>
115+
* @param httpCookieParser
116+
*/
117+
public void setHttpCookieParser(HttpCookieParser httpCookieParser) {
118+
Assert.notNull(readTimeout, "httpCookieParser is required");
119+
this.httpCookieParser = httpCookieParser;
120+
}
121+
108122

109123
@Override
110124
public Mono<ClientHttpResponse> connect(
@@ -120,7 +134,7 @@ public Mono<ClientHttpResponse> connect(
120134
this.httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofPublisher());
121135

122136
return Mono.fromCompletionStage(future)
123-
.map(response -> new JdkClientHttpResponse(response, this.bufferFactory));
137+
.map(response -> new JdkClientHttpResponse(response, this.bufferFactory, this.httpCookieParser));
124138
}));
125139
}
126140

spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java

+6-27
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.http.client.reactive;
1818

19-
import java.net.HttpCookie;
2019
import java.net.http.HttpClient;
2120
import java.net.http.HttpResponse;
2221
import java.nio.ByteBuffer;
@@ -25,10 +24,7 @@
2524
import java.util.Map;
2625
import java.util.concurrent.Flow;
2726
import java.util.function.Function;
28-
import java.util.regex.Matcher;
29-
import java.util.regex.Pattern;
3027

31-
import org.jspecify.annotations.Nullable;
3228
import reactor.adapter.JdkFlowAdapter;
3329
import reactor.core.publisher.Flux;
3430

@@ -38,6 +34,7 @@
3834
import org.springframework.http.HttpHeaders;
3935
import org.springframework.http.HttpStatusCode;
4036
import org.springframework.http.ResponseCookie;
37+
import org.springframework.http.support.HttpCookieParser;
4138
import org.springframework.util.CollectionUtils;
4239
import org.springframework.util.LinkedCaseInsensitiveMap;
4340
import org.springframework.util.LinkedMultiValueMap;
@@ -52,16 +49,12 @@
5249
*/
5350
class JdkClientHttpResponse extends AbstractClientHttpResponse {
5451

55-
private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
56-
57-
58-
5952
public JdkClientHttpResponse(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response,
60-
DataBufferFactory bufferFactory) {
53+
DataBufferFactory bufferFactory, HttpCookieParser httpCookieParser) {
6154

6255
super(HttpStatusCode.valueOf(response.statusCode()),
6356
adaptHeaders(response),
64-
adaptCookies(response),
57+
adaptCookies(response, httpCookieParser),
6558
adaptBody(response, bufferFactory)
6659
);
6760
}
@@ -74,29 +67,15 @@ private static HttpHeaders adaptHeaders(HttpResponse<Flow.Publisher<List<ByteBuf
7467
return HttpHeaders.readOnlyHttpHeaders(multiValueMap);
7568
}
7669

77-
private static MultiValueMap<String, ResponseCookie> adaptCookies(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response) {
70+
private static MultiValueMap<String, ResponseCookie> adaptCookies(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response,
71+
HttpCookieParser httpCookieParser) {
7872
return response.headers().allValues(HttpHeaders.SET_COOKIE).stream()
79-
.flatMap(header -> {
80-
Matcher matcher = SAME_SITE_PATTERN.matcher(header);
81-
String sameSite = (matcher.matches() ? matcher.group(1) : null);
82-
return HttpCookie.parse(header).stream().map(cookie -> toResponseCookie(cookie, sameSite));
83-
})
73+
.flatMap(httpCookieParser::parse)
8474
.collect(LinkedMultiValueMap::new,
8575
(cookies, cookie) -> cookies.add(cookie.getName(), cookie),
8676
LinkedMultiValueMap::addAll);
8777
}
8878

89-
private static ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String sameSite) {
90-
return ResponseCookie.from(cookie.getName(), cookie.getValue())
91-
.domain(cookie.getDomain())
92-
.httpOnly(cookie.isHttpOnly())
93-
.maxAge(cookie.getMaxAge())
94-
.path(cookie.getPath())
95-
.secure(cookie.getSecure())
96-
.sameSite(sameSite)
97-
.build();
98-
}
99-
10079
private static Flux<DataBuffer> adaptBody(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response, DataBufferFactory bufferFactory) {
10180
return JdkFlowAdapter.flowPublisherToFlux(response.body())
10281
.flatMapIterable(Function.identity())

spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.springframework.core.io.buffer.DataBuffer;
2929
import org.springframework.core.io.buffer.JettyDataBufferFactory;
3030
import org.springframework.http.HttpMethod;
31+
import org.springframework.http.support.DefaultHttpCookieParser;
32+
import org.springframework.http.support.HttpCookieParser;
3133
import org.springframework.util.Assert;
3234

3335
/**
@@ -43,6 +45,8 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
4345

4446
private JettyDataBufferFactory bufferFactory = new JettyDataBufferFactory();
4547

48+
private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser();
49+
4650

4751
/**
4852
* Default constructor that creates a new instance of {@link HttpClient}.
@@ -83,6 +87,12 @@ public void setBufferFactory(JettyDataBufferFactory bufferFactory) {
8387
this.bufferFactory = bufferFactory;
8488
}
8589

90+
/**
91+
* Set the cookie parser to use.
92+
*/
93+
public void setHttpCookieParser(HttpCookieParser httpCookieParser) {
94+
this.httpCookieParser = httpCookieParser;
95+
}
8696

8797
@Override
8898
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
@@ -111,7 +121,7 @@ private Mono<ClientHttpResponse> execute(JettyClientHttpRequest request) {
111121
return Mono.fromDirect(request.toReactiveRequest()
112122
.response((reactiveResponse, chunkPublisher) -> {
113123
Flux<DataBuffer> content = Flux.from(chunkPublisher).map(this.bufferFactory::wrap);
114-
return Mono.just(new JettyClientHttpResponse(reactiveResponse, content));
124+
return Mono.just(new JettyClientHttpResponse(reactiveResponse, content, this.httpCookieParser));
115125
}));
116126
}
117127

spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java

+9-27
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,17 @@
1616

1717
package org.springframework.http.client.reactive;
1818

19-
import java.net.HttpCookie;
2019
import java.util.List;
21-
import java.util.regex.Matcher;
22-
import java.util.regex.Pattern;
2320

2421
import org.eclipse.jetty.http.HttpField;
2522
import org.eclipse.jetty.reactive.client.ReactiveResponse;
26-
import org.jspecify.annotations.Nullable;
2723
import reactor.core.publisher.Flux;
2824

2925
import org.springframework.core.io.buffer.DataBuffer;
3026
import org.springframework.http.HttpHeaders;
3127
import org.springframework.http.HttpStatusCode;
3228
import org.springframework.http.ResponseCookie;
29+
import org.springframework.http.support.HttpCookieParser;
3330
import org.springframework.http.support.JettyHeadersAdapter;
3431
import org.springframework.util.CollectionUtils;
3532
import org.springframework.util.LinkedMultiValueMap;
@@ -45,41 +42,26 @@
4542
*/
4643
class JettyClientHttpResponse extends AbstractClientHttpResponse {
4744

48-
private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
49-
50-
51-
public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux<DataBuffer> content) {
45+
public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux<DataBuffer> content, HttpCookieParser httpCookieParser) {
5246

5347
super(HttpStatusCode.valueOf(reactiveResponse.getStatus()),
5448
adaptHeaders(reactiveResponse),
55-
adaptCookies(reactiveResponse),
49+
adaptCookies(reactiveResponse, httpCookieParser),
5650
content);
5751
}
5852

5953
private static HttpHeaders adaptHeaders(ReactiveResponse response) {
6054
MultiValueMap<String, String> headers = new JettyHeadersAdapter(response.getHeaders());
6155
return HttpHeaders.readOnlyHttpHeaders(headers);
6256
}
63-
private static MultiValueMap<String, ResponseCookie> adaptCookies(ReactiveResponse response) {
64-
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
57+
private static MultiValueMap<String, ResponseCookie> adaptCookies(ReactiveResponse response, HttpCookieParser httpCookieParser) {
6558
List<HttpField> cookieHeaders = response.getHeaders().getFields(HttpHeaders.SET_COOKIE);
66-
cookieHeaders.forEach(header ->
67-
HttpCookie.parse(header.getValue()).forEach(cookie -> result.add(cookie.getName(),
68-
ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue())
69-
.domain(cookie.getDomain())
70-
.path(cookie.getPath())
71-
.maxAge(cookie.getMaxAge())
72-
.secure(cookie.getSecure())
73-
.httpOnly(cookie.isHttpOnly())
74-
.sameSite(parseSameSite(header.getValue()))
75-
.build()))
76-
);
59+
MultiValueMap<String, ResponseCookie> result = cookieHeaders.stream()
60+
.flatMap(header -> httpCookieParser.parse(header.getValue()))
61+
.collect(LinkedMultiValueMap::new,
62+
(cookies, cookie) -> cookies.add(cookie.getName(), cookie),
63+
LinkedMultiValueMap::addAll);
7764
return CollectionUtils.unmodifiableMultiValueMap(result);
7865
}
7966

80-
private static @Nullable String parseSameSite(String headerValue) {
81-
Matcher matcher = SAME_SITE_PATTERN.matcher(headerValue);
82-
return (matcher.matches() ? matcher.group(1) : null);
83-
}
84-
8567
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.springframework.http.support;
2+
3+
import org.springframework.http.ResponseCookie;
4+
5+
import java.net.HttpCookie;
6+
import java.util.regex.Matcher;
7+
import java.util.regex.Pattern;
8+
import java.util.stream.Stream;
9+
10+
import org.jspecify.annotations.Nullable;
11+
12+
public final class DefaultHttpCookieParser implements HttpCookieParser {
13+
14+
private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
15+
16+
@Override
17+
public Stream<ResponseCookie> parse(String header) {
18+
Matcher matcher = SAME_SITE_PATTERN.matcher(header);
19+
String sameSite = (matcher.matches() ? matcher.group(1) : null);
20+
return HttpCookie.parse(header).stream().map(cookie -> toResponseCookie(cookie, sameSite));
21+
}
22+
23+
private static ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String sameSite) {
24+
return ResponseCookie.from(cookie.getName(), cookie.getValue())
25+
.domain(cookie.getDomain())
26+
.httpOnly(cookie.isHttpOnly())
27+
.maxAge(cookie.getMaxAge())
28+
.path(cookie.getPath())
29+
.secure(cookie.getSecure())
30+
.sameSite(sameSite)
31+
.build();
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.springframework.http.support;
2+
3+
import org.springframework.http.ResponseCookie;
4+
5+
import java.util.stream.Stream;
6+
7+
public interface HttpCookieParser {
8+
9+
Stream<ResponseCookie> parse(String header);
10+
}

0 commit comments

Comments
 (0)