Skip to content

Commit cef9166

Browse files
committed
Encode IPV6 Zone IDs in ReactorServerHttpRequest
This commit ensures that the zone id in the ReactorServerHttpRequest is properly encoded. Closes gh-30188
1 parent d6460e0 commit cef9166

File tree

3 files changed

+198
-41
lines changed

3 files changed

+198
-41
lines changed

spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.http.server.reactive;
1818

1919
import java.net.InetSocketAddress;
20-
import java.net.URI;
2120
import java.net.URISyntaxException;
2221
import java.util.concurrent.atomic.AtomicLong;
2322

@@ -65,52 +64,13 @@ class ReactorServerHttpRequest extends AbstractServerHttpRequest {
6564
public ReactorServerHttpRequest(HttpServerRequest request, NettyDataBufferFactory bufferFactory)
6665
throws URISyntaxException {
6766

68-
super(HttpMethod.valueOf(request.method().name()), initUri(request), "",
67+
super(HttpMethod.valueOf(request.method().name()), ReactorUriHelper.createUri(request), "",
6968
new NettyHeadersAdapter(request.requestHeaders()));
7069
Assert.notNull(bufferFactory, "DataBufferFactory must not be null");
7170
this.request = request;
7271
this.bufferFactory = bufferFactory;
7372
}
7473

75-
private static URI initUri(HttpServerRequest request) throws URISyntaxException {
76-
Assert.notNull(request, "HttpServerRequest must not be null");
77-
return new URI(resolveBaseUrl(request) + resolveRequestUri(request));
78-
}
79-
80-
private static String resolveBaseUrl(HttpServerRequest request) {
81-
String scheme = request.scheme();
82-
int port = request.hostPort();
83-
return scheme + "://" + request.hostName() + (usePort(scheme, port) ? ":" + port : "");
84-
}
85-
86-
private static boolean usePort(String scheme, int port) {
87-
return ((scheme.equals("http") || scheme.equals("ws")) && (port != 80)) ||
88-
((scheme.equals("https") || scheme.equals("wss")) && (port != 443));
89-
}
90-
91-
private static String resolveRequestUri(HttpServerRequest request) {
92-
String uri = request.uri();
93-
for (int i = 0; i < uri.length(); i++) {
94-
char c = uri.charAt(i);
95-
if (c == '/' || c == '?' || c == '#') {
96-
break;
97-
}
98-
if (c == ':' && (i + 2 < uri.length())) {
99-
if (uri.charAt(i + 1) == '/' && uri.charAt(i + 2) == '/') {
100-
for (int j = i + 3; j < uri.length(); j++) {
101-
c = uri.charAt(j);
102-
if (c == '/' || c == '?' || c == '#') {
103-
return uri.substring(j);
104-
}
105-
}
106-
return "";
107-
}
108-
}
109-
}
110-
return uri;
111-
}
112-
113-
11474
@Override
11575
protected MultiValueMap<String, HttpCookie> initCookies() {
11676
MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.server.reactive;
18+
19+
import java.net.URI;
20+
import java.net.URISyntaxException;
21+
22+
import reactor.netty.http.server.HttpServerRequest;
23+
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* Helper class for creating a {@link URI} from a reactor {@link HttpServerRequest}.
28+
*
29+
* @author Arjen Poutsma
30+
* @since 6.0.8
31+
*/
32+
abstract class ReactorUriHelper {
33+
34+
public static URI createUri(HttpServerRequest request) throws URISyntaxException {
35+
Assert.notNull(request, "HttpServerRequest must not be null");
36+
37+
StringBuilder builder = new StringBuilder();
38+
String scheme = request.scheme();
39+
builder.append(scheme);
40+
builder.append("://");
41+
42+
appendHostName(request, builder);
43+
44+
int port = request.hostPort();
45+
if ((scheme.equals("http") || scheme.equals("ws")) && port != 80 ||
46+
(scheme.equals("https") || scheme.equals("wss")) && port != 443) {
47+
builder.append(':');
48+
builder.append(port);
49+
}
50+
51+
appendRequestUri(request, builder);
52+
53+
return new URI(builder.toString());
54+
}
55+
56+
private static void appendHostName(HttpServerRequest request, StringBuilder builder) {
57+
String hostName = request.hostName();
58+
boolean ipv6 = hostName.indexOf(':') != -1;
59+
boolean brackets = ipv6 && !hostName.startsWith("[") && !hostName.endsWith("]");
60+
if (brackets) {
61+
builder.append('[');
62+
}
63+
if (encoded(hostName, ipv6)) {
64+
builder.append(hostName);
65+
}
66+
else {
67+
for (int i=0; i < hostName.length(); i++) {
68+
char c = hostName.charAt(i);
69+
if (isAllowedInHost(c, ipv6)) {
70+
builder.append(c);
71+
}
72+
else {
73+
builder.append('%');
74+
char hex1 = Character.toUpperCase(Character.forDigit((c >> 4) & 0xF, 16));
75+
char hex2 = Character.toUpperCase(Character.forDigit(c & 0xF, 16));
76+
builder.append(hex1);
77+
builder.append(hex2);
78+
}
79+
}
80+
}
81+
if (brackets) {
82+
builder.append(']');
83+
}
84+
}
85+
86+
private static boolean encoded(String hostName, boolean ipv6) {
87+
int length = hostName.length();
88+
for (int i = 0; i < length; i++) {
89+
char c = hostName.charAt(i);
90+
if (c == '%') {
91+
if ((i + 2) < length) {
92+
char hex1 = hostName.charAt(i + 1);
93+
char hex2 = hostName.charAt(i + 2);
94+
int u = Character.digit(hex1, 16);
95+
int l = Character.digit(hex2, 16);
96+
if (u == -1 || l == -1) {
97+
return false;
98+
}
99+
i += 2;
100+
}
101+
else {
102+
return false;
103+
}
104+
}
105+
else if (!isAllowedInHost(c, ipv6)) {
106+
return false;
107+
}
108+
}
109+
return true;
110+
}
111+
112+
private static boolean isAllowedInHost(char c, boolean ipv6) {
113+
return (c >= 'a' && c <= 'z') || // alpha
114+
(c >= 'A' && c <= 'Z') || // alpha
115+
(c >= '0' && c <= '9') || // digit
116+
'-' == c || '.' == c || '_' == c || '~' == c || // unreserved
117+
'!' == c || '$' == c || '&' == c || '\'' == c || '(' == c || ')' == c || // sub-delims
118+
'*' == c || '+' == c || ',' == c || ';' == c || '=' == c ||
119+
(ipv6 && ('[' == c || ']' == c || ':' == c)); // ipv6
120+
}
121+
122+
private static void appendRequestUri(HttpServerRequest request, StringBuilder builder) {
123+
String uri = request.uri();
124+
int length = uri.length();
125+
for (int i = 0; i < length; i++) {
126+
char c = uri.charAt(i);
127+
if (c == '/' || c == '?' || c == '#') {
128+
break;
129+
}
130+
if (c == ':' && (i + 2 < length)) {
131+
if (uri.charAt(i + 1) == '/' && uri.charAt(i + 2) == '/') {
132+
for (int j = i + 3; j < length; j++) {
133+
c = uri.charAt(j);
134+
if (c == '/' || c == '?' || c == '#') {
135+
builder.append(uri, j, length);
136+
return;
137+
}
138+
}
139+
return;
140+
}
141+
}
142+
}
143+
builder.append(uri);
144+
}
145+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.server.reactive;
18+
19+
import java.net.URI;
20+
import java.net.URISyntaxException;
21+
22+
import org.junit.jupiter.api.Test;
23+
import reactor.netty.http.server.HttpServerRequest;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.mockito.BDDMockito.given;
27+
import static org.mockito.Mockito.mock;
28+
29+
/**
30+
* @author Arjen Poutsma
31+
*/
32+
public class ReactorUriHelperTests {
33+
34+
@Test
35+
public void hostnameWithZoneId() throws URISyntaxException {
36+
HttpServerRequest nettyRequest = mock();
37+
38+
given(nettyRequest.scheme()).willReturn("http");
39+
given(nettyRequest.hostName()).willReturn("fe80::a%en1");
40+
given(nettyRequest.hostPort()).willReturn(80);
41+
given(nettyRequest.uri()).willReturn("/");
42+
43+
URI uri = ReactorUriHelper.createUri(nettyRequest);
44+
assertThat(uri).hasScheme("http")
45+
.hasHost("[fe80::a%25en1]")
46+
.hasPort(-1)
47+
.hasPath("/")
48+
.hasToString("http://[fe80::a%25en1]/");
49+
50+
}
51+
52+
}

0 commit comments

Comments
 (0)