Skip to content

Commit 974dec2

Browse files
committed
Merge branch '6.1.x'
2 parents e1cf203 + bb17ad8 commit 974dec2

File tree

4 files changed

+165
-45
lines changed

4 files changed

+165
-45
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright 2002-2024 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;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.apache.commons.logging.Log;
23+
import org.apache.commons.logging.LogFactory;
24+
25+
import org.springframework.util.StringUtils;
26+
27+
/**
28+
* Represents an ETag for HTTP conditional requests.
29+
*
30+
* @param tag the unquoted tag value
31+
* @param weak whether the entity tag is for weak or strong validation
32+
* @author Rossen Stoyanchev
33+
* @since 5.3.38
34+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7232">RFC 7232</a>
35+
*/
36+
public record ETag(String tag, boolean weak) {
37+
38+
private static final Log logger = LogFactory.getLog(ETag.class);
39+
40+
private static final ETag WILDCARD = new ETag("*", false);
41+
42+
43+
/**
44+
* Whether this a wildcard tag matching to any entity tag value.
45+
*/
46+
public boolean isWildcard() {
47+
return (this == WILDCARD);
48+
}
49+
50+
/**
51+
* Return the fully formatted tag including "W/" prefix and quotes.
52+
*/
53+
public String formattedTag() {
54+
if (isWildcard()) {
55+
return "*";
56+
}
57+
return (this.weak ? "W/" : "") + "\"" + this.tag + "\"";
58+
}
59+
60+
@Override
61+
public String toString() {
62+
return formattedTag();
63+
}
64+
65+
66+
/**
67+
* Parse entity tags from an "If-Match" or "If-None-Match" header.
68+
* @param source the source string to parse
69+
* @return the parsed ETags
70+
*/
71+
public static List<ETag> parse(String source) {
72+
73+
List<ETag> result = new ArrayList<>();
74+
State state = State.BEFORE_QUOTES;
75+
int startIndex = -1;
76+
boolean weak = false;
77+
78+
for (int i = 0; i < source.length(); i++) {
79+
char c = source.charAt(i);
80+
81+
if (state == State.IN_QUOTES) {
82+
if (c == '"') {
83+
String tag = source.substring(startIndex, i);
84+
if (StringUtils.hasText(tag)) {
85+
result.add(new ETag(tag, weak));
86+
}
87+
state = State.AFTER_QUOTES;
88+
startIndex = -1;
89+
weak = false;
90+
}
91+
continue;
92+
}
93+
94+
if (Character.isWhitespace(c)) {
95+
continue;
96+
}
97+
98+
if (c == ',') {
99+
state = State.BEFORE_QUOTES;
100+
continue;
101+
}
102+
103+
if (state == State.BEFORE_QUOTES) {
104+
if (c == '*') {
105+
result.add(WILDCARD);
106+
state = State.AFTER_QUOTES;
107+
continue;
108+
}
109+
if (c == '"') {
110+
state = State.IN_QUOTES;
111+
startIndex = i + 1;
112+
continue;
113+
}
114+
if (c == 'W' && source.length() > i + 2) {
115+
if (source.charAt(i + 1) == '/' && source.charAt(i + 2) == '"') {
116+
state = State.IN_QUOTES;
117+
i = i + 2;
118+
startIndex = i + 1;
119+
weak = true;
120+
continue;
121+
}
122+
}
123+
}
124+
125+
if (logger.isDebugEnabled()) {
126+
logger.debug("Unexpected char at index " + i);
127+
}
128+
}
129+
130+
if (state != State.IN_QUOTES && logger.isDebugEnabled()) {
131+
logger.debug("Expected closing '\"'");
132+
}
133+
134+
return result;
135+
}
136+
137+
138+
private enum State {
139+
140+
BEFORE_QUOTES, IN_QUOTES, AFTER_QUOTES
141+
142+
}
143+
144+
}

spring-web/src/main/java/org/springframework/http/HttpHeaders.java

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@
4040
import java.util.Set;
4141
import java.util.StringJoiner;
4242
import java.util.function.BiConsumer;
43-
import java.util.regex.Matcher;
44-
import java.util.regex.Pattern;
4543
import java.util.stream.Collectors;
4644

4745
import org.springframework.lang.Nullable;
@@ -393,12 +391,6 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
393391
*/
394392
public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new LinkedMultiValueMap<>());
395393

396-
/**
397-
* Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
398-
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
399-
*/
400-
private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
401-
402394
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ENGLISH);
403395

