Encode IPV6 Zone IDs in ReactorServerHttpRequest

This commit ensures that the zone id in the ReactorServerHttpRequest is
properly encoded.

Closes gh-30188
This commit is contained in:
Arjen Poutsma 2023-04-04 16:52:20 +02:00
parent d6460e0d57
commit cef9166833
3 changed files with 198 additions and 41 deletions

View File

@ -17,7 +17,6 @@
package org.springframework.http.server.reactive; package org.springframework.http.server.reactive;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@ -65,52 +64,13 @@ class ReactorServerHttpRequest extends AbstractServerHttpRequest {
public ReactorServerHttpRequest(HttpServerRequest request, NettyDataBufferFactory bufferFactory) public ReactorServerHttpRequest(HttpServerRequest request, NettyDataBufferFactory bufferFactory)
throws URISyntaxException { throws URISyntaxException {
super(HttpMethod.valueOf(request.method().name()), initUri(request), "", super(HttpMethod.valueOf(request.method().name()), ReactorUriHelper.createUri(request), "",
new NettyHeadersAdapter(request.requestHeaders())); new NettyHeadersAdapter(request.requestHeaders()));
Assert.notNull(bufferFactory, "DataBufferFactory must not be null"); Assert.notNull(bufferFactory, "DataBufferFactory must not be null");
this.request = request; this.request = request;
this.bufferFactory = bufferFactory; this.bufferFactory = bufferFactory;
} }
private static URI initUri(HttpServerRequest request) throws URISyntaxException {
Assert.notNull(request, "HttpServerRequest must not be null");
return new URI(resolveBaseUrl(request) + resolveRequestUri(request));
}
private static String resolveBaseUrl(HttpServerRequest request) {
String scheme = request.scheme();
int port = request.hostPort();
return scheme + "://" + request.hostName() + (usePort(scheme, port) ? ":" + port : "");
}
private static boolean usePort(String scheme, int port) {
return ((scheme.equals("http") || scheme.equals("ws")) && (port != 80)) ||
((scheme.equals("https") || scheme.equals("wss")) && (port != 443));
}
private static String resolveRequestUri(HttpServerRequest request) {
String uri = request.uri();
for (int i = 0; i < uri.length(); i++) {
char c = uri.charAt(i);
if (c == '/' || c == '?' || c == '#') {
break;
}
if (c == ':' && (i + 2 < uri.length())) {
if (uri.charAt(i + 1) == '/' && uri.charAt(i + 2) == '/') {
for (int j = i + 3; j < uri.length(); j++) {
c = uri.charAt(j);
if (c == '/' || c == '?' || c == '#') {
return uri.substring(j);
}
}
return "";
}
}
}
return uri;
}
@Override @Override
protected MultiValueMap<String, HttpCookie> initCookies() { protected MultiValueMap<String, HttpCookie> initCookies() {
MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>(); MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();

View File

@ -0,0 +1,145 @@
/*
* Copyright 2002-2023 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.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import reactor.netty.http.server.HttpServerRequest;
import org.springframework.util.Assert;
/**
* Helper class for creating a {@link URI} from a reactor {@link HttpServerRequest}.
*
* @author Arjen Poutsma
* @since 6.0.8
*/
abstract class ReactorUriHelper {
public static URI createUri(HttpServerRequest request) throws URISyntaxException {
Assert.notNull(request, "HttpServerRequest must not be null");
StringBuilder builder = new StringBuilder();
String scheme = request.scheme();
builder.append(scheme);
builder.append("://");
appendHostName(request, builder);
int port = request.hostPort();
if ((scheme.equals("http") || scheme.equals("ws")) && port != 80 ||
(scheme.equals("https") || scheme.equals("wss")) && port != 443) {
builder.append(':');
builder.append(port);
}
appendRequestUri(request, builder);
return new URI(builder.toString());
}
private static void appendHostName(HttpServerRequest request, StringBuilder builder) {
String hostName = request.hostName();
boolean ipv6 = hostName.indexOf(':') != -1;
boolean brackets = ipv6 && !hostName.startsWith("[") && !hostName.endsWith("]");
if (brackets) {
builder.append('[');
}
if (encoded(hostName, ipv6)) {
builder.append(hostName);
}
else {
for (int i=0; i < hostName.length(); i++) {
char c = hostName.charAt(i);
if (isAllowedInHost(c, ipv6)) {
builder.append(c);
}
else {
builder.append('%');
char hex1 = Character.toUpperCase(Character.forDigit((c >> 4) & 0xF, 16));
char hex2 = Character.toUpperCase(Character.forDigit(c & 0xF, 16));
builder.append(hex1);
builder.append(hex2);
}
}
}
if (brackets) {
builder.append(']');
}
}
private static boolean encoded(String hostName, boolean ipv6) {
int length = hostName.length();
for (int i = 0; i < length; i++) {
char c = hostName.charAt(i);
if (c == '%') {
if ((i + 2) < length) {
char hex1 = hostName.charAt(i + 1);
char hex2 = hostName.charAt(i + 2);
int u = Character.digit(hex1, 16);
int l = Character.digit(hex2, 16);
if (u == -1 || l == -1) {
return false;
}
i += 2;
}
else {
return false;
}
}
else if (!isAllowedInHost(c, ipv6)) {
return false;
}
}
return true;
}
private static boolean isAllowedInHost(char c, boolean ipv6) {
return (c >= 'a' && c <= 'z') || // alpha
(c >= 'A' && c <= 'Z') || // alpha
(c >= '0' && c <= '9') || // digit
'-' == c || '.' == c || '_' == c || '~' == c || // unreserved
'!' == c || '$' == c || '&' == c || '\'' == c || '(' == c || ')' == c || // sub-delims
'*' == c || '+' == c || ',' == c || ';' == c || '=' == c ||
(ipv6 && ('[' == c || ']' == c || ':' == c)); // ipv6
}
private static void appendRequestUri(HttpServerRequest request, StringBuilder builder) {
String uri = request.uri();
int length = uri.length();
for (int i = 0; i < length; i++) {
char c = uri.charAt(i);
if (c == '/' || c == '?' || c == '#') {
break;
}
if (c == ':' && (i + 2 < length)) {
if (uri.charAt(i + 1) == '/' && uri.charAt(i + 2) == '/') {
for (int j = i + 3; j < length; j++) {
c = uri.charAt(j);
if (c == '/' || c == '?' || c == '#') {
builder.append(uri, j, length);
return;
}
}
return;
}
}
}
builder.append(uri);
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2002-2023 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.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import org.junit.jupiter.api.Test;
import reactor.netty.http.server.HttpServerRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* @author Arjen Poutsma
*/
public class ReactorUriHelperTests {
@Test
public void hostnameWithZoneId() throws URISyntaxException {
HttpServerRequest nettyRequest = mock();
given(nettyRequest.scheme()).willReturn("http");
given(nettyRequest.hostName()).willReturn("fe80::a%en1");
given(nettyRequest.hostPort()).willReturn(80);
given(nettyRequest.uri()).willReturn("/");
URI uri = ReactorUriHelper.createUri(nettyRequest);
assertThat(uri).hasScheme("http")
.hasHost("[fe80::a%25en1]")
.hasPort(-1)
.hasPath("/")
.hasToString("http://[fe80::a%25en1]/");
}
}