Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a way to set custom cookie parsers #34081

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -50,6 +52,8 @@ public class JdkClientHttpConnector implements ClientHttpConnector {

private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;

private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser();

@Nullable
private Duration readTimeout;

Expand Down Expand Up @@ -106,6 +110,16 @@ public void setReadTimeout(Duration 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
public Mono<ClientHttpResponse> connect(
Expand All @@ -121,7 +135,7 @@ public Mono<ClientHttpResponse> 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));
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand All @@ -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<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)
);
}
Expand All @@ -74,29 +66,15 @@ private static HttpHeaders adaptHeaders(HttpResponse<Flow.Publisher<List<ByteBuf
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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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}.
Expand Down Expand Up @@ -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<ClientHttpResponse> connect(HttpMethod method, URI uri,
Expand Down Expand Up @@ -127,7 +137,7 @@ private Mono<ClientHttpResponse> execute(JettyClientHttpRequest request) {
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));
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -45,42 +41,25 @@
*/
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);
}

private static HttpHeaders adaptHeaders(ReactiveResponse response) {
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);
}

@Nullable
private static String parseSameSite(String headerValue) {
Matcher matcher = SAME_SITE_PATTERN.matcher(headerValue);
return (matcher.matches() ? matcher.group(1) : null);
}

}
Original file line number Diff line number Diff line change
@@ -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<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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}