parent
ba74de997a
commit
667004e5fa
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -17,10 +17,13 @@
|
|||
package org.springframework.http;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
|
@ -235,6 +238,22 @@ public final class ResponseCookie extends HttpCookie {
|
|||
return new DefaultResponseCookieBuilder(name, value, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to obtain a builder that copies from {@link java.net.HttpCookie}.
|
||||
* @param cookie the source cookie to copy from
|
||||
* @return a builder to create the cookie with
|
||||
* @since 7.0
|
||||
*/
|
||||
public static ResponseCookieBuilder from(java.net.HttpCookie cookie) {
|
||||
return ResponseCookie.from(cookie.getName(), cookie.getValue())
|
||||
.domain(cookie.getDomain())
|
||||
.httpOnly(cookie.isHttpOnly())
|
||||
.maxAge(cookie.getMaxAge())
|
||||
.path(cookie.getPath())
|
||||
.secure(cookie.getSecure());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A builder for a server-defined HttpCookie with attributes.
|
||||
|
|
@ -307,6 +326,32 @@ public final class ResponseCookie extends HttpCookie {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract to parse {@code "Set-Cookie"} headers.
|
||||
* @since 7.0
|
||||
*/
|
||||
public interface Parser {
|
||||
|
||||
/**
|
||||
* Parse the given header.
|
||||
*/
|
||||
List<ResponseCookie> parse(String header);
|
||||
|
||||
/**
|
||||
* Convenience method to parse a list of headers into a {@link MultiValueMap}.
|
||||
*/
|
||||
default MultiValueMap<String, ResponseCookie> parse(List<String> headers) {
|
||||
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
|
||||
for (String header : headers) {
|
||||
for (ResponseCookie cookie : parse(header)) {
|
||||
result.add(cookie.getName(), cookie);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class Rfc6265Utils {
|
||||
|
||||
private static final String SEPARATOR_CHARS = new String(new char[] {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -33,9 +33,9 @@ import reactor.core.publisher.Mono;
|
|||
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.support.DefaultHttpCookieParser;
|
||||
import org.springframework.http.support.HttpCookieParser;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
|
|
@ -52,10 +52,10 @@ public class JdkClientHttpConnector implements ClientHttpConnector {
|
|||
|
||||
private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
|
||||
|
||||
private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser();
|
||||
|
||||
private @Nullable Duration readTimeout;
|
||||
|
||||
private ResponseCookie.Parser cookieParser = new JdkResponseCookieParser();
|
||||
|
||||
|
||||
/**
|
||||
* Default constructor that uses {@link HttpClient#newHttpClient()}.
|
||||
|
|
@ -110,13 +110,15 @@ public class JdkClientHttpConnector implements ClientHttpConnector {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Customize the parsing of response cookies.
|
||||
* <p>By default, {@link java.net.HttpCookie#parse(String)} is used, and
|
||||
* additionally the sameSite attribute is parsed and set.
|
||||
* @param parser the parser to use
|
||||
* @since 7.0
|
||||
*/
|
||||
public void setHttpCookieParser(HttpCookieParser httpCookieParser) {
|
||||
Assert.notNull(readTimeout, "httpCookieParser is required");
|
||||
this.httpCookieParser = httpCookieParser;
|
||||
public void setCookieParser(ResponseCookie.Parser parser) {
|
||||
Assert.notNull(parser, "ResponseCookie parser is required");
|
||||
this.cookieParser = parser;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -134,7 +136,10 @@ public class JdkClientHttpConnector implements ClientHttpConnector {
|
|||
this.httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofPublisher());
|
||||
|
||||
return Mono.fromCompletionStage(future)
|
||||
.map(response -> new JdkClientHttpResponse(response, this.bufferFactory, this.httpCookieParser));
|
||||
.map(response -> {
|
||||
List<String> headers = response.headers().allValues(HttpHeaders.SET_COOKIE);
|
||||
return new JdkClientHttpResponse(response, this.bufferFactory, this.cookieParser.parse(headers));
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -34,10 +34,8 @@ 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;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
|
|
@ -50,12 +48,10 @@ import org.springframework.util.MultiValueMap;
|
|||
class JdkClientHttpResponse extends AbstractClientHttpResponse {
|
||||
|
||||
public JdkClientHttpResponse(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response,
|
||||
DataBufferFactory bufferFactory, HttpCookieParser httpCookieParser) {
|
||||
DataBufferFactory bufferFactory, MultiValueMap<String, ResponseCookie> cookies) {
|
||||
|
||||
super(HttpStatusCode.valueOf(response.statusCode()),
|
||||
adaptHeaders(response),
|
||||
adaptCookies(response, httpCookieParser),
|
||||
adaptBody(response, bufferFactory)
|
||||
adaptHeaders(response), cookies, adaptBody(response, bufferFactory)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -67,15 +63,6 @@ class JdkClientHttpResponse extends AbstractClientHttpResponse {
|
|||
return HttpHeaders.readOnlyHttpHeaders(multiValueMap);
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, ResponseCookie> adaptCookies(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response,
|
||||
HttpCookieParser httpCookieParser) {
|
||||
return response.headers().allValues(HttpHeaders.SET_COOKIE).stream()
|
||||
.flatMap(httpCookieParser::parse)
|
||||
.collect(LinkedMultiValueMap::new,
|
||||
(cookies, cookie) -> cookies.add(cookie.getName(), cookie),
|
||||
LinkedMultiValueMap::addAll);
|
||||
}
|
||||
|
||||
private static Flux<DataBuffer> adaptBody(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response, DataBufferFactory bufferFactory) {
|
||||
return JdkFlowAdapter.flowPublisherToFlux(response.body())
|
||||
.flatMapIterable(Function.identity())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.http.client.reactive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.http.ResponseCookie;
|
||||
|
||||
|
||||
/**
|
||||
* Parser that delegates to {@link java.net.HttpCookie#parse(String)} for parsing,
|
||||
* but also extracts and sets {@code sameSite}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
final class JdkResponseCookieParser implements ResponseCookie.Parser {
|
||||
|
||||
private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
|
||||
|
||||
|
||||
/**
|
||||
* Parse the given headers.
|
||||
*/
|
||||
public List<ResponseCookie> parse(String header) {
|
||||
Matcher matcher = SAME_SITE_PATTERN.matcher(header);
|
||||
String sameSite = (matcher.matches() ? matcher.group(1) : null);
|
||||
List<java.net.HttpCookie> cookies = java.net.HttpCookie.parse(header);
|
||||
List<ResponseCookie> result = new ArrayList<>(cookies.size());
|
||||
cookies.forEach(cookie -> result.add(ResponseCookie.from(cookie).sameSite(sameSite).build()));
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.http.client.reactive;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
|
|
@ -27,9 +28,9 @@ import reactor.core.publisher.Mono;
|
|||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.JettyDataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.support.DefaultHttpCookieParser;
|
||||
import org.springframework.http.support.HttpCookieParser;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
|
|
@ -45,7 +46,7 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
|
|||
|
||||
private JettyDataBufferFactory bufferFactory = new JettyDataBufferFactory();
|
||||
|
||||
private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser();
|
||||
private ResponseCookie.Parser cookieParser = new JdkResponseCookieParser();
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -88,12 +89,18 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the cookie parser to use.
|
||||
* Customize the parsing of response cookies.
|
||||
* <p>By default, {@link java.net.HttpCookie#parse(String)} is used, and
|
||||
* additionally the sameSite attribute is parsed and set.
|
||||
* @param parser the parser to use
|
||||
* @since 7.0
|
||||
*/
|
||||
public void setHttpCookieParser(HttpCookieParser httpCookieParser) {
|
||||
this.httpCookieParser = httpCookieParser;
|
||||
public void setCookieParser(ResponseCookie.Parser parser) {
|
||||
Assert.notNull(parser, "ResponseCookie parser is required");
|
||||
this.cookieParser = parser;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
|
||||
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
|
||||
|
|
@ -121,7 +128,8 @@ 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, this.httpCookieParser));
|
||||
List<String> headers = reactiveResponse.getHeaders().getValuesList(HttpHeaders.SET_COOKIE);
|
||||
return Mono.just(new JettyClientHttpResponse(reactiveResponse, content, this.cookieParser.parse(headers)));
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@
|
|||
|
||||
package org.springframework.http.client.reactive;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.reactive.client.ReactiveResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
|
|
@ -26,10 +23,7 @@ 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;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
|
|
@ -42,26 +36,16 @@ import org.springframework.util.MultiValueMap;
|
|||
*/
|
||||
class JettyClientHttpResponse extends AbstractClientHttpResponse {
|
||||
|
||||
public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux<DataBuffer> content, HttpCookieParser httpCookieParser) {
|
||||
public JettyClientHttpResponse(
|
||||
ReactiveResponse reactiveResponse, Flux<DataBuffer> content,
|
||||
MultiValueMap<String, ResponseCookie> cookies) {
|
||||
|
||||
super(HttpStatusCode.valueOf(reactiveResponse.getStatus()),
|
||||
adaptHeaders(reactiveResponse),
|
||||
adaptCookies(reactiveResponse, httpCookieParser),
|
||||
content);
|
||||
super(HttpStatusCode.valueOf(reactiveResponse.getStatus()), adaptHeaders(reactiveResponse), cookies, 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, HttpCookieParser httpCookieParser) {
|
||||
List<HttpField> cookieHeaders = response.getHeaders().getFields(HttpHeaders.SET_COOKIE);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package org.springframework.http.support;
|
||||
|
||||
import org.springframework.http.ResponseCookie;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public interface HttpCookieParser {
|
||||
|
||||
Stream<ResponseCookie> parse(String header);
|
||||
}
|
||||
Loading…
Reference in New Issue