Browse Source

Allow to set custom cookie parsers

Provides a way to be compliant with RFC 6265 section 4.1.1.

See gh-34081
pull/34429/head
m4tt30c91 1 year ago committed by rstoyanchev
parent
commit
ba74de997a
  1. 16
      spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java
  2. 33
      spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java
  3. 12
      spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java
  4. 36
      spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java
  5. 33
      spring-web/src/main/java/org/springframework/http/support/DefaultHttpCookieParser.java
  6. 10
      spring-web/src/main/java/org/springframework/http/support/HttpCookieParser.java

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

@ -34,6 +34,8 @@ import reactor.core.publisher.Mono; @@ -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 { @@ -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 { @@ -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 { @@ -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));
}));
}

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

@ -16,7 +16,6 @@ @@ -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; @@ -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; @@ -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; @@ -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 { @@ -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())

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

@ -28,6 +28,8 @@ import reactor.core.publisher.Mono; @@ -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 { @@ -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 { @@ -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 { @@ -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));
}));
}

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

@ -16,20 +16,17 @@ @@ -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; @@ -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 { @@ -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);
}
}

33
spring-web/src/main/java/org/springframework/http/support/DefaultHttpCookieParser.java

@ -0,0 +1,33 @@ @@ -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();
}
}

10
spring-web/src/main/java/org/springframework/http/support/HttpCookieParser.java

@ -0,0 +1,10 @@ @@ -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);
}
Loading…
Cancel
Save