404396
private static final ZoneId GMT = ZoneId.of("GMT");
@@ -1654,35 +1646,27 @@ public void clearContentHeaders() {
16541646

16551647
/**
16561648
* Retrieve a combined result from the field values of the ETag header.
1657-
* @param headerName the header name
1649+
* @param name the header name
16581650
* @return the combined result
16591651
* @throws IllegalArgumentException if parsing fails
16601652
* @since 4.3
16611653
*/
1662-
protected List<String> getETagValuesAsList(String headerName) {
1663-
List<String> values = get(headerName);
1664-
if (values != null) {
1665-
List<String> result = new ArrayList<>();
1666-
for (String value : values) {
1667-
if (value != null) {
1668-
Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value);
1669-
while (matcher.find()) {
1670-
if ("*".equals(matcher.group())) {
1671-
result.add(matcher.group());
1672-
}
1673-
else {
1674-
result.add(matcher.group(1));
1675-
}
1676-
}
1677-
if (result.isEmpty()) {
1678-
throw new IllegalArgumentException(
1679-
"Could not parse header '" + headerName + "' with value '" + value + "'");
1680-
}
1654+
protected List<String> getETagValuesAsList(String name) {
1655+
List<String> values = get(name);
1656+
if (values == null) {
1657+
return Collections.emptyList();
1658+
}
1659+
List<String> result = new ArrayList<>();
1660+
for (String value : values) {
1661+
if (value != null) {
1662+
List<ETag> tags = ETag.parse(value);
1663+
Assert.notEmpty(tags, "Could not parse header '" + name + "' with value '" + value + "'");
1664+
for (ETag tag : tags) {
1665+
result.add(tag.formattedTag());
16811666
}
16821667
}
1683-
return result;
16841668
}
1685-
return Collections.emptyList();
1669+
return result;
16861670
}
16871671

16881672
/**

spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@
2525
import java.util.Map;
2626
import java.util.Set;
2727
import java.util.TimeZone;
28-
import java.util.regex.Matcher;
29-
import java.util.regex.Pattern;
3028

3129
import jakarta.servlet.http.HttpServletRequest;
3230
import jakarta.servlet.http.HttpServletResponse;
3331
import jakarta.servlet.http.HttpSession;
3432

33+
import org.springframework.http.ETag;
3534
import org.springframework.http.HttpHeaders;
3635
import org.springframework.http.HttpMethod;
3736
import org.springframework.http.HttpStatus;
@@ -53,12 +52,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
5352

5453
private static final Set<String> SAFE_METHODS = Set.of("GET", "HEAD");
5554

56-
/**
57-
* Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
58-
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
59-
*/
60-
private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
61-
6255
/**
6356
* Date formats as specified in the HTTP RFC.
6457
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
@@ -258,20 +251,19 @@ private boolean matchRequestedETags(Enumeration<String> requestedETags, @Nullabl
258251
etag = padEtagIfNecessary(etag);
259252
while (requestedETags.hasMoreElements()) {
260253
// Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3
261-
Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(requestedETags.nextElement());
262-
while (etagMatcher.find()) {
254+
for (ETag requestedETag : ETag.parse(requestedETags.nextElement())) {
263255
// only consider "lost updates" checks for unsafe HTTP methods
264-
if ("*".equals(etagMatcher.group()) && StringUtils.hasLength(etag)
256+
if (requestedETag.isWildcard() && StringUtils.hasLength(etag)
265257
&& !SAFE_METHODS.contains(getRequest().getMethod())) {
266258
return false;
267259
}
268260
if (weakCompare) {
269-
if (etagWeakMatch(etag, etagMatcher.group(1))) {
261+
if (etagWeakMatch(etag, requestedETag.formattedTag())) {
270262
return false;
271263
}
272264
}
273265
else {
274-
if (etagStrongMatch(etag, etagMatcher.group(1))) {
266+
if (etagStrongMatch(etag, requestedETag.formattedTag())) {
275267
return false;
276268
}
277269
}

spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -163,8 +163,8 @@ void ifNoneMatchShouldNotMatchDifferentETag(String method) {
163163
assertOkWithETag(etag);
164164
}
165165

166+
// gh-19127
166167
@SafeHttpMethodsTest
167-
// SPR-14559
168168
void ifNoneMatchShouldNotFailForUnquotedETag(String method) {
169169
setUpRequest(method);
170170
String etag = "\"etagvalue\"";

0 commit comments

Comments
 (0)