Merge branch '4.2.x'

This commit is contained in:
Ryan Baxter
2025-05-15 11:06:44 -04:00
3 changed files with 166 additions and 28 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2023 the original author or authors.
* Copyright 2013-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.
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -40,11 +41,14 @@ import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import static org.springframework.web.servlet.function.RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
@@ -268,6 +272,17 @@ public abstract class MvcUtils {
urls.add(url);
}
public static MultiValueMap<String, String> encodeQueryParams(MultiValueMap<String, String> params) {
MultiValueMap<String, String> encodedQueryParams = new LinkedMultiValueMap<>(params.size());
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
for (String value : entry.getValue()) {
encodedQueryParams.add(UriUtils.encode(entry.getKey(), StandardCharsets.UTF_8),
UriUtils.encode(value, StandardCharsets.UTF_8));
}
}
return CollectionUtils.unmodifiableMultiValueMap(encodedQueryParams);
}
private record ByteArrayInputMessage(ServerRequest request, ByteArrayInputStream body) implements HttpInputMessage {
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2023 the original author or authors.
* Copyright 2013-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.
@@ -37,6 +37,9 @@ import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author raccoonback
*/
public class ProxyExchangeHandlerFunction
implements HandlerFunction<ServerResponse>, ApplicationListener<ContextRefreshedEvent> {
@@ -84,14 +87,14 @@ public class ProxyExchangeHandlerFunction
@Override
public ServerResponse handle(ServerRequest serverRequest) {
URI uri = uriResolver.apply(serverRequest);
boolean encoded = containsEncodedQuery(serverRequest.uri(), serverRequest.params());
MultiValueMap<String, String> params = MvcUtils.encodeQueryParams(serverRequest.params());
// @formatter:off
URI url = UriComponentsBuilder.fromUri(serverRequest.uri())
.scheme(uri.getScheme())
.host(uri.getHost())
.port(uri.getPort())
.replaceQueryParams(serverRequest.params())
.build(encoded)
.replaceQueryParams(params)
.build(true)
.toUri();
// @formatter:on
@@ -131,29 +134,6 @@ public class ProxyExchangeHandlerFunction
return filtered;
}
private static boolean containsEncodedQuery(URI uri, MultiValueMap<String, String> params) {
String rawQuery = uri.getRawQuery();
boolean encoded = (rawQuery != null && rawQuery.contains("%"))
|| (uri.getRawPath() != null && uri.getRawPath().contains("%"));
// Verify if it is really fully encoded. Treat partial encoded as unencoded.
if (encoded) {
try {
UriComponentsBuilder.fromUri(uri).replaceQueryParams(params).build(true);
return true;
}
catch (IllegalArgumentException ignored) {
if (log.isTraceEnabled()) {
log.trace("Error in containsEncodedParts", ignored);
}
}
return false;
}
return false;
}
public interface URIResolver extends Function<ServerRequest, URI> {
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2025-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.cloud.gateway.server.mvc.handler;
import java.net.URI;
import java.util.Collections;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.server.mvc.common.AbstractProxyExchange;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.RequestHttpHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.ResponseHttpHeadersFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author raccoonback
*/
class ProxyExchangeHandlerFunctionTest {
@Test
void keepOriginalEncodingOfQueryParameter() {
TestProxyExchange proxyExchange = new TestProxyExchange();
ProxyExchangeHandlerFunction function = new ProxyExchangeHandlerFunction(proxyExchange, new ObjectProvider<>() {
@Override
public RequestHttpHeadersFilter getObject() throws BeansException {
return null;
}
@Override
public RequestHttpHeadersFilter getObject(Object... args) throws BeansException {
return null;
}
@Override
public RequestHttpHeadersFilter getIfAvailable() throws BeansException {
return null;
}
@Override
public RequestHttpHeadersFilter getIfUnique() throws BeansException {
return null;
}
@Override
public Stream<RequestHttpHeadersFilter> orderedStream() {
return Stream.of((httpHeaders, serverRequest) -> new HttpHeaders());
}
}, new ObjectProvider<>() {
@Override
public ResponseHttpHeadersFilter getObject() throws BeansException {
return null;
}
@Override
public ResponseHttpHeadersFilter getObject(Object... args) throws BeansException {
return null;
}
@Override
public ResponseHttpHeadersFilter getIfAvailable() throws BeansException {
return null;
}
@Override
public ResponseHttpHeadersFilter getIfUnique() throws BeansException {
return null;
}
@Override
public Stream<ResponseHttpHeadersFilter> orderedStream() {
return Stream.of((httpHeaders, serverRequest) -> new HttpHeaders());
}
});
function.onApplicationEvent(null);
MockHttpServletRequest servletRequest = MockMvcRequestBuilders
.get("http://localhost/é?foo=value1 value2&bar=value3=&qux=value4+")
.buildRequest(null);
servletRequest.setAttribute(MvcUtils.GATEWAY_REQUEST_URL_ATTR, URI.create("http://localhost:8080"));
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
function.handle(request);
URI uri = proxyExchange.getRequest().getUri();
assertThat(uri).hasToString("http://localhost:8080/%C3%A9?foo=value1%20value2&bar=value3%3D&qux=value4%2B")
.hasPath("")
.hasParameter("foo", "value1 value2")
.hasParameter("bar", "value3=")
.hasParameter("qux", "value4+");
}
private class TestProxyExchange extends AbstractProxyExchange {
private Request request;
protected TestProxyExchange() {
super(new GatewayMvcProperties());
}
@Override
public ServerResponse exchange(Request request) {
this.request = request;
return ServerResponse.ok().build();
}
public Request getRequest() {
return request;
}
}
}