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;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.support.DefaultHttpCookieParser;
import org.springframework.http.support.HttpCookieParser;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -50,6 +52,8 @@ public class JdkClientHttpConnector implements ClientHttpConnector {
private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser();
private @Nullable Duration readTimeout; private @Nullable Duration readTimeout;
@ -105,6 +109,16 @@ public class JdkClientHttpConnector implements ClientHttpConnector {
this.readTimeout = readTimeout; 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 @Override
public Mono<ClientHttpResponse> connect( public Mono<ClientHttpResponse> connect(
@ -120,7 +134,7 @@ public class JdkClientHttpConnector implements ClientHttpConnector {
this.httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofPublisher()); this.httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofPublisher());
return Mono.fromCompletionStage(future) 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 @@
package org.springframework.http.client.reactive; package org.springframework.http.client.reactive;
import java.net.HttpCookie;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -25,10 +24,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Flow; import java.util.concurrent.Flow;
import java.util.function.Function; 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.adapter.JdkFlowAdapter;
import reactor.core.publisher.Flux; 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.HttpHeaders;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseCookie;
import org.springframework.http.support.HttpCookieParser;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
@ -52,16 +49,12 @@ import org.springframework.util.MultiValueMap;
*/ */
class JdkClientHttpResponse extends AbstractClientHttpResponse { 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, public JdkClientHttpResponse(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response,
DataBufferFactory bufferFactory) { DataBufferFactory bufferFactory, HttpCookieParser httpCookieParser) {
super(HttpStatusCode.valueOf(response.statusCode()), super(HttpStatusCode.valueOf(response.statusCode()),
adaptHeaders(response), adaptHeaders(response),
adaptCookies(response), adaptCookies(response, httpCookieParser),
adaptBody(response, bufferFactory) adaptBody(response, bufferFactory)
); );
} }
@ -74,29 +67,15 @@ class JdkClientHttpResponse extends AbstractClientHttpResponse {
return HttpHeaders.readOnlyHttpHeaders(multiValueMap); 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() return response.headers().allValues(HttpHeaders.SET_COOKIE).stream()
.flatMap(header -> { .flatMap(httpCookieParser::parse)
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));
})
.collect(LinkedMultiValueMap::new, .collect(LinkedMultiValueMap::new,
(cookies, cookie) -> cookies.add(cookie.getName(), cookie), (cookies, cookie) -> cookies.add(cookie.getName(), cookie),
LinkedMultiValueMap::addAll); 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) { private static Flux<DataBuffer> adaptBody(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response, DataBufferFactory bufferFactory) {
return JdkFlowAdapter.flowPublisherToFlux(response.body()) return JdkFlowAdapter.flowPublisherToFlux(response.body())
.flatMapIterable(Function.identity()) .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;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.JettyDataBufferFactory; import org.springframework.core.io.buffer.JettyDataBufferFactory;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.support.DefaultHttpCookieParser;
import org.springframework.http.support.HttpCookieParser;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -43,6 +45,8 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
private JettyDataBufferFactory bufferFactory = new JettyDataBufferFactory(); private JettyDataBufferFactory bufferFactory = new JettyDataBufferFactory();
private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser();
/** /**
* Default constructor that creates a new instance of {@link HttpClient}. * Default constructor that creates a new instance of {@link HttpClient}.
@ -83,6 +87,12 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
this.bufferFactory = bufferFactory; this.bufferFactory = bufferFactory;
} }
/**
* Set the cookie parser to use.
*/
public void setHttpCookieParser(HttpCookieParser httpCookieParser) {
this.httpCookieParser = httpCookieParser;
}
@Override @Override
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri, public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
@ -111,7 +121,7 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
return Mono.fromDirect(request.toReactiveRequest() return Mono.fromDirect(request.toReactiveRequest()
.response((reactiveResponse, chunkPublisher) -> { .response((reactiveResponse, chunkPublisher) -> {
Flux<DataBuffer> content = Flux.from(chunkPublisher).map(this.bufferFactory::wrap); 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 @@
package org.springframework.http.client.reactive; package org.springframework.http.client.reactive;
import java.net.HttpCookie;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.reactive.client.ReactiveResponse; import org.eclipse.jetty.reactive.client.ReactiveResponse;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseCookie;
import org.springframework.http.support.HttpCookieParser;
import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.http.support.JettyHeadersAdapter;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
@ -45,14 +42,11 @@ import org.springframework.util.MultiValueMap;
*/ */
class JettyClientHttpResponse extends AbstractClientHttpResponse { 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, HttpCookieParser httpCookieParser) {
public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux<DataBuffer> content) {
super(HttpStatusCode.valueOf(reactiveResponse.getStatus()), super(HttpStatusCode.valueOf(reactiveResponse.getStatus()),
adaptHeaders(reactiveResponse), adaptHeaders(reactiveResponse),
adaptCookies(reactiveResponse), adaptCookies(reactiveResponse, httpCookieParser),
content); content);
} }
@ -60,26 +54,14 @@ class JettyClientHttpResponse extends AbstractClientHttpResponse {
MultiValueMap<String, String> headers = new JettyHeadersAdapter(response.getHeaders()); MultiValueMap<String, String> headers = new JettyHeadersAdapter(response.getHeaders());
return HttpHeaders.readOnlyHttpHeaders(headers); return HttpHeaders.readOnlyHttpHeaders(headers);
} }
private static MultiValueMap<String, ResponseCookie> adaptCookies(ReactiveResponse response) { private static MultiValueMap<String, ResponseCookie> adaptCookies(ReactiveResponse response, HttpCookieParser httpCookieParser) {
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
List<HttpField> cookieHeaders = response.getHeaders().getFields(HttpHeaders.SET_COOKIE); List<HttpField> cookieHeaders = response.getHeaders().getFields(HttpHeaders.SET_COOKIE);
cookieHeaders.forEach(header -> MultiValueMap<String, ResponseCookie> result = cookieHeaders.stream()
HttpCookie.parse(header.getValue()).forEach(cookie -> result.add(cookie.getName(), .flatMap(header -> httpCookieParser.parse(header.getValue()))
ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue()) .collect(LinkedMultiValueMap::new,
.domain(cookie.getDomain()) (cookies, cookie) -> cookies.add(cookie.getName(), cookie),
.path(cookie.getPath()) LinkedMultiValueMap::addAll);
.maxAge(cookie.getMaxAge())
.secure(cookie.getSecure())
.httpOnly(cookie.isHttpOnly())
.sameSite(parseSameSite(header.getValue()))
.build()))
);
return CollectionUtils.unmodifiableMultiValueMap(result); 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 @@
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 @@
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