Efficient ETag parsing
Closes gh-33372
This commit is contained in:
161
spring-web/src/main/java/org/springframework/http/ETag.java
Normal file
161
spring-web/src/main/java/org/springframework/http/ETag.java
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Represents an ETag for HTTP conditional requests.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.3.38
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7232">RFC 7232</a>
|
||||
*/
|
||||
public class ETag {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ETag.class);
|
||||
|
||||
private static final ETag WILDCARD = new ETag("*", false);
|
||||
|
||||
|
||||
private final String tag;
|
||||
|
||||
private final boolean weak;
|
||||
|
||||
|
||||
public ETag(String tag, boolean weak) {
|
||||
this.tag = tag;
|
||||
this.weak = weak;
|
||||
}
|
||||
|
||||
|
||||
public String tag() {
|
||||
return this.tag;
|
||||
}
|
||||
|
||||
public boolean weak() {
|
||||
return this.weak;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this a wildcard tag matching to any entity tag value.
|
||||
*/
|
||||
public boolean isWildcard() {
|
||||
return (this == WILDCARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fully formatted tag including "W/" prefix and quotes.
|
||||
*/
|
||||
public String formattedTag() {
|
||||
if (this == WILDCARD) {
|
||||
return "*";
|
||||
}
|
||||
return (this.weak ? "W/" : "") + "\"" + this.tag + "\"";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return formattedTag();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse entity tags from an "If-Match" or "If-None-Match" header.
|
||||
* @param source the source string to parse
|
||||
* @return the parsed ETags
|
||||
*/
|
||||
public static List<ETag> parse(String source) {
|
||||
|
||||
List<ETag> result = new ArrayList<>();
|
||||
State state = State.BEFORE_QUOTES;
|
||||
int startIndex = -1;
|
||||
boolean weak = false;
|
||||
|
||||
for (int i = 0; i < source.length(); i++) {
|
||||
char c = source.charAt(i);
|
||||
|
||||
if (state == State.IN_QUOTES) {
|
||||
if (c == '"') {
|
||||
String tag = source.substring(startIndex, i);
|
||||
if (StringUtils.hasText(tag)) {
|
||||
result.add(new ETag(tag, weak));
|
||||
}
|
||||
state = State.AFTER_QUOTES;
|
||||
startIndex = -1;
|
||||
weak = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Character.isWhitespace(c)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == ',') {
|
||||
state = State.BEFORE_QUOTES;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state == State.BEFORE_QUOTES) {
|
||||
if (c == '*') {
|
||||
result.add(WILDCARD);
|
||||
state = State.AFTER_QUOTES;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') {
|
||||
state = State.IN_QUOTES;
|
||||
startIndex = i + 1;
|
||||
continue;
|
||||
}
|
||||
if (c == 'W' && source.length() > i + 2) {
|
||||
if (source.charAt(i + 1) == '/' && source.charAt(i + 2) == '"') {
|
||||
state = State.IN_QUOTES;
|
||||
i = i + 2;
|
||||
startIndex = i + 1;
|
||||
weak = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Unexpected char at index " + i);
|
||||
}
|
||||
}
|
||||
|
||||
if (state != State.IN_QUOTES && logger.isDebugEnabled()) {
|
||||
logger.debug("Expected closing '\"'");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private enum State {
|
||||
|
||||
BEFORE_QUOTES, IN_QUOTES, AFTER_QUOTES
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -40,8 +40,6 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -393,12 +391,6 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
||||
*/
|
||||
public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new LinkedMultiValueMap<>());
|
||||
|
||||
/**
|
||||
* Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
|
||||
*/
|
||||
private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
|
||||
|
||||
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ENGLISH);
|
||||
|
||||
private static final ZoneId GMT = ZoneId.of("GMT");
|
||||
@@ -1568,35 +1560,27 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
||||
|
||||
/**
|
||||
* Retrieve a combined result from the field values of the ETag header.
|
||||
* @param headerName the header name
|
||||
* @param name the header name
|
||||
* @return the combined result
|
||||
* @throws IllegalArgumentException if parsing fails
|
||||
* @since 4.3
|
||||
*/
|
||||
protected List<String> getETagValuesAsList(String headerName) {
|
||||
List<String> values = get(headerName);
|
||||
if (values != null) {
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String value : values) {
|
||||
if (value != null) {
|
||||
Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value);
|
||||
while (matcher.find()) {
|
||||
if ("*".equals(matcher.group())) {
|
||||
result.add(matcher.group());
|
||||
}
|
||||
else {
|
||||
result.add(matcher.group(1));
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Could not parse header '" + headerName + "' with value '" + value + "'");
|
||||
}
|
||||
protected List<String> getETagValuesAsList(String name) {
|
||||
List<String> values = get(name);
|
||||
if (values == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String value : values) {
|
||||
if (value != null) {
|
||||
List<ETag> tags = ETag.parse(value);
|
||||
Assert.notEmpty(tags, "Could not parse header '" + name + "' with value '" + value + "'");
|
||||
for (ETag tag : tags) {
|
||||
result.add(tag.formattedTag());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return Collections.emptyList();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -26,13 +26,12 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.springframework.http.ETag;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -54,12 +53,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
|
||||
|
||||
private static final List<String> SAFE_METHODS = Arrays.asList("GET", "HEAD");
|
||||
|
||||
/**
|
||||
* Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
|
||||
*/
|
||||
private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
|
||||
|
||||
/**
|
||||
* Date formats as specified in the HTTP RFC.
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
|
||||
@@ -289,11 +282,10 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
|
||||
etag = etag.substring(2);
|
||||
}
|
||||
while (ifNoneMatch.hasMoreElements()) {
|
||||
String clientETags = ifNoneMatch.nextElement();
|
||||
Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(clientETags);
|
||||
// Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
|
||||
while (etagMatcher.find()) {
|
||||
if (StringUtils.hasLength(etagMatcher.group()) && etag.equals(etagMatcher.group(3))) {
|
||||
for (ETag requestedETag : ETag.parse(ifNoneMatch.nextElement())) {
|
||||
String tag = requestedETag.tag();
|
||||
if (StringUtils.hasLength(tag) && etag.equals(padEtagIfNecessary(tag))) {
|
||||
this.notModified = true;
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user